【レポート】Pythonで手の形状検出やってみた(OpenCV編)

Python

皆さん、こんにちは!
上越市を拠点にし、「FA設備・装置開発」と「画像処理」に強い会社、NSIです!
私達は豊富な経験と専門知識で、各種業界の自動化・システム化のお手伝いをしています。

9月も終わりに差し掛かり、気温も落ち着いてきました。
ようやく秋という感じがしますね。
季節の変わり目、体調を崩さないよう気を付けていきましょう!

以前、PythonでWebカメラの映像を取得する記事を掲載しました。
今回はその応用で、取得した映像から手の形状検出 をしてみようと思います。
興味がある方や、同じような処理を実装したい方の参考になれば嬉しいです。

前回の記事はこちら

形状検出とは?

形状検出とは、画像や映像から特定の形状を検出することを指します。
例えば、顔の表情や姿勢、特定の物体(コップやスマートフォン)などを検出することができます。
これにより、仮想現実のアプリでユーザーの動作とトラッキングさせたり、ユーザーの表情によってシステムの動作を変えたりといったことが可能となります。

手の形状を検出してみる

ライブラリは引き続き「Opencv」を使用していきます。それでは早速実装していきましょう!
(ここから先は前回の続きとなります。まだご覧になっていない方は、ぜひ前回の記事からご覧ください!)

検出手法について

今回は「グー(Rock)」「チョキ(Scissors)」「パー(Paper)」の3種類の形状を検出してみます。これらを判断するためには、画面内から手を検出し、それを元に指の本数をカウントする必要があります。そのため、処理の流れとしては以下のようになります。

  1. 画面内から手の色と一致する最も大きい領域を手として検出。
  2. 検出した領域の凹みから指の本数を調べる。
  3. 指の本数から手の形状を判断する。

検出処理の作成

今回は以下の画像を例に処理の流れを解説していきます。
とりあえず処理を実行したい!という方は「付録.ソースコード全文」をご覧ください

まず、手の肌色部分を抽出する関数を追加します。
numpyライブラリを呼び出します。(デフォルトで入っているため、インストールは不要です。)

import numpy as np

抽出したい色範囲を定義し、二値化で抽出します。

# 手の肌色部分を抽出
def get_skin_mask(frame):
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)            # 色空間を変換
    lower_skin = np.array([0, 20, 70], dtype=np.uint8)      # 色範囲(最小)
    upper_skin = np.array([20, 255, 255], dtype=np.uint8)   # 色範囲(最大)
    mask = cv2.inRange(hsv, lower_skin, upper_skin)         # 色範囲を元に二値化
    return mask
色範囲に該当する部分を白、その他を黒として抽出します。

続いて、抽出した領域から最も大きい領域の輪郭を取得する関数を追加します。
輪郭がある場合は最大を、ない場合は何も取得しません。

# 最も大きい領域の輪郭を取得
def find_largest_contour(mask):
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)    # 輪郭一覧を取得
    if contours:
        return max(contours, key=cv2.contourArea)   # 輪郭がある場合:最も面積が大きい輪郭を取得
    return None                                     # 輪郭がない場合:何も取得しない
今回は緑枠の部分を抽出しました。

さらに、指の本数から形状を判断する関数を追加します。
まずは凸包を計算します。

# 指の本数から形状を判断
def detect_fingers(contour):
    # 凸包を計算
    hull = cv2.convexHull(contour, returnPoints=False)
    if len(hull) < 3:
        return "Unknown"    # 凸包が3つ未満なら終了

凸包とは、複数の点を囲む最小の凸多角形のことで、イメージとしては、複数のピン全体を包む輪ゴムが張られているような状態です。
また、今回は凸包を計算した後に3個未満かどうか判定しています。2点でも凸包は計算できますが、手のひらとして判定するにはやや不十分であるため、最低3個以上のときに処理を行うようにします。

凸包のイメージ図

続いて、凸包から凹みを検出し、それを元に指の本数を判断します。
指の本数を元に、「グー(Rock)」「チョキ(Scissors)」「パー(Paper)」を判断します。この際、「パー(Paper)」は通常全ての指が開いた状態を指しますが、凹みを元に検出するため、4本以上で「パー(Paper)」であると判断するようにします。

    try:
        # 凹みを検出
        defects = cv2.convexityDefects(contour, hull)
        if defects is not None:
            finger_count = 0
            for i in range(defects.shape[0]):
                s, e, f, d = defects[i, 0]  # s=始点,e=終点,f=凹んでいる点,d=凹みの深さ
                start = tuple(contour[s][0])
                end = tuple(contour[e][0])
                far = tuple(contour[f][0])

                if d > 10000:
                    finger_count += 1       # 凹みが10000以上なら指としてカウント
                    cv2.circle(frame, far, 5, [0, 0, 255], -1)  # 凹みを描画    

            # 判断する基準として、指の数で手の形を分類
            if finger_count >= 4:
                return "Paper"
            elif finger_count == 2:
                return "Scissors"
            else:
                return "Rock"
        return "Unknown"
    except cv2.error as e:
        print(f"OpenCV Error: {e}")
        return "Unknown"

凹みを検出すると、defectsの中にいくつかの情報が保存されるため、凹みの深さを閾値として指の本数をカウントします。凹みの深さは凸包の外側から凹みまでの最短距離となります。

最後に、これらの関数を呼び出します。

# 撮影開始
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # 手のひらの検出
    skin_mask = get_skin_mask(frame)
    largest_contour = find_largest_contour(skin_mask)
    
    if largest_contour is not None:
        cv2.drawContours(frame, [largest_contour], -1, (0, 255, 0), 3)
        hand_shape = detect_fingers(largest_contour)
        cv2.putText(frame, f"Hand Shape: {hand_shape}", (5, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

取得したフレームに対して、手の形状を検出します。画面上には検出した領域と、手の形状を表示します。

閾値以上の点(赤い点)が4箇所なので検出結果は「パー(Paper)」となります。

ちなみに、他の形状を検出するとこんな感じ。

それでは実際に実行してみましょう!

処理の実行

バッチリ検出できました。
今回は正面から見た手の形状を検出してみましたが、例えば「色々な角度から検出する」や「複雑な形状を検出する」といったより高度な検出をするには、機械学習や深度センサーを用いると実現できそうです。

付録.ソースコード全文

前回の処理に、手の形状検出処理を追加したものが以下になります。
OpenCVをインストールし、以下のソースコードをコピペすれば実行できます。

import cv2      # 撮影するライブラリ
import datetime # 現在時刻を取得するライブラリ
import time     # 時間を計測するライブラリ
import numpy as np  # NumPyライブラリ

# 使用するカメラのデバイス番号を指定(通常は0)
cap = cv2.VideoCapture(0)

# 動画保存設定
dt = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")  # ファイル名
codec = cv2.VideoWriter_fourcc(*"mp4v")                 # コーデック
out = None                                              # 保存する動画
start_time = 0                                          # 録画開始時間
recording = False                                       # 録画フラグ

# 手の肌色部分を抽出
def get_skin_mask(frame):
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)            # 色空間を変換
    lower_skin = np.array([0, 20, 70], dtype=np.uint8)      # 色範囲(最小)
    upper_skin = np.array([20, 255, 255], dtype=np.uint8)   # 色範囲(最大)
    mask = cv2.inRange(hsv, lower_skin, upper_skin)         # 色範囲を元に二値化
    return mask

# 最も大きい領域の輪郭を取得
def find_largest_contour(mask):
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)    # 輪郭一覧を取得
    if contours:
        return max(contours, key=cv2.contourArea)   # 輪郭がある場合:最も面積が大きい輪郭を取得
    return None                                     # 輪郭がない場合:何も取得しない

# 指の本数から形状を判断
def detect_fingers(contour):
    # 凸包を計算
    hull = cv2.convexHull(contour, returnPoints=False)
    if len(hull) < 3:
        return "Unknown"    # 凸包が3つ未満なら終了

    try:
        # 凹みを検出
        defects = cv2.convexityDefects(contour, hull)
        if defects is not None:
            finger_count = 0
            for i in range(defects.shape[0]):
                s, e, f, d = defects[i, 0]  # s=始点,e=終点,f=凹んでいる点,d=凹みの深さ
                start = tuple(contour[s][0])
                end = tuple(contour[e][0])
                far = tuple(contour[f][0])

                if d > 10000:
                    finger_count += 1                           # 凹みが10000以上なら指としてカウント
                    cv2.circle(frame, far, 5, [0, 0, 255], -1)  # 凹みを描画     

            # 判断する基準として、指の数で手の形を分類
            if finger_count >= 4:
                return "Paper"
            elif finger_count == 2:
                return "Scissors"
            else:
                return "Rock"
        return "Unknown"
    except cv2.error as e:
        print(f"OpenCV Error: {e}")
        return "Unknown"

# 撮影開始
while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # 手の形状検出
    skin_mask = get_skin_mask(frame)
    largest_contour = find_largest_contour(skin_mask)
    
    if largest_contour is not None:
        cv2.drawContours(frame, [largest_contour], -1, (0, 255, 0), 3)
        hand_shape = detect_fingers(largest_contour)
        cv2.putText(frame, f"Hand Shape: {hand_shape}", (5, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
    
    # 操作説明
    cv2.putText(frame, f"[s]:Start REC", (5, 430), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
    cv2.putText(frame, f"[e]:End REC", (5, 450), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)
    cv2.putText(frame, f"[q]:Quit", (5, 470), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 2)

    # 録画が開始されたらフレームを動画として保存
    if recording:
        out.write(frame)
        elapsed_time = time.time() - start_time
        elapsed_str = time.strftime("%H:%M:%S", time.gmtime(elapsed_time))
        cv2.putText(frame, f"REC:{elapsed_str}", (5, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2)

    # フレームを表示
    cv2.imshow('Frame', frame)

    # キー取得
    key = cv2.waitKey(1) & 0xFF

    # 録画開始:キーが[s] かつ 録画していない
    if key == ord('s') and not recording:
        out = cv2.VideoWriter(f"{dt}.mp4", codec, 20.0, (640, 480))
        recording = True
        start_time = time.time()
        print("録画を開始しました。")

    # 録画終了:キーが[e] かつ 録画中
    elif key == ord('e') and recording:
        recording = False
        out.release()
        print("録画を終了しました。")

    # アプリ終了:キーが[q]
    elif key == ord('q'):
        print("アプリを終了しました。")
        break

# 後片付け
cap.release()
if out:
    out.release()
cv2.destroyAllWindows()

最後に

今回は、Pythonで手の形状検出 をしてみました。
OpenCVだけでここまでのことができるとは正直驚きです。
今回は手の形状を検出しましたが、ある製品の形状が規格内であるか といった検査などにも活用できそうですね。

ここまで読んでいただき、ありがとうございました。
ご質問・ご要望・ご相談などは、下記お問い合わせフォームからお気軽にご連絡ください。
http://www.net-nsi.co.jp/toiawase.html

べんぞうくん
べんぞうくん

この記事が面白いと思ったらGoodボタンを押してね~

タイトルとURLをコピーしました