计算机视觉——车道线(路沿)检测
Posted @李忆如
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了计算机视觉——车道线(路沿)检测相关的知识,希望对你有一定的参考价值。
目录
系列文章目录
完整代码:李忆如 - Gitee.com
本系列博客重点在计算机视觉的概念原理与代码实践,不包含繁琐的数学推导(有问题欢迎在评论区讨论指出,或直接私信联系我)。
第一章 计算机视觉——图像去噪及直方图均衡化(图像增强)_@李忆如的博客
第二章 计算机视觉——车道线(路沿)检测
梗概
本篇博客主要介绍基于Hough变换与深度学习的直线检测。其中介绍并使用了各种算子(尤其Canny)进行图像的边缘检测,并在Hough变换后使用几何特征与空间特征等筛选与确定目标直线。(内附数据与python代码)
一、实验内容与方法
实验内容:针对给定的视频,利用图像处理基本方法实现道路路沿的检测;
提示:可利用Hough变换进行线检测,融合路沿的结构信息实现路沿边界定位(图中红色的点位置)。
实验环境:Pycharm2021+Windows10
二、视频的导入、拆分、合成
本实验给定的数据为视频,所以在图像处理前要对视频继续导入与拆分,步骤如下:
1.视频时长读取
为了对导入的视频自适应拆分,需要先读取出视频时长,方法总结如表1,代码详见:
python3 获取视频文件播放时长(三种方法)_小龙在山东的博客-CSDN博客_python获取视频时长
表1 Python进行视频时长读取的常用方法
1.使用VideoFileClip |
2.使用CV2(最快) |
3.使用FFmpeg |
经比较后发现CV2读取最高效,故本实验使用CV2实现,代码如下:
# 获取视频时长
def get_duration_from_cv2(filename):
cap = cv2.VideoCapture(filename)
if cap.isOpened():
rate = cap.get(5)
frame_num = cap.get(7)
duration = frame_num / rate
return duration
return -1
2.视频的拆分
视频:连续的图像变化每秒超过24帧(frame)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面;看上去是平滑连续的视觉效果,这样连续的画面叫做视频。
故视频拆分为图像的过程实际为视频的帧分解。在得到视频时长后,可利用cv2.VideoCapture读取视频,并通过cv2库中的get、read等函数对视频的特定帧进行访问,再通过imwrite函数对得到的图片进行写入即可,核心代码(以逐帧分解为例)如下:
# 视频拆分
def Video_splitting(filename):
cap = cv2.VideoCapture(filename)
isOpened = cap.isOpened # 判断视频是否可读
print(isOpened)
fps = cap.get(cv2.CAP_PROP_FPS) # 获取图像的帧,即该视频每秒有多少张图片
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) # 获取图像的宽度和高度
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(fps, width, height)
length = math.floor(get_duration_from_cv2(filename)) # 向下取整
i = 0
while isOpened:
if i == 24 * length: # 分解为多少帧(i)
break
# 读取每一帧,flag表示是否读取成功,frame为图片的内容
(flag, frame) = cap.read()
filename = 'img' + str(i) + '.jpg' # 文件的名字
if flag:
cv2.imwrite(filename, frame, [cv2.IMWRITE_JPEG_QUALITY, 100]) # 保存图片
i += 1
return length
拆分样例(02.avi的拆分)如图1:
图3 视频拆分结果样例
3.视频的合成
在后续对图像处理完成之后最终要再形成视频,由(2)中视频定义可知,将图像按设定帧率与顺序连续即可完成视频的合成。在实现上我使用cv2.VideoWriter方法来创建一个video写入器,用cv2.VideoWriter_fourcc创建视频编解码器,代码如下:
# 视频合成
def Video_compositing(length):
img = cv2.imread('img0.jpg')
width = img.shape[0]
height = img.shape[1]
size = (height, width)
print(size)
videoname = "2.mp4" # 要创建的视频文件名称
# fourcc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G') # 编码器
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 编码器修改
fps = 24 # 帧率(多少张图片为输出视频的一秒)
# 1.要创建的视频文件名称 2.编码器 3.帧率 4.size
videoWrite = cv2.VideoWriter(videoname, fourcc, fps, size)
for i in range(fps * length):
filename = 'img_line' + str(i) + '.jpg'
img = cv2.imread(filename)
videoWrite.write(img) # 写入
三、图像处理/边缘检测
在真实环境中存在一定噪声,会影响后续目标检测的精度,故在此之前需进行一定的图像处理,具体方法与步骤如下:
0.尝试
最初本人使用实验一中提到的均值滤波(3x3为例)与中值滤波(卷积核为5为例)对实验图像进行处理,样例如图2所示:
图2 图像进行均值、中值滤波前后(上为均值滤波)
分析:由图2可见,使用均值及中值滤波降噪效果均较差,且滤波后图像模糊,丢失较多信息,不能为后续的边缘检测与路沿识别提供优化,故在本实验中使用其他方法。
1.图像处理->边缘检测(原理)
在图像中边缘即为亮度变化明显的点,边缘检测本质上就是检测并绘制出边缘点的集合,实现了简化图像信息,使用边缘线代表图像所携带信息。样例如图3所示:
图3 边缘检测样例
根据边缘定义,要找到亮度变化明显的点,只需要找到梯度大的点即可,图像梯度即当前所在像素点对于X轴、Y轴的偏导数,所以在图像处理领域可以理解为像素灰度值变化的速度。其中二维函数的微分(处理灰度图)定义如图4所示,梯度相关定义如图5所示:
图4 二维函数的微分(处理灰度图)定义
图5 梯度相关定义
根据原理与相关定义,边缘检测一般步骤总结如表2所示:
表2 边缘检测一般步骤
1.滤波:导数的计算对噪声很敏感,因此必须使用滤波器来改善与噪声有关的边缘检测器的性能。 |
2.增强:增强边缘的基础是确定图像各点邻域强度的变化值(计算梯度幅值)。增强算法可以将邻域(或局部)强度值有显著变化的点突显出来。 |
3.检测:在图像中有许多点的梯度幅值比较大,而这些点在特定的应用领域中并不都是边缘,所以应该用某种方法来确定哪些点是边缘点。最简单的边缘检测判据是梯度幅值阈值判据。 |
4.定位:如果某一应用场合要求确定边缘位置,则边缘的位置可在子像素分辨率上来估计,边缘的方位也可以被估计出来。 |
由上图对图2分析与上述原理与流程的定义可知,本实验进行图像处理的目的是为了更好实现后续的边缘检测与路沿(直线)检测,故图像处理应该与边缘检测的原理紧密结合。本实验给出如下几种边缘检测方法与其对应的图像处理方法。
我们在此介绍一下几种经典的边缘检测算子,Roberts算子、Prewitt算子、Sobel算子、Laplacian算子。其中不同算子的原理对比及缺点汇总如表3所示,样例公式如图6所示:
表3 不同边缘检测算子及其原理与缺点汇总
算子 | 原理 | 缺点 |
Roberts | 基于一阶导数 | 对噪声敏感,难以抑制噪声的影响 提取边缘比较粗 边缘定位不是很准确 |
Prewitt | 基于一阶导数 | 像素平均相当于对图像的低通滤波,所以 Prewitt 算子对边缘的定位不准 |
Sobel | 基于一阶导数 | 由于边缘是位置的标志,对灰度的变化不敏感 |
Laplacian | 基于二阶导数 | 对噪声比较敏感,只适用于无噪声图像 容易丢失边缘方向信息,造成一些不连续的检测边缘 |
图6 几种经典边缘检测算子公式样例
分析:由表3中结论,经典边缘检测方法在使用中或多或少存在一定问题,求得的边缘图存在很多问题,如噪声污染没有被排除、边缘线太过粗宽等,对于本任务均不是最优选择。故在本实验中选择Canny算子进行边缘检测。不同算子进行边缘检测效果对比样如图7所示:
图7 不同边缘检测算子效果对比
2.Canny算子边缘检测(原理)
经过对比,较适合本实验的边缘检测方法为Canny算子。Canny算子是一种非微分边缘检测算子,目标是找到一个最优的边缘检测解或找寻一幅图像中灰度强度变化最强的位置。最优边缘检测主要通过低错误率、高定位性和最小响应三个标准进行评价。相关标准定义如表4,使用Canny算子进行边缘检测流程及相关原理如表5所示:
表4 Canny相关评价标准定义
评价标准 | 定义 |
低错误率 | 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报 |
高定位性 | 标识出的边缘要与图像中的实际边缘尽可能接近 |
最小响应 | 图像中的边缘只能标识一次 |
表5 Canny算子进行边缘检测流程及相关原理
步骤 | 操作 |
1 | 高斯滤波 高斯滤波的原理:根据待滤波的像素点及其邻域点的灰度值按照高斯公式生成的参数规则进行加权平均。 |
2 | 计算梯度图像与角度图像 canny中使用的梯度检测算子是使用高斯滤波器进行梯度计算得到的滤波器,得到的结果类似于sobel算子,即距离中心点越近的像素点权重越大。角度图像的计算则较为简单,其作用为非极大值抑制的方向提供指导。 |
3
4 | 对梯度图像进行非极大值抑制 上一步得到的梯度图像存在边缘粗宽、弱边缘干扰等众多问题,现在可以使用非极大值抑制来寻找像素点局部最大值,将非极大值所对应的灰度值置0,极大值点置1,剔除一大部分非边缘的像素点,因此最后生成的图像应为一副二值图像,边缘理想状态下都为单像素边缘。 使用双阈值进行边缘连接 目前仍存在许多伪边缘,canny算法采用的算法是双阈值法,具体思路是:选取两个阈值,将小于低阈值的点认为是假边缘置0,将大于高阈值的点认为是强边缘置1,介于中间的像素点需要进一步的检查。 |
3.Canny算子边缘检测(实现)
在正式开始边缘检测前,有以下四个重要特征需要了解,后续设计中帮助提高识别率:
Ⅰ、颜色:车道线(路沿)通常为浅色(白色/黄色),而道路则为深色(深灰色)。因此,黑白图像效果更好,因为车道可以很容易地从背景中分离出来。
Ⅱ、形状:车道线(路沿)通常是实线或虚线,所以可以将它们与图像中的其他对象分开。可以用Canny等边缘检测算法找到图像中的所有边缘/线条。然后我们可以使用进一步的信息来决定哪些边可以被限定为车道线。
Ⅲ、方向:公路车道线(路沿)更接近于垂直方向,而不是水平方向。因此,在图像中检测到的直线的斜率可以用来检查它是否可能是车道。
Ⅳ、在图像中的位置:在一个由行车记录仪拍摄的常规公路图像中,车道线(路沿)通常出现在图像的下半部分。因此,可以将搜索区域缩小到感兴趣的区域,以减少噪声。
根据(2)中的原理与流程设计代码,核心实现的设计与解析如下:
3.1 图像转化(彩->灰)
图像转化原因:边缘检测最关键的部分是计算梯度,颜色难以提供关键信息,并且颜色本身非常容易受到光照等因素的影响,所以只需要灰度图像中的信息就足够了。并且灰度化后,简化了矩阵,提高了运算速度。
原理:将彩色图像(Color Image)转换为灰度图(Gray Scale Image),即从三通道RGB图像转为单通道图像。
实现:我们实现彩图转化为灰度图需要用到opencv库中的cv.cvtColor函数,需要用到两个参数:src——输入图片,code——颜色转换代码,具体代码如下:
# 灰度图转换
def grayscale(num_img):
for i in range(num_img):
filename = 'img' + str(i) + '.jpg'
img = cv2.imread(filename)
img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
filename = 'img_gray' + str(i) + '.jpg'
cv2.imwrite(filename, img_gray)
转化样例如图8:
图8 图像转化样例
3.2 高斯滤波
高斯滤波选择原因:因为现实中的噪声分布多是随机,故在图5(0)中使用均值滤波与中值滤波效果不好,而Canny算子一般搭配高斯滤波。
简介、原理及操作:高斯滤波是一种线性平滑滤波,适用于消除高斯噪声。高斯滤波每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值,其中卷积操作原理样例如图9所示:
图9 高斯滤波卷积操作原理样例
Tips:其中 * 表示卷积操作; Gσ 是标准差为σ 的二维高斯核,定义如图10所示:
图10 二维高斯核定义
实现:高斯滤波在代码中的实现可使用自定义函数与库函数两种方法实现,具体如下:
Ⅰ、自定义函数
若使用定义函数实现高斯滤波,流程如表6:
表6 自定义函数实现高斯滤波步骤
1. 对图像进行zero padding |
2. 根据高斯滤波器的核大小和标准差大小实现高斯滤波器 |
3. 使用高斯滤波器对图像进行滤波(相乘再相加) |
4. 输出高斯滤波后的图像 |
函数实现如下:
def GaussianFilter(img):
h,w,c = img.shape
# 高斯滤波
K_size = 3
sigma = 1.3
# 零填充
pad = K_size//2
out = np.zeros((h + 2*pad,w + 2*pad,c),dtype=np.float)
out[pad:pad+h,pad:pad+w] = img.copy().astype(np.float)
# 定义滤波核
K = np.zeros((K_size,K_size),dtype=np.float)
for x in range(-pad,-pad+K_size):
for y in range(-pad,-pad+K_size):
K[y+pad,x+pad] = np.exp(-(x**2+y**2)/(2*(sigma**2)))
K /= (sigma*np.sqrt(2*np.pi))
K /= K.sum()
# 卷积的过程
tmp = out.copy()
for y in range(h):
for x in range(w):
for ci in range(c):
out[pad+y,pad+x,ci] = np.sum(K*tmp[y:y+K_size,x:x+K_size,ci])
out = out[pad:pad+h,pad:pad+w].astype(np.uint8)
return out
Ⅱ、库函数(本实验方法)
Opencv库中内置高斯滤波函数,用法为cv2.GaussianBlur(src, ksize, sigmaX, sigmaY, borderType)-> dst。经对比后,发现库函数实现与使用较方便,且效果较好,故本实验选择库函数实现高斯滤波,前后对比样例(以高斯矩阵的长与宽为5,标准差取0为例)如图11所示:
图11 图像进行高斯滤波前后
3.3 Canny边缘检测
在进行完图像转化与高斯滤波等图像处理后,正式进入Canny边缘检测,按表4中步骤设计边缘检测代码,如高斯滤波实现,可自定义函数实现,也可直接使用库函数实现。手写实现可见:Python实现Canny算子边缘检测 | Z Blog (yueyue200830.github.io),本实验使用库函数进行Canny边缘检测。其中每一步生成图像样例及结果对比如图12所示:
图12 Canny边缘检测每一步生成的图像及不同实现方法效果对比
本实验直接使用opencv库中的cv.Canny函数,其中使用到的参数为:src——输入图像,low_threshold ——低阈值,high_threshold——高阈值,边缘检测样例(低阈值=75,高阈值=225为例)如图13所示:
图13 库函数实现边缘检测样例
3.4 生成Mask掩膜,提取 ROI
通过观察我们不难发现,本实验中路沿在图像中的位置基本处于中间偏右,这意味着我们可以对图像进行区域选取,排除其他边缘与线的影响,使识别效果更好,详解如下:
简介:Mask掩模的作用为降低计算代价,核心为遮挡非感兴趣区,只在我们感兴趣部分(ROI)进行算法的计算(mask最终需要与要作用到的输入图像的尺寸与类型保持一致)。本实验中我们感兴趣的部分为路沿,故可设计Mask掩模如图14所示:
图14 Mask掩模样例
Tips:实际任务中根据不同感兴趣区域进行Mask掩模,若需分析区域变化过大不适宜使用。
实现:Mask掩模设计与实现步骤如表7所示,实现样例(以构图点为(280, 0), (340, 0), (500, 480), (340, 480)为例)如图15所示:
表7 Mask掩模设计与实现步骤
1.生成一个与原图大小维度一致的mask矩阵,并初始化为全0,即全黑 |
2.对照原图在该mask上构建感兴趣区域 |
3.利用opencv中cv.fillpoly()函数对所限定的多边形轮廓进行填充,填充为1,即全白 |
4. 利用opencv中cv.bitwise()函数与canny边缘检测后的图像按位与,保留原图相中对应感兴趣区域内的白色像素值,剔除黑色像素值 |
代码如下:
# 生成感兴趣区域即Mask掩模
def region_of_interest(image, vertices):
mask = np.zeros_like(image) # 生成图像大小一致的zeros矩
# 填充顶点vertices中间区域
if len(image.shape) > 2:
channel_count = image.shape[2]
ignore_mask_color = (255,) * channel_count
else:
ignore_mask_color = 255
# 填充函数
cv2.fillPoly(mask, vertices, ignore_mask_color)
masked_image = cv2.bitwise_and(image, mask)
return masked_image
图15 Mask掩模实现样例(左为原图、中为原图mask、右为边缘mask)
至此,本实验的图像处理与边缘检测部分基本结束,实现及效果在前文中有详述,接下来进入到本实验的核心任务——路沿检测。
四、基于Hough变换的路沿检测
本部分将完成路沿检测,核心为基于Hough变换的直线检测,详解如下:
1.Hough变换(原理)
Hough变换是一种使用表决方式的参数估计技术,其原理是利用图像空间和Hough参数空间的线-点对偶性,把图像空间中的检测问题转换到参数空间中进行。空间映射样例如图16所示:
图16 Hough变换空间映射样例
分析:由于这种实现方式(y=mx+b)不能表示垂直线(斜率为无穷大),故在实际操作中选择极坐标系。根据直角坐标系和极坐标系变换域之间的关系,总结Hough变换主要性质如表8所示,映射样例如图17所示,Hough直线检测步骤如表9所示,Hough直线检测样例如图18所示:
表8 Hough变换主要性质
直角坐标系中的一点对应于极坐标中的一条正弦曲线 |
变换域极坐标系中一点对应于直角坐标系中的一条直线 |
直角坐标系一条直线上的N个点对应于极坐标系中共点的N条曲线 |
图17 Hough变换空间映射样例(极坐标系)
表9 Hough直线检测步骤
1.构建(参数空间)变换域累加器数组,并将其初始化为0 |
2.读入一幅二值化图像,遍历图像像素点 |
3.对每一个像素点,进行霍夫变换,按照r和θ的值在变换域累加器数组中的相应位置上加1 |
4.遍历累加器数组,寻找局部极大值 |
图18 Hough直线检测样例
2.基于Hough变换的路沿检测
基于(1)中的原理介绍与分析,使用Hough变换进行路沿检测,首先可以使用ImageEnhance.Contrast(img).enhance(n)函数增加图片对比度,如图19所示:
图19 对比度增加样例
然后使用Opencv封装好的cv.HoughLinesP函数进行路沿(直线)检测,其中参数及其解释如下:
2.1 函数参数解释
Ⅰ、第一个参数:InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像。
Ⅱ、第二个参数:InputArray类型的lines,经过调用HoughLinesP函数后后存储了检测到的线条的输出矢量,每一条线由具有四个元素的矢量(x_1,y_1, x_2, y_2) 表示,其中,(x_1, y_1)和(x_2, y_2) 是是每个检测到的线段的结束点。
Ⅲ、第三个参数:double类型的rho, 以像素为单位的距离精度(直线搜索时的进步尺寸的单位半径)。
Ⅳ、第四个参数:double类型的theta,以弧度为单位的角度精度(直线搜索时的进步尺寸的单位角度)。
Ⅴ、第五个参数:int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。 大于阈值 threshold 的线段才可以被检测通过并返回到结果中。
Ⅵ、第六个参数:double类型的minLineLength,有默认值0,表示最低线段的长度,比这个设定参数短的线段就不能被显现出来。
Ⅶ、第七个参数:double类型的maxLineGap,有默认值0,允许将同一行点与点之间连接起来的最大的距离。
Ⅷ、输出:输出将是线,它将只是一个数组,包含通过霍夫变换检测到的所有线段的端点(x1、y1、x2、y2)。
2.2 直线检测
在了解cv.HoughLinesP函数参数与解释后,使用其在本任务中进行直线检测,返回直线坐标。其中必须根据你的需求调整参数,在本实验中两组较优参数如下:
lines = cv2.HoughLinesP(img_canny, 0.5, np.pi / 180, 20, np.array([]), minLineLength=30,
maxLineGap=10) # test1较优参数
lines = cv2.HoughLinesP(img_canny, 1, np.pi / 180, 100, np.array([]), minLineLength=100,
maxLineGap=8) # ta参
Tips:调参过程切忌过拟合,会降低代码(模型)的泛化能力,在后续过程还可以进行直线的分类与判断确定所需的目标直线。
2.3 直线绘制
在2.2中利用Hough变换返回了检测到的直线的坐标,在本部分进行直线的绘制,绘制函数为opencv库中的cv2.line(image, (x1, y1), (x2, y2), color, pixel)。绘制样例(无mask掩模)如图20所示:
图20 直线绘制样例(无mask掩模)
分析:如图20所示,直接Hough变换存在过度检测、过度判断等问题,故对于目标直线定义一个合理的判断逻辑也至关重要,因此在直线绘制过程中加入如下判断:
Ⅰ、直线的几何特征与空间的结构特征判断
对于过度检测的情况,可以利用直线的几何特征或空间的结构特征对直线进行筛选。空间结构如mask掩模,上文有详述。其对于直线的筛选样例如图21所示。
几何特征如直线斜率,观察本实验数据不难分析,目标路沿斜率变化不大且在某个区间,故利用python进行统计分析,在mask掩模+抽帧方法输出相关直线斜率信息,样例如表10所示:
表10 直线斜率分析样例
直线类型 | 斜率 | 视频 | 帧数 |
路沿 | 2.113 | 1 | 1 |
其他 | |