OpenCV提供了强大的图像处理功能,与Python的结合堪称完美。。。
这一次,我们试一下用帧差法来完成对运动目标的检测与跟踪。
帧差法的原理是这样的:由于摄像机采集的视频序列具有连续性的特点,所以如果所采集场景内没有运动目标的时候,连续帧的变化很小,如果存在有运动的目标,则连续的帧和帧之间会有明显地变化。我们将连续的两帧或三帧图像进行差分运算,取其的灰度差的绝对值,如果该值超过我们所定的阈值时,就判定为运行目标。其原理如图所示。
先上源码,这个是两帧法,就是把前一帧定义为变量background,后一帧与其对比的差值:
源码呈现
源码是从网上找的,原作者写的很精炼,分析的也好,就是没有逐句分析,所以我们先上源码,再分析:
import cv2
import numpy as np
camera = cv2.VideoCapture(0) # 参数0表示第一个摄像头
# 判断视频是否打开
if (camera.isOpened()):
print('摄像头成功打开')
else:
print('摄像头未打开')
# 测试用,查看视频size
size = (int(camera.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT)))
print('size:'+repr(size))
es = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 4))
background = None
while True:
# 读取视频流
grabbed, frame_lwpCV = camera.read()
# 对帧进行预处理,先转灰度图,再进行高斯滤波。
# 用高斯滤波进行模糊处理,进行处理的原因:每个输入的视频都会因自然震动、光照变化或者摄像头本身等原因而产生噪声。对噪声进行平滑是为了避免在运动和跟踪时将其检测出来。
gray_lwpCV = cv2.cvtColor(frame_lwpCV, cv2.COLOR_BGR2GRAY)
gray_lwpCV = cv2.GaussianBlur(gray_lwpCV, (21, 21), 0)
# 将第一帧设置为整个输入的背景
if background is None:
background = gray_lwpCV
continue
# 对于每个从背景之后读取的帧都会计算其与北京之间的差异,并得到一个差分图(different map)。
# 还需要应用阈值来得到一幅黑白图像,并通过下面代码来膨胀(dilate)图像,从而对孔(hole)和缺陷(imperfection)进行归一化处理
diff = cv2.absdiff(background, gray_lwpCV)
diff = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1] # 二值化阈值处理
diff = cv2.dilate(diff, es, iterations=2) # 形态学膨胀
# 显示矩形框
contours, hierarchy = cv2.findContours(diff.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 该函数计算一幅图像中目标的轮廓
for c in contours:
if cv2.contourArea(c) < 1500: # 对于矩形区域,只显示大于给定阈值的轮廓,所以一些微小的变化不会显示。对于光照不变和噪声低的摄像头可不设定轮廓最小尺寸的阈值
continue
(x, y, w, h) = cv2.boundingRect(c) # 该函数计算矩形的边界框
cv2.rectangle(frame_lwpCV, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.imshow('contours', frame_lwpCV)
cv2.imshow('dis', diff)
key = cv2.waitKey(1) & 0xFF
# 按'q'健退出循环
if key == ord('q'):
break
# When everything done, release the capture
camera.release()
cv2.destroyAllWindows()
源码分析
1.导入模块
import cv2
import numpy as np
这两句是导入两个所需要的模块,第一个是opencv,这个肯定是要的,另一个是NumPy,NumPy(Numerical Python) 是 Python 语言的一个扩展程序库,支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库。import numpy as np是指在程序中用np来代替numpy的调用,所以只要是看到np地方,就是采用了numpy的库运算。例如:
kernel = np.ones((5, 5), np.uint8)。
而同理,程序中出现了cv2的地方,就是调用了opencv模块。
2.视频信息初处理
下面的五行是摄像头进行处理。。。
camera = cv2.VideoCapture(0)
if (camera.isOpened()):
print('摄像头成功打开')
else:
print('摄像头未打开')
size = (int(camera.get(cv2.CAP_PROP_FRAME_WIDTH)),int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT)))
print('size:'+repr(size))
第一行:camera = cv2.VideoCapture(0)
创建一个视频捕获对象camera。cv2.VideoCapture()中的参数如果是0,表示打开本机,也就是运行该程序的笔记本的内置摄像头;如果不需要监测摄像头,而是对已有视频进行监测,那么在括号中引入视频文件的路径就可以打开视频,如cap = cv2.VideoCapture(“E:/test.avi”)
第二至五行:如果视频捕获对象camera创建成功,也就是相应的摄像头或视频存在,可以形成视频流,那么camera.isOpened()返回为真,输出“摄像头成功打开”,表示摄像头已打开。否则输出“摄像头未打开”
第六到七行:cv2.CAP_PROP_FRAME_WIDTH为视频信息的宽度属性;cv2.CAP_PROP_FRAME_HEIGHT为视频信息的高度属性,size = (int(camera.get(cv2.CAP_PROP_FRAME_WIDTH)),int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT)))的意思就是将获取到的视频信息的宽度和高度属性转换为整型值,赋给size变量,然后将其用repr() 函数转换成string格式进行输出。
与CAP_PROP_FRAME_WIDTH类似的视频属性有很多,下面列几个,这些参数有get来进行获取,用set进行设置。
cv2.CAP_PROP_FRAME_WIDTH, 1080 # 宽度
cv2.CAP_PROP_FRAME_HEIGHT, 960 # 高度
cv2.CAP_PROP_FPS, 30 # 帧数
cv2.CAP_PROP_BRIGHTNESS, 1 # 亮度 1
cv2.CAP_PROP_CONTRAST,40 # 对比度 40
cv2.CAP_PROP_SATURATION, 50 # 饱和度 50
cv2.CAP_PROP_HUE, 50 # 色调 50
cv2.CAP_PROP_EXPOSURE, 50 # 曝光 50
3.图像预处理
在介绍这一部分内容之前,我们先来了解一下形态学。形态学原本是生物学中研究动物和植物结构的一个学科分支,后来被应用到数学中,形成了数学形态学。
数学形态学是以形态为基础对图像进行分析的数学工具,其基础是集合论,思路是用具有一定形态的结构元素去度量和提取图像中的对应形状以达到对图像分析和识别的目的。
而将数学形态学采用一定的方法呈现出来,就形成了OpenCV的图像处理的基础。
es = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 4))
background = None
在OpenCV当中,定义结构元素用到的命令是cv2.getStructuringElement(c1,c2),其中c1参数表示核的形状,主要有三种选择:
矩形:MORPH_RECT;
交叉形:MORPH_CROSS;
椭圆形:MORPH_ELLIPSE;
在本例中,选择的是椭圆形。
第二个参数c2是核的尺寸,我们在原代码中插一句print(es),将它打印出来,如下所示:
[[0 0 0 0 1 0 0 0 0]
[0 1 1 1 1 1 1 1 0]
[1 1 1 1 1 1 1 1 1]
[0 1 1 1 1 1 1 1 0]]
可以看出它是一个矩阵,内含4个小矩阵,每一个小矩阵都只有一行元素,每行元素是9个。
当然我们也可以不用cv2.getStructuringElement(c1,c2)来定义结构元素,例如我们需要在图象中提取数字2,那么就定义一个“2”这样的形态,再利用形态学中的膨胀、腐蚀、开运算、闭运算将它提取出来。
在图像识别中,定义结构元素是提取特定图像的基础,也关系到最终提取的结果,理论上讲,几乎没有利用形态学提取不了的特征,如果一次不够,那就多用几次。
后面的一句:background = None,先定义前一帧为空。
4.定义结构化元素
接着就进入了一个永真循环,不停地从摄像头或视频中截取图像进行处理,先来看循环当中的图像预处理。
grabbed, frame_lwpCV = camera.read()
gray_lwpCV = cv2.cvtColor(frame_lwpCV, cv2.COLOR_BGR2GRAY)
gray_lwpCV = cv2.GaussianBlur(gray_lwpCV, (21, 21), 0)
if background is None:
background = gray_lwpCV
continue
第一句grabbed, frame_lwpCV = camera.read(),camera.read()是按帧读取视频,它的返回值有两个,第一个grabbed是一个布尔值,如果读取帧是正确的则返回True,如果文件读取到结尾,它的返回值就为False。后面的 frame_lwpCV 返回的是当前循环所提取的图像,是一个三维矩阵。
第二句gray_lwpCV = cv2.cvtColor(frame_lwpCV, cv2.COLOR_BGR2GRAY),将提取的图像转为灰度,为进一步处理作准备。
第三句gray_lwpCV = cv2.GaussianBlur(gray_lwpCV, (21, 21), 0),用高斯滤波进行模糊处理,进行处理的原因:每个输入的视频都会因自然震动、光照变化或者摄像头本身等原因而产生噪声。对噪声进行平滑是为了避免在运动和跟踪时将其检测出来。在cv2.GaussianBlur(gray_lwpCV, (21, 21), 0)函数中,其中gray_lwpCV是要进行滤波的原图像,(21, 21)是高斯核的大小,blur1和blur2的选取一般是奇数,blur1和blur2的值可以不同。最后的参数0表示标准差取0。
第四至六句定义前一帧作为背景帧,后面会用当前帧与背景帧相对比,以确定运动目标。
5.图像处理
diff = cv2.absdiff(background, gray_lwpCV)
diff = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1]
diff = cv2.dilate(diff, es, iterations=2)
第一句:diff = cv2.absdiff(background, gray_lwpCV),这句话表示将得到的gray_lwpCV与background进行对比,就是将两幅图像作差,获取差分图,返回的结果代表他们的差异之处,这里用的是灰度图,类型是uint8,在 OpenCV单通道使用的数据类型是 uint8,两个uint8的数相减得不到负数,会得到差的补码。
第二句:diff = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1],对得到的差分图diff进行二值化处理。
cv2.threshold()有四个参数,第一个原图像,在本例中就是差分图;第二个25是定义了进行分类的阈值,第三个是高于(低于)阈值时赋予的新值255,第四个是一个方法选择参数,常用的有:
• cv2.THRESH_BINARY(黑白二值)
• cv2.THRESH_BINARY_INV(黑白二值反转)
• cv2.THRESH_TRUNC (得到的图像为多像素值)
• cv2.THRESH_TOZERO
• cv2.THRESH_TOZERO_INV
threshold函数有两个返回值,第一个是得到的阈值,用cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[0]可以得到,第二个就是阈值化后的图像,用cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1]来得到。
总之,这句话是把diff图像当中阈值高于25的部分转为255。
第三句:diff = cv2.dilate(diff, es, iterations=2) ,cv2.dilate( ) 是对图像进行膨胀操作。
膨胀操作是取核中像素值的最大值代替锚点位置的像素值,这样会使图像中较亮的区域增大,较暗的区域减小。如果是一张黑底,白色前景的二值图,就会使白色的前景物体颜色面积变大,就像膨胀了一样。示例如图1为正常图片,图2为灰度图片,图3为膨胀的图片。
图1正常图片
图2灰度图片
图3膨胀图片
cv2.dilate( )的格式是 cv2.dilate(src, kernel, iteration),其中src表示输入的图片, kernel表示所引用的结构元素, iteration表示迭代的次数。
6.追踪目标,作出标记并显示
准备工作作完,就该找目标了,
contours, hierarchy = cv2.findContours(diff.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
if cv2.contourArea(c) < 1500:
continue
(x, y, w, h) = cv2.boundingRect(c)
cv2.rectangle(frame_lwpCV, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.imshow('contours', frame_lwpCV)
cv2.imshow('dis', diff)
第一行:contours, hierarchy = cv2.findContours(diff.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) ,这句是用来找出目标的轮廓值,参数有三个,第一个为寻找轮廓的图像,这里用了处理后的图片的拷贝,第二个参数表示轮廓的检索模式,有四种:
cv2.RETR_EXTERNAL 只检测外轮廓
cv2.RETR_LIST 检测的轮廓不建立等级关系
cv2.RETR_CCOMP 建立两个等级的轮廓,上面的一层为外边界,里面的一层为内孔的边界信息
cv2.RETR_TREE 建立一个等级树结构的轮廓
第三个为轮廓的近似算法:
cv2.CHAIN_APPROX_SIMPLE 压缩水平方向,垂直方向,对角线方向的元素,只保留该方向的终点坐标(矩形只需四顶点)
cv2.CHAIN_APPROX_TC89_L1 使用teh-Chinl chain 近似算法的一种
CV_CHAIN_APPROX_TC89_KCOS 使用teh-Chinl chain 近似算法的一种
cv2.CHAIN_APPROX_NONE 存储所有的轮廓点,相邻的两个点的像素位置差不超过1
cv2.findContours( )的返回值:
OpenCV2和OpenCV4中,findContours这个轮廓提取函数会返回两个值:
①轮廓的点集(contours) ②各层轮廓的索引(hierarchy)
OpenCV3中,则会返回三个值:①处理的图像(image) ②轮廓的点集(contours) ③各层轮廓的索引(hierarchy)
如果运行时出现ValueError: not enough values to unpack (expected 3, got 2),意思是值错误:没有足够的值解包(应为3,得到2),那么解决的办法就是,它要几个就给他几个就可以了。
第二至五行:为找到的目标绘制矩形。for循环是对所找到的所有轮廓逐一进行绘制,每一个c值对应一个,如果当前c值对应的轮廓面积太小,则直接找下一个。如果不小的话,开始绘制矩形框。
(x, y, w, h) = cv2.boundingRect© :矩形边框(Bounding Rectangle)是说,用一个最小的矩形,把找到的形状包起来。返回四个值,分别是x,y,w,h;x,y是矩阵左上点的坐标,w,h是矩阵的宽和高
cv2.rectangle(frame_lwpCV, (x, y), (x+w, y+h), (0, 255, 0), 2):绘制矩形,参数说明:frame_lwpCV要绘制图片,(x, y)表示矩阵左上角的位置,(x+w, y+h)表示矩阵右下角的位置, (0, 255, 0)表示颜色,2表示线条
注意,现在绘制的对象是原图,也就是找到目标的那个彩色的图。显示用下一句完成cv2.imshow(‘contours’, frame_lwpCV),对话框上显示的名称为contours,图像对象为frame_lwpCV,如图:
最后一句cv2.imshow(‘dis’, diff),显示处理后图像:
7.总结
最后还有一点程序,主要完成一些边界性操作:
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
camera.release()
cv2.destroyAllWindows()
key = cv2.waitKey(1) :waitKey()方法本身表示等待键盘输入,参数是1,表示延时1ms切换到下一帧图像,而cv2.waitKey(1) 与0xFF作“&”运算,是只取waitKey()返回整数值当中的所“&”的8位,其他位都取0。
最后调用release()释放摄像头,调用destroyAllWindows()关闭所有图像窗口。
本文代码是做一个基于帧差法的运动检测,主要考虑的是“背景帧”与其它帧之间的差异。这种方法检测需要提前设置背景帧,如果是在室外,光线的变化就会引起误检测,如果光线变化不大,识别率还是可以的。