前回に続きMilk-V Duo256をいじくっている話の続きです。
Duo256にはAIを処理する為のTPU(Tensor Processing Unit)が載っています。
前回書いた様に、トラ技2024年12月号にはこれを使ったサンプルの実行例が2件載っていたので、これに従い下記の動作を確認する事が出来ました。
- 顔検出
カメラの動作テストに含まれている。PC側でVLCを実行して動画をモニターでき、人間の顔を検出すると枠で囲んで表示する。 - YOLOv5
写真ファイルに対して物体検出&分類を実行し結果をテキストで表示する。
動画でYOLOv5を動かしてみる。
で、この先です。
YOLOを動画でも動かせるのかなーと思って色々調べたところ、開発ツールのcvitek-tdl-sdk-sg200xの下、sample/cvi_tdl ディレクトリにsample_vi_od.cというソースコードが付属していて、これをコンパイルすれば実行できそうな感じです。
それにはPC上でクロスコンパイルする必要があるのでWSL(Windows Subsystem for Linux)上で、下記ページに従い実行しました。
https://milkv.io/docs/duo/application-development/tdl-sdk/tdl-sdk-introduction

そしてコンパイルしたファイルをscpでDuo256に転送し、実行権も付けておきます。
このプログラムを実行するにはモデル名とモデルパスの2つの引数が必要です。
モデルパスは学習済AIモデルへのファイルパスで、モデル名はAIモデルに応じた動作をプログラムに伝える為の名前です。なのですが実際に何を指定したらいいのか、ズバッと書いた説明が見つかりません。
そこで引数無しで実行した時に表示されるUsageにモデル名一覧が書いてあったので、この中から”yolov3″で試してみます。そして学習済みモデルはhttps://github.com/milkv-duo/tdl-models/tree/milkv/cv181xにあったのをダウンロードしてきました。これを~/tdl-models/cv181x下に置いてこの中にあるyolov3.cvimodelを指定してみます。
こんな感じで・・・
./sample_vi_od yolov3 /root/tdl_models/cv181x/yolov3.cvimodel
しかしOut of memoryとか出てきて動作しません。
そこでモデル名に指定した引数がどう扱われるのかソースをみていくと、get_od_model_info()という関数に渡していました。更にこの関数を辿っていくとUsageには含まれていなかったYOLOv5も動作する様な記述があります。
という事で以下のコマンドラインでそれらしき動作を開始しました。
(コマンドが長ったらしくなるのが嫌なのでAIモデルは同じディレクトリにコピー済です)
./sample_vi_od yolo5 yolo5_s.cvimodel
でも下記の様にyolo5_mの方が認識結果が良い様です。
./sample_vi_od yolo5 yolo5_m.cvimodel
VLCでモニターすると・・

おお!それっぽく検出していますね。でも検出した物が何なのかを表示していないです。
コマンド画面上でも下記の様に何個の物体を見つけたかという数字は出ていますが、これが何なのかは表示していません。

ちょっといじくる。
という事で更にソースコード(sample_vi_od.c)を見てみます。
どうやらこのプログラムの中ではビデオデータを受け取るために二つのスレッドを生成し、一方はコマンド画面への結果表示、もう一方は検出した物体を囲む枠と文字情報を生成して映像に重ねた上でRTSPに流す(これをVLCで受けて表示している)仕組みになっている様です。
そして映像に枠を書く方の関数run_venc()の中を見ていくと、68行あたりに下の記述がありました。
sprintf(name, "%s: %.2f", stObjMeta.info[oid].name, stObjMeta.info[oid].bbox.score)
この行ではstObjMeta.info[oid].nameに物体名が入っている事を期待してそうですが表示されていません。
そこでstObjMeta.info[oid].classsというメンバー変数を表示させてみるとクラス番号が入っていました。YOLOのクラス番号は下表の様に80種の物体と番号が対応していて、クラス番号が15なら猫、16なら犬となります。

ならばこの番号を元に名前に変換してやれば良さそうです。
そこで提供された開発環境内のヘッダファイルにこの表に相当するコードがあるかと探しましたが見当たりません。
仕方ないので新たにyolov5class.hという名前のファイルを作ってこの中に記述し、これをソースにインクルードした上で先ほどの行を下の様に変更してみます。
sprintf(name, "%s: %.2f", yolov5class[stObjMeta.info[oid].classes], stObjMeta.info[oid].bbox.score);
すると・・・

いいんじゃないでしょうか。
そしてテキスト表示スレッド内のrun_tdl_thread()にも変更を加え、何を見つけたのを表示させました。

ねこカメラを作りたい。
という事でここからが本題です。我が家の庭はよく猫が通っていくので、猫を検出したら写真を残す「ねこカメラ」を作ってみたいと思います。
上記の通りYOLOv5は猫を見つけると15番が返ってきます。その時に画像を保存すれば出来上がり・・・と簡単に考えていたのですが画像をファイルに落とす方法が判らずハマりました。VIDEO_FRAME_Sという構造体に画像データが入っていそうなのですが取り出せないのです。
この状態だと進まないので、とりあえずは苦し紛れの策で対処したいと思います。
上記のテキスト表示スレッドで猫を示す15番を検出するとGPIO(G15)にパルスを一発出して、これを外部デジカメのレリーズ端子に入れて撮影するのです。
パルスを一発出した後の5秒間は出力無しにしています。またパルスと一緒にLEDも点灯して動作が分かる様にしました。
使ったカメラはちょっと古いCanonのデジタル一眼レフです。このカメラのレリーズ端子は2.5mmの3極ミニプラグで、シャッター、フォーカス、GNDの3端子があり、シャッター端子とGND端子を短絡するとシャッターが切られる仕組みです。
ではこんな回路で・・・

フォーカス回路も含めていますが自動でピントを合わせると時間がかかるので、マニュアルでピントを合わせておく事にします。 Duo256のGPIO端子の内、G15がシャッター、G14がフォーカスで、念のためにフォトカプラを経由にしました。
なお2.5mmのミニプラグが無かったので3.5mmへの変換ケーブルを急遽Amazonで購入。

そしてユニバーサル基板に搭載してハードは完成。

カメラは箱に入れ、内側をスプレー缶で黒に塗りました。Duo256本体は箱の裏側にテープで貼り付けています。


しかしこんなラズピコサイズのボード1枚で物体検出だの分類だのが出来てしまうというの、オドロキですね。。。
動作実験
2つのカメラを庭に向けて設置し、画角がほぼ一致する様に調整します。
そしてデジタル一眼レフはマニュアルでフォーカスを合わせておきました。

なおテスト用としてカップにも反応する様にしておいたので試してみます。
ちゃんとデジカメのシャッターが下りて撮影されましたね。

あとは猫が通るまで放置します。。。
結果
夕方の終了直前、お隣との塀の上を2匹で歩いている姿が撮れていました。
お隣が写っているままだとマズいのでぼかしを入れています。

翌日はカメラを下に向けて隣家が写らない角度に。。。
今回も一枚撮れていましたがマニュアルフォーカスをミスってピンボケになりました。

という事で・・・
最低限の状態で動いたという感じです。
でもやっぱりカメラ2台体制ではなくDuo256だけで完結したいですよね。。。
もうちょっとドキュメントを読んでみます。
参考
今回のプログラムはこれ→/data/nekocam.tar.gz
再コンパイルする場合は以下をインストールしておく必要があります。
- duo-examples : https://milkv.io/docs/duo/application-development/wiringxを参考にインストール。
- cvitek-tdl-sdk-sg200xとhost-tools : https://milkv.io/docs/duo/application-development/tdl-sdk/tdl-sdk-introductionを参考にインストール。
これらのファイルが以下の配置にある前提でMakefileを作成しているので、異なる場合は修正が必要です。
~/milkv
├duo-examples
├cvitek-tdl-sdk-sg200x
├host-tools
└myproj
├Makefile
├yolov5class.h
└nekocam.c