MobileNetV2-SSDモデルを使って「人」を物体検知して、その人が居る場所に向かって走って近づくロボット子犬を作成。ROS(ロボット用OS)を使用することで、非常に簡単に実現できる。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載は、AI/機械学習/データサイエンス、場合によってはもっと広げてPythonなどの幅広い技術を活用して、業務データの利活用や日常作業の効率化、身の回りの趣味や遊びの高度化などを試していく連載です。筆者らが試したことを、読者の皆さんが追体験/疑似体験して楽しめることを目標としています。それが読者の皆さんにとって「現実問題でのAI/データサイエンス活用」を考えるヒントや練習にもなればよいなと考えています。
前回に続き、今回も業務というよりも趣味や遊び、研究的な内容になります。前回の記事「自作できる小型ロボット『犬』に物体追跡AIを搭載してみよう」では、MangDang Technology(マンダン・テクノロジー)社の「Mini Pupper」(ミニ・パッパー、日本名「ミニぷぱ」)という小型ロボット犬を制作し、
という両方のOSで動かしました。その後で、ROS版にオプションで搭載可能なAIカメラ「OAK-D-LITE(OpenCV AI Kit - Depth - Lite)」をミニぷぱ本体の上に乗せました。そのAIカメラ+DepthAI APIを使うことで、機械学習済みモデル(MobileNet-SSD)により検知した物体(前回の例では「ボトル」)に対して、ロボット犬が体ごと視線を向ける動作をさせてみました(図1)。
ここまでの挙動はほぼチュートリアル通りの動作でした。今回はその応用例として、独自の処理を実装してみたいと思います。
とはいえ、チュートリアル通りに動かすまでが大変でした……。今回の内容も苦労するかなと思いましたが、拍子抜けするくらいに簡単に実現できてしまいました。ROSは理解すれば簡単で便利そうです。
今回の目標、お題は、前回も掲げましたが、
「妻の帰宅時に出迎えをするペット犬を作りたい」
というものです。要するに、人(できれば妻)を見たら駆け寄ってくるワンコちゃんです。これを行うには、
という2つのステップが必要になると思います。この方針で実装方法を考えていこうと思います。
具体的に「どんな挙動になるか」のイメージ図を掲載しておきます(図2〜4)。これらの図が実際に本稿のプログラムが完成した状態です。
わざと右や左の後ろに下がりながら、撮影しました。それに合わせて向きを変えて近づいてくるのが分かるでしょうか。接触するぐらいまで近づくと止まります。
これが出来た時、すごく感動しました! ロボット子犬の飼い主として本当にうれしい。だけど、人物までは検知できていないので、誰にでも近寄ってくるバカ犬なんですよね……(苦笑)。
これはカワイイですね! バカ犬なんかじゃないですよ!
それでは、(1)物体検知から実装していきます。
前回は物体「ボトル」を検知しました。その方法は、DepthAI APIを使ったMobileNetV2-SSDモデル(入力はRGBカラーフレーム)による物体検知でした。
MobileNetV2とはモバイル(持ち歩き端末)向けに軽量化した画像認識用の事前学習済みモデルのことで、SSD(Single Shot MultiBox Detector:シングル・ショット・マルチボックス検出器)とは物体の境界枠(bounding box)とそのカテゴリーを検出するための手法です。つまりMobileNetV2-SSDとは、両方の技術を組み合わせることで実現した「物体検知ができるMobileNetモデル」を指します。
このモデルでは、20カテゴリーの物体が検出できます。具体的な項目は、前回の記事のリスト7や、公式サンプルコード(リスト1)で確認できます。
# MobileNetV2-SSDモデルのラベルテキスト
labelMap = ["background", "aeroplane", "bicycle", "bird", "boat", "bottle", "bus",
"car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike",
"person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]
前回は6番目(=0スタートなのでインデックスは5)にある「bottle」(ボトル)を使用しました。よく見ると16番目(=インデックスは15)に「person」(人)があります。これを使えば人の位置を認識できそうです。今回は少し手抜きですが、これを使うことにしました。
検知できる物体のカテゴリーラベルのインデックスですが、ここではPythonのIntEnumを使って定義してみました(リスト2)。今回は、/home/ubuntu/catkin_ws/src/minipupper_ros/mini_pupper_detect/scripts/ディレクトリー内にwife_detect.pyファイルを新規作成し、そのファイル内に全ての処理コードを記載していきます。
#!/usr/bin/env python3
# TODO: リスト3のコードをここに追記予定
from enum import IntEnum # オブジェクト番号の定義で使用
class MobilenetObject(IntEnum):
BACKGROUND = 0
AEROPLANE = 1
BICYCLE = 2
BIRD = 3
BOAT = 4
BOTTLE = 5
BUS = 6
CAR = 7
CAT = 8
CHAIR = 9
COW = 10
DININGTABLE = 11
DOG = 12
HORSE = 13
MOTORBIKE = 14
PERSON = 15
POTTEDPLANT = 16
SHEEP = 17
SOFA = 18
TRAIN = 19
TVMONITOR = 20
OBJ_CLASS = MobilenetObject.PERSON # 今回は「人」を検知する
# TODO: リスト4のコードをここに追記予定
MobilenetObject.PERSON(=15)という数値を持つ定数OBJ_CLASS(検知する物体のクラス分類番号)を宣言しています。このOBJ_CLASSの使用は後ほど(後掲のリスト5で)説明しますが、この値を切り替えることで、検知する物体を変えられる仕様にしています。
ここでROSに関連するモジュールをインポートしておきましょう。リスト3のコードを、前掲のリスト2の中に書き加えます。
import rospy # ROSを使うためのライブラリー全体のモジュール
from vision_msgs.msg import Detection2DArray # (1)用: 物体検知の情報
from geometry_msgs.msg import Twist # (2)用: ロボット走行速度の情報
前回も説明しましたが、ROSにはROS 1とROS 2があり、DepthAIのROSエコシステム「depthai-ros」ではその両方に対応していますが、前回と今回で使用しているのはROS 1です。ROSでは、実行プログラムをノード(Node)という単位で扱い、ノード間のメッセージのやり取りは同名のトピック(Topic)に対応するパブリッシャー(Publisher、発行者)とサブスクライバー(Subscriber、講読者)を通じて行われます。
今回の処理を行う実行プログラム(wife_detect.pyファイル)もROSのノード(名前は例えば「wife_detect」)として初期化する必要があります。その上で、wife_detectノード内で使用するパブリッシャーとサブスクライバーも作成します。具体的にはリスト4のコードを、前掲のリスト2の中に書き加えます。
# TODO: リスト5のコードをここに追記(callback関数は修正)予定
def callback(data):
pass
pub_vel = None
if __name__ == '__main__':
# ROSの「wife_detect」ノードを作成して初期化
rospy.init_node('wife_detect', anonymous=True)
print('Ready: wife_detect Node')
# 「cmd_vel」トピック(型:Twist、キューサイズ:10)に対応するパブリッシャーを作成
pub_vel = rospy.Publisher('/cmd_vel', Twist, queue_size=10)
print('Ready: cmd_vel Publisher')
# 「mobilenet_detections」トピック(型:Detection2DArray、呼び出し関数:callback)に対応するサブスクライバーを作成
rospy.Subscriber('/mobilenet_publisher/color/mobilenet_detections', Detection2DArray, callback)
print('Ready: mobilenet_detections Subscriber')
# 無限ループで、[Ctrl]+[C]キーによる終了を待つ
rospy.spin()
リスト4では、(2)ロボット犬を走らせるためのコマンドを送信(publish)するために「/cmd_vel」というトピックに対応するパブリッシャー(変数pub_vel)を定義しています。変数pub_velを使用するコードは後述します(後掲のリスト6)。
また、(1)物体検知した情報を受信(subscribe)するために、「/mobilenet_publisher/color/mobilenet_detections」というトピックに対応するサブスクライバー(コールバック関数callback)を定義しておきます。callback()関数を定義するコードは後述します(後掲のリスト5)。
以上で今回の基本的なROSの構造は構築できました。次に物体検知の情報を取得してみましょう。
リスト4のコードにより、物体が検知されるとcallback()関数が自動的に呼び出されます。引数dataがその物体検知情報で、その値の型はvision_msgs/Detection2DArray(リスト3やリスト4にも記載)です。Detection2DArray型オブジェクトの中には、vision_msgs/Detection2DのPythonリスト型の値を格納するdetections変数があります。
1つのフレーム画像から複数の物体が検知されることがあるので、リスト型になっています。このリストの中から目的の物体(=リスト2のOBJ_CLASS)と一致するものあるかを条件判定すればよさそうですね。リスト5がそれを実現したコードです。
SCORE_CRITERIA = 0.25 # 確率スコア:25%以上
# TODO: リスト7のコードをここに追記予定
def callback(data):
rate = rospy.Rate(200) # 200hz(1秒間に200回プログラムを実行する)
no_action = True
for obj in data.detections: # 物体検知されたオブジェクト
bb = obj.bbox # 検知物体を囲む境界枠
res = obj.results # 検知結果
res0 = res[0] # リストの0番目に入っているものだけを使う
score = res0.score # 確率スコア(信頼性スコア)
if res0.id == OBJ_CLASS and score >= SCORE_CRITERIA:
# TODO: リスト8のコードをここに追記予定
no_action = False
break
if no_action:
rate.sleep() # 上記指定hzのRateを実現するための間隔でスリープ
return # 物体が何も検知されない場合は何もしない
# TODO: リスト6のコードをここに追記予定
rate.sleep() # 上記指定hzのRateを実現するための間隔でスリープする
rate = rospy.Rate()というコードは、1秒間に何回(単位:Hz、ヘルツ)、このコールバック関数を呼び出すかを指定するためのものです。rate.sleep()関数と組み合わせて使用することで機能します。
for obj in data.detections:というコードで、検知した全ての物体の情報を確認していますね。objオブジェクトは前述した通りDetection2D型で、その中のbb変数から境界枠(bounding box)の情報を(後述のコード、具体的には後掲のリスト8で使用予定)、res変数から検知結果を取得できます。resの値はPythonリスト型になっているため、先頭の要素(res0 = res[0])だけを使っています。
res0変数の中のscore変数に確率スコア(信頼性スコア)の数値(0.0〜1.0、つまり0%〜100%)が格納されています。この値が任意に決めた基準値(=定数SCORE_CRITERIAの値)を上回っているかどうかもチェックしています。つまり、ある一定基準の精度(確率スコア)を満たしている場合にだけ「走る」などのアクションを起こすようにしています。
以上で(1)物体検知のコード実装部分は完了しました。あとは、(2)ロボット走行のコードを「アクションを起こす」部分に書けば完成ですね。
どうやったら、ミニぷぱを前進させたり左右に向けたりできるでしょうか。
ROS版でロボットを移動させるときには、先ほど作成した「cmd_vel」トピックに対応するパブリッシャーに速度情報を送信するだけです。具体的には変数pub_velのpublish()メソッドを呼び出すだけです(参考:ROS公式チュートリアル「cmd_vel: Moving the base through velocity commands(速度コマンドを通してロボットを移動させる方法)」)。リスト6はそのサンプルコードです。
#from geometry_msgs.msg import Twist # ロボット走行速度の情報(リスト3)
velocity = Twist() # 速度情報
moving_forward = 1.0 # 1.0(前に進む)〜-1.0(後ろに進む)
turning_left = 0.0 # 1.0(左を向く)〜-1.0(右を向く)
# 移動速度(=並進速度/線形速度)
velocity.linear.x = moving_forward # x軸で、前が正方向
velocity.linear.y = 0.0 # y軸で、左が正方向
velocity.linear.z = 0.0 # z軸で、上が正方向
# 回転速度(=角速度)で、反時計回りが正方向
velocity.angular.x = 0.0 # ロール(roll)軸
velocity.angular.y = 0.0 # ピッチ(pitch)軸
velocity.angular.z = turning_left # ヨー(yaw)軸
pub_vel.publish(velocity) # 速度コマンドを発行してロボットを移動させる
最初はROSの知識が無くて、ミニぷぱのコードを参考に動かす方法を模索しました。具体的には前回のリスト3で見たroslaunch champ_teleop teleop.launchというキーボード操作コマンドのコードを調べて、champ_teleop/champ_teleop.pyファイルにたどり着き、それを参考に実装コードを書いた……のだけど、結局は上記のROS公式チュートリアルと同じことをしているだけでした。リスト6のコードは、ミニぷぱ独自のロボット移動方法というわけではなく、ROS共通のロボット移動方法みたいです。
リスト6を見ると分かるように、今回の目標では2つの項目に値を入力するだけでした(※ちなみにx/y/z軸については図5を参考にしてください)。
Copyright© Digital Advantage Corp. All Rights Reserved.