お題

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

「指で示した方向にドローンを移動させる」というふうに、ジェスチャーでドローン操作ができるようにします。

  • 具体的にはこんなイメージです:
    • 👆 : 上昇 (up)
    • 👇 : 下降 (down)
    • 👉 : 右に移動 (right)
    • 👈 : 左に移動 (left)
    • 🤚 : 前進 (forward)
    • ✋ : 後退 (back)

ハンドジェスチャーを認識するための仕組みとしては、お手軽に使えるAIモデルの MediaPipe を利用します。

MediaPipe の使い方

pip コマンドでインストールします。

pip install mediapipe

利用方法については Python Solution API のページ が参考になります。

このページに載っているサンプルコードの中盤 # For webcam input: 以降が動画から手を認識する処理です。

これをそのまま動かすと以下のように Webカメラで捉えた画像から手指を認識して状態を可視化して表示してくれます。

手指の可視化

  • 画像の中の ● が手指の関節を表しています。関節の位置はプログラムから数値として取得できるため、この関節位置情報を使えば指の形を判定することができそうです。

MediaPipe での節位置検出

先述のサンプルコードの中から MediaPipe で関節位置を検出するために最低限必要な処理を抜き出すと以下のようになります。

import mediapipe as mp

mp_hands = mp.solutions.hands

with mp_hands.Hands(
    model_complexity=0,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5) as hands:

  while (...):
    image = ...

    # results に関節位置が格納される
    results = hands.process(image)

    # 手の関節位置が検出できた場合は
    if results.multi_hand_landmarks:
        # 片手分の関節位置を取得
        for hand_landmarks in results.multi_hand_landmarks:
            (...)
  • 実際にやるべきことは以下3つだけです:
    1. mp_hands.Hands() を呼び出して hands 変数に格納する。
    2. hands.process() の引数に画像を渡して画像解析させ、解析結果を戻り値として受け取る。
    3. hands.process() の戻り値のなかに各関節の位置情報(xyz座標)が格納されているのでそれを使って処理をする。

HandSensorクラスの作成

この仕組みを使って「手指の形をドローンに送信するコマンドに変換する」という機能を HandSensor クラスとして実装します。

HandSensorの追加

  • HandSensor クラスは以下の処理を行います:
    1. Info から画像を取り出す (画像は事前に DroneVideoReceiver がドローンから取得して info に格納してくれています)
    2. 取り出した画像を MediaPipe で解析して、関節位置情報を取り出す
    3. 関節位置情報をコマンドに変換する
    4. 変換したコマンドを Info に設定する (Info に設定しておけば DroneCommandRequester がドローンに送信してくれます)

これを素直に実装すると以下のようになります。

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

        with mp_hands.Hands(model_complexity=0) as hands:
            while info.is_active():
                # 1. info から画像を取り出す
                image = info.get_image()
                if image is None:
                    continue

                # 2. 取り出した画像を MediaPipe で解析して、関節位置情報を取り出す
                results = hands.process(image)

                # 3. 関節位置情報をコマンドに変換する
                command = self.__get_command(results.multi_hand_landmarks)

                # 4. 変換したコマンドを Info に設定する
                info.entry_command(command)

        logger.info("done")

関節位置情報をコマンドに変換する __get_command 関数は以下のように実装します。

    def __get_command(self, multi_hand_landmarks: list) -> str:
        # 手の関節位置が検出できた場合は
        if multi_hand_landmarks:

            # 片手ずつ関節位置を取り出し
            for hand_landmarks in multi_hand_landmarks:

                # 片手分の関節位置をコマンドに変換して
                command = self.__get_command_by_hand(hand_landmarks.landmark)

                # コマンド変換に成功したらそのコマンドを返す
                if command != "":
                    return command

        # コマンド変換できなかった場合は空文字列を返す
        return ""

multi_hand_landmarks は「片手分の関節位置情報」を要素とするリストとなっているため、リストから片手分の関節位置情報をひとつずつ取り出して __get_command_by_hand 関数で処理します。

__get_command_by_hand 関数は以下のように実装します。

    def __get_command_by_hand(self, hlm: Sequence[Landmark]) -> str:
        # 指の形が👆ならば、コマンド "up 20" を返す
        if self.__is_up(hlm):
            return "up 20"

        return ""

__get_command_by_hand 関数の引数 hlm は以下の Landmark クラスのリストです。 これを用いて各関節の位置情報を表現します。 例えば、人差し指の指先位置は hlm[8].x (横位置), hlm[8].y (縦位置), hlm[8].z (前後位置) で取得できます。

class Landmark:
    x: float
    y: float
    z: float

各関節の番号 (配列のインデックス) は下図の通りです。

各関節の番号

この情報を使って「指の形が👆かどうか」を判断する __is_up 関数を実装します。

    def __is_up(self, hlm: Sequence[Landmark]) -> bool:
        # 人差し指が立っていない場合は false を返す
        if not (hlm[8].y < hlm[7].y < hlm[6].y < hlm[5].y < hlm[0].y):
            return False

        # 中指が立っている場合は false を返す
        if hlm[12].y < hlm[11].y < hlm[10].y < hlm[0].y:
            return False

        # 薬指が立っている場合は false を返す
        if hlm[16].y < hlm[15].y < hlm[14].y < hlm[0].y:
            return False

        # 小指が立っている場合は false を返す
        if hlm[20].y < hlm[19].y < hlm[18].y < hlm[0].y:
            return False

        # 条件を満たした (人差し指だけが立っている) ので true を返す
        return True
  • 実装ポイント:
    • シンプルに「それぞれの関節がより高い位置にあるかどうか」だけで判定。
    • 関節の高さだけがわかればいいので x, z 軸は無視して y 軸の情報のみを使用。
    • 親指に関しては無視。
    • y 軸の値は「より小さいもの=より高い位置」となる。直感とは逆なのでハマりがち。

ひとまずこれで HandSensor クラスができました。 まだ、「人差し指が立っていることを認識して up コマンドを発行する」だけしかできませんが動作確認は可能です。

コード全体はこちらになります。

main.py の修正

作成した HandSensor クラスを起動時に読み込ませるために、 main.py に以下を追加します (差分)。

startables.append(HandSensor())

動作確認

ドローンなしで試せるスタブ版でさくっと動作確認してみます。

起動例:

python main.py -s
動作確認 (人差し指)

期待通り、人差し指だけが立っている状態になった時に「up 20」と表示されました。

次回

次回は、

  • 👇 : 下降 (down)
  • 👉 : 右に移動 (right)
  • 👈 : 左に移動 (left)

を実装します。

Tello 関連記事

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