各位同学好,今天和大家分享一下如何使用Opencv完成停车场的车位检测,及空余车位计数,先放张图看效果。
红框代表该车位有车,绿框代表该车位空余,左上角记录有几个空余车位,黄色数字代表该车位内的像素个数,用于判断车位上是否有车。
从图中可以看到,由于视频拍摄角度的问题,车位不是横平竖直的,并且车位在屏幕上的大小和角度也是不相同的。需要用到旋转矩形的操作,并调整单个矩形框使其能够用于所有车位。
首先,由于车位是矩形框经过一定角度旋转之后得到的,因此在先定义一个处理旋转矩形的函数,把它其他py文件放在同路径的目录内,这里命名为rotaParking.py
假设对图片上任意点(x,y),绕一个坐标点(rx0,ry0)顺时针旋转a角度后的新的坐标设为(x0, y0),有公式:
x0 = (x - rx0) * cos(a) - (y - ry0) * sin(a) + rx0
y0 = (x - rx0) * sin(a) + (y - ry0) * cos(a) + ry0
根据此公式,创建函数recRota完成旋转矩形操作,后续在车位处理的py文件中直接调用它。返回值是img绘制旋转矩形图之后的图像,以及angle是旋转后的矩形的四个角的坐标构成的列表。
# 矩形框顺时针旋转
import cv2
import math
# 传入旋转的参考点坐标,矩形框左上角坐标(x,y),框的宽w和高h,旋转角度a
def angleRota(center_x, center_y, x, y, w, h, a):
# 角度转弧度
a = (math.pi/180)*a
# 旋转前左上角坐标
x1, y1 = x, y
# 右上角坐标
x2, y2 = x+w, y
# 右下角坐标
x3, y3 = x+w, y+h
# 左下角坐标
x4, y4 = x, y+h
# 旋转后的左上角坐标,像素坐标是整数
px1 = int((x1 - center_x) * math.cos(a) - (y1 - center_y) * math.sin(a) + center_x)
py1 = int((x1 - center_x) * math.sin(a) + (y1 - center_y) * math.cos(a) + center_y)
# 右上角坐标
px2 = int((x2 - center_x) * math.cos(a) - (y2 - center_y) * math.sin(a) + center_x)
py2 = int((x2 - center_x) * math.sin(a) + (y2 - center_y) * math.cos(a) + center_y)
# 右下角坐标
px3 = int((x3 - center_x) * math.cos(a) - (y3 - center_y) * math.sin(a) + center_x)
py3 = int((x3 - center_x) * math.sin(a) + (y3 - center_y) * math.cos(a) + center_y)
# 左下角坐标
px4 = int((x4 - center_x) * math.cos(a) - (y4 - center_y) * math.sin(a) + center_x)
py4 = int((x4 - center_x) * math.sin(a) + (y4 - center_y) * math.cos(a) + center_y)
# 保存每一个角的坐标
pt1 = (px1, py1)
pt2 = (px2, py2)
pt3 = (px3, py3)
pt4 = (px4, py4)
# 存储每个角的坐标
angle = [pt1, pt2, pt3, pt4]
# 返回调整后的坐标
return angle
# 绘制旋转后的矩形框
def drawLine(img, angle, color, thickness):
# 分别绘制四条边
cv2.line(img, angle[0], angle[1], color, thickness)
cv2.line(img, angle[1], angle[2], color, thickness)
cv2.line(img, angle[2], angle[3], color, thickness)
cv2.line(img, angle[3], angle[0], color, thickness)
# 返回绘制好旋转矩形的图像
return img
# 矩形旋转
def recRota(img, center_x, center_y, x1, y1, w, h, rota, draw=True):
'''
img: 原图像
(center_X, center_y): 旋转参考点的坐标
(x1, y1): 矩形框左上角坐标
w: 矩形框的宽
h: 矩形框的高
rota: 顺时针的旋转角度,如:30°
'''
color = (255,255,0) # 绘制停车线的线条颜色
thickness = 2 # 停车线线条宽度
#(1)计算旋转一定角度后的四个角的坐标
angle = angleRota(x1, y1, x1, y1, w, h, rota)
#(2)绘制旋转后的矩形
if draw == True:
img = drawLine(img, angle, color, thickness)
# 返回绘制后的图像,以及矩形框的四个角的坐标
return img, angle
else:
return angle
这里用到鼠标响应 cv2.setMouseCallback() 可以在图像上用鼠标操作,触发一些事件。
参数:
winname: 窗口的名字
onMouse: 鼠标响应函数,回调函数。指定窗口里每次鼠标时间发生的时候,被调用的函数指针。
userdate:传给回调函数的参数
event: 是 CV_EVENT_* 变量之一
x和y: 鼠标指针在图像坐标系的坐标(像素坐标)
flags: 是CV_EVENT_FLAG的组合
param: 是用户定义的传递到setMouseCallback函数调用的参数
cv2_EVENT_MOUSEMOVE 0 # 滑动
cv2_EVENT_LBUTTONDOWN 1 # 左键点击
cv2_EVENT_RBUTTONDOWN 2 # 右键点击
cv2_EVENT_MBUTTONDOWN 3 # 中间点击
cv2_EVENT_LBUTTONUP 4 # 左键释放
cv2_EVENT_RBUTTONUP 5 # 右键释放
cv2_EVENT_MBUTTONUP 6 # 中间释放
cv2_EVENT_LBUTTONDBLCLK 7 # 左键双击
cv2_EVENT_RBUTTONDBLCLK 8 # 右键双击
cv2_EVENT_MBUTTONDBLCLK 9 # 中间释放
cv2_EVENT_FLAG_LBUTTON 1 # 左键拖拽
cv2_EVENT_FLAG_RBUTTON 2 # 右键拖拽
cv2_EVENT_FLAG_MBUTTON 4 # 中间拖拽
。。。。。。。。。。。。。。。。。。。
在本案例中,如果鼠标左键单机图像,那么就以鼠标所在位置坐标作为矩形框的左上角坐标,每点一个就生成一个矩形框,将坐标保存在 posList 中。如果画错了,那么就右键点击需要删除的矩形框,如果鼠标位置在遍历后的某个矩形框内部,那么就在posList中删除该矩形框的左上角坐标,并在下一帧图上不显示该矩形框。
将每一次点击后更新好了的posList坐标信息保存在txt文件中,这样在下一次打开文件时就不需要重复标定矩形框。在读取坐标信息时使用try和except方法,如果这是第一次标定,没有生成过txt文件,那么try方法抛出异常,执行except的方法。
在这一帧中标定的坐标,会在在下一帧中绘制,遍历poList,以pos鼠标所在坐标为旋转参考点坐标,绘制旋转矩形框。
import cv2
import pickle # 将对象以文件的形式存放在磁盘上
from rotaParking import recRota # 导入自定义的旋转矩形框函数
#(1)读入图片
filepath = 'C:\\GameDownload\\Deep Learning\\parking_car.jpg'
filename = 'parking_position.txt' # 保存的车位坐标
# 只需要确定一个停车位的大小,就可以将这个矩形框用于所有车位
w, h = 90, 160 # 矩形框的宽和高
# 运行时读取保存的车位坐标,保留前一次画好了的车位线
try:
with open(filename, 'rb') as file_object:
posList = pickle.load(file_object)
# 第一次运行时没有写过文件,那就执行下面的,生成一个列表保存坐标
except:
# 创建一个列表接收触发鼠标事件后的坐标信息
posList = []
#(2)创建一个回调函数,当鼠标事件触发时,该函数执行
# cv2.setMouseCallback的参数,在图像上点击鼠标会发生什么
def onMouse(events, x, y, flag, params):
# 如果在图像上点击鼠标左键,那么就将鼠标位置的(x,y)坐标保存起来
if events == cv2.EVENT_LBUTTONDOWN:
# 接收鼠标所在位置为xy坐标
posList.append((x,y))
# 如果右键点击就将这个坐标从posList中删除
if events == cv2.EVENT_RBUTTONDOWN:
# 如果当前的坐标点在某一个检测框中就删除这个检测框
for index, pos in enumerate(posList):
# x和y代表鼠标当前所在位置
if pos[0]
绘制后的单帧图像如下
在对单帧图像处理时,我们已经将所有的车位分割出来,并记录下左上角坐标。接下来对视频图像处理时只需直接导入左上角坐标点的txt文件。
从上图可以看到,每个车位框是倾斜的,如果要分割出每个车位,必须使用切片方法,但切片出来的图像是横平竖直的。因此针对每一个车位框,都以该框的左上角为旋转参考,旋转整张帧图像,将车位框摆正之后再切片。
center: 图像旋转的参考点坐标
angle: 旋转角度(°),正数代表逆时针,负数代表顺时针
scale: 旋转后的图像是原来缩放后的多少倍
M: 计算得到的旋转矩阵
src: 需要变化的图像
M: 旋转变换矩阵
dsize: 输出图像的大小
flags: 插值方法的组合(int 类型)
borderMode: 边界像素模式(int 类型)
borderValue: 边界填充值,默认情况下,它为0,也就是边界填充默认是黑色。
flags 插值方式如下:
cv2.INTER_LINEAR # 线性插值(默认)
cv2.INTER_NEAREST # 最近邻插值
cv2.INTER_AREA # 区域插值
cv2.INTER_CUBIC # 三次样条插值
cv2.INTER_LANCZOS4 # Lanczos插值
将每个检测框摆正之后就可以使用切片方法 rota_img[pos[1]:pos[1]+h, pos[0]:pos[0]+w],横平竖直地分割出每个车位。pos[1]中存放y坐标,pos[0]中存放x坐标。切片时先指定高再指定宽。
为了避免切割出来的每个车位有绘图的痕迹,因此在绘图之前将原图像复制一份,imgCopy = img.copy(),在复制的图像上绘制矩形框,这样就不会在分割后的车位图像上出现绘图痕迹。
处理视频的代码如下:
# 处理视频图像
import cv2
import pickle
from rotaParking import recRota # 导入自定义的旋转矩形框函数
from rotaParking import drawLine # 导入绘制旋转矩形线条函数
#(1)读取视频
filepath = 'C:\\GameDownload\\Deep Learning\\parking.mp4'
cap = cv2.VideoCapture(filepath)
#(2)导入先前记录下来的车位矩形框的左上角坐标
filename = 'parking_position.txt' # 保存的车位坐标
with open(filename, 'rb') as f:
posList = pickle.load(f)
#(3)处理每一帧图像
while True:
# 返回图像是否读取成功,以及读取的帧图像img
success, img = cap.read()
# 为了使裁剪后的单个车位里面没有绘制的边框,需要在画车位框之前,把原图像复制一份
imgCopy = img.copy()
# 获得整每帧图片的宽和高
img_w, img_h = img.shape[:2] #shape是(w,h,c)
# 由于这个视频比较短,就循环播放这个视频
if cap.get(cv2.CAP_PROP_POS_FRAMES) == cap.get(cv2.CAP_PROP_FRAME_COUNT):
# 如果当前帧==总帧数,那就重置当前帧为0
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
#(4)绘制停车线矩形框
w, h = 90, 160 # 矩形框的宽和高
# 遍历所有的矩形框坐标
for pos in posList:
# 得到旋转后的矩形的四个角坐标,传入原图,旋转参考点坐标,矩形框左上角坐标,框的宽w和高h,逆时针转4°
angle = recRota(img, pos[0], pos[1], pos[0], pos[1], w, h, -4, draw=False) # 裁剪的车位不绘制车位图
#(5)裁剪所有的车位框,由于我们的矩形是倾斜的,先要把矩形转正之后再裁剪
# 变换矩阵,以每个矩形框的左上坐标为参考点,顺时针寻转4°,旋转后的图像大小不变
rota_params = cv2.getRotationMatrix2D(angle[0], angle=-4, scale=1)
# 旋转整张帧图片,输入img图像,变换矩阵,指定输出图像大小
rota_img = cv2.warpAffine(img, rota_params, (img_w, img_h))
# 裁剪摆正了的矩形框,先指定高h,再指定宽w
imgCrop = rota_img[pos[1]:pos[1]+h, pos[0]:pos[0]+w]
# 显示裁剪出的图像
cv2.imshow(f'imgCrop:{pos[0],pos[1]}', imgCrop)
#(6)绘制所有车位的矩形框
# 在复制后的图像上绘制车位框
imgCopy = drawLine(imgCopy, angle, (255,255,0), 3)
#(7)显示图像,输入窗口名及图像数据
cv2.imshow('img', imgCopy)
if cv2.waitKey(10) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
分割出每一个车位的图像
检测思路是:如果车位上没有车,那么二值图中白点很少,如果有车,车身的白点很多,因此通过计算每个车位框内的像素个数来确定该车位内是否有车。
(1)先将读入的图像变成灰度图,cv2.cvtColor(img, cv2.COLOR_BGR2GRAY);
(2)对灰度图滤波操作,消除噪声,采用高斯滤波,cv2.GaussianBlur(img, ksize, 标准差x, 标准差y),ksize卷积核大小,滑窗宽度高度为奇数;标准差x代表沿x方向的卷积核的标准差;标准差y代表沿y方向的卷积核的标准差,不设置的话则和x轴的标准差一致。
(3)对滤波后的灰度图转换为二值图,采用自适应阈值方法:
cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)
src: 灰度化的图片
maxValue: 当图像像素超过了阈值或低于阈值(由type决定),所赋予的值
adaptiveMethod: 自适应方法。有2种,
ADAPTIVE_THRESH_MEAN_C: 为局部邻域块的平均值,该算法是先求出块中的均值。
ADAPTIVE_THRESH_GAUSSIAN_C: 为局部邻域块的高斯加权和。该算法是在区域中(x, y)周围的像素根据高斯函数按照他们离中心点的距离进行加权计算。
thresholdType: 二值化方法,有两种
cv2.THRESH_BINARY: 二值法,超过阈值thresh部分取maxval(设定的最大值),否则取0
cv2.THRESH_BINARY_INV: 超过阈值的部分取0,小于阈值取maxval
blockSize: 分割计算的区域大小,取奇数
C: 常数,每个区域计算出的阈值的基础上在减去这个常数作为这个区域的最终阈值,可以为负数
(4)消除上的零散白点,对二值化之后的图像使用中值滤波,cv2.medianBlur(img, ksize)
,ksize代表滤波模板的尺寸大小,填一个数值,必须是大于1的奇数
(5)由于车位框内有车的话白点密集,没车的话车位内一片黑,因此扩充白点数目,为了增大区别。使用膨胀方法,cv2.dilate(img, kernel, iterations),kernel为卷积核大小,iterations为迭代次数。
下图中,第一张是二值化之后的图像,第二张是中值滤波后的,第三张是膨胀操作后的
(6)计算每个分割出来的车位框中的白点个数,countNonZero(),返回灰度值不为0的像素数量。经过分析,如果白点数量大于3000,那么就表明车位上有车。
空余车位检测的代码如下:
# 处理视频图像
import cv2
import pickle
import numpy as np
from rotaParking import recRota # 导入自定义的旋转矩形框函数
from rotaParking import drawLine # 导入绘制旋转矩形线条函数
#(1)读取视频
filepath = 'C:\\GameDownload\\Deep Learning\\parking.mp4'
cap = cv2.VideoCapture(filepath)
# 初始的车位框颜色
color = (255,255,0)
#(2)导入先前记录下来的车位矩形框的左上角坐标
filename = 'parking_position.txt' # 保存的车位坐标
with open(filename, 'rb') as f:
posList = pickle.load(f)
#(3)处理每一帧图像
while True:
# 记录有几个空车位
spacePark = 0
# 返回图像是否读取成功,以及读取的帧图像img
success, img = cap.read()
# 为了使裁剪后的单个车位里面没有绘制的边框,需要在画车位框之前,把原图像复制一份
imgCopy = img.copy()
# 获得整每帧图片的宽和高
img_w, img_h = img.shape[:2] #shape是(w,h,c)
# ==1== 转换灰度图,通过形态学处理来检测车位内有没有车
imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# ==2== 高斯滤波,卷积核3*3,沿x和y方向的卷积核的标准差为1
imgGray = cv2.GaussianBlur(imgGray, (3,3), 1)
# ==3== 二值图,自适应阈值方法
imgThresh = cv2.adaptiveThreshold(imgGray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 101, 20)
# ==4== 删除零散的白点,
# 如果车位上有车,那么车位上的像素数量(白点)很多,如果没有车,车位框内基本没什么白点
imgMedian = cv2.medianBlur(imgThresh, 5)
# ==5== 扩张白色部分,膨胀
kernel = np.ones((3,3), np.uint8) # 设置卷积核
imgDilate = cv2.dilate(imgMedian, kernel, iterations=1) # 迭代次数为1
# 由于这个视频比较短,就循环播放这个视频
if cap.get(cv2.CAP_PROP_POS_FRAMES) == cap.get(cv2.CAP_PROP_FRAME_COUNT):
# 如果当前帧==总帧数,那就重置当前帧为0
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
#(4)绘制停车线矩形框
w, h = 90, 160 # 矩形框的宽和高
# 遍历所有的矩形框坐标
for pos in posList:
# 得到旋转后的矩形的四个角坐标,传入原图,旋转参考点坐标,矩形框左上角坐标,框的宽w和高h,逆时针转4°
angle = recRota(imgDilate, pos[0], pos[1], pos[0], pos[1], w, h, -4, draw=False) # 裁剪的车位不绘制车位图
#(5)裁剪所有的车位框,由于我们的矩形是倾斜的,先要把矩形转正之后再裁剪
# 变换矩阵,以每个矩形框的左上坐标为参考点,顺时针寻转4°,旋转后的图像大小不变
rota_params = cv2.getRotationMatrix2D(angle[0], angle=-4, scale=1)
# 旋转整张帧图片,输入img图像,变换矩阵,指定输出图像大小
rota_img = cv2.warpAffine(imgDilate, rota_params, (img_w, img_h))
# 裁剪摆正了的矩形框,先指定高h,再指定宽w
imgCrop = rota_img[pos[1]:pos[1]+h, pos[0]:pos[0]+w]
# 显示裁剪出的图像
cv2.imshow('imgCrop', imgCrop)
#(6)计算每个裁剪出的单个车位有多少个像素点
count = cv2.countNonZero(imgCrop)
# 将计数显示在矩形框上
cv2.putText(imgCopy, str(count), (pos[0]+5, pos[1]+20), cv2.FONT_HERSHEY_COMPLEX, 0.8, (0,255,255), 2)
#(7)确定车位上是否有车
if count < 3000: # 像素数量小于2500辆就是没有车
color = (0,255,0) # 没有车的话车位线就是绿色
spacePark += 1 # 每检测到一个空车位,数量就加一
else:
color = (0,0,255) # 有车时车位线就是红色
#(8)绘制所有车位的矩形框
# 在复制后的图像上绘制车位框
imgCopy = drawLine(imgCopy, angle, color, 3)
# 绘制目前还剩余几个空车位
cv2.rectangle(imgCopy, (0,150), (200,210), (255,255,0), cv2.FILLED)
cv2.rectangle(imgCopy, (5,155), (195,205), (255,255,255), 3)
cv2.putText(imgCopy, 'FREE: '+str(spacePark), (31,191), cv2.FONT_HERSHEY_COMPLEX, 1, (255,0,255), 3)
#(9)显示图像,输入窗口名及图像数据
cv2.imshow('img', imgCopy) # 原图
cv2.imshow('imgGray', imgGray) # 高斯滤波后
cv2.imshow('imgThresh', imgThresh) # 二值化后
cv2.imshow('imgMedian', imgMedian) # 模糊后
cv2.imshow('imgDilate', imgDilate) # 膨胀
if cv2.waitKey(1) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
最终检测效果图如下: