MediaPipe 手势识别:“猜拳”游戏(基础篇)
2023-05-30牟奕炫
牟奕炫
在第14期的《基于MediaPipe的Python编程手势识别应用》一文中,我们借助MediaPipe实现了手部21个关键点的精准识别。MediaPipe不仅可以判断识别各个独立的关键点,如果结合某些点的区域划分进行相关的逻辑运算,就能够非常方便地进行很多手势信息的“解读”,比如识别0-9十个数,进而在树莓派中实现简易“猜拳”游戏。
1.五个指尖关键点与“凸包”区域
为了对单手所表示的十个数进行手势识别,判断五个指尖关键点(4、8、12、16、20)与手掌心范围的相互位置是非常重要的环节。手掌心范围的界定可以通过“凸包”(convexhull)来实现,建立列表变量round_points,其值为[0,1,2,3,6,10,14,19,18,17,10],表示从手腕根部0开始,向上沿大拇指依次经过1、2、3点位,转至食指的6、中指的10、无名指的14,最终从小拇指的19、18、17点位返回至手腕根部0,如此便构建了一个包含手掌心在内的闭合区域(如图1)。
通过对五个指尖点是否在“凸包”内的判断(单独的某个指尖或是几个指尖的不同组合),就能够表示出0-9这十个数字,并且将相关的代码封装成函数。
2.手势数字的判断函数
导入“mediapipeasmp”“cv2”“numpyasnp”“math”库模块。
计算两个矢量角度finger_angle(point1,point2)函数:借助numpy库再通过两次数学计算,建立变量two_angle,赋值为“np.dot(point1,point2)/(np.sqrt(np.sum(point1**2))*np.sqrt(np.sum(point2**2)))”,再赋值为“np.arccos(two_angle)/math.pi*180”,最后将该值返回即可。
判断并返回手势信息finger_sign(tip_finger,list_data)函数的编写:1和9的共同点是均通过单根食指(直立或弯曲)来表示,判断条件是“iflen(tip_finger)==1andtip_finger[0]==8:”(其中的tip_finger存储的是“凸包”外的指尖关键点),意思是“凸包”外只检测到有一个指尖(即一根手指)并且该指尖关键点是8(即食指指尖);建立两个矢量point1、point2,赋值为“list_data[6]-list_data[7]”“list_data[8]-list_data[7]”,分别计算食指关键点6至7、8至7的矢量值;再通过调用函数为变量two_angle赋值:“finger_angle(point1,point2)”,并且进行“iftwo_angle<160:”的判断,如果该角度值小于160度则认定为“弯曲的食指”,表示手勢识别的结果是数字9;条件不成立,则认定为“直立的食指”,变量finger_sign存储的手势识别结果即为数字1。
两根手指可以表示2、6和8三种情况。对数字2的判断条件为“eliflen(tip_finger)==2andtip_finger[0]==8andtip_finger[1]==12:”,意思是共有两个指尖在“凸包”外,并且对应的指尖关键点分别是8(食指指尖)和12(中指指尖),表示伸出了食指和中指,对应的手势识别数字为2。数字6对应的两个指尖关键点是拇指指尖4和小指指尖20:“eliflen(tip_finger)==2andtip_finger[0]==4andtip_finger[1]==20:”,而数字8对应的则是拇指指尖4和食指指尖8。
数字3和7涉及三根手指。3的判断条件为“eliflen(tip_finger)==3andtip_finger[0]==8andtip_finger[1]==12andtip_finger[2]==16:”,意思是共有三个指尖在“凸包”外,关键点是8(食指指尖)、12(中指指尖)和16(无名指指尖);数字7的判断条件为“eliflen(tip_finger)==3andtip_finger[0]==4andtip_finger[1]==8andtip_finger[2]==12:”,对应的指尖关键点除了8(食指指尖)和12(中指指尖)外,用4(拇指指尖)代替了16(无名指指尖)。
数字4的判断条件为“eliflen(tip_finger)==4andtip_finger[0]==8andtip_finger[1]==12andtip_finger[2]==16andtip_finger[3]==20:”,即检测到有四个指尖处于“凸包”外;
数字5的判断条件为“eliflen(tip_finger)==5:”,表示检测到五个指尖全部处于“凸包”外;
数字0的判断条件为“eliflen(tip_finger)==0:”,表示在“凸包”外没有检测到任何一个指尖关键点。
如果以上if和elif十种可能性均不符合条件的话,则认定没有检测到有效的数字,变量finger_sign值为空("");最后打印输出提示信息并将finger_sign返回:“print("检测到的手势数字为:",finger_sign)”、“returnfinger_sign”(如图2)。(源代码请至壹零社公众号下载。)
3.编写main()主程序代码
参考第14期代码,调用摄像头进行检测对象的定义等相关初始化操作:“camera=cv2.VideoCapture(0)”“mpHands=mp.solutions.hands”“hands=mpHands.Hands()”“mpDraw=mp.solutions.drawing_utils”;在“whileTrue:”循环结构中,先读取摄像头所捕获的画面信息(包括画面的宽度和高度值)、将BGR模式转换为RGB模式等操作,再进行所有指尖关键点二维坐标值的采集:建立变量list_data(赋值为空列表),通过“foriinrange(21):”循环,获取对应的(x,y)坐标值:“x,y=int(hand.landmark[i].x*w),int(hand.landmark[i].y*h)”,并将其追加至list_data中:“list_data.append([x,y])”。
接着进行“凸包”区域的界定,包括三个语句:“list_data=np.array(list_data,dtype=np.int32)”“round_points=[0,1,2,3,6,10,14,19,18,17,10]”和“hull_data=cv2.convexHull(list_data[round_points,:])”,再通过语句“cv2.polylines(img,[hull_data],True,(0,0,255),3)”实现“凸包”的红色线框绘制;然后,进行“凸包”区域外指尖关键点的查找及画面结果信息的顯示标注:变量tip_list值为“[4,8,12,16,20]”,对应五个指尖的关键点编号,在“foriintip_list:”循环中先建立变量position,赋值为“(int(list_data[i][0]),int(list_data[i][1]))”,即点的坐标;建立变量dist,赋值为“cv2.pointPolygonTest(hull_data,position,True)”,作用是检测这些点是否在“凸包”内,判断条件“ifdist<0:”成立的话,说明关键点在“凸包”外,则将该数据追加:“tip_finger.append(i)”,循环结束后完成所有处于“凸包”外的点的收集。然后建立变量draw_finger_sign,调用函数“finger_sign(tip_finger,list_data)”,参数tip_finger和list_data分别表示“凸包”外关键点的列表和关键点的(x,y)坐标;语句“cv2.putText(img,'%s'%(draw_finger_sign),(530,120),cv2.FONT_HERSHEY_SIMPLEX,5,(0,0,255),4,cv2.LINE_AA)”实现的功能是在视频画面的右上角位置显示输出手势识别的数字(红色);下面的“foriintip_list:”循环作用是将五个指尖关键点进行二次描绘,先获取(x,y)坐标值:“int(hand.landmark[i].x*w),int(hand.landmark[i].y*h)”,再使用粉红色绘制圆点:“cv2.circle(img,(x,y),7,(255,0,255),-1)”。
最后,进行视频窗口名称设置、热键退出、摄像头资源的释放及窗口的关闭等操作。
4.测试
将程序保存为“[01]Recognize_Number.py”,按F5键运行测试;程序能够快速准确地进行手势识别——提示信息显示有“检测到的手势数字为:X”,同时摄像头画面右上角也同步显示有该数字。