[マシンビジョンケース](14)手の認識、ジェスチャデモンストレーションPPT、完全なPythonコード

皆さん、こんにちは。今日は、opencv + Mediapipeを使用してジェスチャ認識を通じてPPTをデモンストレーションする方法を紹介します。まず、写真を入れて効果を確認します。

親指だけを上げる、ページを左に向けます;小指だけを上げる、ページを右に向けます;人差し指と中指を上げると、次のようにマウスポインタが移動します。黄色のポインター;人差し指だけ上げるが黒板を描画します;すべての指を曲げると、黒板を消去します。


 1.ツールキットをインストールします

pip install opencv_python==4.2.0.34  # 安装opencv
pip install mediapipe  # 安装mediapipe
# pip install mediapipe --user  #有user报错的话试试这个
pip install cvzone  # 安装cvzone
 
# 导入工具包
import cv2
import cvzone
import numpy as np
from cvzone.HandTrackingModule import HandDetector  # 导入手部检测模块
import os

21ハンドキーポイント情報は以下の通りです


2.手のキーポイントの検出

(1)cvzone.HandTrackingModule.HandDetector()  は、手のキーポイント検出方法です。

パラメータ:

モード:デフォルトはFalseで、入力画像をビデオストリームとして扱います。最初の入力画像で手を検出し、検出に成功した後、手の座標をさらに特定しようとします後続の画像では、すべてのmaxHandsの手が検出され、対応する手の座標が特定されると、どちらかの手を追跡できなくなるまで、別の検出を呼び出さずにそれらの座標を追跡しますこれにより、待ち時間が短縮され、ビデオフレームの処理に最適です。Trueに設定されている場合は、関連のない可能性のある画像の静的バッチに対して、各入力画像で手の検出を実行します

maxHands:最大で検出するハンドの数。デフォルトは2です。

DetectionCon:手の検出モデルの最小信頼値(0-1の間)。しきい値を超えると、検出は成功します。デフォルトは0.5です

minTrackingCon:座標追跡モデルの最小信頼値(0-1の間) 。これは、手の座標を成功した追跡として扱い、失敗した場合は次の入力画像で自動的に手の検出を呼び出すために使用されますこれをより高い値に設定すると、レイテンシーが高くなりますが、ソリューションの堅牢性が向上します。モードがTrueの場合、このパラメーターは無視され、すべての画像で手の検出が実行されます。デフォルトは0.5です

そのパラメーターと戻り値は、公式関数mediapipe.solutions.hands.Hands()に似ています

MULTI_HAND_LANDMARKS:検出/追跡された手のセット。各手は21個の手のランドマークのリストとして表され、それぞれがx、y、zで構成されます。xとyは、それぞれ画像の幅と高さによって[0、1]に正規化されます。Zはランドマークの深さを表します。

MULTI_HANDEDNESS:検出/追跡された手が左手または右手のどちらのセットであるか。各手はラベルとスコアで構成されています。labelは、「Left」または「Right」値の文字列です。スコアは、左手と右手を予測する推定確率です。

(2)cvzone.HandTrackingModule.HandDetector.findHands()    は、手のキーポイントを見つけて描画します

パラメータ:

img:キーポイントを検出する必要のあるフレーム画像。形式はBGRです。

描画:元の画像にキーポイントと認識ボックスを描画するかどうか

briefType:画像を反転する必要があるかどうか、ビデオ画像が自分自身とミラー関係にない場合は、Trueに設定できます

戻り値:

手:検出された手情報、0または1または2の辞書のリスト両手が検出された場合、それは2つの辞書のリストです。辞書には21個のキーポイント座標(x、y、z)、検出フレームの左上の座標とその幅と高さ、検出フレームの中心点の座標、および検出される手が含まれています。

img:キーポイントと線が描かれた画像を返します

(3)PPT画像の処理

すべてのpptsを画像としてエクスポートし、フォルダーに入れます。すべての画像ファイルのファイル名は、pathImage変数に保存されます。pathFullImageにすべての画像を保存するためのパス。

コードは次のように表示されます。

import cv2
import cvzone
from cvzone.HandTrackingModule import HandDetector  # 导入手部跟踪模块 
import os

#(1)视频捕获
cap = cv2.VideoCapture(0)  # 获取电脑摄像头
cap.set(3, 1280)  # 读入图像的宽度
cap.set(4, 720)  # 图像的高度

# 获取ppt图片所在文件夹的路径
folderpath = 'ppt'
# 列表,存放每个文件的名称。根据文件名排序后存放到变量pathImage中
pathImage = sorted(os.listdir(folderpath), key=len)  

# 设置当前展示的ppt的编号
imgNumeber = 0

# 相机的帧图显示在ppt上的size
alpha = 0.8  # 缩放比例
ws, hs = int(320*alpha), int(180*alpha)  # 16:9的图像显示比例

#(2)手部识别模块配置
detector = HandDetector(maxHands=1, detectionCon=0.8)  # 最多检测1只手,检测置信度0.8

#(3)处理每一帧图像
while True:

    # 图像是否读取成功success,读取的帧图像img
    success, img = cap.read()  # 每次执行读取一帧

    # 文件路径拼接,如果各组件名首字母不包含'/',则函数会自动加上,如果最后一个组件为空,则生成的路径以一个’/’分隔符结尾
    pathFullImage = os.path.join(folderpath, pathImage[imgNumeber])

    # 获取指定路径的图片文件
    imgCurrent = cv2.imread(pathFullImage)

    #(4)手部关键点检测
    # 翻转图像,使电脑图像和我们自己呈镜像关系
    img = cv2.flip(img, flipCode=1)  # flipCode=0上下翻转,1左右翻转

    # 返回手部信息hands,绘制关键点后的图像img
    hands, img = detector.findHands(img) 
    
    print(hands)  # 查看手部信息

    #(5)显示图像
    # 在ppt图片旁边接一张摄像机的帧图像
    imgSmall = cv2.resize(img, (ws, hs))  # 将摄像机帧图片缩小

    h, w, _ = imgCurrent.shape  # 获取ppt图像的高,宽,不要通道数 

    # ppt图片的右下角位置上显示视频图像,[h,w]
    imgCurrent[h-hs:h, w-ws:w] = imgSmall

    cv2.imshow('imgCurrent', imgCurrent)  # 显示ppt图像
    cv2.imshow('img', img)  # 显示摄像头图像

    k = cv2.waitKey(1)  # 每帧滞留1毫秒后消失
    if k & 0xFF == 27:  # ESC键退出程序
        break 

# 释放视频资源
cap.release()
cv2.destroyAllWindows()

手の情報を出力した結果は次のようになります。

[{'lmList': [[440, 446, 0], [498, 408, -30], [540, 348, -37], [561, 290, -42], [567, 241, -46], [495, 280, -6], [502, 211, -20], [501, 169, -35], [499, 133, -45], [461, 277, -3], [467, 206, -15], [468, 159, -31], [468, 122, -43], [426, 286, -5], [429, 218, -20], [430, 174, -33], [433, 139, -42], [388, 303, -10], [385, 250, -26], [385, 213, -35], [388, 177, -40]], 
'bbox': (385, 122, 182, 324), 
'center': (476, 284), 
'type': 'Right'}]

効果図は次のとおりです。


3.PPTページの変更

次に、指のキーポイントを処理して、どの指が上にあるかを判別し、 detector.fingersUp()関数を使用して、手の情報の手を渡し、どの指が上にあるかを判別し、リストを返します。1は上を意味し、0は上を意味します。 [1,0,0,0,0]のように曲がっている場合は、親指だけが持ち上げられることを意味します。

フォームフィード操作の領域を制限するために、フォームフィード操作は、手の中心点(cx、cy)が画面の中央上部にある場合にのみアクティブにできますcx、cy =hand['center']

ページ変更が速すぎるようにフレームごとに実行されるページ変更操作を回避するために、遅延ボタン遅延は、ページ変更操作が30フレームごとにのみ実行できるように指定するように設定されています buttonCounterを使用して、最後のページが現在のフレームから変更されてから経過したフレーム数を記録しますページフィード操作が実行され、buttonPressed = Trueの場合、次の30フレームの間ページを変更できません。

上記のコードに追加されました。

import cv2
import cvzone
from cvzone.HandTrackingModule import HandDetector  # 导入手部跟踪模块 
import os

#(1)视频捕获
cap = cv2.VideoCapture(0)  # 获取电脑摄像头
cap.set(3, 1280)  # 读入图像的宽度
cap.set(4, 720)  # 图像的高度

# 获取ppt图片所在文件夹的路径
folderpath = 'ppt'
# 列表,存放每个文件的名称。根据文件名排序后存放到变量pathImage中
pathImage = sorted(os.listdir(folderpath), key=len)  

# 设置当前展示的ppt的编号
imgNumeber = 0

# 相机的帧图显示在ppt上的size
alpha = 0.8  # 缩放比例
ws, hs = int(320*alpha), int(180*alpha)  # 16:9的图像显示比例

# 在相机上画一条线,手在线条以上做手势才能触发
threshold = 350  # 线条的高度阈值

# 处理换页过快的问题
buttonPressed = False   # 只有当False才能换一次页
buttonCounter = 0  # 记录距离上一次换页已经过去多少帧了
buttonDelay = 30  # 每30帧才能执行一次换页

#(2)手部识别模块配置
detector = HandDetector(maxHands=1, detectionCon=0.8)  # 最多检测1只手,检测置信度0.8

#(3)处理每一帧图像
while True:

    # 图像是否读取成功success,读取的帧图像img
    success, img = cap.read()  # 每次执行读取一帧

    # 文件路径拼接,如果各组件名首字母不包含'/',则函数会自动加上,如果最后一个组件为空,则生成的路径以一个’/’分隔符结尾
    pathFullImage = os.path.join(folderpath, pathImage[imgNumeber])

    # 获取指定路径的图片文件
    imgCurrent = cv2.imread(pathFullImage)

    #(4)手部关键点检测
    # 翻转图像,使电脑图像和我们自己呈镜像关系
    img = cv2.flip(img, flipCode=1)  # flipCode=0上下翻转,1左右翻转

    # 返回手部信息hands,绘制关键点后的图像img
    hands, img = detector.findHands(img)

    # 在相机上画一条线,线以上做手势才能触发
    cv2.line(img, (0,threshold), (1280,threshold), (0,255,0), 3)

    #(5)关键点处理
    if hands and buttonPressed is False:  # 如果检测到了手,并且距离上一次换页已经过去了规定时间
        
        # 获取一只手的所有信息
        hand = hands[0]  # hands是一个由多个字典组成的列表,只有第一个字典有信息,其他都是空

        # 统计多少根手指翘起,返回一个列表,1表示手指是翘起的,0表示弯曲
        fingers = detector.fingersUp(hand)  # 最好手掌正对摄像机
        print(fingers)  #[0,1,1,1,1]

        # 手部中心点坐标,hand是一个字典
        cx, cy = hand['center']  # 返回中心点坐标

        # 如果手的中心点坐标在线条以上就能继续操作
        if cy < threshold: # 在图像上,坐标y向下为正,x向右为正

            # 手掌正对摄像机,只有大拇指翘起,执行向左换页操作
            if fingers == [1,0,0,0,0]:
                print('to left')

                # 如果当前的ppt不是第一张才能再向前移动一张
                if imgNumeber > 0:
                    
                    # 完成一次操作后,下次就不能再换页操作了
                    buttonPressed = True

                    # 当前展示的ppt编号向前移动一个,编号减一
                    imgNumeber -= 1  # pathImage[imgNumeber]指向的图片文件名改变
            
            # 手掌正对摄像机,只有小拇指翘起,执行向右换页操作
            if fingers == [0,0,0,0,1]:
                print('to right')

                # 如果当前的ppt不是最后一张才能再向后移动一张
                if imgNumeber < len(pathImage)-1:  # pathImage列表,存放所有的图片文件名
                    
                    # 完成一次操作后,下次就不能再换页操作了
                    buttonPressed = True

                    # 当前展示的ppt编号向后移动一个,编号加一
                    imgNumeber += 1   # pathImage[imgNumeber]指向的图片文件名改变

    #(6)设置延时器
    if buttonPressed is True:  # 此时已经换过页了
        buttonCounter += 1  # 延时器计数加一
        if buttonCounter > buttonDelay:  # 如果延时器超过规定的帧数
            buttonPressed = False  # 下一帧可以换页
            buttonCounter = 0  # 延时计时器重置

    #()显示图像
    # 在ppt图片旁边接一张摄像机的帧图像
    imgSmall = cv2.resize(img, (ws, hs))  # 将摄像机帧图片缩小

    h, w, _ = imgCurrent.shape  # 获取ppt图像的高,宽,不要通道数 

    # ppt图片的右下角位置上显示视频图像,[h,w]
    imgCurrent[h-hs:h, w-ws:w] = imgSmall

    cv2.imshow('imgCurrent', imgCurrent)  # 显示ppt图像
    cv2.imshow('img', img)  # 显示摄像头图像

    k = cv2.waitKey(1)  # 每帧滞留1毫秒后消失
    if k & 0xFF == 27:  # ESC键退出程序
        break 

# 释放视频资源
cap.release()
cv2.destroyAllWindows()

レンダリングは次のとおりです。親指を上げたら、ページを左に回します。


 4.PPTボードの書き込み

人差し指と中指の両方を上げる、人差し指の先端の位置はレーザーポインターの位置と同じになります。ppt画像の各フレームが入力されているため、レーザーポインターの最初の数フレームの位置は保持されず、フレームごとに1つのレーザーポインターの位置しかありません。pptの各フレームに円を描くだけです。

人差し指だけを上げる、黒板を書くのと同じです。ppt画像では、現在のフレームより前の多くのフレームの人差し指の座標を描画し、ポイント間の接続を描画します。各図面の始点と終点の描画点のセットを黒板に記録するために、 2次元リスト注釈= [[]]が定義され各サブリストには、完全な描画ごとの曲線点の座標セットが格納されます。 。annotationNumber変数は、現在の完全な図面が保存されているインデックスに対応するサブリストを指定するために使用されます。

カメラのエッジフレームの位置で手を動かすと、キーポイントの検出効果が低下するという問題を回避するため。小さな長方形のボックスに指を移動し、この小さな領域をコンピューター画面全体のより大きな領域にマッピングしますnp.interp()関数を使用して、 人差し指の指先座標の移動範囲をマップします(lmList [8] [0]、lmList [8] [1]) 。

annotationStart == Falseの場合 、現在のフレームが黒板スクリプトを描画するための開始点であることを意味します。現在のフレームの指先を、2次元リスト注釈のインデックスannotationNumber、annotations[annotationNumber]に対応するサブリストに保存します。 append((xval、yval))人差し指を上げるときだけでなく、黒板への書き込みが終了するたびに、annotationStart=Falseは指先の座標の保存を停止します。

黒板に描画する場合、注釈内の各サブリストをトラバースしてから、各サブリストのすべての座標要素をトラバースすると、すべての座標点を取得し、2つの座標点間の接続を描画できます。

上記のコードに追加します。

import cv2
import cvzone
from cvzone.HandTrackingModule import HandDetector  # 导入手部跟踪模块 
import numpy as np
import os

#(1)视频捕获
cap = cv2.VideoCapture(0)  # 获取电脑摄像头
cap.set(3, 1280)  # 读入图像的宽度
cap.set(4, 720)  # 图像的高度

# 获取ppt图片所在文件夹的路径
folderpath = 'ppt'
# 列表,存放每个文件的名称。根据文件名排序后存放到变量pathImage中
pathImage = sorted(os.listdir(folderpath), key=len)  

# 设置当前展示的ppt的编号
imgNumeber = 0

# 相机的帧图显示在ppt上的size
alpha = 0.8  # 缩放比例
ws, hs = int(320*alpha), int(180*alpha)  # 16:9的图像显示比例

# 在相机上画一条线,手在线条以上做手势才能触发
threshold = 350  # 线条的高度阈值

# 处理换页过快的问题
buttonPressed = False   # 只有当False才能换一次页
buttonCounter = 0  # 记录距离上一次换页已经过去多少帧了
buttonDelay = 30  # 每30帧才能执行一次换页

# 保存板书的每一个坐标点
annotations = [[]] # 二维列表,每个列表保存连续绘制一次后的坐标点
annotationNumber = -1  # 当前使用的是第几个列表中的一次绘图后的关键点
annotationStart = False  # 开始绘图后,需要知道一次绘图的终点和起点

#(2)手部识别模块配置
detector = HandDetector(maxHands=1, detectionCon=0.8)  # 最多检测1只手,检测置信度0.8

#(3)处理每一帧图像
while True:

    # 图像是否读取成功success,读取的帧图像img
    success, img = cap.read()  # 每次执行读取一帧

    # 文件路径拼接,如果各组件名首字母不包含'/',则函数会自动加上,如果最后一个组件为空,则生成的路径以一个’/’分隔符结尾
    pathFullImage = os.path.join(folderpath, pathImage[imgNumeber])

    # 获取指定路径的图片文件
    imgCurrent = cv2.imread(pathFullImage)

    #(4)手部关键点检测
    # 翻转图像,使电脑图像和我们自己呈镜像关系
    img = cv2.flip(img, flipCode=1)  # flipCode=0上下翻转,1左右翻转

    # 返回手部信息hands,绘制关键点后的图像img
    hands, img = detector.findHands(img)

    # 在相机上画一条线,线以上做手势才能触发
    cv2.line(img, (0,threshold), (1280,threshold), (0,255,0), 3)
    # 在相机上画一个手指移动映射区域
    cv2.rectangle(img, (300, 0), (1100,360), (255,0,0), 3)

    #(5)关键点处理
    if hands and buttonPressed is False:  # 如果检测到了手,并且距离上一次换页已经过去了规定时间
        
        # 获取一只手的所有信息
        hand = hands[0]  # hands是一个由多个字典组成的列表,只有第一个字典有信息,其他都是空

        # 统计多少根手指翘起,返回一个列表,1表示手指是翘起的,0表示弯曲
        fingers = detector.fingersUp(hand)  # 最好手掌正对摄像机
        print(fingers)  #[0,1,1,1,1]


        # 将手指移动的边界限制在一个框中,框的大小映射到屏幕大小
        lmList = hand['lmList']  # 获取21个关键点的xyz坐标
        # indexFinger = lmList[8][0], lmList[8][1]  # 获取食指指尖的xy坐标
        # 设置映射区域
        xval = int(np.interp(lmList[8][0], [300, 1280], [0, 1280]))  # x的映射区域
        yval = int(np.interp(lmList[8][1], [0, 360], [0, 720]))  # y的映射区域

        # 手部中心点坐标,hand是一个字典
        cx, cy = hand['center']  # 返回中心点坐标

        # 如果手的中心点坐标在线条以上就能继续操作
        if cy < threshold: # 在图像上,坐标y向下为正,x向右为正

            # ① 手掌正对摄像机,只有大拇指翘起,执行向左换页操作
            if fingers == [1,0,0,0,0]:
                print('to left')

                # 如果当前的ppt不是第一张才能再向前移动一张
                if imgNumeber > 0:
                    
                    # 完成一次操作后,下次就不能再换页操作了
                    buttonPressed = True

                    # 当前展示的ppt编号向前移动一个,编号减一
                    imgNumeber -= 1  # pathImage[imgNumeber]指向的图片文件名改变
            
            # ② 手掌正对摄像机,只有小拇指翘起,执行向右换页操作
            if fingers == [0,0,0,0,1]:
                print('to right')

                # 如果当前的ppt不是最后一张才能再向后移动一张
                if imgNumeber < len(pathImage)-1:  # pathImage列表,存放所有的图片文件名
                    
                    # 完成一次操作后,下次就不能再换页操作了
                    buttonPressed = True

                    # 当前展示的ppt编号向后移动一个,编号加一
                    imgNumeber += 1   # pathImage[imgNumeber]指向的图片文件名改变

        # ③ 指针设置,如果食指和中指同时竖起就绘制一个圈,不需要在线条以上
        if fingers == [0,1,1,0,0]:
            print('circle')

            # 在ppt图片上的食指指尖绘制圆圈
            cv2.circle(imgCurrent, (xval, yval), 10, (0,255,255), -1) # -1代表颜色填充
            cv2.circle(imgCurrent, (xval, yval), 11, (0,0,255), 3)

        # ④ 板书设置,如果只有食指竖起就按食指轨迹移动绘制线条
        if fingers == [0,1,0,0,0]:
            # 如果之前没绘制过图那么annotationStart=False
            if annotationStart == False:
                annotationStart = True  # 那么当前帧开始绘图
                annotationNumber += 1  # 将当前绘图结果到保存在该索引指向的列表中
                annotations.append([])  # 在二维列表中添加一个子列表用来保存坐标

            cv2.circle(imgCurrent, (xval, yval), 10, (0,255,255), -1)
            # 将食指的每一帧坐标都保存在指定的索引列表中
            annotations[annotationNumber].append((xval, yval))
        # 如果不绘制板书了,当前一次绘图的坐标都保存好
        else:
            annotationStart = False  # 不绘制了 

        # ⑤ 删除前一次的板书,不全删
        if fingers == [0,1,1,1,0]:
            if annotations:
                annotations.pop(-1)  # 删除最后一次绘图的坐标
                annotationNumber -= 1  # 绘图索引向前移动一个

        # ⑥ 擦除全部板书,如果没有手指竖起就删除所有板书
        if fingers == [0,0,0,0,0]:
            annotations = [[]] # 二维列表重置
            annotationNumber = -1  # 重置当前使用的列表索引
            annotationStart = False  # 重置绘图过程

    #(6)设置延时器
    if buttonPressed is True:  # 此时已经换过页了
        buttonCounter += 1  # 延时器计数加一
        if buttonCounter > buttonDelay:  # 如果延时器超过规定的帧数
            buttonPressed = False  # 下一帧可以换页
            buttonCounter = 0  # 延时计时器重置

    #(7)绘制板书,将第④步保存的坐标点都绘制出来
    for i in range(len(annotations)):  # 取出绘图列表
        # 遍历每个列表,绘制一次绘图的点
        for j in range(len(annotations[i])): 
            # 绘制每两个点之间的线条
            if j != 0:
                cv2.line(imgCurrent, annotations[i][j-1], annotations[i][j], (0,0,255),5)

    #(8)显示图像
    # 在ppt图片旁边接一张摄像机的帧图像
    imgSmall = cv2.resize(img, (ws, hs))  # 将摄像机帧图片缩小

    h, w, _ = imgCurrent.shape  # 获取ppt图像的高,宽,不要通道数 

    # ppt图片的右下角位置上显示视频图像,[h,w]
    imgCurrent[h-hs:h, w-ws:w] = imgSmall

    cv2.imshow('imgCurrent', imgCurrent)  # 显示ppt图像
    cv2.imshow('img', img)  # 显示摄像头图像

    k = cv2.waitKey(1)  # 每帧滞留1毫秒后消失
    if k & 0xFF == 27:  # ESC键退出程序
        break 

# 释放视频资源
cap.release()
cv2.destroyAllWindows()

効果図は次のとおりです。

おすすめ

転載: blog.csdn.net/dgvv4/article/details/123682731