お題

トイドローン Tello のプログラミング記事です。

今回からは「ハンドジェスチャーでドローンを操作する」をテーマにして何回かに分けて実装を進めます。これまでのような「画面からのコマンド入力」ではなく、『指で示した方向に移動させる』というようなハンドジェスチャーでドローン操作ができるようにしたいと思います。

初回は事前準備として「ドローンのバッテリー残量を気にせずに、心行くまでハンドジェスチャーの試行錯誤ができる仕組みを作る」です。

「ハンドジェスチャーでドローンを操作する」を実現するためには、ハンドジェスチャーを画像認識してドローンへのコマンドに変換する必要がありますが、この「画像認識」にはかなりの試行錯誤が必要になるはずです。試行錯誤のたびに毎回ドローンを飛ばすとなると、バッテリーの替えを用意していても、なかなかつらい状況になるだろうと予想できます。このとき「ドローンから受信した動画の代わりに PC のWebカメラから受信した動画を使う」ということができれば、バッテリー消費はゼロになるので、心行くまでハンドジェスチャーの試行錯誤ができます。

ということで今回はこの仕組みを実装してみたいと思います。

実装方針

現在のモジュール構成はこのようになっています。

現在の構成

やりたいことは「ドローンのカメラの代わりにPCのWebカメラが使えるようになる」なので、 DroneVideoReceiver は変更が必要そうです。また、 DroneStateReceiverDroneCommandRequester に関してもドローンと直接通信を行っているため、修正の必要がありそうです。

修正方針をまとめると以下になります。

  • DroneVideoReceiver: PCのカメラから画像を受信する機能に変更する
  • DroneStateReceiver: ドローンの状態ではなくダミーの状態を設定する機能に変更する
  • DroneCommandRequester: 何もしない実装に変更する

クラス名も役割に合わせて Drone~: から Stub~ に変更します。

スタブ版の構成

  • DroneVideoReceiver → StubVideoReceiver
  • DroneStateReceiver → StubStateReceiver
  • DroneCommandRequester → StubCommandRequester

スタブの実装

(1) StubVideoReceiver

StubVideoReceiver は以下のように実装しました (全体)。

class StubVideoReceiver(Startable):
    # WebカメラのID
    device_id: int

    def __init__(self, device_id: int = 0) -> None:
        super().__init__()
        self.device_id = device_id

    def start(self, info: Info) -> None:
        logger = get_logger(__name__)
        logger.info("start")

        # 指定したIDのカメラ画像を取得する
        cap = cv2.VideoCapture(self.device_id)

        while info.is_active():
            success, image = cap.read()
            if not success:
                continue
            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
            info.set_image(image)

        cap.release()
        logger.info("done")
  • cv2.VideoCapture の呼び出し以外は DroneVideoReceiver と同じコード。
  • cv2.VideoCapture の引数として指定している Web カメラのIDは実行環境ごとに異なるため、コンストラクタで指定可能にしています。

(2) StubStateReceiver

StubStateReceiver は以下になります (全体)。

class StubStateReceiver(Startable):
    def start(self, info: Info) -> None:
        logger = get_logger(__name__)
        logger.info("start")

        while info.is_active():
            # 5秒毎にダミーの状態(バッテリー:38%)を設定する
            time.sleep(5)
            info.set_states({"bat": 38.0})

        logger.info("done")
  • 5秒毎にダミーの状態(バッテリー:38%)を設定するだけの実装です。

(3) StubCommandRequester

StubCommandRequester は以下になります (全体)。

class StubCommandRequester(Startable):
    def start(self, info: Info) -> None:
        logger = get_logger(__name__)
        logger.info("start")

        while info.is_active():
            time.sleep(0.1)
            msg = info.pick_command()
            if msg == "":
                continue

            # (どこにも送信してないけど) 送信済みコマンドとして msg を設定
            info.set_sent_command(msg)

            # 1秒待つ
            time.sleep(1)

            # コマンド送信結果として「OK」を設定
            info.set_command_result("OK")

        logger.info("done")
  • 「コマンドを送信して、1秒後に「OK」が受信できたふり」をするだけの実装です。

(4) main.py の修正

main.py を修正して、「本物の実装 (Drone~) を呼び出すのか、スタブ実装 (Stub~) を呼び出すのか」をコマンドライン引数指定で切り替えられるようにします (差分)。

# argparse を使用してコマンドライン引数を取得する
parser = argparse.ArgumentParser()
parser.add_argument(
    "-s", "--stub", help="use stubs instead of a drone", action="store_true"
)
parser.add_argument(
    "-w", "--webcam", help="webcam device id (for stub)", type=int, default=0
)
args = parser.parse_args()

if args.stub:
    # スタブ指定の場合
    startables = [
        StubCommandRequester(),
        StubStateReceiver(),
        StubVideoReceiver(args.webcam),
    ]
else:
    # 本物の実装を使用する場合
    startables = [DroneCommandRequester(), DroneStateReceiver(), DroneVideoReceiver()]

argparse を用いて以下のコマンドライン引数を利用可能にしています。

  • -s: スタブ実行するかどうか
  • -w id: WebカメラのデバイスID (デフォルトは0)

実行例

  • 通常実行モード:
    python main.py
  • スタブ実行モード:
    python main.py -s -w 1
    • デバイスIDに 1 を指定した場合の例

次回

次回からはいよいよ本題のハンドジェスチャーによる操作に入ります。

Tello 関連記事

かわかみしんいち。島根県津和野町在住のフリーランスエンジニア。複合現実(Mixed Reality)と3DUXでおもちゃを作るのが趣味。 https://github.com/ototadana