お題

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

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

前回は、

  • 👆 : 上昇 (up)

をやりました。今回はダダっと

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

をやって上下左右移動を制覇します。

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

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

各関節の番号

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

👇 : 下降 (down)

まずは 👇 (down) の実装 (差分)。

    def __get_command_by_hand(self, hlm: Sequence[Landmark]) -> str:
        if self.__is_up(hlm):
            return "up 20"
        if self.__is_down(hlm):
            return "down 20"
        return ""

    def __is_down(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
        return True

👆 (up) の場合は「人差し指が上方向に延びているか」を判定していましたが、👇 (down) の場合は「人差し指が下方向に延びているか」を判定する必要があります。👆 (up) の実装では「上方向に延びているかどうか」の判定は関節位置の大小比較で実現していました。この大小比較を逆にすることで「下方向に延びているか」を判定しています。従って前回実装した __is_up 関数と今回の __is_down の違いは単純に < 記号がすべて > に変わっただけ、ということになります。

👈 👉  : 左右に移動 (left / right)

👈 👉 (left / right) はこんな感じで実装しました (差分)。

    def __get_command_by_hand(self, hlm: Sequence[Landmark]) -> str:
        if self.__is_up(hlm):
            return "up 20"
        if self.__is_down(hlm):
            return "down 20"
        if self.__is_left(hlm):
            return "left 20"
        if self.__is_right(hlm):
            return "right 20"
        return ""

    def __is_left(self, hlm: Sequence[Landmark]) -> bool:
        if not (hlm[8].x < hlm[7].x < hlm[6].x < hlm[5].x < hlm[0].x):
            return False
        if hlm[12].x < hlm[11].x < hlm[10].x < hlm[0].x:
            return False
        if hlm[16].x < hlm[15].x < hlm[14].x < hlm[0].x:
            return False
        if hlm[20].x < hlm[19].x < hlm[18].x < hlm[0].x:
            return False
        return True

    def __is_right(self, hlm: Sequence[Landmark]) -> bool:
        if not (hlm[8].x > hlm[7].x > hlm[6].x > hlm[5].x > hlm[0].x):
            return False
        if hlm[12].x > hlm[11].x > hlm[10].x > hlm[0].x:
            return False
        if hlm[16].x > hlm[15].x > hlm[14].x > hlm[0].x:
            return False
        if hlm[20].x > hlm[19].x > hlm[18].x > hlm[0].x:
            return False
        return True

上下検出用の実装とほぼ同じ作りですが、y 軸の代わりに x 軸を使うところだけが異なります。

動作改善

これを動かしてテストしてみると微妙に想定通りの動きにならず、もどかしい感じになります。 具体的には「左右として検出してほしい指が上下として検出されてしまう」です。

そういう動きになってしまう理由は以下の個所にあります。

    def __get_command_by_hand(self, hlm: Sequence[Landmark]) -> str:
        if self.__is_up(hlm):
            return "up 20"
        if self.__is_down(hlm):
            return "down 20"
        if self.__is_left(hlm):
            return "left 20"
        if self.__is_right(hlm):
            return "right 20"
        return ""

このコードでは、検出の順番が以下のようになっています。

  1. up
  2. down
  3. left
  4. right

この順番のせいで「左右を指しているように見える指でも、少しでも上下に向いていれば上下と判定されてしまう」という問題が起こります。それならば処理順番を入れ替えかえれば…ということで「1. left, 2. right, 3. up, 4. down」としても、今度は「上下として検出してほしい指が左右として検出されてしまう」という問題が発生するだけなので、問題解決には至りません。

これに対応するためには、「人差し指が示している方向 (角度)」を取得して「上下方向を示しているのか左右方向を示しているのか」を判断条件に加えるのがよさそうです。そうすれば検出の順番とは関係なく上下と左右が正しく判定できるはずです。

指が示している方向 (角度) は以下の関数で検出します。

    def __angle(self, la: Landmark, lb: Landmark, lc: Landmark) -> float:
        a = np.array([la.x, la.y])
        b = np.array([lb.x, lb.y])
        c = np.array([lc.z, lc.y])
        ba = a - b
        bc = c - b
        cosine_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
        angle = np.arccos(cosine_angle)
        return np.degrees(angle)

この関数に人差し指の関節の値を渡すと指が示している方向が下図のように 0 - 180° の範囲で検出できます。

検出できる角度の範囲

ここで取得した角度を __get_command_by_hand の判断条件に加えます (差分)。

    def __get_command_by_hand(self, hlm: Sequence[Landmark]) -> str:
        # 指が示している方向を 0 - 180° の角度として取得する
        a = self.__angle(hlm[5], hlm[6], hlm[8])

        # up / down は 60 - 120° の範囲
        if self.__is_up(hlm) and a > 60 and a < 120:
            return "up 20"
        if self.__is_down(hlm) and a > 60 and a < 120:
            return "down 20"

        # left は 150 - 180° の範囲
        if self.__is_left(hlm) and a > 150:
            return "left 20"

        # right は 0 - 30° の範囲
        if self.__is_right(hlm) and a < 30:
            return "right 20"
        return ""

上記の条件を図にすると以下のイメージになります。

角度による判定条件

動作確認

上下左右の実装ができたので、前回同様ドローンなしで試せるスタブ版で動作確認してみます。

起動例:

python main.py -s
動作確認 (上下左右)

次回

次回は前進と後退を実装します。全ての指を立てた状態でドローンに対して手の甲を向ける 🤚 の形で前進の指示、手のひらを向ける ✋ で後退の指示とします。

次回でハンドジェスチャーの実装が完成です!

Tello 関連記事

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