🖐
暗所でジェスチャーを認識する
💡やること
赤外線カメラとmediapipeを使って、暗所でのジェスチャー認識を作ります。
🏁デモ
つくるもの
- 赤外線カメラを使って、暗所で撮影をする
- ラズパイにて撮影した画像をmediapipe - handsに渡す
- 検出すると各関節の座標値が取得できる
- 座標値から、手の状態を判定する
- 手の状態の履歴から、ジェスチャーとして認識させる
- 例: パー ⇒ 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
上記を実施後、下記のコマンドを入力します。
$ 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がインストールされます。
下記の記事を参考にさせていただきました。
📝手順
- 赤外線カメラの調整
- 確認用アプリケーション
赤外線カメラの調整
本モジュールには、赤外線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()
- 下記の記事を参考にしました
- 各指が開いているかどうかを検出
- 時間軸方向の検出
- 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マウスやキーボードと連携したり、
ホームオートメーションに使ったりと用途はさまざまです。
ラズパイの活用方法を
としてまとめ中です。
参考サイト
Discussion