目录
系列文章目录
完整代码:李忆如 - Gitee.com
本系列博客重点在计算机视觉的概念原理与代码实践,不包含繁琐的数学推导(有问题欢迎在评论区讨论指出,或直接私信联系我)。
第一章 计算机视觉——图像去噪及直方图均衡化(图像增强)_@李忆如的博客
第二章 计算机视觉——车道线(路沿)检测
梗概
本篇博客主要介绍基于Hough变换与深度学习的直线检测。其中介绍并使用了各种算子(尤其Canny)进行图像的边缘检测,并在Hough变换后使用几何特征与空间特征等筛选与确定目标直线。(内附数据与python代码)
一、实验内容与方法
实验内容:针对给定的视频,利用图像处理基本方法实现道路路沿的检测;
提示:可利用Hough变换进行线检测,融合路沿的结构信息实现路沿边界定位(图中红色的点位置)。
实验环境:Pycharm2021+Windows10
结果样例:02_result-CSDN直播
二、视频的导入、拆分、合成
本实验给定的数据为视频,所以在图像处理前要对视频继续导入与拆分,步骤如下:
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 |
其他 | -3.45 | 1 | 24 |
路沿 | 2.087 | 2 | 48 |
路沿 | 3.114 | 2 | 72 |
其他 | 1.03 | 2 | 96 |
路沿 | 3.432 | 3 | 120 |
其他 | -1.112 | 3 | 144 |
分析:对于表10及其他汇总数据进行统计分析,可知本实验中路沿斜率一般为2+,故在本实验中设置低阈值为2,高阈值为3.5。在直线绘制时时加入判断,其对直线的筛选样例如图22所示:
图21 空间的结构特征判断筛选直线样例
图22 直线的几何特征判断筛选直线样例
分析:如图21与图22所示,经过直线的几何特征或空间的结构特征后可以保留路沿相关直线,效果较好,在后续中需在几条直线中提取路沿直线,方法如下:
Ⅱ、路沿直线确定
在经过直线的几何特征与空间的结构特征判断后,仍存在不止一条直线,在其中要确定我们的目标直线(路沿),还需要增加一定的限制条件。在本实验中,确定路沿线以如下三个特点为例:
特点1——直线位置:本实验中路沿直线一般在筛选后直线中间,可以使用拟合后直线x轴坐标用于排除两边直线。
特点2——斜率聚类:对图像几何特征进行分析,图像从左至右直线斜率不断增大,可利用斜率对于筛选后直线聚类,根据特点1,选择中部斜率类进行保留。
特点3——x轴差值:根据特点2,可根据x轴差值(拟合直线在本图像中的最大x差值)确定路沿直线,利用python进行统计分析,在输出相关直线x轴差值信息,样例如表11所示:
表11 筛选后直线x轴差值分析样例
直线类型 | X轴差值 | 视频 | 帧数 |
路沿 | 84 | 2 | 48 |
路沿 | 113 | 2 | 72 |
其他 | 162 | 2 | 96 |
路沿 | 121 | 3 | 120 |
其他 | 62 | 3 | 144 |
分析:对于表11及其他汇总数据进行统计分析,可知本实验中路沿x轴差值区间为[80,130]。在直线绘制前时加入如上三大特点,其优化效果样例如图23所示:
图23 路沿直线确定优化效果
Tips:不同任务下待检测直线的确定特点有所不同,故路沿(目标)直线确定这一步添加需谨慎,需针对不同实际任务需求做修改。直接迁移有可能起到适得其反的效果,降低模型的泛化能力。在简单任务中也可直接使用直线的几何特征与空间的结构特征判断+后续修正即可,可以确定与目标直线相关的几条直线。
Ⅲ、过度判断的修正
在直线检测中还可能出现过度判断的情况,即未检测出直线,本实验中需对这种情况进行修正。
通过对数据视频的观察,不难发现待检测直线位置随视频帧变化较小,故简单情况下可直接使用上一帧直线+位置修正的方法,核心为存储每一帧的目标直线信息,在过度判断时利用两帧图片的位置偏差对上一帧图片进行位置修正后作为本帧目标直线。用其对过度判断修正样例如图24所示:
图24 上一帧直线+位置修正样例
分析:由图24可见,使用上一帧直线+位置修正可以较好地处理简单情况下过度判断的问题,除此之外,可使用泛化性能更强的卡尔曼滤波对该问题进行优化,原理与方法如下:
卡尔曼滤波:卡尔曼滤波是一种常见的目标追踪算法,作用就是基于传感器的测量值来更新预测值,以达到更精确的估计,如图25所示,核心公式如图26所示,在本实验中跟踪路沿线的步骤如表12所示:
图25 卡尔曼滤波样例
图26 卡尔曼滤波核心公式
表12 卡尔曼滤波跟踪车道线的算法步骤
1.根据当前帧Hough 变换提取的车道线参数,利用卡尔曼滤波跟踪当前车道线 |
2.每隔5s 扫描一次,比较Hough 变换检测的前、后两帧车道线的参数。如果在一定误差范围内(角度偏差小于±3o) ,让车辆继续沿原车道线行驶;如果角度偏差大于误差范围,转新的车道线行驶 |
3.继续跟踪时,转步骤(1)进行 |
分析:根据原理与流程分析,卡尔曼滤波器通过将测量误差和先前状态相加来平均沿车道检测到的线的变化。使检测到的车道标记线随时间推移保持稳定,并且由于其在以前的状态下仍具有预测特性,因此在环境影响较大的情况下(低光照、模糊、强变化),它可以通过记住先前视频帧中的先前检测到的车道来预测与检测车道。故卡尔曼滤波的优化效果与鲁棒性优于直接使用上一帧直线+位置修正。
Ⅳ、其他难点问题的优化效果
在本实验中存在的其他难点问题有二,解决方案与优化效果详述如下:
难点问题1——模糊:本实验中部分视频存在模糊情况(02.avi尤为明显),在检测时可能会出现限制条件后仍有多条直线的情况,因为视频连续两帧间筛选后直线数量不会突变,故解决方案为卡尔曼滤波+直线数量限制(num_line_this < num_line * 2),优化前后样例如图27所示:
图27 模糊视频帧直线检测优化
分析:如图27,优化后对于目标直线(路沿)的检测与定位准确,解决了模糊视频图像中直线检测误差问题,效果较好。
难点问题2——过弯:本实验部分视频存在过弯情况(03.avi)拐弯处由于路沿长短的变化以及角度变动幅度比较大,会导致检测过程中出现一系列问题。本实验解决方案为函数参数优化+条件限制(上文有详述),过弯样例如图27所示:
图27 过弯路沿检测样例
Tips:本实验我绘制的直线定长,故存在超出弯道部分,在实际中调节长度即可。
分析:由图27分析,过弯前后均可快速并正确地检测出路沿,较好地解决了过弯问题。
2.4 直线处理
在本实验中为使绘制出的直线更加稳定,可使用对齐(延长)直线与平滑化处理,具体如下:
Ⅰ、对齐(延长)直线
已知检测出直线的两点(x1,y1),(x2,y2),利用直线特征将直线延长至定长定起点实现对齐,代码实现样例(以y=250、y=480为例)如下:
x_temp = int(((x2 - x1) * (480 - y2) / (y2 - y1) + x2)) # 直线延长
x_temp_min = int(((x2 - x1) * (250 - y2) / (y2 - y1) + x2)) # 直线延长
对齐(延长)前后样例如图28所示:
图28 直线对齐样例
分析:使用直线对齐后,检测出直线相对规整,也便于聚类。
Ⅱ、平滑化处理
在路沿部分可能不止一条直线,而属于同一类直线,但探测到的车道线还是不够平滑,我们需要优化,基本思路就是对这些直线的斜率和截距取平均值然后将所有探测出点绘制到一条直线上,实现平滑化处理。
2.5 图像融合与视频合成
在进行完直线检测+直线绘制+直线处理后得到了带绘制灰度图,可以直接按1(3)中的视频合成方法生成最终视频(灰度,本实验使用方法)。或进行图像融合得到待绘制彩色图后按1(3)中的视频合成方法生成最终视频。
图像融合可以使用函数cv.addWeighted(),输入原始彩色图像与绘制灰度图按一定权重融合即可,代码如下:
# 原图像与车道线图像按照a:b比例融合
def weighted_img(img, initial_img, a=0.8, b=1., c=0.):
return cv2.addWeighted(initial_img, a, img, b, c)
样例(以α=0.8,β=1为例)如图29所示:
图29 图像融合样例
分析:由图29可见,图像融合后对原彩图较好地还原,并在此绘制检测出的直线,有不错的效果。
五、其他路沿检测方法补充
本实验使用了Canny算子边缘检测+Hough变换直线提取的经典方法进行路沿(车道线)检测,在代码实现方面,参数调节、直线筛选与限制条件判断等条件上编写较麻烦;在检测效果上,由于传统方法的限制,直线检测偶有不稳定现象,且模型对于数据集与环境条件依赖过大,可迁移性不强。
故尝试查找相关资料,发现车道线检测领域是一个热门研究领域,与无人驾驶领域有很大的相关性,在对部分论文进行研读后,在此补充并综述一些其他路沿(车道线)检测方法,详情如下:
1.传统图像方法综述
如前文所述,传统图像方法通过边缘检测滤波等方式分割出车道线区域,然后结合Hough变换、RANSAC等算法进行车道线检测。这类算法需要人工手动去调滤波算子,根据算法所针对的街道场景特点手动调节参数曲线,工作量大且鲁棒性较差,当行车环境出现明显变化时,车道线的检测效果不佳。主流方式如表13:
表13 传统图像方法进行车道线检测方法综述
基于Hough变换的车道线检测 |
基于LSD直线的车道线检测 |
基于俯视图变换的车道线检测 |
基于拟合的车道线检测 |
基于平行透视灭点的车道线检测 |
缺点:应用场景受限;Hough变换检测方法准确但很难做弯道检测,拟合方法可以检测弯道但不稳定,仿射变换可以做多车道检测但在遮挡等情况下干扰严重。透视变换操作会对相机有一些具体的要求,在变换前需要调正图像,而且摄像机的安装和道路本身的倾斜都会影响变换效果。其次,这些方法都无法满足实时性要求。
2.深度学习方法综述
近年来,随着深度学习在计算机视觉领域的快速发展,对应方法由于其鲁棒性和实时性迅速获得了关注,大致分为四类:基于分割的方法、基于检测的方法、基于参数曲线的方法、基于关键点的方法。下面对四类方法进行简介:
2.1 基于分割的方法
基于分割的方法将车道线检测建模为逐像素分类问题,每个像素分为车道线区域或背景。这类模型通常是在语义分割模型的基础上,增加一个车道线实例判别头,来对车道线是否存在进行监督学习。经典算法有:SCNN、RESA、LaneNet等。
其中以SCNN为例,为了区分不同的车道线,SCNN将不同的车道线作为不同的类别,从而将车道检测转化为多类分割任务。提出一个切片CNN结构(RESA对结构进行改进,加入切片间的不同步幅大小的信息传递,同时解耦相邻层之间的时序上的依赖,增加并行处理能力),以使消息跨行和列传递,原理如图30所示:
图30 SCNN原理
总结:分割模型大,处理速度慢。在严重遮挡情况下表现差,未充分利用车道先验知识。
2.2 基于检测的方法
基于检测的方法通常采用自顶向下的方法来预测车道线,这类方法利用车道线在驾驶视角自近处向远处延伸的先验知识,构建车道线实例。基于Anchor的方法设计线型Anchor,并对采样点与预定义Anchor的偏移量进行回归。应用非极大值抑制(NMS)选择置信度最高的车道线。经典算法有:LineCNN、LaneATT等。
其中以lineCNN为例,其使用从图像边界以特定方向发出的直线射线作为一组Anchor,原理如图31所示:
图31 lineCNN原理
总结:自顶向下的设计能够更好的利用车道线的先验知识,提高检测实时性,同时在处理严重遮挡等情况下能够获得连续的车道线检测实例。但预设Anchor形状会影响检测的灵活性。
2.3 基于参数曲线的方法
基于参数曲线的方法使用预先设定的参数曲线,对车道线形状进行检测,经典算法有:PolyLaneNet、B´ezierLaneNet等。
以PolyLaneNet为例,其通过多项式曲线回归,输出表示图像中每个车道线的多项式。并维持高效性(115FPS),原理如图32所示:
图32 PolyLaneNet原理
总结:基于曲线的方法可以自然地学习整体车道表示,具有较高的推理速度,但在准确度上不高。
2.4 基于关键点的方法
基于关键点的方法直接对车道线的实例进行检测,使用后处理对实例进行划分。经典算法有:FOLOLane、GANet等。
以FOLOLane为例,其对局部模式进行建模,并以自下而上的方式实现对全局结构的预测,原理如图33所示:
图33 FOLOLane原理
总结:此类方法兼具灵活性和实时性,在处理遮挡问题时如何构建全局信息是需要考虑的问题。
六、深度学习方法实践
本部分以LaneNet为例,对深度学习方法在车道线检测方面进行简单实践,具体如下:
Towards End-to-End Lane Detection: an Instance Segmentation Approach论文(2018):Towards End-to-End Lane Detection: an Instance Segmentation Approach
代码(tensorflow):https://github.com/MaybeShewill-CV/lanenet-lane-detection
数据集(tuSimple):https://github.com/TuSimple/tusimple-benchmark/issues/3
1.数据集介绍
LaneNet官方使用数据集为tuSimple数据集,是车道线识别中常用数据集(网络最早公开的数据集),基本为高速公路车道线检测,相关信息如表14:
表14 tuSimple数据集相关信息
复杂性 | 良好和中等天气条件、白天不同、多车道与高速公路不同的交通状况 |
规模 | 训练:3626个视频剪辑(每个剪辑最后一帧带有标注) 测试:2782个视频剪辑 |
相机与视频片段 | 1s剪辑20帧 相机的视线方向非常接近行车方向 |
注释类型 | 车道标记折线 |
2.算法介绍
LaneNet是一种端到端(输入原数据,无需特征提取)的车道线检测方法,包含 LanNet + H-Net 两个网络模型。是将语义分割和对像素进行向量表示结合起来的多任务模型,最后利用聚类完成对车道线的实例分割。
H-Net 是个小的网络结构,负责预测变换矩阵 H,使用转换矩阵 H 对同属一条车道线的所有像素点进行重新建模(即:学习给定输入图像的透视变换参数,该透视变换能够对坡度道路上的车道线进行良好地拟合)。
整体网络架构与核心流程如图34所示,LaneNet网络架构与实现如图35所示:
图34 LaneNet整体网络架构与核心流程
图35 LaneNet网络架构与实现
优点:通过embedding vector与cluster配合使用,能检测不限条数的车道线。通过HNet学得的perspective transformation,使得lane fitting能够更鲁棒。
缺陷:cluster很耗时,实际工程应用上很难满足实时性的要求。
3.代码实践
Tips:代码详细实践过程可见github或深度学习车道线检测之 LaneNet (使用tensorflow2.4.0)
将数据集与项目代码准备好后,导入到Pycharm,按如下方式部署与运行:
3.1 环境的搭建与部署
可使用anaconda管理环境,使用pip install -r requirements.txt命令安装相关依赖即可。
3.2 训练
在部署好环境后即可进入训练,可使用tuSimple数据集(本次探究使用)与个人数据集(更改data_provider\lanenet_data_feed_pipline.py,需自己采集与标注),训练操作流程如表15:
表15 LaneNet训练操作流程
1.使用项目lanenet-lane-detection中的脚本generate_tusimple_dataset.py产生训练数据 |
2.根据.json文件转换训练集,生成图片文件夹以及文本文件 |
3.将标注格式转换成TFRecord |
4.运行 tools/train_lanenet_tusimple.py,开始训练 |
可使用tensorboard可查看训练过程,损失与变化曲线如图36所示,还可以查看模型在训练过程中的分割分支和嵌入分支输出到预测图,如图37所示:
图36 LaneNet训练损失与变化曲线
图37 LaneNet分支预测图样例
3.3 测试
在训练结束后得到模型,将权重文件放在model目录下,即可输入图片进行车道线检测,检测样例如图38所示:
图38 LaneNet车道线检测样例
分析:由图38可见,LaneNet能较好地对测试图像的车道线进行检测,项目实践成功。
七、总结
1.路沿检测步骤
本次实验的核心任务为路沿检测,对实验步骤进行总结,基于Hough变换的路演检测步骤如图39所示:
图39 基于Hough变换的直线(路沿)检测步骤
2.路沿检测问题与解决
本次实验在实现路沿检测的过程中,在各部分均出现了一定问题,几个典型问题及解决方案总结如下:
2.1 图像处理方式的选择
本实验最初尝试使用实验一的均值与中值滤波对图像进行预处理,效果不佳。在后续选择与Canny算子相适配的高斯滤波,在Hough变换前加入对比度增强,解决了此问题。故在实际问题中应该根据算法与数据选择相应的图像处理方式。
2.2 参数设置问题
无论是Canny算子的高低阈值,还是Hough变换中的七个参数,在本实验中的选择均对结果有较大影响。故在本实验中需人工调参,经过不断迭代与优化参数确定较优参数,同时需兼顾参数的准确性与泛化能力。
2.3 目标直线的筛选与确定
在Hough变换后检测出多条直线,如何筛选与确定目标直线(路沿)也是一个问题。本实验中首先使用直线的几何特征(斜率)与空间的结构特征(mask掩模)判断,再利用直线位置、斜率聚类、x轴差值等特点确定目标直线,并使用上一帧直线+位置修正或卡尔曼滤波修正过度判断的情况。对于本实验中出现的视频模糊与过弯情况,使用上述方法也可以较好解决。
3.基于Hough变换直线检测的分析与补充
在利用Hough变换及相关判断完成本实验路沿识别后,对实验效果进行分析如下:
3.1 优点
基于Hough变换进行直线检测的优点在于实现方便、代码复杂度低(高效),对于简单数据处理情况较好。
3.2 缺陷
基于Hough变换进行直线检测的缺陷在于参数调整过程麻烦,最优参数确定困难,模型对环境因素依赖性过强,在变化过大等情况下表现不佳,且泛化能力较差,代码迁移性低。
3.3 补充
在传统图像方法上,除了基于Hough变换的直线检测,还有基于LSD直线、基于俯视图变换、基于拟合、基于平行透视灭点的车道线检测等。
在深度学习方法上,由于其在直线检测任务上表现出的高实时性与鲁棒性,多数方法涌现。大致分为如下四类:基于分割的方法、基于检测的方法、基于参数曲线的方法、基于关键点的方法。
八、参考资料
1.传统车道线检测-canny边缘检测-霍夫变换-完整代码(python)_悬悬小的博客-CSDN博客
2.基于Python的复杂环境中车道线自动检测系统 - 知乎 (zhihu.com)
3.2022-车道线检测综述_棒冰柠檬味的博客-CSDN博客_车道线检测的背景和意义
4.车道线检测传统方法&深度学习方法概览+两篇论文领读LaneATT+LaneNet
5.深度学习车道线检测之 LaneNet (使用tensorflow2.4.0跑通)_tiger&sheep的博客
总结
本次实验为计算机视觉的第二次实验,通过本次实验,我熟悉了熟悉图像处理基本操作,包括但不限于视频的拆分与合并、图像灰度化。在图像滤波方面,分别使用高斯滤波与卡尔曼滤波;在边缘检测方面,使用Canny算子,并尝试了深度学习方法。并掌握了图像基本特征抽取以及在实际问题中的应用。
本实验耗时近5天,基本每天都不停在查阅相关资料(f**k),算是本学期最麻烦的实验了。除了实验要求任务,对于理论部分与深度学习部分本人也做了一定探究,收获颇丰。在本次实验中遇到的各种疑难问题,在查阅实验指导、与同学交流、网上与书本查阅资料搭配个人的独立思考后都基本解决,实验结果符合实验设计与实验预期,完成情况较好。