【案例共创】基于华为云开发环境桌面版的剪刀手手势识别

本案例由开发者:华为2024年第三批次协同育人项目-南航金城学院-孙福清教师提供

一、概述

1. 案例介绍

本案例是基于华为云主机的 Python 开发环境,引导学生学习和实践人工智能计算机视觉的一个应用案例—基于计算机视觉 Python 开源模块 opencv 以及 meidiapipe 实现自拍剪刀手V型手势的识别。

华为开发者空间,是为全球开发者打造的专属开发者空间,致力于为每位开发者提供一台云主机、一套开发工具和云上存储空间,汇聚昇腾、鸿蒙、鲲鹏、GaussDB、欧拉等华为各项根技术的开发工具资源,并提供配套案例指导开发者 从开发编码到应用调测,基于华为根技术生态高效便捷的知识学习、技术体验、应用创新。

2. 适用对象

  • 个人开发者
  • 高校学生

3. 案例时间

本案例总时长预计90分钟。

4. 案例流程

说明:

  1. 安装验证 Python 模块 opencv;
  2. opencv基础函数介绍;
  3. 安装验证 Python 模块 mediapipe;
  4. mediapipe 获取手部关键地标点;
  5. 识别剪刀手(V)手势。

5. 资源总览

本案例预计花费0元。

资源名称规格单价(元)时长(分钟)
华为开发者空间 - 云主机鲲鹏通用计算增强型 kc2 | 4vCPUs | 8G | Ubuntu免费90

二、安装验证 Python 模块 opencv

1. 开发者空间配置

面向广大开发者群体,华为开发者空间提供一个随时访问的“开发桌面云主机”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。

进入华为开发者空间工作台界面,点击打开云主机 > 进入桌面连接云主机。 如果还没有领取云主机进入工作台界面后点击配置云主机,选择Ubuntu操作系统。

2. 安装 Python 模块 opencv

opencv 是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在 Linux、Windows、Android 和 Mac OS操作系统上。 它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB 等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。 关于 opencv 相关的重要网站: 官网地址: https://opencv.org/ 开源代码: https://github.com/opencv/opencv 基于python的开源模块:https://pypi.org/project/opencv-python/

Python 的 opencv 有两个包: Packages for standard desktop environments (Windows, macOS, almost any GNU/Linux distribution) Option 1 - Main modules package: pip install opencv-python Option 2 - Full package (contains both main modules and contrib/extra modules): pip install opencv-contrib-python (check contrib/extra modules listing from OpenCV documentation) opencv-python 是只包含了主要模块的包,opencv-contrib-python包含了主要模块以及扩展模块,扩展模块主要是包含了一些带专利的收费算法以及一些在测试的新的算法。 本案例使用opencv-contrib-python。

下面我们在云主机中安装opencv。 1. 进入终端terminal

  1. 切换到root账号 设置root账号新密码:
sudo passwd root

切换到root用户:

su root

输入密码:

  1. 创建项目文件夹 依次执行如下命令:
#创建项目文件夹
mkdir hand-gestrue-v
#查看
ls
#进入项目文件夹
cd hand-gestrue-v/

  1. 创建虚拟环境 若是未安装python3,则先按照命令安装python3.12-venv,然后再执行:
apt install python3.12-venv

然后创建虚拟环境:

python3 -m venv venvHandGestrue
  1. 激活虚拟环境
source ./venvHandGestrue/bin/activate

  1. 确定 Python 的pip源已经正确配置 确认华为云主机的 Python平台的pip源配置为华为Python镜像源,华为云主机与华为Python pip源在同一个数据中心,网络情况最好,下载Python模块速度最快:接下来安装 Python 模块 opencv 以及 mediapipe,第一个原因这些模块文件比较大,第二个原因有很多依赖的Python模块包。 参考案例《【案例共创】华为云主机Linux Python开发环境配置》“六、配置python的pip软件源” 完成软件源的检查和配置。

  2. 安装Python模块opencv

pip3 install opencv-contrib-python
  1. 验证opencv安装成功可以导入 依次输入如下命令验证安装:
python3

#导入cv
import cv2

#退出
exit()

3. 验证 Python 模块 opencv

  1. 下载某个图片,通过【我的云存储】将本地图片上传至空间,然后在云主机内下载,图片下载到默认的下载目录:/home/developer/Downloads/,也可以在华为云主机直接使用云主机内部默认安装的火狐浏览器去图片网站上(百度图片image.baidu.com)上进行下载,默认的下载目录也是:/home/developer/Downloads/,因操作较为简单,在此不做赘述。

  1. 将下载的图片文件复制到项目文件夹下
cp /home/developer/Downloads/xiaolongnv.jpg ./111.jpg

  1. 新建空白py代码文件准备编写py代码 使用文本编辑器gedit创建打开一个新的py文件:
gedit opencvOK.py

如果gedit未授权,还需要对gedit进行root账号的X图形服务器权限授权,允许 root 用户从本地连接到 X 服务器:

xhost +local:root

将如下代码复制到空白文件中,保存。

import cv2
img = cv2.imread("111.jpg")
img = cv2.resize(img,None,fx=(640/img.shape[0]),fy=(640/img.shape[0]),interpolation=cv2.INTER_CUBIC)
cv2.imshow("img",img)
cv2.waitKey(0)
cv2.destroyAllWindows()

:代码中的图片文件名,一定要和项目文件夹下的图片文件名相同才能获取到,如果开发者自己定义了图片名,请注意修改代码中的文件名。

  1. 运行py代码验证opencv模块
python3 opencvOK.py

就可以看到图片展示出来了。

三、opencv 基础函数介绍

1. 命令行方式编写调试 Python 代码

云主机上编写和调试 Python 代码,案例中没有使用 Python 集成开发环境IDE,因为代码量少,功能简单,直接使用最为简单的命令行方式,更为简单和易于实践,运行快。 因此可以使用多终端的方式,一个终端激活虚拟环境用于运行py代码,另一个终端使用文本编辑器gedit来查看修改py代码。

2. 导入模块opencv

接下来将简单介绍或者示范本案例中用到的 opencv函数,函数的具体参数和含义,大家可以使用大模型去查找,非常详细,本案例不做赘述。 本小节中的示范代码:

import cv2
img = cv2.imread("111.jpg")
img = cv2.resize(img,None,fx=(640/img.shape[0]),fy=(640/img.shape[0]),interpolation=cv2.INTER_CUBIC)
cv2.imshow("img",img)
cv2.waitKey(0)
cv2.destroyAllWindows()

导入安装的 Python 模块opencv,安装后的模块位置在项目文件夹的虚拟环境文件夹中,名字为cv2:import cv2,本质是导入这个模块安装后的文件夹名字。 :虚拟环境已经安装了opencv模块(名字cv2),但没有激活虚拟环境,就执行py代码,也会提示导入模块失败:No module named ‘cv2’。

3. 读取图片文件

Python 模块 opencv 读取图片:cv2.imread()。 读取的文件名一定要真实存在,如果错误或者不存在,本行代码不会报错,而是在后面读入图片对象的代码语句时出错。

示例代码:img = cv2.imread("111.jpg") 函数原型:cv2.imread(filename[, flags])

参数名称含义
filename窗口的名称,类型为字符串,每个窗口名称必须是唯一的。如果使用相同的名称调用 cv2.imshow,则会更新该窗口的内容。
flags(可选)用于指定读取图像的方式。默认值为 cv2.IMREAD_COLOR。
返回值如果图像成功加载,cv2.imread 会返回一个 NumPy 数组,表示图像的像素数据。

4. 显示读取到的图片文件

Python模块 opencv 显示图片:cv2.imshow() 示例代码:cv2.imshow("img",img) 函数原型:cv2.imshow(winname, mat)

参数名称含义
winname窗口的名称,类型为字符串,每个窗口名称必须是唯一的。如果使用相同的名称调用 cv2.imshow,则会更新该窗口的内容。
mat要显示的图像数据,通常是一个 NumPy 数组。图像数据可以是灰度图像(单通道)或彩色图像(三通道,BGR 格式)。

5. 等待键盘输入并退出

cv2.waitkey()会等待用户键盘输入,然后才退出程序。函数参数表示停等的时间不同。 函数原型:cv2.waitKey([delay])

参数名称含义
delay(可选)等待时间,单位是毫秒(ms)。如果不指定,默认值为 0,表示无限期等待用户按键。如果指定一个正整数(如 1000),表示等待指定的毫秒数后自动继续执行程序。

程序停等,等待用户在图片上输入任意键退出。

6. 调整图片大小利于处理和显示

Python 模块 opencv 调整图片大小函数cv2.resize()。 示例代码:将图片缩放调整为固定高度为640像素,长高等比例调整。

img = cv2.resize(img,None,fx=(640/img.shape[0]),fy=(640/img.shape[0]),interpolation=cv2.INTER_CUBIC)

函数原型:cv2.resize(src, dsize[, dst[, fx[, fy[, interpolation]]]])

参数名称含义
src输入图像,通常是一个 NumPy 数组,表示图像的像素数据。
dsize输出图像的大小,格式为 (width, height)。如果指定了 dsize,则 fx 和 fy 会被忽略,如果不指定 dsize,则必须指定 fx 和 fy。
dst(可选)输出图像,如果未指定,则会自动创建一个与目标大小匹配的图像。
fx(可选)水平方向的缩放比例,是一个浮点数,如果未指定 dsize,则必须指定 fx 和 fy。
fy(可选)垂直方向的缩放比例,是一个浮点数,如果未指定 dsize,则必须指定 fx 和 fy。
interpolation(可选)插值方法,用于控制图像缩放时的像素插值方式。cv2.INTER_LINEAR:双线性插值(默认值),适用于缩放和旋转。

:resize函数非常关键,开发者电脑屏幕尺寸大小不一,需要根据自己显示器大小调整图片尺寸大小,否则会出现显示图片过大,不全等问题。本代码是按照长宽比例等比例适应的屏幕尺寸大小。

7. 在图片上绘制小圆点

Python模块opencv在图片上绘制小圆点:cv2.circle。 示例代码:在图片的坐标x=20,y=20的位置绘制一个半径为3像素的实心圆。

cv2.circle(img, (20, 20), 3, (0,0,255), -1)

函数原型:cv2.circle(img, center, radius, color[, thickness[, lineType[, shift]]]) | 参数名称 | 含义 | |------------------------------------------------------------------------------|------------------------------------------------------------------------------------| | img | 图像对象,即要在其上绘制圆形的图像。通常是一个 NumPy 数组,表示图像的像素数据。 | | center | 圆心的坐标,格式为 (x, y),其中 (x, y) 是圆心在图像中的位置,坐标值是整数,表示像素位置。 | | radius | 圆的半径,是一个整数值。 | | color | 圆的颜色,用 BGR(蓝、绿、红)格式表示,对于灰度图像,颜色值是一个单通道的灰度值(0-255);对于彩色图像,颜色值是一个三通道的元组,例如 (255, 0, 0) 表示红色。| | thickness(可选) | 圆的轮廓线的粗细程度,单位是像素,如果设置为 -1,则会填充整个圆形,默认值为 1。| | lineType(可选) | 圆的轮廓线类型,可以是以下几种:cv2.LINE_4:4 邻接的线条,o cv2.LINE_8(默认值):8 邻接的线条,cv2.LINE_AA:抗锯齿的线条。| | shift(可选) | 圆心坐标和半径的缩放比例,通常用于高精度绘图,默认值为 0。|

8. 在图片上添加文本

Python模块opencv在图片上增加文本内容:cv2.putText()。 示例代码:

cv2.putText(img,"welcome",(100,100),cv2.FONT_HERSHEY_COMPLEX,1,(0,0,255),2,8)

函数原型: cv2.putText(img, text, org, fontFace, fontScale, color[, thickness[, lineType[, bottomLeftOrigin]]])

参数名称含义
img图像对象,即要在其上绘制圆形的图像。通常是一个 NumPy 数组,表示图像的像素数据。
text要绘制的文本内容,类型为字符串。
org文本的左下角位置,格式为 (x, y),其中 (x, y) 是文本在图像中的位置,坐标值是整数,表示像素位置。
fontFace字体类型,OpenCV 提供了多种预定义字体,例如:cv2.FONT_HERSHEY_SIMPLEX:普通大小的无衬线字体。
fontScale字体大小因子,是一个浮点数,用于控制字体的大小,例如,fontScale=1.0 表示正常大小,fontScale=2.0 表示两倍大小。
color文本的颜色,用 BGR(蓝、绿、红)格式表示,对于灰度图像,颜色值是一个单通道的灰度值(0-255);对于彩色图像,颜色值是一个三通道的元组,例如 (255, 0, 0) 表示红色。
thickness(可选)文本轮廓线的粗细程度,单位是像素,默认值为 1。
lineType(可选)文本轮廓线类型,可以是以下几种,cv2.LINE_4:4 邻接的线条;cv2.LINE_8(默认值):8 邻接的线条;cv2.LINE_AA:抗锯齿的线条。
bottomLeftOrigin(可选)当为 True 时,图像数据原点是左下角(主要用于 Windows 的 GDI+ 图像);默认值为 False,表示图像数据原点是左上角。

9. 图片颜色空间转换

Python 模块 opencv 可以转变图片的颜色空间,对应不同的处理算法:cv2.cvtColor 是将图像从一种颜色空间转换为另一种颜色空间的函数。 颜色空间是指用来表示颜色的模型,常见的颜色空间包括 RGB、BGR、HSV、HSL、灰度等。在图像处理中,根据不同的需求,可能需要在不同的颜色空间之间进行转换。 示例代码:彩色BGR转换为灰度。

imgGray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
cv2.imshow("gray",imgGray)

函数原型:cv2.cvtColor(src, code[, dst[, dstCn]])

参数名称含义
src输入图像,通常是一个 NumPy 数组,图像数据可以是单通道(灰度图像)或多通道(彩色图像)。
code颜色空间转换的标志,指定从哪种颜色空间转换到哪种颜色空间。opencv 提供了多种颜色空间转换标志,例如:cv2.COLOR_BGR2RGB:将 BGR 图像转换为 RGB 图像,cv2.COLOR_RGB2BGR:将 RGB 图像转换为 BGR 图像等,更多颜色空间转换标志可以在 OpenCV 文档中查找。
dst(可选)输出图像,如果未指定,则会自动创建一个与输入图像大小和类型匹配的输出图像。
dstCn(可选)输出图像的通道数。如果未指定,会根据输入图像和转换代码自动推断。

四、安装验证 Python 模块 mediapipe

1. Python 开源模块 mediapipe

mediapipe 是由 Google Research 开发并开源的一个跨平台多媒体处理框架,专注于计算机视觉和音频处理任务。它提供了一系列预训练的机器学习模型和工具,能够快速构建和部署各种应用,例如人脸检测、姿态估计、手势识别、头像分割等。 mediapipe 提供了 Python、C++、JavaScript 等多种语言的接口,开发者可以根据自己的需求选择合适的开发环境。 网址:https://pypi.org/project/mediapipe/

2. 安装 Python 模块 mediapipe

  1. 进入华为云主机,打开终端,进入项目文件夹,激活虚拟环境(因步骤二已经有相同操作,此处不做赘述)。 按照如下命令安装mediapipe:
pip install mediapipe

  1. 验证可以成功导入模块mediapipe Python 模块 mediapipe 的导入名字为 mediapipe,通过交互式 Python 的环境,验证是否可以正常导入。 依次输入如下命令,可以看到mediapipe文件夹已经生成。
python3

#导入cv
import mediapipe

#退出
exit()

3. 验证 Python 模块 mediapipe

  1. 安装验证opencv模块的步骤都已经完成,并且下载高清人脸大图并复制到项目文件夹,上述步骤已经在验证opencv时详细说明,此处不再赘述。

  2. 新建空白py代码文件准备编写py代码。

gedit mediapipeOK.py

如果gedit未授权,还需要对gedit进行root账号的X图形服务器权限授权,允许 root 用户从本地连接到 X 服务器:

xhost +local:root

将如下代码复制到文件中。

import cv2
import mediapipe as mp
model=mp.solutions.face_mesh.FaceMesh(
    static_image_mode=False,
    refine_landmarks=True,
    max_num_faces=5,
    min_detection_confidence=0.5,
    min_tracking_confidence=0.5,
)
img = cv2.imread("face.png")
img = cv2.resize(img,None,fx=(640/img.shape[0]),fy=(640/img.shape[0]),interpolation=cv2.INTER_CUBIC)
img_RGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
results = model.process(img_RGB)
if results.multi_face_landmarks:
    for faceLms in results.multi_face_landmarks:
        mp.solutions.drawing_utils.draw_landmarks(
        image=img,
        landmark_list=faceLms,
        connections=mp.solutions.face_mesh.FACEMESH_CONTOURS,
        landmark_drawing_spec=None,
            connection_drawing_spec=mp.solutions.drawing_styles.get_default_face_mesh_contours_style() )
cv2.imshow("img",img)
cv2.waitKey(0)
cv2.destroyAllWindows() 

:示例代码中,直接将代码从本地计算机复制进去粘贴在gedit打开的代码文件中,只改动图片文件名,其他无需改动。

  1. 运行py代码验证mediapipe模块。
python3 mediapipeOK.py

五、mediapipe 获取手部的关键地标点

1. mediapipe 的手部地标点 hand landmarks

"landmark"在医学或生物学领域,通常被翻译为"标志",用来指代身体上的特定位置或特征,以便于定位或识别。 "hand landmark"在医学或生物学领域,通常指的是手部的一个特定标志或特征。 Hand Landmark,即手部关键点,是指在图像或视频中手部的重要特征点。这些关键点通常包括手指的关节、指尖以及手掌的某些特定位置,在手部追踪、手势识别以及人机交互、机器人技术、游戏和增强现实等领域具有广泛的应用前景和巨大的潜力。 mediapipe 的 hand 解决方案可用于进行手部检测和跟踪,它能够实时检测手部的 21 个关键地标点(landmarks),这些地标点可以用于手势识别、手部动作捕捉等多种应用。 mediapipe Hands 通过深度学习模型检测手部的21个关键点,包括手腕、手指关节等部位,每个地标点都有一个唯一的索引编号和对应的坐标位置(通常是归一化后的屏幕坐标):

手部名称编号
手腕(Wrist)编号 0,位于手腕的中心位置。
拇指(Thumb)编号 1:拇指根部(靠近手腕)。编号 2:拇指第一个关节。编号 3:拇指第二个关节。编号 4:拇指指尖。
食指(Index Finger)编号 5:食指根部(靠近手腕)。编号 6:食指第一个关节。编号 7:食指第二个关节。编号 8:食指指尖。
中指(Middle Finger)编号 9:中指根部(靠近手腕)。编号 10:中指第一个关节。编号 11:中指第二个关节。编号 12:中指指尖。
无名指(Ring Finger)编号 13:无名指根部(靠近手腕)。编号 14:无名指第一个关节。编号 15:无名指第二个关节。编号 16:无名指指尖。
小指(Pinky Finger)o 编号 17:小指根部(靠近手腕)。编号 18:小指第一个关节。编号 19:小指第二个关节。编号 20:小指指尖。

每个地标点的坐标通常是一个三维向量(x, y, z),其中: - x 和 y:表示地标点图像在中的二维位置,通常是归一化后的坐标,范围在 [0, 1] 之间。例如,(0, 0) 表示图像的左上角,(1, 1) 表示图像的右下角。 - z:表示地标点的深度信息,它是一个相对值,表示该点与手腕(编号 0)之间的深度差异。z 值越大,表示该点越远离摄像头;z 值越小,表示该点越靠近摄像头。 这些地标点可以用于: 手势识别:通过分析地标点之间的相对位置和角度,可以识别出各种手势。例如,判断拇指和食指是否捏在一起(OK 手势),可以通过计算编号 4(拇指指尖)和编号 8(食指指尖)之间的距离来实现。 动作捕捉:实时跟踪手部的运动轨迹,可以用于虚拟现实、增强现实等场景。例如,通过地标点的坐标变化,可以模拟手部在虚拟空间中的运动。 人机交互:通过手势控制设备或软件,实现更加自然和直观的交互方式。例如,通过挥手、握拳等手势来控制音乐播放、暂停或切换曲目。

2. 从手部图片中获取地标点 hand landmarks

  1. 准备手部图片(清晰、大图),复制到项目文件夹。 图片可以通过浏览器下载,也可以本地上传云空间,此操作在步骤二中已经详细讲述,此处不再赘述。

  2. 新建空白py代码文件,编写获取手部地标点的py示例代码。

gedit handlandmarks.py

如果gedit未授权,还需要对gedit进行root账号的X图形服务器权限授权,允许 root 用户从本地连接到 X 服务器,若已授权,跳过此步:

xhost +local:root

将如下代码复制到文件中。

import cv2
import mediapipe

#-----------------------------------------------------------------------
def imgHeightResize(img, fixedHeight):
    imgResized = img
    imgH,imgW,imgChs = imgResized.shape    
    ratioH = fixedHeight/imgH
    imgResized = cv2.resize(img,None,fx=ratioH,fy=ratioH,interpolation=cv2.INTER_CUBIC)
    return imgResized

#-----------------------------------------------------------------------
if __name__ == '__main__':
    imgOrg = cv2.imread("hand.png")
    img = imgHeightResize(imgOrg, 640)
    cv2.imshow("hand",img)

    imgRGB = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
    handDetector = mediapipe.solutions.hands.Hands(static_image_mode=True,
                                    model_complexity=1,
                                    max_num_hands=2,
                                    min_detection_confidence=0.8,
                                    min_tracking_confidence=0.8)
    results = handDetector.process(imgRGB)
    if (results.multi_hand_landmarks):
        for handlms in results.multi_hand_landmarks:
            mediapipe.solutions.drawing_utils.draw_landmarks(
                                 img, 
                             handlms, 
                             mediapipe.solutions.hands.HAND_CONNECTIONS)
    else:
        cv2.putText(img,"no hands detected",(100,100),cv2.FONT_HERSHEY_COMPLEX,1,(0,0,255),2,8)

    cv2.imshow("hand-landmarks",img)
    cv2.waitKey(0)
cv2.destroyAllWindows()

  1. 执行py代码,显示手部地标点的图片。
python3 handlandmarks.py

3. 过程代码解析

  1. 导入模块。
import mediapipe

就可以直接使用模块名字来引用其中的函数或者变量、宏等,例如:

mediapipe.solutions.hands.Hands(), 
mediapipe.solutions.hands.HAND_CONNECTIONS

有些网上的导入模块代码为:

import mediapipe as mp

这其实是给mediapipe起个字符串数量更少的别名,减少代码量,本质一样,引用模块的函数、变量或宏就变为:

mp.solutions.hands.Hands(), 
mp.solutions.hands.HAND_CONNECTIONS
  1. opencv 读入图片并调整尺寸大小适合显示。 见前面章节关于 opencv 基础函数介绍部分
#-----------------------------------------------------------------------
def imgHeightResize(img, fixedHeight):
    imgResized = img
    imgH,imgW,imgChs = imgResized.shape    
    ratioH = fixedHeight/imgH
    imgResized = cv2.resize(img,None,fx=ratioH,fy=ratioH,interpolation=cv2.INTER_CUBIC)
    return imgResized

#-----------------------------------------------------------------------
if __name__ == '__main__':
    imgOrg = cv2.imread("hand.png")
    img = imgHeightResize(imgOrg, 640)
  1. 图片转换色彩空间。
imgRGB = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)

使用 cv2.imread 打开图片会得到一个numpy数组[width,height],数组中每个元素就是一个像素点,像素点使用三维BGR表示[0-blue,1-gree,2-red]。 MediaPipe的像素点使用 RGB 表示,即[0-red,1-gree,2-blue], cv2.cvtColor 函数可以将图像从一种颜色空间转换为另一种颜色空间。 BGR 和 RGB 的区别 - BGR:是 OpenCV 默认的颜色空间格式。它将图像的每个像素表示为一个三通道的值,分别是蓝色(Blue)、绿色(Green)和红色(Red),顺序为 BGR。 - RGB:是大多数图像处理库(如 PIL、Matplotlib 等)以及许多深度学习框架(如 TensorFlow、PyTorch 等)使用的颜色空间格式。它的顺序是红色(Red)、绿色(Green)和蓝色(Blue)。

  1. 创建手部检测器并初始化。
handDetector = mediapipe.solutions.hands.Hands(static_image_mode=True,
                                    model_complexity=1,
                                    max_num_hands=2,
                                    min_detection_confidence=0.8,
                                    min_tracking_confidence=0.8)

Hands 类在初始化时可以接收以下参数: - static_image_mode:布尔值,表示输入图像是否为静态图片。如果为 True,则每次输入的图像被视为独立的静态图像;如果为 False,则输入被视为视频流,模型会利用前一帧的信息来优化跟踪效果。默认值为 False。 - max_num_hands:整数,表示最多检测的手的数量。默认值为 2。 - model_complexity:整数,取值为 0 或 1,表示模型的复杂度。复杂度越高,检测精度越高,但计算速度会变慢。默认值为 1。 - min_detection_confidence:浮点数,表示检测的最小置信度阈值,取值范围为 [0.0, 1.0]。默认值为 0.5。 - min_tracking_confidence:浮点数,表示跟踪的最小置信度阈值,取值范围为 [0.0, 1.0]。默认值为 0.5。

  1. 调用检测器对读入的图片进行检测。
results = handDetector.process(imgRGB)

使用 mediapipe.solutions.hands.Hands 的 process 方法时,返回一个数据结构,名字随意,这里命名为results,包含了手部检测和跟踪的结果。 results 是一个 mediapipe.framework.formats.detection_pb2.Detection 对象,其中包含了手部的关键点(landmarks)、手性(handedness)以及其他相关信息。 以下是 results 数据结构中各个字段的详细解释: - 1. results.multi_hand_landmarks 这是一个列表,包含每只手的关键点(landmarks)。每只手的关键点是一个 mediapipe.framework.formats.landmark_pb2.LandmarkList 对象,其中包含 21 个关键点的坐标信息。 - landmark:一个包含 21 个 mediapipe.framework.formats.landmark_pb2.Landmark 对象的列表,每个对象表示一个关键点。 - x:关键点的 x 坐标(归一化值,范围为 [0, 1])。 - y:关键点的 y 坐标(归一化值,范围为 [0, 1])。 - z:关键点的 z 坐标(相对深度值,范围为 [-1, 1],表示关键点相对于手腕的深度)。 - visibility:关键点的可见性分数(范围为 [0, 1],值越大表示关键点越可见)。 - 2. results.multi_handedness 这是一个列表,包含每只手的手性(handedness)信息。每只手的手性是一个 mediapipe.framework.formats.classification_pb2.ClassificationResult 对象。 - classification:一个包含 mediapipe.framework.formats.classification_pb2.Classification 对象的列表,每个对象表示手性的分类结果。 - index:分类结果的索引(通常为 0)。 - score:分类的置信度分数(范围为 [0, 1],值越大表示分类越可靠)。 - label:分类的标签(字符串,值为 "Left" 或 "Right",表示左手或右手)。 - 3. results.multi_hand_world_landmarks 这是一个列表,包含每只手的关键点在世界坐标系中的位置。每只手的关键点是一个 mediapipe.framework.formats.landmark_pb2.LandmarkList 对象,其中包含 21 个关键点的三维坐标信息。 - landmark:一个包含 21 个 mediapipe.framework.formats.landmark_pb2.Landmark 对象的列表,每个对象表示一个关键点。 - x:关键点的 x 坐标(世界坐标系中的值,单位为米)。 - y:关键点的 y 坐标(世界坐标系中的值,单位为米)。 - z:关键点的 z 坐标(世界坐标系中的值,单位为米)。

我们这里只关注results.multi_hand_landmarks:

  1. 对检测到的手部地标点进行点位和连线绘图
    if (results.multi_hand_landmarks):
            for handlms in results.multi_hand_landmarks:
                mediapipe.solutions.drawing_utils.draw_landmarks(
                            img, 
                                handlms, 
                                mediapipe.solutions.hands.HAND_CONNECTIONS)

示例代码中使用 mediapipe 模块内置的点位和连线绘制函数。可以按照自己的需求,自己编写绘制点位和连线的函数,例如只需要某个绘制手指的地标点点位,不需要连线等。

4. 地标点坐标归一化值转换为图片像素值

  1. 每个手型,由21个地标点组成,每个地标点为(x,y,z),值为(0-1),相对于图像的长宽比例,需将xy的比例坐标转换成图像像素坐标:
  2. 比例坐标x乘以宽度width,得到坐标点的X轴的像素坐标;
  3. 比例坐标y乘以宽度height,得到坐标点的Y轴的像素坐标;
  4. 像素坐标需要取整。

示例代码:

h, w, c = img.shape  # 得到图像的 长、宽、通道数
for handlms in results.multi_hand_landmarks:
    for index, lm in enumerate(handlms.landmark):        
        cx ,cy =  int(lm.x * w), int(lm.y * h)
        print("index=",index,"x=",cx,"y=",cy)
  1. 新建空白py代码文件,编写获取手部地标点的py示例代码,地标点坐标从归一化值转换为图片像素值并绘制到图片上。
gedit handmarks.py

如果gedit未授权,还需要对gedit进行root账号的X图形服务器权限授权,允许 root 用户从本地连接到 X 服务器,若已授权,跳过此步:

xhost +local:root
  1. 将如下代码复制到文件中。
import cv2
import mediapipe

#-----------------------------------------------------------------------
def imgHeightResize(img, fixedHeight):
    imgResized = img
    imgH,imgW,imgChs = imgResized.shape    
    ratioH = fixedHeight/imgH
    imgResized = cv2.resize(img,None,fx=ratioH,fy=ratioH,interpolation=cv2.INTER_CUBIC)
    return imgResized
#------------------------------------------------------------------------------ 
def imgshow_hand_landmarks(img,handLms):
    h, w = img.shape[0], img.shape[1]             
    for idx, coord in enumerate(handLms.landmark):
        cx = int(coord.x * w)
        cy = int(coord.y * h)
        cv2.circle(img, (cx, cy), 1, (255,0,0), -1)
        img = cv2.putText(img, 
                         ( "%d(%d,%d)"%(idx,cx,cy) ), 
                         (cx, cy), 
                         cv2.FONT_HERSHEY_SIMPLEX,
                         0.35,(0, 0, 0), 1, 1)
#-----------------------------------------------------------------------
if __name__ == '__main__':
    imgOrg = cv2.imread("hand.png")
    img = imgHeightResize(imgOrg, 640)
    cv2.imshow("hand",img)

    imgRGB = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
    handDetector = mediapipe.solutions.hands.Hands(static_image_mode=True,
                                    model_complexity=1,
                                    max_num_hands=2,
                                    min_detection_confidence=0.8,
                                    min_tracking_confidence=0.8)
    results = handDetector.process(imgRGB)
    if (results.multi_hand_landmarks):
        for handlms in results.multi_hand_landmarks:
            #mediapipe.solutions.drawing_utils.draw_landmarks(
            #                     img, 
        #                     handlms, 
        #                     mediapipe.solutions.hands.HAND_CONNECTIONS)
            imgshow_hand_landmarks(img, handlms)
    else:
        cv2.putText(img,"no hands detected",(100,100),cv2.FONT_HERSHEY_COMPLEX,1,(0,0,255),2,8)

    cv2.imshow("hand-landmarks",img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

  1. 执行代码。
python3 handmarks.py

六、识别剪刀手(V)手势

1. 算法原理

  1. 识别剪刀手手势的过程:

  2. 算法思想: 识别出剪刀手V型手势的准则: 原则1. 食指和中指是伸直状态:

    • 食指和中指的有序四个坐标点处于直线状态(不完全的高度近似直线)。 原则2. 大拇指、无名指、小指都是弯曲状态:
    • 这3个手指的有序4个坐标点不处于直线状态。 原则3. 食指和中指之间的 夹角大于一个角度。

判断某个手指是否伸直或蜷曲的简单算法:

2. 识别过程

  1. 准备几张剪刀手手势的图片,复制到项目文件夹中,此过程不再赘述。

  2. 新建空白py文件,编写复制示例代码,保存。

gedit vShape.py

:每次创建空白py文件时,需先激活虚拟环境。

如果gedit未授权,还需要对gedit进行root账号的X图形服务器权限授权,允许 root 用户从本地连接到 X 服务器,若已授权,跳过此步:

xhost +local:root
  1. 将如下代码复制到文件中。
import cv2
import mediapipe as mp
import numpy as np
import math
import sys


#-------------------------------------
def imgshow_piont(textimg,idx,cx,cy):
  cv2.circle(textimg, (cx, cy), 1, (255,0,0), -1)
  img = cv2.putText(textimg, 
            #( "%d(%d,%d)"%(idx,cx,cy) ), 
            ( "%d"%(idx) ), 
            (cx, cy), 
            cv2.FONT_HERSHEY_SIMPLEX,
            0.3,(0, 0, 0), 1, 1)
#-------------------------------------
def imgshow_finger_landmark(img,handNo,fingerName,
                                idx_0,p0_x,p0_y,
                                idx_1,p1_x,p1_y,
                                idx_2,p2_x,p2_y,
                                idx_3,p3_x,p3_y ):
  h, w, c = img.shape    
  imgSpace = np.full((60,w,3),255,np.uint8)
  fingerImg = np.vstack((img,imgSpace))

  imgshow_piont(fingerImg,idx_0,p0_x,p0_y)
  imgshow_piont(fingerImg,idx_1,p1_x,p1_y)
  imgshow_piont(fingerImg,idx_2,p2_x,p2_y)
  imgshow_piont(fingerImg,idx_3,p3_x,p3_y)

  cv2.putText(fingerImg,fingerName,
             (10,50+h),
             cv2.FONT_HERSHEY_SIMPLEX,1,(0,0,0),2)
  #cv2.imshow("handNo-"+str(handNo)+"-finger-"+fingerName,fingerImg)  

#-------------------------------------
def isFingerStraight(handIdx,fingerName,p0_x,p0_y,p1_x,p1_y,p2_x,p2_y,p3_x,p3_y):
  #判断四个坐标点是否近似直线,手指的4个地标点是否近似伸直
  #方法:
  # 计算3个线段的线段(0-1,1-2,2-3)长度和L01+L12+L23,首尾两点(0-3)之间的线段长度L03,
  # 如果L01+L12+L23 接近 L03,说明手指是伸直的;
  # 如果 L01+L12+L23 超过 L03较多,说明手指弯曲
  # 这个超过程度,通过比例因子来衡量
  straight = 0

  distance_p0_p1 = math.sqrt( (p1_x - p0_x)**2 + (p1_y - p0_y)**2 )
  distance_p1_p2 = math.sqrt( (p2_x - p1_x)**2 + (p2_y - p1_y)**2 )
  distance_p3_p2 = math.sqrt( (p3_x - p2_x)**2 + (p3_y - p2_y)**2 )
  distance_p0_p3 = math.sqrt( (p3_x - p0_x)**2 + (p3_y - p0_y)**2 )

  total = distance_p0_p1 + distance_p1_p2 + distance_p3_p2

  ratio = 0.99

  if ( distance_p0_p3 < (total * ratio)  ):
    straight = 0 #finger bent
  else:
    straight = 1

  print("handIdx=%d,fingerName=%s,L01+L12+L23=%.2f L03=%.2f actual ratio=%.3f"%(handIdx,fingerName,total,distance_p0_p3,(distance_p0_p3/total)))

  return straight

#-------------------------------------
def calculateFitLineDegree(p0_x,p0_y,p1_x,p1_y,p2_x,p2_y,p3_x,p3_y):
  """
  拟合一条直线,并计算每个点到这条直线的垂直距离。
  输入:points: 一个包含四个坐标的列表
  返回:拟合直线的角度
  """

  # 使用numpy.polyfit拟合直线,得到斜率和截距
  slope, intercept = np.polyfit((p0_x,p1_x,p2_x,p3_x), (p0_y,p1_y,p2_y,p3_y), 1)

  # 计算夹角(单位为弧度)
  theta_radians = np.arctan(slope)

  # 将弧度转换为度数
  theta_degrees = np.degrees(theta_radians)

  return theta_degrees    
#-------------------------------------
# 判断手势V型(剪刀手)的原则 : 只有食指和中指竖起,且两者之间夹角大于10度?
# 掌根 0
# 拇指(Thumb),1,2,3,4
# 食指(Index Finger),四个地标点序号: 5,6,7,8
# 中指(Middle Finger),四个地标点序号: 9,10,11,12
# 无名指(Ring Finger),13,14,15,16
# 小指(Pinky Finger),17,18,19,20
def hand_gesture_vShape(img,handResult):
  h, w, c = img.shape    
  imgSpace = np.full((60,w,3),255,np.uint8)
  textImg = np.vstack((img,imgSpace))    

  vShapeCnt = 0
  finger_straight_cnt = 0

  for index,handlms in enumerate(results.multi_hand_landmarks):

    p01_x = int(handlms.landmark[1].x * w )
    p01_y = int(handlms.landmark[1].y * h )
    p02_x = int(handlms.landmark[2].x * w )
    p02_y = int(handlms.landmark[2].y * h )
    p03_x = int(handlms.landmark[3].x * w )
    p03_y = int(handlms.landmark[3].y * h )
    p04_x = int(handlms.landmark[4].x * w )
    p04_y = int(handlms.landmark[4].y * h )
    p05_x = int(handlms.landmark[5].x * w )
    p05_y = int(handlms.landmark[5].y * h )
    p06_x = int(handlms.landmark[6].x * w )
    p06_y = int(handlms.landmark[6].y * h )
    p07_x = int(handlms.landmark[7].x * w )
    p07_y = int(handlms.landmark[7].y * h )
    p08_x = int(handlms.landmark[8].x * w )
    p08_y = int(handlms.landmark[8].y * h )
    p09_x = int(handlms.landmark[9].x * w )
    p09_y = int(handlms.landmark[9].y * h )
    p10_x = int(handlms.landmark[10].x * w)
    p10_y = int(handlms.landmark[10].y * h)
    p11_x = int(handlms.landmark[11].x * w)
    p11_y = int(handlms.landmark[11].y * h)
    p12_x = int(handlms.landmark[12].x * w)
    p12_y = int(handlms.landmark[12].y * h)
    p13_x = int(handlms.landmark[13].x * w)
    p13_y = int(handlms.landmark[13].y * h)
    p14_x = int(handlms.landmark[14].x * w)
    p14_y = int(handlms.landmark[14].y * h)
    p15_x = int(handlms.landmark[15].x * w)
    p15_y = int(handlms.landmark[15].y * h)
    p16_x = int(handlms.landmark[16].x * w)
    p16_y = int(handlms.landmark[16].y * h)
    p17_x = int(handlms.landmark[17].x * w)
    p17_y = int(handlms.landmark[17].y * h)
    p18_x = int(handlms.landmark[18].x * w)
    p18_y = int(handlms.landmark[18].y * h)
    p19_x = int(handlms.landmark[19].x * w)
    p19_y = int(handlms.landmark[19].y * h)
    p20_x = int(handlms.landmark[20].x * w)
    p20_y = int(handlms.landmark[20].y * h)


    imgshow_finger_landmark(img,index,"damuzhi",
                                1,p01_x,p01_y,
                                2,p02_x,p02_y,
                                3,p03_x,p03_y,
                                4,p04_x,p04_y    )

    imgshow_finger_landmark(img,index,"shizhi",
                                5,p05_x,p05_y,
                                6,p06_x,p06_y,
                                7,p07_x,p07_y,
                                8,p08_x,p08_y    )

    imgshow_finger_landmark(img,index,"zhongzhi",
                                9 ,p09_x,p09_y,
                                10,p10_x,p10_y,
                                11,p11_x,p11_y,
                                12,p12_x,p12_y    )

    imgshow_finger_landmark(img,index,"wumingzhi",
                                13,p13_x,p13_y,
                                14,p14_x,p14_y,
                                15,p15_x,p15_y,
                                16,p16_x,p16_y    )

    imgshow_finger_landmark(img,index,"xiaozhi",
                                17,p17_x,p17_y,
                                18,p18_x,p18_y,
                                19,p19_x,p19_y,
                                20,p20_x,p20_y    )

    straightDamuzhiFlag = isFingerStraight(index,"damuzhi",p01_x,p01_y,p02_x,p02_y,p03_x,p03_y,p04_x,p04_y)
    finger_straight_cnt += straightDamuzhiFlag
    print("handNo=%d,straightDamuzhiFlag=%d,finger_straight_cnt=%d"%(index,straightDamuzhiFlag,finger_straight_cnt))

    straightShizhiFlag = isFingerStraight(index,"shizhi",p05_x,p05_y,p06_x,p06_y,p07_x,p07_y,p08_x,p08_y)
    finger_straight_cnt += straightShizhiFlag
    print("handNo=%d,straightShizhiFlag=%d,finger_straight_cnt=%d"%(index,straightShizhiFlag,finger_straight_cnt))

    straightZhongzhiFlag = isFingerStraight(index,"zhongzhi",p09_x,p09_y,p10_x,p10_y,p11_x,p11_y,p12_x,p12_y)
    finger_straight_cnt += straightZhongzhiFlag
    print("handNo=%d,straightZhongzhiFlag=%d,finger_straight_cnt=%d"%(index,straightZhongzhiFlag,finger_straight_cnt))

    straightWumingzhiFlag = isFingerStraight(index,"wumingzhi",p13_x,p13_y,p14_x,p14_y,p15_x,p15_y,p16_x,p16_y)
    finger_straight_cnt += straightWumingzhiFlag
    print("handNo=%d,straightWumingzhiFlag=%d,finger_straight_cnt=%d"%(index,straightWumingzhiFlag,finger_straight_cnt))

    straightXiaozhiFlag = isFingerStraight(index,"xiaozhi",p17_x,p17_y,p18_x,p18_y,p19_x,p19_y,p20_x,p20_y)
    finger_straight_cnt += straightXiaozhiFlag
    print("handNo=%d,straightXiaozhiFlag=%d,finger_straight_cnt=%d"%(index,straightXiaozhiFlag,finger_straight_cnt))

    if( (0==straightXiaozhiFlag) and (0==straightWumingzhiFlag) and (0==straightDamuzhiFlag) and
      (1==straightZhongzhiFlag)  and (1==straightShizhiFlag)  ):

      imgshow_piont(textImg,5,p05_x,p05_y)
      imgshow_piont(textImg,6,p06_x,p06_y)
      imgshow_piont(textImg,7,p07_x,p07_y)
      imgshow_piont(textImg,8,p08_x,p08_y)            
      imgshow_piont(textImg,9,p09_x,p09_y)
      imgshow_piont(textImg,10,p10_x,p10_y)
      imgshow_piont(textImg,11,p11_x,p11_y)
      imgshow_piont(textImg,12,p12_x,p12_y)

      degree_shizhi = calculateFitLineDegree( p05_x,p05_y,p06_x,p06_y,p07_x,p07_y,p08_x,p08_y )
      print("handNo=%d,degree_shizhi=%d"%(index,degree_shizhi))
      degree_zhongzhi = calculateFitLineDegree( p09_x,p09_y,p10_x,p10_y,p11_x,p11_y,p12_x,p12_y )
      print("handNo=%d,degree_zhongzhi=%d"%(index,degree_zhongzhi))
      angle = abs(degree_shizhi - degree_zhongzhi)
      if(angle > 10):
         vShapeCnt += 1
      else:
         print("angle_theta_zhongzhi_shizhi=%.2f too small(<10)"%(angle))

  cv2.putText(textImg,"vShapeCnt:"+str(vShapeCnt),
             (10,50+h),
             cv2.FONT_HERSHEY_SIMPLEX,1,(0,0,0),2)
  cv2.imshow("textImg",textImg)

#------------------------------------------------------------------------------    
def imgHeightResize(img, fixedHeight):
  imgResized = img
  imgH,imgW,imgChs = imgResized.shape    
  ratioH = fixedHeight/imgH
  imgResized = cv2.resize(img,None,fx=ratioH,fy=ratioH,interpolation=cv2.INTER_CUBIC)
  return imgResized
#------------------------------------------------------------------------------ 
def imgshow_hand_landmarks(img,handLms):
  h, w = img.shape[0], img.shape[1]             
  for idx, coord in enumerate(handLms.landmark):
    cx = int(coord.x * w)
    cy = int(coord.y * h)
    cv2.circle(img, (cx, cy), 1, (255,0,0), -1)
    img = cv2.putText(img, 
              ( "%d"%(idx) ),
              (cx, cy), 
              cv2.FONT_HERSHEY_SIMPLEX,
              0.35,(0, 0, 0), 1, 1)
#------------------------------------------------------------------------------
if __name__ == '__main__':
  # 检查命令行参数 python your_script.py example.txt
  if len(sys.argv) < 2:
      print("请提供文件名作为命令行参数!python3 your_script.py example.txt")
      sys.exit(1)

  # 获取文件名
  file_name = sys.argv[1]

  # 打开并读取文件
  try:
    with open(file_name, 'r', encoding='utf-8') as file:
      print(f"文件 '{file_name}' 找到")
  except FileNotFoundError:
    print(f"错误:文件 '{file_name}' 未找到!")
    sys.exit(1)
  except Exception as e:
    print(f"发生错误:{e}")
    sys.exit(1)

  imgOrg = cv2.imread(file_name)
  img = imgHeightResize(imgOrg, 640)

  imgRGB = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
  handDetector = mp.solutions.hands.Hands(static_image_mode=True,
                    model_complexity=1,
                    max_num_hands=10,
                    min_detection_confidence=0.5,
                    min_tracking_confidence=0.5)
  results = handDetector.process(imgRGB)
  if (results.multi_hand_landmarks):
    hand_gesture_vShape(img,results)
  else:
    cv2.putText(img,"no hands detected",(100,100),cv2.FONT_HERSHEY_COMPLEX,1,(0,0,255),2,8)

  cv2.imshow("img",img)
  cv2.waitKey(0)
  cv2.destroyAllWindows()

  1. 执行代码,命令行带参数-图片文件名。
python3 vShape.py v8.png

可以多测试不同的剪刀手图片,得到合适的算法阈值:准直线判断阈值、夹角阈值等:

3. 算法优化

一个出现识别误差的图片: 上述图片中: 标注一有个剪刀手手势未识别,是因为 mediapipe 未检测出手型,需要调整mediapipe的手部检测器的参数。因为案例中的识别算法非常简单,没有考虑其他判断准则,例如排查在手掌掌心范围内的伸直的手指等,开发者可以去优化和实践,本案例到此就已经完成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值