手势点灯任务三:利用mediapipe实现手势识别数字0~6

任务三:利用mediapipe实现手势识别数字


tis1:什么是mediapipe?
Mediapipe是google的⼀个开源项⽬,可以提供开源的、跨平台的常用机器学习(machine
learning)方案。实际上就是一个集成的机器学习视觉算法的工具库,包含了人脸检测、人脸关键点、手势识别、头像分割和姿态识别等各种模型。

本任务将调用mediapipe中训练好的模型进行应用。
mediapipe包在手势识别的应用方法中主要分为两个部分:
第一部分是发现并提取手部特征,并实现手部特征的可视化(标记手部关节和手部连线)
第二部分是根据提取的手部特征实现手势数字识别。
该模型可以识别出手部的20个关键节点。通过对20个关键节点位置距离的计算和比较就可以很容易地区分不同的手势。


tis2:该部分可以参考B站教程

一、mediapipe的安装及导入

  • 理论上一个pip install就可以,but可能出现兼容性错误的问题
    博主在做的时候出现了TensorFlow和Numpy版本不兼容的现象,通过查询网上资料发现可能是TensorFlow版本过低了,升级了过后就能正常调用了

  • 至于为啥扯到了TensorFlow,因为当时博主在网上查到说下mediapipe需要先下好TensorFlow(后面回看好像也不是刚需…)

二、对mediapipe和opencv库相关知识的学习

(一)关于opencv库的调用

摄像头的捕捉的基本代码
# 调用笔记本内置摄像头,参数为0
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

while True:
	# 从摄像头读取图片
	success, img = cap.read()
	if success:
		# 显示摄像头
		cv2.imshow(" img ", img)
	# 通过 esc 键退出摄像
	if cv2.waitKey(1) == 27:
		cv2.destroyAllWindows()
		break
# 关闭摄像头
cap.release()
  • cv2.VideoCapture(0, cv2.CAP_DSHOW):第一个参数为对应摄像头代号,第二个参数 cv2.CAP_DSHOW 是一个可选的参数
  • cap.read():用于从摄像头读取图像帧,返回一个布尔值 success 用于指示读取是否成功,以及图像数据 img
  • cv2.imshow(" img “, img):” img " 是显示窗口的标题,img 是要显示的图像
  • cv2.destroyAllWindows():关闭窗口
  • cap.release():关闭摄像头

(二)关于mediapipe的学习

  • 以下代码可以copy(毕竟能成功实现真的是件很快乐的事~~),不过要注意实现思路和逻辑
1、关于BGR和RGB的知识

  • R:red、G:green、B:blue
  • 大多数情况下为RGB(红绿蓝),包括在mediapipe中;但python和openCV默认的顺序是BGR(蓝绿红),故涉及转换
  • RGB中蓝色占据最不重要的“区域”(32位和24位格式的字节),绿色第二,红色最大
  • RGB颜色存储在结构或无符号整数中,例如:color=(0, 0, 255)表示全红
  • 在python中书写代码时按照BGR写,但要传入图像给mediapipe时,就应当加上转换
  • 转换很简单,用imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)即可,cv2.cvtColor()是一个用于颜色空间转换的函数(cv2.COLOR_BGR2RGB中2=two=to)
2、先码一码实现一波
  • findHind()函数
def findHind(img, hands, draw):
        
	imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 转换为RGB
	
	handlmsstyle = draw.DrawingSpec(color=(0, 0, 255), thickness=5)
	handconstyle = draw.DrawingSpec(color=(0, 255, 0), thickness=5)

	results = hands.process(imgRGB)
	if results.multi_hand_landmarks:
		for handLms in results.multi_hand_landmarks:
			draw.draw_landmarks(img, handLms, mp.solutions.hands.HAND_CONNECTIONS, handlmsstyle, handconstyle)
    return results.multi_hand_landmarks
  • detectNumber()函数
def detectNumber(hand_landmarks, img):

     # 找出我们所需的手指特征点
    thumb_tip_id = 4  # 大拇指指尖
    index_finger_tip_id = 8  # 食指指尖
    middle_finger_tip_id = 12  # 中指指尖
    ring_finger_tip_id = 16  # 无名指指尖
    pinky_finger_tip_id = 20  # 小指指尖
    pinky_finger_mcp_id = 17  # 小指指根(用于判断4和5)
    wrist_id = 0  # 手腕(用于识别数字6)

	# 提取上述所有特征点的坐标
	#提取y坐标
	thumb_tip_y = hand_landmarks[thumb_tip_id].y * h
	index_finger_tip_y = hand_landmarks[index_finger_tip_id].y * h
    middle_finger_tip_y = hand_landmarks[middle_finger_tip_id].y * h
    ring_finger_tip_y = hand_landmarks[ring_finger_tip_id].y * h
    pinky_finger_tip_y = hand_landmarks[pinky_finger_tip_id].y * h
    pinky_finger_mcp_y = hand_landmarks[pinky_finger_mcp_id].y * h
    wrist_y = hand_landmarks[wrist_id].y * h
	
	# 提取x坐标
	thumb_tip_x = hand_landmarks[thumb_tip_id].x * w
    index_finger_tip_x = hand_landmarks[index_finger_tip_id].x * w
    middle_finger_tip_x = hand_landmarks[middle_finger_tip_id].x * w
    ring_finger_tip_x = hand_landmarks[ring_finger_tip_id].x * w
    pinky_finger_tip_x = hand_landmarks[pinky_finger_tip_id].x * w
    pinky_finger_mcp_x = hand_landmarks[pinky_finger_mcp_id].x * w
    wrist_x = hand_landmarks[wrist_id].x * w

	# 计算大拇指到所有点的距离
    dist_thumb2index = math.sqrt((thumb_tip_x - index_finger_tip_x)**2 + (thumb_tip_y - index_finger_tip_y)**2)
	dist_thumb2middle = math.sqrt((thumb_tip_x - middle_finger_tip_x) ** 2 + (thumb_tip_y - middle_finger_tip_y) ** 2)
	dist_thumb2ring = math.sqrt((thumb_tip_x - ring_finger_tip_x) ** 2 + (thumb_tip_y - ring_finger_tip_y) ** 2)
    dist_thumb2pinky = math.sqrt((thumb_tip_x - pinky_finger_tip_x) ** 2 + (thumb_tip_y - pinky_finger_tip_y) ** 2)
    dist_thumb2pinky_mcp = math.sqrt((thumb_tip_x - pinky_finger_mcp_x) ** 2 + (thumb_tip_y - pinky_finger_mcp_y) ** 2)

    # 食指、中指、无名指指尖到手腕的距离
    dist_index2wrist = math.sqrt((index_finger_tip_x - wrist_x) ** 2 + (index_finger_tip_y - wrist_y) ** 2)
    dist_middle2wrist = math.sqrt((middle_finger_tip_x - wrist_x) ** 2 + (middle_finger_tip_y - wrist_y) ** 2)
    dist_ring2wrist = math.sqrt((ring_finger_tip_x - wrist_x) ** 2 + (ring_finger_tip_y - wrist_y) ** 2)

    # 识别数字
    # 这里博主宏定义dist_limit=150,dist_limit1=100(读者可自行增减)
    # 识别数字1
    if dist_thumb2pinky < dist_limit and dist_thumb2index > dist_limit1 and dist_thumb2middle < dist_limit and dist_thumb2ring < dist_limit:
		return 1
	# 识别数字2
	elif dist_thumb2pinky < dist_limit and dist_thumb2index > dist_limit and dist_thumb2middle > dist_limit and dist_thumb2ring < dist_limit:
		return 2
	# 识别数字3
	elif dist_thumb2pinky < dist_limit1 and dist_thumb2index > dist_limit and dist_thumb2middle > dist_limit and dist_thumb2ring > dist_limit:
		return 3
	# 识别数字5
	elif dist_thumb2pinky > dist_limit and dist_thumb2index > dist_limit and dist_thumb2middle > dist_limit and dist_thumb2ring > dist_limit and dist_thumb2pinky_mcp > dist_limit:
		return 5
	# 识别数字4
	elif dist_thumb2pinky > dist_limit and dist_thumb2index > dist_limit and dist_thumb2middle > dist_limit and dist_thumb2ring > dist_limit and dist_thumb2pinky_mcp < dist_limit:
		return 4
	# 识别数字6
	elif dist_index2wrist < dist_limit and dist_middle2wrist < dist_limit and dist_ring2wrist < dist_limit:
		return 6
	# 识别数字0
	elif dist_thumb2pinky < dist_limit and dist_thumb2index < dist_limit and dist_thumb2middle < dist_limit and dist_thumb2ring < dist_limit:
		return 0
	else:
		return -1        
  • 主体结构
hands = mp.solutions.hands.Hands()
draw = mp.solutions.drawing_utils

# 调用笔记本内置摄像头,参数为0
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

while True:
	# 从摄像头读取图片
	success, img = cap.read()
	if success:
		# 调用hands_landmarks函数找到手的特征
		hands_landmarks = findHind(img, hands, draw)
        if hands_landmarks:
			# 调用detectNumber函数
            resultNumber = detectNumber(hands_landmarks, img)
            if resultNumber >= 0:
				# 显示数字到视频窗口
				cv2.putText(img, str(resultNumber), (150, 150), 19, 5, (255, 0, 255), 5, cv2.LINE_AA)
                # 参数:视频窗口,显示内容,开始位置,字体,字体大小,字体颜色,画笔笔粗细,线条种类
                if resultNumber == 0:
                    ser.write(b'0')
                elif resultNumber == 1:
                    ser.write(b'1')
                elif resultNumber == 2:
                    ser.write(b'2')
                elif resultNumber == 3:
                    ser.write(b'3')
                elif resultNumber == 4:
                    ser.write(b'4')
                elif resultNumber == 5:
                    ser.write(b'5')
                elif resultNumber == 6:
                    ser.write(b'6')
            else:
                cv2.putText(img, "NO NUMBER", (150, 150), 20, 1, (0, 0, 255))
            # 显示摄像头
            cv2.imshow("Gesture Recognition", img)
	# 通过 esc 键退出摄像
 	if cv2.waitKey(1) == 27:
 		cv2.destroyAllWindows()
        break
# 关闭摄像头
cap.release()
3、涉及到的函数注解
  • draw.DrawingSpec():用于定义绘图样式后两个参数比较好理解,前一个是颜色(注意是BGR),后 一个是绘制线条长度

  • hands.process():Mediapipe库函数,用于手部识别

    • 接受一张图像帧作为输入
    • 然后进行手部检测、手部关键点提取
    • 将检测到的手部关键点信息组织成适当的数据结构,并将其作为输出返回给调用方
  • results.multi_hand_landmarks:一个包含多个手部的列表

    for handLms in results.multi_hand_landmarks:

    • 对该列表中每个HandLms进行遍历
    • handLms在手部检测中通过Mediapipe库检测到并返回的手部关键点信息
  • draw.draw_landmarks(img, handLms, mp.solutions.hands.HAND_CONNECTIONS, handlmsstyle, handconstyle)
    传递mp.solutions.hands.HAND_CONNECTIONS参数来绘制手部关键点之间的连接线

  • h, w, c = img.shape 高度、宽度、通道(如果⼀个像素点,有RGB三种颜⾊来描述它,就是三通道)

  • myHand = hand_landmarks[0]:找到第一只手,提取出其特征(hand_landmarks是手的列表)

  • hand_landmarks = myHand.landmark 获取手部的关键点信息(一个手中21个关键点的具体位置信息,每个关键点由其索引、位置的x、y和z坐标表示)

  • thumb_tip_y = hand_landmarks[thumb_tip_id].y * h
    *h的意义:将手势图像中的 y 坐标转换为与图像或屏幕高度相对应的数值

  • cv2.putText(img, str(resultNumber), (150, 150), 19, 5, (255, 0, 255), 5, cv2.LINE_AA):参数极多是吧。。。包括:视频窗口,显示内容,开始位置,字体,字体大小,字体颜色,画笔笔粗细,线条种类(写的时候看提示吧。。。)

4、简单解释一下detectNumber()函数的数学原理
  • 其实也没啥好解释的。。。
  • 判断0~5:利用大拇指指尖到其他指尖的距离作为判断依据
  • 判断6:利用指尖到手腕作为判断依据
  • (此时的你一定也发现了一些小问题吧)
5、效果展示
  • findhand()函数测试
    在这里插入图片描述
  • detectNumber()函数测试
    这里就不放VCR了(因为效果实在是不好,观赏性极差╭(╯^╰)╮)
    读者可以尝试调参提高识别率(博主试了下,好像不大行)

(三)方法改进-----提高识别率!!

1、核心:手与屏幕间距离对识别结果的影响很大
  • 我发现上述方法能成功识别有一个先决条件:手与屏幕间距离在一定范围内
  • 手拿近了会导致4和5不好区分,手拿远了会导致0和6分不清楚
  • 特别是0和6,很难区分
2、解决问题的一些想法及实施
  • 一些想法

    • 使用距离比值进行替换,或者将某段长度作为单位量表示其它量(二者原理一致)

    • 使用指尖到手腕间的长度作为比较值,可以在一定程度上放大差异

  • 想法实施

    • 以大拇指到手腕的距离为基准得到相关参数
      a3e41cf01f5534ed3016bf32c4c76c85.jpeg
      上图中最后一行写错位了。。
      另外:
      • 向下的箭头表示数据沿用(作者太懒了,确信)
      • ①~④表示各指指尖到手腕的距离/大拇指指尖到手腕的距离;⑤是大拇指与小拇指指尖的距离/大拇指指尖到手腕的距离
    • 优化参数的问题及思路
      • 不一定都要取上下界,对没有必要的上下界应当删除
      • 取得值范围太小,冗余过大,可以适当放宽
      • 最终结果
        f92be10467374747df1ea68ed7c83de2.jpeg
  • 所以只需要改下detectNumber()函数的逻辑就可以啦!

def detectNumber(hand_landmarks, img):
    """

    :param hand_landmarks: 手势特征
    :param img: 实时图像
    :return: 返回识别到的数字,如果没有则返回-1
    """

    h, w, c = img.shape

    myhand = hand_landmarks[0]
    hand_landmark = myhand.landmark

    thumb_tip_id = 4  # 大拇指指尖
    index_finger_tip_id = 8  # 食指指尖
    middle_finger_tip_id = 12  # 中指指尖
    ring_finger_tip_id = 16  # 无名指指尖
    pinky_finger_tip_id = 20  # 小指指尖
    pinky_finger_mcp_id = 17  # 小指指根(用于判断4和5)
    wrist_id = 0  # 手腕(用于识别数字6)

    # 提取y坐标
    thumb_tip_y = hand_landmark[thumb_tip_id].y * h
    index_tip_y = hand_landmark[index_finger_tip_id].y * h
    middle_tip_y = hand_landmark[middle_finger_tip_id].y * h
    ring_tip_y = hand_landmark[ring_finger_tip_id].y * h
    pinky_tip_y = hand_landmark[pinky_finger_tip_id].y * h
    pinky_mcp_y = hand_landmark[pinky_finger_mcp_id].y * h
    wrist_y = hand_landmark[wrist_id].y * h

    # 提取x坐标
    thumb_tip_x = hand_landmark[thumb_tip_id].x * w
    index_tip_x = hand_landmark[index_finger_tip_id].x * w
    middle_tip_x = hand_landmark[middle_finger_tip_id].x * w
    ring_tip_x = hand_landmark[ring_finger_tip_id].x * w
    pinky_tip_x = hand_landmark[pinky_finger_tip_id].x * w
    pinky_mcp_x = hand_landmark[pinky_finger_mcp_id].x * w
    wrist_x = hand_landmark[wrist_id].x * w

    dist_thumb2wrist = math.sqrt((thumb_tip_x - wrist_x)**2 + (thumb_tip_y - wrist_y)**2)
    dist_index2wrist = math.sqrt((index_tip_x - wrist_x) ** 2 + (index_tip_y - wrist_y) ** 2)
    dist_middle2wrist = math.sqrt((middle_tip_x - wrist_x) ** 2 + (middle_tip_y - wrist_y) ** 2)
    dist_ring2wrist = math.sqrt((ring_tip_x - wrist_x) ** 2 + (ring_tip_y - wrist_y) ** 2)
    dist_pinky2wrist = math.sqrt((pinky_tip_x - wrist_x) ** 2 + (pinky_tip_y - wrist_y) ** 2)

    dist_pinky_mcp2wrist = math.sqrt((thumb_tip_x - pinky_mcp_x)**2 + (thumb_tip_y - pinky_mcp_y)**2)

    # 相当于取dist_thumb2wrist_ratio == 1
    dist_index2wrist_ratio = dist_index2wrist / dist_thumb2wrist
    dist_middle2wrist_ratio = dist_middle2wrist / dist_thumb2wrist
    dist_ring2wrist_ratio = dist_ring2wrist / dist_thumb2wrist
    dist_pinky2wrist_ratio = dist_pinky2wrist / dist_thumb2wrist
    dist_pinky_mcp2wrist_ratio = dist_pinky_mcp2wrist / dist_thumb2wrist

    # print(dist_index2wrist_ratio, dist_middle2wrist_ratio, dist_ring2wrist_ratio, dist_pinky2wrist_ratio, dist_pinky_mcp2wrist_ratio)

    if dist_index2wrist_ratio < 1.9 and dist_middle2wrist_ratio < 1.8 and dist_ring2wrist_ratio < 1.6 and dist_pinky2wrist_ratio < 1.4 and dist_pinky_mcp2wrist_ratio < 0.8:
        return 0
    elif 2.0 < dist_index2wrist_ratio and dist_middle2wrist_ratio < 1.8 and dist_ring2wrist_ratio < 1.6 and dist_pinky2wrist_ratio < 1.4 and dist_pinky_mcp2wrist_ratio < 0.8:
        return 1
    elif 2.0 < dist_index2wrist_ratio and 1.9 < dist_middle2wrist_ratio and dist_ring2wrist_ratio < 1.6 and dist_pinky2wrist_ratio < 1.4 and dist_pinky_mcp2wrist_ratio < 0.8:
        return 2
    elif 2.0 < dist_index2wrist_ratio and 1.9 < dist_middle2wrist_ratio and 1.75 < dist_ring2wrist_ratio and dist_pinky2wrist_ratio < 1.4 and dist_pinky_mcp2wrist_ratio < 0.8:
        return 3
    elif 2.0 < dist_index2wrist_ratio and 1.9 < dist_middle2wrist_ratio and 1.75 < dist_ring2wrist_ratio and 1.5 < dist_pinky2wrist_ratio and dist_pinky_mcp2wrist_ratio < 0.8:
        return 4
    elif dist_index2wrist_ratio > 0.5 and dist_middle2wrist_ratio > 0.5 and dist_ring2wrist_ratio > 0.5 and 0.9 < dist_pinky_mcp2wrist_ratio < 1.2:
        return 5
    elif dist_index2wrist_ratio < 0.5 and dist_middle2wrist_ratio < 0.5 and dist_ring2wrist_ratio < 0.5:
        return 6
    else:
        return -1
3、效果呈现

手势识别(距离比版本)

三、硬件部分代码书写

  • 该部分难度不大,就是任务二的扩展
  • 这里就不给大家直接放源码啦,自己写写熟悉一下(~ ̄▽ ̄)~ 23c76678d14ef56d44c8a3641a59fd4d.jpeg
    b5dcfc4f77fd694b6e2b3a2f6f1ca36c.jpeg
    9d908190e0d1e4e9b5ce5a5808e34928.jpeg
  • 示例有一定省略(因为截图截不下来了)
  • 没截到的部分是类似重复片段

四、最终成果

  • 这里是博主的最终成果

手势点灯最终效果

  • 相信你一定会发现,这个方法依然会有问题
  • 不过没有关系,至少现在你已经实现了任务目标
  • 是不是很有成就感呢✺◟(∗❛ัᴗ❛ั∗)◞✺

其实关于手势识别数字,网上还有其它办法

  • 在下一个任务点,我们会讨论另外一个方法(效果比这个好得多!)

特别声明:以上的图片部分来自于网络,感谢CSDN、知乎等平台上各位博主的分享,本文用作交流学习予以引用,在此一并表示感谢!

  • 37
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值