今年上报了一个大学生SRTP项目,准备用树莓派作为主控板实现一个智能捡网球机。由于opencv开发库提供了大量图像处理算法的接口,因此只要一定程度上熟悉python语法和opencv库,实现这个程序并不是特别麻烦。本文会主要写程序实现的过程,不会涉及到很多的理论性的知识,有兴趣深入了解的可以自行查找网络上的各种资料。
本项目以网球的颜色作为识别特征。
识别过程为:获取视频帧----转换到HSV颜色空间----规定阈值、切割图像----识别轮廓获得网球位置信息
在树莓派上搭载摄像头获取视频帧不用多提,连接上摄像头然后直接opencv调用即可。因为在树莓派上安装opencv可以能涉及到到需要重新编译等各种问题,想避免麻烦的同学可以尝试直接pip opencv-python。树莓派摄像头可以选择官方的CSI摄像头,我为了便于在电脑上进行分析和调试,选用了usb线传输的摄像头模块,这种模块某宝上很多,使用起来也很简单。
在HSV颜色空间下进行颜色识别能够很好地降低调试难度,提高识别精度。就作者本人的理解,RGB空间下的三通道是互相独立互不关联的,而HSV空间下的三通道值之间具有一定的关系或者说是主次区别,更容易分级调试确定阈值和修正因环境光影响产生的阈值偏移问题,同时HSV空间更符合人的视觉习惯。
转换颜色空间可用opencv的cvtColor函数实现。
hsv_img = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
注意!!!经过cvtColor函数转换后的图像HSV三通道的取值范围与普遍认知下的HSV通道取值范围不同,可能是为了统一数据类型,opencv将原范围(h:0~360°, s:0~1, v:0~1)映射到了(h:0~180, s:0~255, v:0~255)的空间上。
在颜色空间转换完成后,下一步需要确定网球颜色范围。首先用我们的摄像头拍摄一张照片,从中截取出网球部分进行颜色分析,可以调用calcHist函数和matplotlab包快速实现。
cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate ]]) ->hist
calcHist()是用于计算颜色直方图的函数,参数意义分别是:
images:输入图像。
channels:图像通道。
mask:掩膜,不使用时置None。
histSize:产生的直方图尺寸,一般与通道值范围有关。
ranges:像素值的范围。
calcHist()会返回hist格式数据,可以直接使用plot()函数进行可视化处理。代码如下:
from matplotlib import pyplot as plt
img = cv2.imread('/test.jpg')
hsv_img = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
hist_H = cv2.calcHist([hsv_img],[0],None,[181],[0,180])
hist_S = cv2.calcHist([hsv_img],[1],None,[256],[0,255])
hist_V = cv2.calcHist([hsv_img],[2],None,[256],[0,255]) #获得不同通道的颜色直方图
plt.plot(hist_V),plt.xticks(np.arange(0,255,10)) #画图
plt.show()
对截取出的网球图像进行颜色分析得出三通道图:
从图中我们可以直接观察出网球颜色的大致分布区间,先用得到大致分布区间进行图像的预切割。
图像切割函数为:
cv2.inRange(src, lowerb, upperb[, dst]) → dst
参数意义为:
src:输入图像
lowerb:阈值下限
upperb:阈值上限
返回跟原图大小相同的图像矩阵,满足阈值的位置将被置255,其他位置置0。
对图像进行预切割的效果如下(原图来自网络):
从预切割效果图中可以看出,我们直接观察得到的阈值可以把一部分网球提取出来,但由于网球与摄像头距离、角度的不同,初始阈值并不完全适用与所有网球,因此还需要进一步的调整。作者采用的方法是通过滑动条来实现阈值的动态调整。
cv2.namedWindow('value')
cv2.createTrackbar('h','value',0,180,nothing)
cv2.createTrackbar('_h','value',0,50,nothing)
cv2.createTrackbar('s','value',0,255,nothing)
cv2.createTrackbar('_s','value',0,100,nothing)
cv2.createTrackbar('v','value',0,255,nothing)
cv2.createTrackbar('_v','value',0,100,nothing)
while 1:
k = cv2.waitKey(10)
if k == ord('m'):
cv2.destroyAllWindows()
break
elif k == ord('n'):
print(cv2.getTrackbarPos('h', 'value'), cv2.getTrackbarPos('s', 'value'), cv2.getTrackbarPos('v', 'value'))
lower_green = np.array([15,180,150])
lower_green = np.array(
[cv2.getTrackbarPos('h', 'value')-cv2.getTrackbarPos('_h','value'), \
cv2.getTrackbarPos('s', 'value')-cv2.getTrackbarPos('_s','value'), \
cv2.getTrackbarPos('v', 'value')-cv2.getTrackbarPos('_v','value')])
higher_green = np.array([30,255,255])
higher_green = np.array(
[cv2.getTrackbarPos('h', 'value') + cv2.getTrackbarPos('_h', 'value'), \
cv2.getTrackbarPos('s', 'value') + cv2.getTrackbarPos('_s', 'value'), \
cv2.getTrackbarPos('v', 'value') + cv2.getTrackbarPos('_v', 'value')])
mask = cv2.inRange(hsv_img, lower_green, higher_green)
cv2.imshow("balls", mask)
通过滑动条调整阈值大小,直到程序能够识别出大部分网球且不会出现大量误判为止,大体效果如下:
记录当前阈值,用于识别程序。
经过切割图像后我们获得了含有网球位置信息的黑白掩膜图像,读取出网球的位置信息我们就初步实现了识别程序。黑白掩膜图像是单通道灰度图象,且边缘处像素值突变明显,用轮廓识别函数就可以很简单的获得相关位置信息。轮廓识别函数为:
cv2.findContours(image, mode, method[, contours[, hierarchy[, offset ]]])
参数意义:
image:输入图像
mode:轮廓识别模式。为了避免网球内部噪点的影响,这里可使用cv2.RETR_EXTERNAL只检测外轮廓。
method:输出的轮廓数据类型。这里我们单纯需要简单位置信息,可以使用cv2.CHAIN_APPROX_SIMPLE获得四点坐标。
findContours()会返回一个轮廓列表,然后利用列表中数据在图中画出识别框。
contours, hierarchy = cv2.findContours(mask,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
x,y,w,h = cv2.boundingRect(c)
cv2.rectangle(img, (x-d,y-d), (x+w+d,y+h+d),(0,0,255),2)
识别结果如下:
目前,我们就大体上完成了网球识别程序,在实际应用过程中光照影响还是比较严重的,使用时可能需要调整阈值范围。可以尝试监听鼠标点击获得当前网球颜色值调整范围等办法,在此就不一一详述了。