お題

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

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

前回までに、

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

の指示が出せるようになっています。

今回は、

  • 🤚 : 前進 (forward)
  • ✋ : 後退 (back)

に対応して、ハンドジェスチャーによるドローン操作を完成させたいと思います。

  • 今回実装するハンドジェスチャーの仕様:
    • 前進も後退も4本以上の指が立っている
    • ドローンのカメラに手の甲が見えていれば前進
    • ドローンのカメラに手のひらが見えていれば後退

相手に対して「こっちこっち」と手を振るときには手の甲を見せ、「ちょっとまって」と相手を止めるときには手のひらを見せるという日常的なジェスチャーを模しています。

とっても大事な画像 (再々掲)

前回・前々回も提示しましたが、これがないと実装できないので今回も提示しておきます。

各関節の番号

この情報を使って指の形を判断する処理を実装します。

実装(1) おおまかな処理分岐

まず実装コードをシンプルにするため、前回までに実装した「上下左右に関する処理」と今回追加する「前後移動に関する処理」とは別関数にしておきたいと思います。 そのため、__get_command_by_hand を以下のように変更して、これらの関数を呼び出す分岐処理のみを記述します。

def __get_command_by_hand(self, hlm: Sequence[Landmark], left_hand: bool) -> str:
    # 4本の指が伸びていれば
    if self.__is_four_up(hlm):
        # 前進もしくは後退コマンドを取得する
        return self.__get_forward_or_back(hlm, left_hand)

    # そうでなければ上下左右を取得する
    return self.__get_direction(hlm)

これに伴ってこれまで __get_command_by_hand で行っていた処理は __get_direction に移動します。

実装(2) 4本の指が伸びていることを検出する

手のひらにせよ手の甲にせよ、「4本の指が全て伸びていること」が条件になるので、これを検出する関数を作ります。

def __is_four_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 not (hlm[12].y < hlm[11].y < hlm[10].y < hlm[0].y):
        return False

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

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

    # 条件を満たしたので True を返す
    return True

こちらは前回までの実装をすこし変えるだけなので簡単ですね。

実装(3) 手のひらと手の甲の判別

すこし工夫が必要なのは「手のひらと手の甲の判別」です。

  • 手のひらと手の甲の判別は以下の2条件の組み合わせでできそうです:

    • 人差し指と小指のどちらが右にあるか
    • 検出した手のひらが右手なのか左手なのか
  • 具体的には以下の4パターンです:

    • 人差し指が小指よりも右にあり、左手の場合は手の甲
    • 人差し指が小指よりも右にあり、右手の場合は手のひら
    • 人差し指が小指よりも左にあり、左手の場合は手のひら
    • 人差し指が小指よりも左にあり、右手の場合は手の甲

これをコードとして実装してみます。

def __get_forward_or_back(self, hlm: Sequence[Landmark], left_hand: bool) -> str:
    # 人差し指の指先と小指の指先の距離 (左右比較なのでx軸のみ) を求める
    #   distanceがマイナス値 -> 人差し指が小指よりも右にある
    #   distanceがプラス値   -> 人差し指が小指よりも左にある
    distance = hlm[8].x - hlm[20].x

    # 距離が短すぎる場合は、「手のひらでもなく手の甲でもない」と判断する
    if distance < 0.05 and distance > -0.05:
        return ""

    if distance < 0:
        # マイナス値で左手ならば手の甲(forward)、マイナス値で右手ならば手のひら(back)
        return "forward 20" if left_hand else "back 20"
    else:
        # プラス値で左手ならば手のひら(back)、マイナス値で右手ならば手の甲(forward)
        return "back 20" if left_hand else "forward 20"

手のひらや手の甲がはっきりと見えていないケース、例えば「手刀」や「手を合わせた時の形」は検出対象から外したいので、距離が短すぎる場合は除外しています。

実装(4) 右手と左手の識別

検出した手が右手なのか左手なのかを識別するには mediapipe の処理結果に含まれる multi_handedness を利用します。

multi_headedness は以下のような形で情報を保持しています。

# 左手の場合
handedness classification {
  index: 0
  score: 0.9594031572341919
  label: "Left"
}

# 右手の場合
handedness classification {
  index: 1
  score: 0.9452934861183167
  label: "Right"
}

index の数値 (0=左, 1=右)や label に設定された文字列 (Left / Right) を用いればどちらの手かを判定できます。

今回は文字列による判定を用い、既存のコードに対して以下のように組み込んでみました。

# mediapipe で画像を処理し、結果を result で受け取る 
results = hands.process(image)

# results.multi_handedness を __get_command の引数として渡す
command = self.__get_command(
    results.multi_hand_landmarks, results.multi_handedness
)

def __get_command(self, multi_hand_landmarks: list, multi_handedness: list) -> str:
    if multi_hand_landmarks:
        for i, hand_landmarks in enumerate(multi_hand_landmarks, 0):

            # 取得した手指の関節情報が左手かどうかを取得する
            left_hand = multi_handedness[i].classification[0].label == "Left"

コード全体は以下になります。

動作確認

動作確認してみます。

起動例:

python main.py -s
動作確認 (前進後退)

実機を使って試すとこんな感じになります。

動作確認 (前進後退)

Tello 関連記事