🖐

暗所でジェスチャーを認識する

2022/05/16に公開

💡やること

赤外線カメラとmediapipeを使って、暗所でのジェスチャー認識を作ります。

🏁デモ

https://twitter.com/tw_kotatu/status/1525856250143322112

つくるもの

  1. 赤外線カメラを使って、暗所で撮影をする
  2. ラズパイにて撮影した画像をmediapipe - handsに渡す
    1. 検出すると各関節の座標値が取得できる
    2. 座標値から、手の状態を判定する
  3. 手の状態の履歴から、ジェスチャーとして認識させる
    1. 例: パー ⇒ 1本、パー ⇒ きつね

🔧パーツ一覧

no 部品名 個数 備考
1 ラズベリーパイ 1 今回は4Bで確認
2 赤外線カメラモジュール 1 Amazon

接続図

回路図はありません。カメラモジュールをラズパイに接続します。

💻環境

開発環境

64bit版 - Linux rpi 5.15.32

  • ラズベリーパイ
    • Linux raspberrypi 5.15.32-v8+ #1538 SMP PREEMPT Thu Mar 31 19:40:39 BST 2022 aarch64
  • Python
    • Python 3.9.2 (default, Feb 28 2021, 17:03:44)

ラズベリーパイの設定

カメラモジュールを使用できるように設定する必要があります。
カメラの有効化は、以下のコマンドから実施できます。

$ sudo raspi-config
  • Interface Optionsを選択
  • Legacy Cameraを選択
    • 2022.04以降 - 64bit版
      • Image from Gyazo
  • "はい"(or "Yes")を選択
  • これで有効化されます(1回行えばOKです)

上記を実施後、下記のコマンドを入力します。

$ sudo modprobe bcm2835-v4l2 
$ vcgencmd get_camera
supported=1 detected=1, libcamera interfaces=0

"supported=1 detected=1"と表示されていれば、認識OKです。

モジュールのインストール

apt

必要なモジュールをインストールします。

$ sudo apt install -y python3-dev protobuf-compiler python3-pip git make libssl-dev

pip

Pythonに関するモジュールをインストールします。

$ python3 -m venv env
$ source env/bin/activate
$ git clone https://github.com/PINTO0309/mediapipe-bin && cd mediapipe-bin
$ ./v0.8.4/download.sh
$ unzip v0.8.4.zip
$ cd v0.8.4/numpy120x/py39/debian11/
$ pip install *.whl

mediapipe, opencvがインストールされます。
下記の記事を参考にさせていただきました。

https://zenn.dev/karaage0703/articles/63fed2a261096d

📝手順

  • 赤外線カメラの調整
  • 確認用アプリケーション

赤外線カメラの調整

本モジュールには、赤外線LEDライトが付属されています。
ライトは、可変抵抗で調整できます。

使用する環境下で撮影し、調整します。
撮影用のコードは、以下となります。

cap_oneshot.py
import cv2
from datetime import datetime

# /dev/video0を指定
DEV_ID = 0

# パラメータ
WIDTH = 640
HEIGHT = 480

def main():
    # /dev/video0を指定
    cap = cv2.VideoCapture(DEV_ID)

    # 解像度の指定
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)

    # キャプチャの実施
    ret, frame = cap.read()
    if ret:
        # ファイル名に日付を指定
        date = datetime.now().strftime("%Y%m%d_%H%M%S")
        path = "./" + date + ".jpg"
        cv2.imwrite(path, frame)

    # 後片付け
    cap.release()
    cv2.destroyAllWindows()
    return


if __name__ == "__main__":
    main()

確認用アプリケーション

GUIは、↓となります。

以下を実装しました。

  • GUI実装
    • pysimpleguiで実装
    • 表示のために、numpy -> PIL -> TKImageへ変換
      • コード箇所 - cv2_to_tk()
  • mediapipe - hands動作
    • 準備 - mp_hands.Hands()
    • 検出の実施 - hands.process()
  • 指の状態の認識
    • 各指が開いているかどうかを検出
      • detectFingerPose()
      • 下記の記事を参考にしました

https://gist.github.com/TheJLifeX/74958cc59db477a91837244ff598ef4a

  • 時間軸方向の検出
    • 15検出の中から最も頻度の高いものを判定としてます
    • detectActionPose()

詳細は、コメントに記載していあります。

sample.py
import PySimpleGUI as sg
from PIL import Image, ImageTk
import cv2
import numpy as np
import mediapipe as mp
import math
import statistics

# from gpiozero import LED
# from gpiozero.pins.pigpio import PiGPIOFactory


HAND_MIN_DETECTION_CONFIDENCE = 0.7 # 検出信頼度
HAND_MIN_TRACKING_CONFIDENCE = 0.5 # 追跡信頼度

# /dev/video0を指定
DEV_ID = 0

# # LEDのピン設定
# PIN_LED1 = 16
# PIN_LED2 = 20
# PIN_LED3 = 21

# led_pins = {
#     "red": PIN_LED1,
#     "green": PIN_LED2,
#     "blue": PIN_LED3,
# }



def cv2_to_tk(cv2_image, size=None):
    """CV2Image->TKImage
    """
    # BGR -> RGB
    rgb_cv2_image = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)

    # NumPyArray -> PIL Image
    pil_image = Image.fromarray(rgb_cv2_image)

    # Scaling
    if size is not None:
        pil_image = pil_image.resize(size)

    # PIL Image -> Tkinter
    tk_image = ImageTk.PhotoImage(pil_image)

    return tk_image

def calcDistance(p0, p1):
    """ 2頂点の距離の計算
    """
    a1 = p1.x-p0.x
    a2 = p1.y-p0.y
    return math.sqrt(a1*a1 + a2*a2)

def calcAngle(p0, p1, p2):
    """ 3頂点の角度の計算
    """
    a1 = p1.x-p0.x
    a2 = p1.y-p0.y
    b1 = p2.x-p1.x
    b2 = p2.y-p1.y
    try:
        angle = math.acos( (a1*b1 + a2*b2) / math.sqrt((a1*a1 + a2*a2)*(b1*b1 + b2*b2)) ) * 180/math.pi
    except ZeroDivisionError:
        angle = 0
    return angle


def cancFingerAngle(p0, p1, p2, p3, p4):
    """ 指の角度の合計の計算
    """
    result = 0
    result += calcAngle(p0, p1, p2)
    result += calcAngle(p1, p2, p3)
    result += calcAngle(p2, p3, p4)
    return result


def detectFingerPose(landmarks):
    """ 指のオープン・クローズ
    """
    thumbIsOpen = cancFingerAngle(landmarks[0], landmarks[1], landmarks[2], landmarks[3], landmarks[4]) < 70
    firstFingerIsOpen = cancFingerAngle(landmarks[0], landmarks[5], landmarks[6], landmarks[7], landmarks[8]) < 100
    secondFingerIsOpen = cancFingerAngle(landmarks[0], landmarks[9], landmarks[10], landmarks[11], landmarks[12]) < 100
    thirdFingerIsOpen = cancFingerAngle(landmarks[0], landmarks[13], landmarks[14], landmarks[15], landmarks[16]) < 100
    fourthFingerIsOpen = cancFingerAngle(landmarks[0], landmarks[17], landmarks[18], landmarks[19], landmarks[20]) < 100

    # Pose検出
    if (calcDistance(landmarks[4], landmarks[8]) < 0.1 and secondFingerIsOpen and thirdFingerIsOpen and fourthFingerIsOpen):
        return "OK"
    elif (calcDistance(landmarks[4], landmarks[12]) < 0.1 and calcDistance(landmarks[4], landmarks[16]) < 0.1 and firstFingerIsOpen and fourthFingerIsOpen):
        return "Fox"
    elif (thumbIsOpen and (not firstFingerIsOpen) and (not secondFingerIsOpen) and (not thirdFingerIsOpen) and (not fourthFingerIsOpen)):
        return "Good"
    elif (thumbIsOpen and firstFingerIsOpen and secondFingerIsOpen and thirdFingerIsOpen and fourthFingerIsOpen):
        return "5"
    elif ((not thumbIsOpen) and firstFingerIsOpen and secondFingerIsOpen and thirdFingerIsOpen and fourthFingerIsOpen):
        return "4"
    elif ((not thumbIsOpen) and firstFingerIsOpen and secondFingerIsOpen and thirdFingerIsOpen and (not fourthFingerIsOpen)):
        return "3"
    elif ((not thumbIsOpen) and firstFingerIsOpen and secondFingerIsOpen and (not thirdFingerIsOpen) and (not fourthFingerIsOpen)):
        return "2"
    elif ((not thumbIsOpen) and firstFingerIsOpen and (not secondFingerIsOpen) and (not thirdFingerIsOpen) and (not fourthFingerIsOpen)):
        return "1"
    return "0"


def detectActionPose(actions):
    """ "5" -> ?の検出
    """
    if len(actions) < 15:
        return False, None

    # 最頻度のPoseを判定
    ret = statistics.mode(actions)
    if ret == "OK":
        return True, "5toOK"
    if ret == "Fox":
        return True, "5toFox"
    if ret == "Good":
        return True, "5toGood"
    if ret == "5":
        return True, "5to5"
    if ret == "4":
        return True, "5to4"
    if ret == "3":
        return True, "5to3"
    if ret == "2":
        return True, "5to2"
    if ret == "1":
        return True, "5to1"
    if ret == "0":
        return True, "5to0"

    return False, None



def main():

    # # 通知用 - LED準備
    # leds = {}
    # for key, pin in led_pins.items():
    #     leds[key] = LED(pin, pin_factory=PiGPIOFactory())

    is_wait_action = False
    action_hist = []
    last_apose = ""

    # /dev/video0をオープン, バッファを1
    cap = cv2.VideoCapture(DEV_ID)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

    # model - hands
    mp_drawing = mp.solutions.drawing_utils
    mp_hands = mp.solutions.hands
    hands = mp_hands.Hands(
        min_detection_confidence=HAND_MIN_DETECTION_CONFIDENCE,
        min_tracking_confidence=HAND_MIN_TRACKING_CONFIDENCE,
        max_num_hands=1                 # 最大検出数
    )

    # GUI - 定義
    layout = [
        [sg.Image(filename="", key="image")],
        [sg.Checkbox("hand", key="-cb_hand-", default=True)],
        [sg.Checkbox("prev", key="-cb_prev-", default=True), sg.Combo(["x1", "x1.5", "x2.0"], key="-comb_prev-")],
    ]
    # GUI - ウィンドウを作成
    window = sg.Window("detect hand", layout, resizable=True)
    event, values = window.read(timeout=0.1)
    window["-comb_prev-"].update("x1")

    while True:
        event, values = window.read(timeout=0.1)

        # プログラムの終了処理
        if event in (None, '-exit-'):
            break

        # カメラ画のキャプチャ
        if values["-cb_hand-"] or values["-cb_prev-"]:
            ret, image = cap.read()
            if not ret:
                continue

        # 推論, ジェスチャー判定
        if values["-cb_hand-"]:
            image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB)
            image.flags.writeable = False   # 参照渡しのためにイメージを書き込み不可としてマーク
            results = hands.process(image)  # mediapipeの処理
            image.flags.writeable = True    # 画像に手のアノテーションを描画
            image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
            image_height, image_width, _ = image.shape

            if results.multi_hand_landmarks:
                # leds["green"].on()
                # 手の骨格描画
                for hand_landmarks in results.multi_hand_landmarks:
                    mp_drawing.draw_landmarks(
                        image, hand_landmarks, mp_hands.HAND_CONNECTIONS)

                # ポーズの検出 - 5,4,3,2,1,0,ok,fox
                fpose = detectFingerPose(hand_landmarks.landmark)

                # "5" -> ???へのポーズ判定
                if is_wait_action:
                    action_hist.append(fpose)

                if fpose == "5":
                    action_hist = []
                    is_wait_action = True
                
                ret, apose = detectActionPose(action_hist)
                if ret:
                    action_hist = []
                    is_wait_action = False
                    # leds["blue"].blink(on_time=0.2, off_time=0.2, n=3)
                    last_apose = apose

                # 検出結果の表示
                cv2.putText(image, f"{fpose} , a:{last_apose}", (20, 450),
                            cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 255, 0), 3)
            else:
                # leds["green"].off()
                pass

        # プレビューの表示
        if values["-cb_prev-"]:
            if "x1.5" in values["-comb_prev-"]:
                size = (int(image_width*1.5), int(image_height*1.5))
            elif "x2.0" in values["-comb_prev-"]:
                size = (int(image_width*2), int(image_height*2))
            else:
                size = None
            window["image"].update(data=cv2_to_tk(image, size))

    # プログラムの終了
    window.close()

if __name__ == "__main__":
    main()

実行手順

(env) $ python sample.py 

デモのような映像が表示されます。

🔎ポイント

赤外線カメラ

カメラに使用されているCCDは、赤外の波長をとらえることができます。
ただし、人の目には赤外が見えないため、通常は赤外線のフィルターが装着されています。
このフィルターがついていないものが赤外線カメラとなります。

さいごに

今回は検出に焦点を当てました。
これをトリガーに、以前作ったBleマウスやキーボードと連携したり、
ホームオートメーションに使ったりと用途はさまざまです。

ラズパイの活用方法を

https://zenn.dev/kotaproj/books/raspberrypi-tips

としてまとめ中です。

参考サイト

https://gist.github.com/TheJLifeX/74958cc59db477a91837244ff598ef4a

https://zenn.dev/karaage0703/articles/63fed2a261096d

https://zenn.dev/ninzin/articles/94b05fdb9edf53

GitHubで編集を提案

Discussion