每个人儿时都有一个武侠梦,梦想自己有一天10块钱买到一本如来神掌,从此仗剑走天涯,不过在桃花岛,真的有一本秘籍:隔空玩游戏,今天,就带领大家翻阅翻阅这本秘籍。
这本秘籍全部都是用python来完成的哦,想要学会它然后仗剑走天涯,就要先学会一部分python知识,好了,我们先来看看效果吧~
演示完毕,现在我们来进入教学阶段吧~本次我们只讨论手势识别的过程,用python写游戏,我们放在后续的章节放出来吧。
首先,一个很复古很经典的问题:把大象放进冰箱需要几步?
1. 打开冰箱门
2. 把大象放进去
3. 关上门
手势识别的过程也是这样:
1. 打开摄像头
2. 识别手势
3. 关闭摄像头
4. 本文结束哈哈哈
好了,进入正题吧!这次手势识别用到的python库主要有OpenCV(一个很强大很好用免费开源的计算机视觉工具)、NumPy(矩阵运算)。
打开摄像头:
我们在python中导入必要的模块
import cv2
import numpy as np
定义好摄像头
camera = cv2.VideoCapture(0) # 0代表本机自带摄像头
写一个循环,不停的从摄像头中取数据,摄像头会把每一帧画面都传回来
while camera.isOpened():
ret, frame = camera.read()# 读取摄像头的数据 frame为画面 ret 为标识
将图像初步处理并显示
frame = cv2.bilateralFilter(frame, 5, 50, 100) # 双边滤波
frame = cv2.flip(frame, 1) # 图片翻转 Y轴翻转
cv2.rectangle(frame, (int(cap_region_x_begin * frame.shape[1]), 0),
(frame.shape[1], int(cap_region_y_end * frame.shape[0])), (255, 0, 0), 2) # 画框框
cv2.imshow('original', frame)
这里我们首先使用了bilateralFilter-双边滤波,为什么要使用呢,双边滤波是减除噪声并边缘保留的图像平滑滤波器,在图像的每个像素强度值处被从附近的像素强度值的加权平均来取代,让图像去除噪声更平滑。
例:
原图
双边滤波后(图像平滑不少,边缘未改变)
然后将图片沿中心左右对称,因为摄像头返回的图像显示出来之后不是镜面的,我们需要翻转后,让我们看到的画面像镜子一样。
镜面翻转后
之后在画面的右上角画了一个矩形,为什么要这样呢,因为我们最后只在矩形框内拾取,矩形外的画面不会进行检测识别,说的简单粗暴一点,矩形框就是我们玩游戏用的操作空间。
如图:
最后,我们用cv2.imshow()函数将画面显示出来。
2. 手势检测识别
在OpenCV中,它已经帮我们封装好了一个动态运动物体检测的函数:BackgroudSubtractorMOG2()。
这个方法的原理就是通过混合高斯分布对背景图像建模,然后做图像比对,最后找出动态运动物体,数学公式推到什么的太长,有兴趣自己找找论文。
它的算法原理用最简单粗暴的话来说,可以理解为:背景是不变的,首先对背景图像的前200帧每一个像素点建模,如果新读入的图像中某处像素点与背景模型做匹配,如果存在与新像素匹配的单模型,就认为该像素点是背景,然后提升该点处模型的权重,如果匹配未找到,该像素则认定为前景。
前景提取:
首先我们建立背景模型:
bgModel = cv2.createBackgroundSubtractorMOG2(50, bgSubThreshold)
然后提取前景:
fgmask = bgModel.apply(frame) # 提取前景
kernel = np.ones((3, 3), np.uint8)
fgmask = cv2.erode(fgmask, kernel, iterations=1) # 腐蚀
res = cv2.bitwise_and(frame, frame, mask=fgmask) # 和运算
用背景模型和新读入的图像做比对(apply函数)。
然后定义一个3x3的kernel核,把前景图像做腐蚀操作。腐蚀的作用主要用作于一些去除噪声、弱化或分割图像之间的半岛型连接,腐蚀的方法其实就是用卷积核去图像上进行卷积操作。
腐蚀前前景图
腐蚀之后前景图
最后再用图像和运算(就是相加)来得到原图中的前景,再提取图像中某个区域,这里我们就把前景中手部提取出来了。
和运算后的前景
图像提取
前景处理:
截至目前,我们提取到了前景图像。接下来,我们要对前景图像进行处理。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转成灰度图
blur = cv2.GaussianBlur(gray, (blurValue, blurValue), 0) # 高斯滤波 去噪
cv2.imshow(‘blur’, blur)
ret, thresh = cv2.threshold(blur, threshold, 255, cv2.THRESH_BINARY) # 二值化
cv2.imshow(‘ori’, thresh)
首先我们把彩色图转换成灰度图(cvtColor),彩色图像由RGB三通道组成,灰度图只有单通道,灰度图是不是有种小时候家里黄河黑白电视机的感觉,彩色图就是现在的大彩电。
灰度图(很明显,有很多噪声)
然后再次进行高斯滤波,去除噪声。
高斯滤波去噪()
最后再将图像二值化,就是将图像的所有像素点转换成0/1两种数值,这才是真正意义上的黑白图。
二值化
手势识别:
现在我们已经完成了视频中手部的图像提取与处理,接下来,就是要对手势进行判断了。
判断的方法由很多种:1.训练机器学习模型 2.特征法判断,训练模型在后面的文章中会详细讲解,尽情期待,今天我们就用最传统的特征法来判断吧。
首先我们用findContours()函数来拟合图像中的外轮廓,传进来的图像是二值化图像。
_, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 拟合轮廓
拟合出轮廓之后,我们可以在面积最大的轮廓中寻找凸包,就是这个轮廓凸出的地方,比如三角形的顶点等等。
for i in range(length): # 找到面积最大的轮廓
res = contours[ci]
hull = cv2.convexHull(res) # 寻找凸包,就是凸出来的点
drawing = np.zeros(img.shape, np.uint8)
cv2.drawContours(drawing, [res], 0, (0, 255, 0), 2) # 画出轮廓
cv2.drawContours(drawing, [hull], 0, (0, 0, 255), 3)
现在我们已经可以找到轮廓凸包处的位置和手部的轮廓了,并在屏幕上已经用线条画出来了,接下来,我们就要检测当前的手势是什么了。
defects = cv2.convexityDefects(res, hull) # 凸缺陷检测
凸缺陷其实就是对象上的任何凹陷,找到凹陷之后,经过计算,我们就可以得到我们手指窝(不知道这样描述的对不对)的位置了。通常,我们五指自然张开,每两个相邻指头的夹角不会超过90°(自然张开!,自然张开!,自然张开!,重要的事说三遍)。
我们判断凹陷部位凸点处的夹角,如果小于90°,就认定为手指窝。然后根据手指窝的数量来判断当前的手势。
cnt = 0
for i in range(defects.shape[0]): # calculate the angle
s, e, f, d = defects[i][0]
start = tuple(res[s][0])
end = tuple(res[e][0])
far = tuple(res[f][0])
a = math.sqrt((end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2)
b = math.sqrt((far[0] - start[0]) ** 2 + (far[1] - start[1]) ** 2)
c = math.sqrt((end[0] - far[0]) ** 2 + (end[1] - far[1]) ** 2)
angle = math.acos((b ** 2 + c ** 2 - a ** 2) / (2 * b * c)) # cosine theorem
if angle <= math.pi / 2: # angle less than 90 degree, treat as fingers
cnt += 1
cv2.circle(drawing, far, 8, [211, 84, 0], -1)
这里,cnt就是手指窝的个数,根据手指窝的数量,就可以判断当前手势:0: 握拳、 1:伸出两根手指。。。。
手势
手势识别判定就到这里了,控制游戏的话,代码如下:
if cnt == 2:
our_plane.move_up()
elif cnt == 0:
our_plane.move_down()
elif cnt == 4:
our_plane.move_left()
elif cnt == 3:
our_plane.move_right()
就是根据手势,执行控制函数,游戏不属于这篇文章的范畴,如有兴趣,后面会做分享。
这就是一个最基本最基础的计算机视觉应用了,这次手势识别使用最原始的图像处理方法来实现,也有一些问题值得思考:
如果摆出了rock的手势呢,用手指窝数量的方式不就识别不出来了?
如果背景也是持续在变化的呢?
如果光线变化呢?
这些问题都需要得到优化解决,不过作为欢乐的计算机视觉第一篇文章,就不讲太多了,后面会用计算机视觉领域中其他的算法来解决这些问题,敬请期待。