0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MediaPipeの新ソリューションでリアルタイムHandLandmark検出をする 2

Posted at

はじめに

前回はmediapipeのHand Landmarkerタスクを実行したが、可視化の処理をしていないため、座標の情報がひたすらprintされるというあまり面白味のないコードになった。
今回は前回の基本部分に可視化処理を加えて、実際に手が検出されているのを表示させる。
下記のコードを実行するには、ここからHandLandmarker(完全版)をクリックしhand_landmarker.taskファイルをダウンロードする。
15行目
base_options=BaseOptions(model_asset_path='hand_landmarker.task'),
部分がダウンロードしたモデルのファイルを指定している。ダウンロードしたファイルの位置へパス表記を変える。

前回まで
import mediapipe as mp
import cv2 as cv
import time

BaseOptions = mp.tasks.BaseOptions
HandLandmarker = mp.tasks.vision.HandLandmarker
HandLandmarkerOptions = mp.tasks.vision.HandLandmarkerOptions
HandLandmarkerResult = mp.tasks.vision.HandLandmarkerResult
VisionRunningMode = mp.tasks.vision.RunningMode

def print_result(result: HandLandmarkerResult, output_image: mp.Image, timestamp_ms: int):
    print('hand landmarker result: {}'.format(result))

options = HandLandmarkerOptions(
    base_options=BaseOptions(model_asset_path='hand_landmarker.task'),
    running_mode=VisionRunningMode.LIVE_STREAM,
    result_callback=print_result)
    
with HandLandmarker.create_from_options(options) as landmarker:
    cap = cv.VideoCapture(0)
    
    if not cap.isOpened():
        print("Cannot open camera")
        exit()
        
    while True:
        ret, frame = cap.read()
    
        if not ret:
            print("Can't receive frame (stream end?). Exiting ...")
            break
            
        gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
        
        mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=frame)
        frame_timestamp_ms = int(time.time() * 1000)
        landmarker.detect_async(mp_image, frame_timestamp_ms)

        cv.imshow('frame', gray)
        
        if cv.waitKey(1) == ord('q'):
            break
    
    cap.release()
    cv.destroyAllWindows() 

手を2つ同時に検出できるようにする

オプションにnum_handsを指定することで、手を複数同時に検出できる。デフォルトは1のため、現在手を2つ映しても片方しか検出してくれない。両手を一緒に検出するために、14行目HandLandmarkerOptions内にnum_hands=2を追加する。

作成中
options = HandLandmarkerOptions(
    base_options=BaseOptions(model_asset_path='hand_landmarker.task'),
    running_mode=VisionRunningMode.LIVE_STREAM,
+   num_hands=2,
    result_callback=print_result)

可視化の関数

検出された結果を編集しているだけなので、自分でも好きなように可視化の処理を行うことができるが、公式から用意された可視化の関数があるので、今回は活用する。

1. まずはコピペ

上記は公式が用意しているコードサンプル。Visualization utilities部分にコードを表示するがあるのでクリックすると可視化の関数が表示される。これを拝借しよう。

作成中
import mediapipe as mp
import cv2 as cv
import time
### 追加↓ ###
from mediapipe import solutions
from mediapipe.framework.formats import landmark_pb2
import numpy as np

MARGIN = 10  # pixels
FONT_SIZE = 1
FONT_THICKNESS = 1
HANDEDNESS_TEXT_COLOR = (88, 205, 54) # vibrant green

def draw_landmarks_on_image(rgb_image, detection_result):
    hand_landmarks_list = detection_result.hand_landmarks
    handedness_list = detection_result.handedness
    annotated_image = np.copy(rgb_image)

    # Loop through the detected hands to visualize.
    for idx in range(len(hand_landmarks_list)):
    hand_landmarks = hand_landmarks_list[idx]
    handedness = handedness_list[idx]

    # Draw the hand landmarks.
    hand_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
    hand_landmarks_proto.landmark.extend([
        landmark_pb2.NormalizedLandmark(x=landmark.x, y=landmark.y, z=landmark.z) for landmark in hand_landmarks
    ])
    solutions.drawing_utils.draw_landmarks(
        annotated_image,
        hand_landmarks_proto,
        solutions.hands.HAND_CONNECTIONS,
        solutions.drawing_styles.get_default_hand_landmarks_style(),
        solutions.drawing_styles.get_default_hand_connections_style())

    # Get the top left corner of the detected hand's bounding box.
    height, width, _ = annotated_image.shape
    x_coordinates = [landmark.x for landmark in hand_landmarks]
    y_coordinates = [landmark.y for landmark in hand_landmarks]
    text_x = int(min(x_coordinates) * width)
    text_y = int(min(y_coordinates) * height) - MARGIN

    # Draw handedness (left or right hand) on the image.
    cv.putText(annotated_image, f"{handedness[0].category_name}",
                (text_x, text_y), cv.FONT_HERSHEY_DUPLEX,
                FONT_SIZE, HANDEDNESS_TEXT_COLOR, FONT_THICKNESS, cv.LINE_AA)

    return annotated_image
### ここまで↑ ###
BaseOptions = mp.tasks.BaseOptions
HandLandmarker = mp.tasks.vision.HandLandmarker
HandLandmarkerOptions = mp.tasks.vision.HandLandmarkerOptions
HandLandmarkerResult = mp.tasks.vision.HandLandmarkerResult
VisionRunningMode = mp.tasks.vision.RunningMode

def print_result(result: HandLandmarkerResult, output_image: mp.Image, timestamp_ms: int):
    print('hand landmarker result: {}'.format(result))

options = HandLandmarkerOptions(
    base_options=BaseOptions(model_asset_path='hand_landmarker.task'),
    running_mode=VisionRunningMode.LIVE_STREAM,
    num_hands=2,
    result_callback=print_result)
    
with HandLandmarker.create_from_options(options) as landmarker:
    cap = cv.VideoCapture(0)
    
    if not cap.isOpened():
        print("Cannot open camera")
        exit()
        
    while True:
        ret, frame = cap.read()
    
        if not ret:
            print("Can't receive frame (stream end?). Exiting ...")
            break
            
        gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
        
        mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=frame)
        frame_timestamp_ms = int(time.time() * 1000)
        landmarker.detect_async(mp_image, frame_timestamp_ms)

        cv.imshow('frame', gray)
        
        if cv.waitKey(1) == ord('q'):
            break
    
    cap.release()
    cv.destroyAllWindows() 

作成中のものと合わせるためcv2をcvに修正している。

2. 単一画像入力のモードでのdraw_landmarks_on_image関数の呼び出し方を確認する

可視化の関数を拝借した公式のサンプルは、単一画像入力のモードでの一連の処理が書かれている。Visualization utilitiesの下、Running inference and visualizing the resultsでどのように呼び出しているか確認する。

コードサンプル
# STEP 1: Import the necessary modules.
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision

# STEP 2: Create an HandLandmarker object.
base_options = python.BaseOptions(model_asset_path='hand_landmarker.task')
options = vision.HandLandmarkerOptions(base_options=base_options,
                                       num_hands=2)
detector = vision.HandLandmarker.create_from_options(options)

# STEP 3: Load the input image.
image = mp.Image.create_from_file("image.jpg")

# STEP 4: Detect hand landmarks from the input image.
detection_result = detector.detect(image)

# STEP 5: Process the classification result. In this case, visualize it.
annotated_image = draw_landmarks_on_image(image.numpy_view(), detection_result)
cv2_imshow(cv2.cvtColor(annotated_image, cv2.COLOR_RGB2BGR))

draw_landmarks_on_image関数は第一引数にフレーム画像、第二引数にHandLandmarkerResultを渡していて、返り値に可視化の処理を施した画像を渡している。

単一画像入力のモードだとdetection_result = detector.detect(image)で検出結果のHandLandmarkerResultを受け取り、draw_landmarks_on_image関数の第二引数に渡している。

detection_result = landmarker.detect_async(mp_image, frame_timestamp_ms)のようにしたいところだが、landmarker.detect_asyncの返り値はNoneなのでこのような受け取りができない。
LIVE_STREAM:ライブ配信モードで、検出結果のHandLandmarkerResultを受け取りをしたい場合は、設定したコールバック関数で受け取りができる。

3. コールバック関数でHandLandmarkerResultを受け取る

detect_asyncするとコールバック関数に結果が第一引数で渡されている。
では現在のコールバック関数がどのようになっているか確認しよう。

作成中
def print_result(result: HandLandmarkerResult, output_image: mp.Image, timestamp_ms: int):
    print('hand landmarker result: {}'.format(result))

今回はprint_result関数第一引数のresultに渡されている。

この中でHandLandmarkerResultについて処理をすることができる。例えば現在の処理print('hand landmarker result: {}'.format(result))はprint_result関数内でresultに渡されたHandLandmarkerResultをprintしている。

コールバック関数に渡される値については公式ドキュメントにある以下の通り

The result_callback provides:

  • The hand landmarks detection results.
  • The input image that the hand landmarker runs on.
  • The input timestamp in milliseconds.

それぞれ

  • 手のランドマーク検出結果(HandLandmarkerResult)
  • ハンドランドマークが実行される入力画像(MediapipeのImageオブジェクトに変換されたフレーム画像)
  • 入力タイムスタンプ

def print_result(result: HandLandmarkerResult, output_image: Image, timestamp_ms: int):としているので、print_result関数内では先程記述したようにresultにHandLandmarkerResult、他にはoutput_iamgeにMediapipeのImageオブジェクトに変換されたフレーム画像、timestamp_msに入力タイムスタンプが渡されている。

4. draw_landmarks_on_image関数を呼び出す

ではdraw_landmarks_on_image関数を呼び出す。
HandLandmarkerResultを受け取り、draw_landmarks_on_image関数の第二引数に渡すが、HandLandmarkerResultの受け取りはコールバック関数内で行われているため、コールバック関数内でdraw_landmarks_on_image関数を呼び出す。

第一引数はNumPy配列のndarray形式のフレーム画像を渡したい。output_imageがフレーム画像なのでこれを渡したいが、これはMediapipeのImageオブジェクト形式なので、Mediapipeが提供するnumpy_view()で形式を変更する。numpy_view()

作成中
def print_result(result: HandLandmarkerResult, output_image: mp.Image, timestamp_ms: int):
    print('hand landmarker result: {}'.format(result))
+   rgb_image = output_image.numpy_view()
+   annotated_image  = draw_landmarks_on_image(rgb_image, result)

draw_landmarks_on_image関数の返り値である、可視化処理を施した画像をannotated_imageで受け取る。コード例だとこのannotated_imageを.imshow()で表示している。コールバック関数内にあるannotated_imageをメインスレッドで受け取る方法だが、インターネットで他の方々のコードを見るにglobal変数で受け取るか、手の検出のクラスを作成しクラス変数で受け取る方法が取られている。今回はglobal変数で受け取るように処理する。

作成中
+ annotated_image = None
def print_result(result: HandLandmarkerResult, output_image: Image, timestamp_ms: int):
-   print('hand landmarker result: {}'.format(result))
+   global annotated_image
    rgb_image = output_image.numpy_view()
    annotated_image  = draw_landmarks_on_image(rgb_image, result)

annotated_imageをNoneで初期化しておく。関数内でglobal変数を使用する際は関数内でglobal宣言をする必要があるのでそれもしておいた。また、可視化で検出を確認できるのでprint文は消した。

5. ウィンドウに表示する

annotated_imageをimshow()で表示する。これまで表示していたモノクロカメラ画像を消した。

作成中
- gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
- cv.imshow('frame', gray)
+ cv.imshow("annotated_image", annotated_image)

6. エラーの解決

このまま実行するとerror: (-215:Assertion failed) size.width>0 && size.height>0 in function 'cv::imshow'エラーが出るはずである。

detect_asyncは非同期処理なので、detect_async()からコールバック関数が呼び出され、結果のannotated_imageをimshow()するという順番が前後することがある。コールバック関数の処理より先にimshow()に移ってしまうことがあるので、annotated_imageが最初に設定したNoneのまま、それをimshow()しようとすることが実行からしばらくの間ある。
そのためNoneの場合.imshow()をしないように記述する。

作成中
+ if annotated_image is not None:
    cv.imshow("annotated_image", annotated_image)

if文にimshow()の処理を入れてあげる。

完成

import mediapipe as mp
import cv2 as cv
import time

from mediapipe import solutions
from mediapipe.framework.formats import landmark_pb2
import numpy as np

MARGIN = 10  # pixels
FONT_SIZE = 1
FONT_THICKNESS = 1
HANDEDNESS_TEXT_COLOR = (88, 205, 54) # vibrant green

def draw_landmarks_on_image(rgb_image, detection_result):
    hand_landmarks_list = detection_result.hand_landmarks
    handedness_list = detection_result.handedness
    annotated_image = np.copy(rgb_image)

    # Loop through the detected hands to visualize.
    for idx in range(len(hand_landmarks_list)):
        hand_landmarks = hand_landmarks_list[idx]
        handedness = handedness_list[idx]

        # Draw the hand landmarks.
        hand_landmarks_proto = landmark_pb2.NormalizedLandmarkList()
        hand_landmarks_proto.landmark.extend([
            landmark_pb2.NormalizedLandmark(x=landmark.x, y=landmark.y, z=landmark.z) for landmark in hand_landmarks
        ])
        solutions.drawing_utils.draw_landmarks(
            annotated_image,
            hand_landmarks_proto,
            solutions.hands.HAND_CONNECTIONS,
            solutions.drawing_styles.get_default_hand_landmarks_style(),
            solutions.drawing_styles.get_default_hand_connections_style())

        # Get the top left corner of the detected hand's bounding box.
        height, width, _ = annotated_image.shape
        x_coordinates = [landmark.x for landmark in hand_landmarks]
        y_coordinates = [landmark.y for landmark in hand_landmarks]
        text_x = int(min(x_coordinates) * width)
        text_y = int(min(y_coordinates) * height) - MARGIN

        # Draw handedness (left or right hand) on the image.
        cv.putText(annotated_image, f"{handedness[0].category_name}",
                    (text_x, text_y), cv.FONT_HERSHEY_DUPLEX,
                    FONT_SIZE, HANDEDNESS_TEXT_COLOR, FONT_THICKNESS, cv.LINE_AA)

    return annotated_image

BaseOptions = mp.tasks.BaseOptions
HandLandmarker = mp.tasks.vision.HandLandmarker
HandLandmarkerOptions = mp.tasks.vision.HandLandmarkerOptions
HandLandmarkerResult = mp.tasks.vision.HandLandmarkerResult
VisionRunningMode = mp.tasks.vision.RunningMode

annotated_image = None

def print_result(result: HandLandmarkerResult, output_image: mp.Image, timestamp_ms: int):
    global annotated_image
    rgb_image = output_image.numpy_view()
    annotated_image  = draw_landmarks_on_image(rgb_image, result)

options = HandLandmarkerOptions(
    base_options=BaseOptions(model_asset_path='hand_landmarker.task'),
    running_mode=VisionRunningMode.LIVE_STREAM,
    num_hands=2,
    result_callback=print_result)
    
with HandLandmarker.create_from_options(options) as landmarker:
    cap = cv.VideoCapture(0)
    
    if not cap.isOpened():
        print("Cannot open camera")
        exit()
        
    while True:
        ret, frame = cap.read()
    
        if not ret:
            print("Can't receive frame (stream end?). Exiting ...")
            break
            
        mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=frame)
        frame_timestamp_ms = int(time.time() * 1000)
        landmarker.detect_async(mp_image, frame_timestamp_ms)

        if annotated_image is not None:
            cv.imshow("annotated_image", annotated_image)  

        if cv.waitKey(1) == ord('q'):
            break
    
    cap.release()
    cv.destroyAllWindows()  

おまけ

画面を反転させて表示するのはちょいと手間がかかる。
frameをflipさせてからmp_imageにして検出させると、反転した画像から左右の判定を行うので、左手にRight、右手にLeftが表示されてしまう。
print_result関数内でrgb_imageをflipさせてからdraw_landmarks_on_imageに渡すと、検出された座標のまま反転した画像に可視化処理をするので、手の上からずれてしまう。
.imshowの直前でannotated_imageをflipすると、可視化処理された画像ごと反転させるので、Right、Leftが鏡文字になってしまう。
考えた解決策として、frameをflipさせてからmp_imageにして検出させ、draw_landmarks_on_image関数内でRight、Leftを反対に書き換える。

def draw_landmarks_on_image(rgb_image, detection_result):
...
        # Draw handedness (left or right hand) on the image.
+        if handedness[0].category_name == "Right":
+            flip_handedness = "Left"
+        elif handedness[0].category_name == "Left":
+            flip_handedness = "Right"
-        cv.putText(annotated_image, f"{handedness[0].category_name}",
+        cv.putText(annotated_image, f"{flip_handedness}",
                    (text_x, text_y), cv.FONT_HERSHEY_DUPLEX,
                    FONT_SIZE, HANDEDNESS_TEXT_COLOR, FONT_THICKNESS, cv.LINE_AA)

この記事はIPFactory Advent Calender 2025 9日目の記事です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?