本篇将具体介绍一个实际应用项目——车辆检测及计数,在交通安全中是很重要的一项计数;当然,本次完全采用OpenCV进行实现,和目前落地的采用深度学习的算法并不相同,但原理是一致的;本篇将从基础开始介绍,一步步完成车辆检测计数的项目;
本质:具有相同颜色或强度的连续点的曲线;
作用:
1、可用于图形分析;
2、应用于物体的识别与检测;
注意点:
1、为了检测的准确性,需要先对图像进行二值化或Canny操作;
2、画轮廓的时候回修改输入的图像,需要先深拷贝原图;
轮廓查找的函数原型:
findContours(img,mode,ApproximationMode…)
mode
RETR_EXTERNAL=0,表示只检测外轮廓;
RETR_LIST=1,检测的轮廓不建立等级关系;(常用)
RETR_CCOMP=2,每层最多两级;
RETR_TREE=3,按树形结构存储轮廓,从右到左,从大到小;(常用)
ApproximationMode
CHAIN_APPROX_BOBE:保存轮廓上所有的点;
CHAIN_APPROX_SIMPLE:只保存轮廓的角点;
代码实战:
img = cv2.imread('./contours1.jpeg')
# 转变成单通道
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化
ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
# 轮廓查找
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours输出结果:
(array([[[ 0, 0]],
[[ 0, 435]],
[[345, 435]],
[[345, 0]]], dtype=int32),)
可以看出,我们找最外层轮廓,找出了一个矩形轮廓的四个点;
当然,我们不需要通过画形状来绘制轮廓,可以通过一个内置函数来绘制轮廓;
绘制轮廓函数原型:
drawContours(img,contours,contoursIdx,color,thickness,…)
代码案例:
img = cv2.imread('./contours1.jpeg')
img2 = img.copy()
# 转变成单通道
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化
ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
# 轮廓查找
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img, contours, -1, (0, 0, 255), 1)
cv2.drawContours(img2, contours, -1, (0, 0, 255), -1)
cv2.imshow('org', img)
cv2.imshow('org2', img2)
cv2.waitKey(0)
如上图所示,左图是线宽设置为1,右图为线宽设置为-1,也就是填充的效果;
当然,OpenCV还提供了计算轮廓周长和面积的方法;
轮廓面积函数原型:
contourArea(contour)
轮廓周长函数原型:
arcLength(curve,closed)
上述两个函数比较简单,在这就不做代码演示了;
多边形逼近函数原型:
approxPolyDP(curve,epsilon,closed)
凸包的函数原型:
convexHull(points,clockwise,…)
首先我们看一下基于轮廓查找输出的轮廓形状:
可以看出轮廓点十分密集,接下来看一下基于多变形逼近和凸包的效果:
代码案例:
img = cv2.imread('./hand.png')
img2 = img.copy()
# 转变成单通道
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化
ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
# 轮廓查找
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# cv2.drawContours(img, contours, -1, (0, 0, 255), 1)
e = 20
approx = cv2.approxPolyDP(contours[0], e, True) # 多边形逼近
approx = (approx, )
cv2.drawContours(img, approx, 0, (0, 0, 255), 3)
hull = cv2.convexHull(contours[0])
hull = (hull, )
cv2.drawContours(img2, hull, 0, (0, 0, 255), 3) # 凸包
cv2.imshow('org', img)
cv2.imshow('org2', img2)
cv2.waitKey(0)
这里需要注意一点,绘制轮廓的函数对于轮廓的传入需要为元组,需要将得到的数组放到一个元组中!
当然,多边形逼近这里设置的精度为20,所以比较粗糙,设置小一些可以达到更好的效果;
外接矩阵分为最大外接矩阵和最小外接矩阵,如下图所示:
最小外接矩阵还有一个功能,就是计算旋转角度,从上图的绿框应该可以很明显看出;
最小外接矩阵函数原型:
minAreaRect(points)
返回值:起始点(x,y)、宽高(w,h)、角度(angle)
最大外接矩形函数原型:
boundingRect(array)
返回值:起始点(x,y)、宽高(w,h)
代码案例:
img = cv2.imread('./hello.jpeg')
img2 = img.copy()
# 转变成单通道
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值化
ret, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
# 轮廓查找
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 获取最小外接矩形
r = cv2.minAreaRect(contours[1])
box = cv2.boxPoints(r) # 提取其中的点
box = np.int0(box) # 将浮点型转换为整型
cv2.drawContours(img, (box, ), 0, (0, 0, 255), 2)
# 获取最大外接矩形
x, y, w, h = cv2.boundingRect(contours[1])
cv2.rectangle(img2, (x, y), (x+w, y+h), (0, 0, 255), 2)
cv2.imshow('org', img)
cv2.imshow('org2', img2)
cv2.waitKey(0)
涉及的知识点:
实现流程:
加载视频 —— 通过形态学识别车辆 —— 对车辆进行统计 —— 显示统计信息
这里就是一个简单加载视频的实现:
cap = cv2.VideoCapture('video.mp4')
while True:
ret, frame = cap.read()
if(ret == True):
cv2.imshow('video', frame)
key = cv2.waitKey(1)
if(key == 27): # Esc退出
break
cap.release()
cv2.destroyAllWindows()
函数原型:
createBackgroundSubtractorMOG()
具体实现原理比较复杂,用到了一些视频序列关联信息,把像素值不变的认为是背景;
注意:在opencv中已经不支持该函数,而是用createBackgroundSubtractorMOG2()替代;如果需要使用可以安装opencv_contrib模块,在其中的bgsegm中保留了该函数;
代码实现:
cap = cv2.VideoCapture('video.mp4')
bgsubmog = cv2.bgsegm.createBackgroundSubtractorMOG()
while True:
ret, frame = cap.read()
if(ret == True):
# 灰度处理
cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 高斯去噪
blur = cv2.GaussianBlur(frame, (3, 3), 5)
mask = bgsubmog.apply(blur)
cv2.imshow('video', mask)
key = cv2.waitKey(1)
if(key == 27): # Esc退出
break
cap.release()
cv2.destroyAllWindows()
这里尽量采用旧版的MOG函数,新版的MOG2函数比较精细,会将树叶等信息输出,去除效果没那么好;
这里主要是为了处理一些小的噪声点以及目标中的黑色块;
代码实现:
cap = cv2.VideoCapture('video.mp4')
bgsubmog = cv2.bgsegm.createBackgroundSubtractorMOG()
# 形态学kernel
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
while True:
ret, frame = cap.read()
if(ret == True):
# 灰度处理
cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 高斯去噪
blur = cv2.GaussianBlur(frame, (3, 3), 5)
mask = bgsubmog.apply(blur)
# 腐蚀
erode = cv2.erode(mask, kernel)
# 膨胀
dilate = cv2.dilate(erode, kernel, 3)
# 闭操作
close = cv2.morphologyEx(dilate, cv2.MORPH_CLOSE, kernel)
close = cv2.morphologyEx(close, cv2.MORPH_CLOSE, kernel)
contours, h = cv2.findContours(close, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE,)
for (i, c) in enumerate(contours):
(x, y, w, h) = cv2.boundingRect(c)
cv2.rectangle(frame, (x, y), (x+w, y+h), (0,0,255), 2)
cv2.imshow('video', frame)
key = cv2.waitKey(1)
if(key == 27): # Esc退出
break
cap.release()
cv2.destroyAllWindows()
从图中效果来看,还是会有很多小的检测框,接下来就是处理重合检测框以及去掉一些多余的检测框,类似于NMS去重,当然原理还不太一样;
首先需要过滤一些小的矩形,已经检测框的长和宽,设定一些阈值即可;
代码实现:
cap = cv2.VideoCapture('video.mp4')
bgsubmog = cv2.bgsegm.createBackgroundSubtractorMOG()
# 保存车辆中心点信息
cars = []
# 统计车的数量
car_n = 0
# 形态学kernel
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
while True:
ret, frame = cap.read()
if(ret == True):
# 灰度处理
cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# 高斯去噪
blur = cv2.GaussianBlur(frame, (3, 3), 5)
mask = bgsubmog.apply(blur)
# 腐蚀
erode = cv2.erode(mask, kernel)
# 膨胀
dilate = cv2.dilate(erode, kernel, 3)
# 闭操作
close = cv2.morphologyEx(dilate, cv2.MORPH_CLOSE, kernel)
close = cv2.morphologyEx(close, cv2.MORPH_CLOSE, kernel)
contours, h = cv2.findContours(close, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE,)
# 画一条线
cv2.line(frame, (10, 550), (1200, 550), (0, 255, 255), 3)
for (i, c) in enumerate(contours):
(x, y, w, h) = cv2.boundingRect(c)
# 过滤小的检测框
isshow = (w >= 90) and (h >= 90)
if(not isshow):
continue
# 保存中心点信息
cv2.rectangle(frame, (x, y), (x+w, y+h), (0,0,255), 2)
centre_p = (x + int(w/2), y + int(h/2))
cars.append(centre_p)
cv2.circle(frame, (centre_p), 5, (0,0,255), -1)
for (x, y) in cars:
if(593 < y < 607):
car_n += 1
cars.remove((x, y))
cv2.putText(frame, "Cars Count:" + str(car_n), (500, 60), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 5)
cv2.imshow('video', frame)
key = cv2.waitKey(1)
if(key == 27): # Esc退出
break
cap.release()
cv2.destroyAllWindows()
简单的效果已经出来了,对于大部分车辆都能够很好的检测并且计数了;
存在问题:
由于是用中心点与线的距离来判断,车速过慢可能会在两帧内重复计数,车速过快可能会计数不到;这就是传统算法存在的一个问题,基于深度学习的方法可以很好解决这些问题,可关注目标跟踪实战的那一篇文章!
项目到这里就介绍了,通过该项目主要是将所学的知识点进行串联,重点在于形态学的运用!当然这个效果可能达不到实际应用的标准,这也是传统算法的一个弊端;有能力的可以采用深度学习的方法进行实现,也可以关注我后续的目标跟踪是实现车辆计数,效果会远比这个好。