AIで人を画像認識して走ってくるロボット犬を作ってみようAI・データサイエンスで遊ぼう

MobileNetV2-SSDモデルを使って「人」を物体検知して、その人が居る場所に向かって走って近づくロボット子犬を作成。ROS(ロボット用OS)を使用することで、非常に簡単に実現できる。

» 2022年05月30日 05時00分 公開
[一色政彦デジタルアドバンテージ]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「AI・データサイエンスで遊ぼう」のインデックス

連載目次

 本連載は、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)。

図1 ROS版のOAK-D-LITEで検知した物体に視線を向けるロボット犬の例 図1 ROS版のOAK-D-LITEで検知した物体に視線を向けるロボット犬の例

 ここまでの挙動はほぼチュートリアル通りの動作でした。今回はその応用例として、独自の処理を実装してみたいと思います。


一色

 とはいえ、チュートリアル通りに動かすまでが大変でした……。今回の内容も苦労するかなと思いましたが、拍子抜けするくらいに簡単に実現できてしまいました。ROSは理解すれば簡単で便利そうです。


今回のお題

 今回の目標、お題は、前回も掲げましたが、


一色

 「妻の帰宅時に出迎えをするペット犬を作りたい」


というものです。要するに、人(できれば妻)を見たら駆け寄ってくるワンコちゃんです。これを行うには、

  • (1)まずは物体検知(できれば人物検知)を行い、その位置情報を把握する
  • (2)次に、その位置情報に合わせてロボット犬を走らせる

という2つのステップが必要になると思います。この方針で実装方法を考えていこうと思います。

 具体的に「どんな挙動になるか」のイメージ図を掲載しておきます(図2〜4)。これらの図が実際に本稿のプログラムが完成した状態です。

図2 撮影者(人)に向かってロボット犬が駆け寄ってくる様子(1) 図2 撮影者(人)に向かってロボット犬が駆け寄ってくる様子(1)

図3 撮影者(人)に向かってロボット犬が駆け寄ってくる様子(2) 図3 撮影者(人)に向かってロボット犬が駆け寄ってくる様子(2)

図4 撮影者(人)に向かってロボット犬が駆け寄ってくる様子(3) 図4 撮影者(人)に向かってロボット犬が駆け寄ってくる様子(3)

 わざと右や左の後ろに下がりながら、撮影しました。それに合わせて向きを変えて近づいてくるのが分かるでしょうか。接触するぐらいまで近づくと止まります。


一色

 これが出来た時、すごく感動しました! ロボット子犬の飼い主として本当にうれしい。だけど、人物までは検知できていないので、誰にでも近寄ってくるバカ犬なんですよね……(苦笑)。



かわさき

 これはカワイイですね! バカ犬なんかじゃないですよ!


 それでは、(1)物体検知から実装していきます。

やってみた(1): MobileNetV2-SSDによる物体検知

MobileNetV2-SSDモデルとは?

 前回は物体「ボトル」を検知しました。その方法は、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"]

リスト1 MobileNetV2-SSDモデルで検知可能な物体

 前回は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のコードをここに追記予定

リスト2 IntEnumを使ったラベルインデックスの定義(wife_detect.py)

 MobilenetObject.PERSON(=15)という数値を持つ定数OBJ_CLASS(検知する物体のクラス分類番号)を宣言しています。このOBJ_CLASSの使用は後ほど(後掲のリスト5で)説明しますが、この値を切り替えることで、検知する物体を変えられる仕様にしています。

ROSの仕組みの構築

 ここでROSに関連するモジュールをインポートしておきましょう。リスト3のコードを、前掲のリスト2の中に書き加えます。

import rospy  # ROSを使うためのライブラリー全体のモジュール
from vision_msgs.msg import Detection2DArray  # (1)用: 物体検知の情報
from geometry_msgs.msg import Twist  # (2)用: ロボット走行速度の情報

リスト3 ROSに関連するモジュールのインポート(wife_detect.py)

 前回も説明しましたが、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 ROSに関連するモジュールのインポート(wife_detect.py)

 リスト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を実現するための間隔でスリープする

リスト5 物体を検知して処理するコールバック関数(wife_detect.py)

 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.01.0、つまり0%100%)が格納されています。この値が任意に決めた基準値(=定数SCORE_CRITERIAの値)を上回っているかどうかもチェックしています。つまり、ある一定基準の精度(確率スコア)を満たしている場合にだけ「走る」などのアクションを起こすようにしています。

 以上で(1)物体検知のコード実装部分は完了しました。あとは、(2)ロボット走行のコードを「アクションを起こす」部分に書けば完成ですね。

やってみた(2): 位置情報に合わせたロボット犬の走行

走行:前方移動と左右方向転換

 どうやったら、ミニぷぱを前進させたり左右に向けたりできるでしょうか。

 ROS版でロボットを移動させるときには、先ほど作成した「cmd_vel」トピックに対応するパブリッシャーに速度情報を送信するだけです。具体的には変数pub_velpublish()メソッドを呼び出すだけです(参考: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)  # 速度コマンドを発行してロボットを移動させる

リスト6 ミニぷぱの前方移動と左右方向転換(wife_detect.py)


一色

 最初はROSの知識が無くて、ミニぷぱのコードを参考に動かす方法を模索しました。具体的には前回のリスト3で見たroslaunch champ_teleop teleop.launchというキーボード操作コマンドのコードを調べて、champ_teleop/champ_teleop.pyファイルにたどり着き、それを参考に実装コードを書いた……のだけど、結局は上記のROS公式チュートリアルと同じことをしているだけでした。リスト6のコードは、ミニぷぱ独自のロボット移動方法というわけではなく、ROS共通のロボット移動方法みたいです。


 リスト6を見ると分かるように、今回の目標では2つの項目に値を入力するだけでした(ちなみにx/y/z軸については図5を参考にしてください)。

図5 ミニぷぱのx/y/z軸(ROSの場合は右手座標系) 図5 ミニぷぱのx/y/z軸(ROSの場合は右手座標系)

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。