Python图像处理入门学习——基于霍夫变换的车道线和路沿检测

文章目录

  • 前言
  • 一、实验内容与方法
  • 二、视频的导入、拆分、合成
    • 2.1 视频时长读取
    • 2.2 视频的拆分
    • 2.3 视频的合成
  • 三、路沿检测
    • 3.1 路沿检测算法整体框架
    • 3.2 尝试
    • 3.3 图像处理->边缘检测(原理)
    • 3.4 Canny算子边缘检测(原理)
    • 3.5 Canny算子边缘检测(实现)
      • 3.5.1 高斯滤波
      • 3.5.2 图像转化(彩色->灰度)
      • 3.5.3 Canny边缘检测
      • 3.5.4 生成Mask掩膜,提取ROI
  • 四、基于Hough变换的路沿检测
    • 4.1 Hough变换(原理)
    • 4.2 基于Hough变换的路沿检测
      • 4.2.1 函数参数解释
      • 4.2.2 直线检测
      • 4.2.3 直线绘制
  • 五、车道检测(白色与黄色车道)
    • 5.1 车道检测实现流程
    • 5.2 高斯滤波
    • 5.3 色彩切片
      • 5.3.1 白色车道线切片
      • 5.3.2 黄色车道线切片
      • 5.3.3 切片后图像相加
    • 5.4 删除天空(ROI提取)
    • 5.5 形态学运算
  • 六、基于Hough变换的车道检测
    • 6.1 虚实线检测
    • 6.2 黄白线的检测
    • 6.3 Canny边缘检测
    • 6.4 绘制车道线(解决霍夫变换检测到多条直线问题)
    • 6.5 图像融合
    • 6.6 视频合成
  • 七、总结与思考
    • 7.1 路沿检测综述
    • 7.2 车道检测综述
    • 7.3思考和体会
  • 参考


前言

  PS:马上临近期末,趁这两天复习的间隙将之前的《数字图像处理》课程的大作业归纳总结一些,写成技术博客分享给大家,如有错误,也请大家帮我指正出来,博主后面期末周比较忙碌,后面也会抽时间再次完善优化。


一、实验内容与方法

  车道线检测是自动驾驶和驾驶辅助系统中的重要任务。然而,车道线检测在实际场景中面临一些挑战,如光照变化、阴影、道路纹理变化、天气条件等。这些因素可能对算法的性能和鲁棒性产生影响,除了车道线检测,路沿检测也是驾驶安全的重要组成部分。准确地检测和识别路沿可以帮助驾驶员保持车辆的稳定性和车道保持。因此,在实验中研究路沿检测车道线检测的算法和技术是非常有意义的。

  基于此,经过一段时间的研究实验和理论技术学习,本文根据边缘提取+霍夫变换,针对日常车辆行驶的情况进行了算法研究和编程实现,实现了路沿检测和车道线检测(白色与黄色车道)。

  本文研究的研究过程,大致经历了四个部分。第一部分是对视频(图片)素材的获取第二部分,是对算法的大致流程和过程中可能会用到的方法进行讨论,对检测结果综合处理分析,画出算法流程图第三部分,是对检测算法研究与实现,主要是边缘提取算法、色彩切片算法和直线检测算法第四部分,对实验结果进行了分析,阐述了算法的不足和对后续的展望

  最后实验结果表明,可以做到路沿和车道线的精准跟踪,但是由于算法限制,我们一般只能跟踪监测出正常情况下行驶所拍摄的道路线,没办法实现复杂场景下的车道线或路沿检测,后续期望融入机器学习的方法提取特征,实现复杂场景下的检测判断与跟踪。

二、视频的导入、拆分、合成

  本实验需要处理的数据为视频,所以在图像处理前要对视频继续导入与拆分,步骤如下:

2.1 视频时长读取

  为了对导入的视频自适应拆分,需要先读取出视频时长,方法总结如表1,经比较后发现CV2读取最高效,故本实验使用CV2实现。

表1 Python进行视频时长读取的常用方法

在这里插入图片描述

🔥 读取视频时长
🌟 算法描述:

  使用OpenCV库中的VideoCapture类来读取视频文件和获取相关信息。使用cap.get(5)获取视频的帧率(每秒播放的帧数),cap.get(7)获取视频的总帧数,即视频中的帧数。以秒为单位计算视频的时长,通过将frame_num除以rate即可得到视频的时长,如果视频文件无法打开或获取相关信息失败,则返回-1。

1.	# 获取视频时长  
2.	def get_duration_from_cv2(filename):  
3.	    cap = cv2.VideoCapture(filename)  
4.	    if cap.isOpened():  
5.	        rate = cap.get(5)  
6.	        frame_num = cap.get(7)  
7.	        duration = frame_num / rate  
8.	        return duration  
9.	    return -1  

2.2 视频的拆分

  连续的图像变化每秒超过24帧(frame)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面;看上去是平滑连续的视觉效果,这样连续的画面叫做视频。故视频拆分为图像的过程实际为视频的帧分解

🔥 视频拆分成图片保存
🌟 算法描述:
  在得到视频时长后,可利用cv2.VideoCapture读取视频,并通过cv2库中的get、read等函数对视频的特定帧进行访问,再通过imwrite函数对得到的图片进行写入即可。

1.	# 视频拆分  
2.	def Video_splitting(video_name, img_save_path):  
3.	    cap = cv2.VideoCapture(video_name)  
4.	    isOpened = cap.isOpened  # 判断视频是否可读  
5.	    print(isOpened)  
6.	    fps = cap.get(cv2.CAP_PROP_FPS)  # 获取图像的帧,即该视频每秒有多少张图片  
7.	    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))  # 获取图像的宽度和高度  
8.	    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))  
9.	    print(fps, width, height)  
10.	    length = math.floor(get_duration_from_cv2(video_name))  # 向下取整  
11.	  
12.	    i = 0  
13.	    while isOpened:  
14.	        if i == 24 * length:  # 分解为多少帧(i)  
15.	            break  
16.	        # 读取每一帧,flag表示是否读取成功,frame为图片的内容  
17.	        (flag, frame) = cap.read()  
18.	        filename = img_save_path + 'img' + str(i) + '.jpg'  # 文件的名字  
19.	        if flag:  
20.	            cv2.imwrite(filename, frame, [cv2.IMWRITE_JPEG_QUALITY, 100])  # 保存图片  
21.	        i += 1  
22.	    return length  
拆分样例(prj_video.mp4的拆分)如图2-1:

在这里插入图片描述

图2-1 视频拆分结果样例

2.3 视频的合成

  在后续对图像处理完成之后最终要再形成视频,由2.2中视频定义可知,将图像按设定帧率与顺序连续即可完成视频的合成。

🔥 视频拆分成图片保存
🌟 算法描述:
  在实现上使用cv2.VideoWriter方法来创建一个video写入器,用cv2.VideoWriter_fourcc创建视频编解码,这里使用的编码器是mp4v,使用循环,从图像文件中读取每一帧,并将其写入视频文件即可。

1.	# 视频合成  
2.	def Video_compositing(length):  
3.	    img = cv2.imread('img0.jpg')  
4.	    width = img.shape[0]  
5.	    height = img.shape[1]  
6.	    size = (height, width)  
7.	    print(size)  
8.	  
9.	    videoname = "2.mp4"  # 要创建的视频文件名称  
10.	    # fourcc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G') # 编码器  
11.	    fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # 编码器修改  
12.	    fps = 24  # 帧率(多少张图片为输出视频的一秒)  
13.	  
14.	    # 1.要创建的视频文件名称 2.编码器 3.帧率 4.size  
15.	    videoWrite = cv2.VideoWriter(videoname, fourcc, fps, size)  
16.	    for i in range(fps * length):  
17.	        filename = 'img_line' + str(i) + '.jpg'  
18.	        img = cv2.imread(filename)  
19.	        videoWrite.write(img)  # 写入  

三、路沿检测

3.1 路沿检测算法整体框架

  本章的核心任务为路沿检测,对实验步骤进行描述,基于Hough变换的路沿检测步骤如图3-1所示:

在这里插入图片描述

图3-1 基于Hough变换的直线(路沿)检测步骤

  对于路沿检测,整体思路如上所示。首先利用opencv提取出视频中的每一帧,然后对每一帧图像进行预处理。此处的预处理包含了灰度变换、图像二值化、均值滤波、再次二值化等,其目的主要在于将图像处理成路沿较容易被识别出来的状态。对图像进行预处理后需要进行兴趣区域提取。此处的兴趣区域提取,我首先根据图像的几何信息规定了一个大概的范围,然后再通过Hough变换找出路基与路标线的区域最后将此区域作为最后的兴趣区域。最后再进行常规的边缘检测与hough变换,并对结果进行过滤即可较好地检测出路沿。
  在真实环境中存在一定噪声,会影响后续目标检测的精度,故在此之前需进行一定的图像处理,具体方法与步骤如下。

3.2 尝试

  最初我使用的是均值滤波(3x3为例)与中值滤波(卷积核为5为例)对实验图像进行处理,样例如图3-2和图3-3所示:

在这里插入图片描述

图3-2 图像进行均值滤波前后(左为原图,右图为均值滤波后)

在这里插入图片描述

图3-3图像进行中值滤波前后(左为原图,右图为中值滤波后)

  分析:由上述图可见,使用均值及中值滤波降噪效果均较差,且滤波后图像模糊,丢失较多信息,不能为后续的边缘检测与路沿识别提供优化,故在本实验中使用其他方法。

3.3 图像处理->边缘检测(原理)

  在图像中边缘即为亮度变化明显的点,边缘检测本质上就是检测并绘制出边缘点的集合,实现了简化图像信息,使用边缘线代表图像所携带信息。
  根据边缘定义,要找到亮度变化明显的点,只需要找到梯度大的点即可,图像梯度即当前所在像素点对于X轴、Y轴的偏导数,所以在图像处理领域可以理解为像素灰度值变化的速度。其中二维函数的微分(处理灰度图)定义如图3-4所示,梯度相关定义如图3-5所示:

在这里插入图片描述

图3-4 二维函数的微分(处理灰度图)定义

在这里插入图片描述

图3-5 梯度相关定义

根据原理与相关定义,边缘检测一般步骤总结如图3-6所示:

在这里插入图片描述

图3-6 边缘检测一般步骤

  由上图分析与上述原理与流程的定义可知,本实验进行图像处理的目的是为了更好实现后续的边缘检测与路沿(直线)检测,故图像处理应该与边缘检测的原理紧密结合。本实验给出如下几种边缘检测方法与其对应的图像处理方法。

  边缘提取有几种经典的边缘检测算子,Roberts算子、Prewitt算子、Sobel算子、Laplacian算子。在之前的实验中我们已经使用C#语言根据原理实现了不同算子的边缘提取,并归纳总结了不同算的原理对比及缺点,如表3-1所示。

表3-1 不同边缘检测算子及其原理与缺点汇总

在这里插入图片描述

  分析:由表3-1中结论,经典边缘检测方法在使用中或多或少存在一定问题,求得的边缘图存在很多问题,如噪声污染没有被排除、边缘线太过粗宽等,对于本任务均不是最优选择。故在本实验中选择Canny算子进行边缘检测。不同算子进行边缘检测效果对比样如图3-7所示:

在这里插入图片描述

图3-7 不同边缘检测算子效果对比

3.4 Canny算子边缘检测(原理)

  经过对比,较适合本实验的边缘检测方法为Canny算子。Canny算子是一种非微分边缘检测算子,目标是找到一个最优的边缘检测解或找寻一幅图像中灰度强度变化最强的位置。最优边缘检测主要通过低错误率、高定位性和最小响应三个标准进行评价。相关标准定义如表3-2,使用Canny算子进行边缘检测流程及相关原理如图3-8所示:

表3-2 Canny相关评价标准定义

在这里插入图片描述

根据上述分析,Canny算子进行边缘检测流程及相关原理如图3-8:

在这里插入图片描述

图3-8 Canny算子进行边缘检测流程

3.5 Canny算子边缘检测(实现)

  在正式开始边缘检测前,有以下四个重要特征需要了解,后续设计中帮助提高识别率:
  Ⅰ、颜色:车道线(路沿)通常为浅色(白色/黄色),而道路则为深色(深灰色)。因此,黑白图像效果更好,因为车道可以很容易地从背景中分离出来。
  Ⅱ、形状:车道线(路沿)通常是实线或虚线,所以可以将它们与图像中的其他对象分开。可以用Canny等边缘检测算法找到图像中的所有边缘/线条。然后我们可以使用进一步的信息来决定哪些边可以被限定为车道线。
  Ⅲ、方向:公路车道线(路沿)更接近于垂直方向,而不是水平方向。因此,在图像中检测到的直线的斜率可以用来检查它是否可能是车道。
  Ⅳ、在图像中的位置:在一个由行车记录仪拍摄的常规公路图像中,车道线(路沿)通常出现在图像的下半部分。因此,可以将搜索区域缩小到感兴趣的区域,以减少噪声。
  根据上述原理与流程设计代码,核心实现的设计与解析如下述子章节叙述:

3.5.1 高斯滤波

  高斯滤波选择原因:因为现实中的噪声分布多是随机,故在路沿检测图像中使用均值滤波与中值滤波效果不好,而Canny算子一般搭配高斯滤波。

  简介、原理及操作:高斯滤波是一种线性平滑滤波,适用于消除高斯噪声。高斯滤波每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值,其中卷积操作原理样例如图3-9所示:
I_σ=I*G_σ

图3-9 高斯滤波卷积操作原理样例

其中 * 表示卷积操作; Gσ 是标准差为σ 的二维高斯核,定义如图3-10所示:
G_σ=1/2πσ e(-(x2+y2)/2σ2 )

图3-10 二维高斯核定义

   实现:高斯滤波在代码中的实现可使用自定义函数与库函数两种方法实现,为了更好的理解高斯滤波的原理,这里我采用自定义函数实现,算法流程如图3-11:
在这里插入图片描述

图3-11 自定义函数实现高斯滤波步骤

🔥 图像预处理——高斯滤波
🌟 算法描述:
  首先获取输入图像的高度 h、宽度 w 和通道数 c,然后对输入图像进行零填充,通过创建一个新的图像 out,其尺寸为 (h + 2 * pad, w + 2 * pad, c),其中 pad = K_size // 2 是滤波器的半径。接着将输入图像复制到零填充的图像中心,使用两层循环遍历滤波器的每个元素,计算高斯权重值,并将其保存到滤波器核 K 中。高斯权重值根据欧式距离计算,对滤波器核 K 进行归一化处理,将每个元素除以标准差乘以系数,使得滤波器核的总和为 1。最后进行卷积操作,遍历输入图像的每个像素点 (y, x) 和每个通道 ci,将滤波器核 K 与子图像进行逐元素相乘,并对结果求和,返回输出图像。

1.	# 自定义的高斯滤波  
2.	def GaussianFilter(img):  
3.	    h, w, c = img.shape  
4.	    # 高斯滤波  
5.	    K_size = 3  
6.	    sigma = 1.3  
7.	  
8.	    # 零填充  
9.	    pad = K_size // 2  
10.	    out = np.zeros((h + 2 * pad, w + 2 * pad, c), dtype=float) # np.float 改为float  
11.	    out[pad:pad + h, pad:pad + w] = img.copy().astype(float)  
12.	  
13.	    # 定义滤波核  
14.	    K = np.zeros((K_size, K_size), dtype=float)  
15.	  
16.	    for x in range(-pad, -pad + K_size):  
17.	        for y in range(-pad, -pad + K_size):  
18.	            K[y + pad, x + pad] = np.exp(-(x ** 2 + y ** 2) / (2 * (sigma ** 2)))  
19.	    K /= (sigma * np.sqrt(2 * np.pi))  
20.	    K /= K.sum()  
21.	  
22.	    # 卷积的过程  
23.	    tmp = out.copy()  
24.	    for y in range(h):  
25.	        for x in range(w):  
26.	            for ci in range(c):  
27.	                out[pad + y, pad + x, ci] = np.sum(K * tmp[y:y + K_size, x:x + K_size, ci])  
28.	  
29.	    out_image = out[pad:pad + h, pad:pad + w].astype(np.uint8)  
30.	  
31.	    return out_image  

  前后对比样例(以高斯矩阵的长与宽为5,标准差取0为例)如图3-12所示:

在这里插入图片描述

图3-12 图像进行高斯滤波前后

3.5.2 图像转化(彩色->灰度)

  图像转化原因:边缘检测最关键的部分是计算梯度,颜色难以提供关键信息,并且颜色本身非常容易受到光照等因素的影响,所以只需要灰度图像中的信息就足够了。并且灰度化后,简化了矩阵,提高了运算速度。
  原理:将彩色图像(Color Image)转换为灰度图(Gray Scale Image),即从三通道RGB图像转为单通道图像。
  实现:我们实现彩图转化为灰度图需要用到opencv库中的cv.cvtColor函数,需要用到两个参数:src——输入图片,code——颜色转换代码,具体代码如下:

🔥 图像预处理——灰度图转换

1.	# 灰度图转换  
2.	def grayscale(num_img):  
3.	    for i in range(num_img):  
4.	        filename = main.origin_img_save_path + 'img' + str(i) + '.jpg'  
5.	        img = cv2.imread(filename)  
6.	        img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)  
7.	        filename = main.gray_img_save_path + 'img_gray' + str(i) + '.jpg'  
8.	        cv2.imwrite(filename, img_gray)  

转化样例图如下图3-13:

在这里插入图片描述

图3-13 图像转化样例

3.5.3 Canny边缘检测

  在进行完图像转化与高斯滤波等图像处理后,正式进入Canny边缘检测,按照图3-6边缘检测一般步骤总结来设计边缘检测代码,可以使用自定义函数实现,也可直接使用库函数实现。本实验使用库函数进行Canny边缘检测。其中每一步生成图像样例及结果对比如图3-14所示:

在这里插入图片描述

图3-14 Canny边缘检测每一步生成的图像及不同实现方法效果对比

🔥 Canny边缘检测——库函数实现
🌟 算法描述:
  本实验直接使用opencv库中的cv.Canny函数,其中使用到的参数为:src——输入图像,low_threshold ——低阈值,high_threshold——高阈值。

9.	# 灰度图转换  
10.	def grayscale(num_img):  
11.	    for i in range(num_img):  
12.	        filename = main.origin_img_save_path + 'img' + str(i) + '.jpg'  
13.	        img = cv2.imread(filename)  
14.	        img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)  
15.	        filename = main.gray_img_save_path + 'img_gray' + str(i) + '.jpg'  
16.	        cv2.imwrite(filename, img_gray)  

边缘检测样例(低阈值=75,高阈值=225)如图3-15所示:
在这里插入图片描述

图3-15 库函数实现边缘检测样例

3.5.4 生成Mask掩膜,提取ROI

  通过观察我们不难发现,本实验中路沿在图像中的位置基本处于中间偏右,这意味着我们可以对图像进行区域选取,排除其他边缘与线的影响,使识别效果更好,详解如下:

  简介:Mask掩模的作用为降低计算代价,核心为遮挡非感兴趣区,只在我们感兴趣部分(ROI)进行算法的计算(mask最终需要与要作用到的输入图像的尺寸与类型保持一致)。本实验中我们感兴趣的部分为路沿,故可设计Mask掩模如图3-16所示:

在这里插入图片描述

图3-16 Mask掩模样例

  实际任务中根据不同感兴趣区域进行Mask掩模,若需分析区域变化过大不适宜使用。根据以上算法原理,Mask掩模设计与实现步骤如图3-17所示:

在这里插入图片描述

图3-17 Mask掩模设计与实现步骤

🔥 提取ROI
🌟 算法描述:
  根据以上的算法流程图,设计如下代码,其中需要自行的设计并调整参数的部分是多边形顶点vertices的定义。
  在python中图像的起始点即原点是在左上角,向右为x轴,最大值为图像的宽度width;向下为y轴,最大值为图像的高度height,这一点需要注意,因此我们顶点的定义实际上是(width1,height1),后面就需要调整大小,达到符合我们预期的ROI即可。
  我这里尝试出来的vertices = np.array([[line0.4, row0.1], [line0.5, row], [line0.9, row], [line0.5, row0.1]], np.int32) 是比较符合期望的,其中line是图像的宽度width,row是图像的高度。

1.	# 针对路沿检测  
2.	# 生成ROI感兴趣区域即Mask掩模  
3.	def region_of_interest(image):  
4.	    mask = np.zeros_like(image)  # 生成图像大小一致的空白掩模 mask,np.zeros_like 函数生成一个与输入图像大小一致的全零矩阵  
5.	  
6.	    # 填充顶点vertices中间区域  
7.	    # 判断输入图像的维度数,如果大于2,则表示输入图像是一个彩色图像,需要考虑通道数  
8.	    if len(image.shape) > 2:  
9.	        # 对于彩色图像,使用通道数来生成一个颜色元组  
10.	        channel_count = image.shape[2]  
11.	        ignore_mask_color = (255,) * channel_count  
12.	    # 否则,表示输入图像是一个灰度图像  
13.	    else:  
14.	        # 对于灰度图像,直接使用灰度值 255  
15.	        ignore_mask_color = 255  
16.	  
17.	    row = image.shape[0]  # 行 y  height  
18.	    line = image.shape[1]  # 列 x  width  
19.	    # 定义多边形的顶点坐标 1280*720  
20.	    # 缩放为 640*360  
21.	    # 1   4  
22.	    # 2   3                           1                 2                3                  4  
23.	    vertices = np.array([[line*0.4, row*0.1], [line*0.5, row], [line*0.9, row], [line*0.5, row*0.1]], np.int32)  
24.	  
25.	    # 将多边形的顶点数组组合成列表  
26.	    polygons = [vertices]  
27.	  
28.	    # 填充函数  
29.	    # 根据给定的顶点坐标 vertices,将掩模 mask 中对应的区域填充为忽略掩模颜色  
30.	    cv2.fillPoly(mask, polygons, ignore_mask_color)  
31.	    masked_image = cv2.bitwise_and(image, mask)  
32.	    # 将输入图像 image 和掩模 mask 进行按位与运算,得到在感兴趣区域内的图像  
33.	    return masked_image  

在这里插入图片描述

图3-18 Mask掩模实现样例

  至此,本实验的图像处理与边缘检测部分基本结束,在下一章节中,我们将进入到本实验的核心任务之一——路沿检测。

四、基于Hough变换的路沿检测

  本部分将完成路沿检测,核心为基于Hough变换的直线检测,详解如下述分章节:

4.1 Hough变换(原理)

   Hough变换是一种使用表决方式的参数估计技术,其原理是利用图像空间和Hough参数空间的线-点对偶性,把图像空间中的检测问题转换到参数空间中进行。空间映射样例如图4-1所示:

在这里插入图片描述

图4-1 Hough变换空间映射样例

  分析:由于这种实现方式(y=mx+b)不能表示垂直线(斜率为无穷大),故在实际操作中选择极坐标系。根据直角坐标系和极坐标系变换域之间的关系,总结Hough变换主要性质如表4-1所示:
表4-1 Hough变换主要性质

在这里插入图片描述
Hough变换空间映射样例(极坐标系)如图4-2所示

在这里插入图片描述

图4-2 Hough变换空间映射样例(极坐标系)

根据以上原理,Hough直线检测步骤如下图4-3:
在这里插入图片描述

图4-3 Hough直线检测步骤

Hough直线检测样例如图4-4(图片来自网络)

在这里插入图片描述

图4-4 Hough直线检测样例

4.2 基于Hough变换的路沿检测

  基于4.1子章节中的原理介绍与分析,使用Hough变换进行路沿检测,首先可以使用ImageEnhance.Contrast(img).enhance(n)函数增加图片对比度,如图4-5所示:在这里插入图片描述

图4-5 对比度增加样例

  然后使用Opencv封装好的cv.HoughLinesP函数进行路沿(直线)检测,其中参数及其解释如下:

4.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)。

🔥 Hough变换

1.	# 霍夫变换  
2.	def hough_lines(img):  
3.	    rho = 0.5  # 霍夫像素单位  
4.	    theta = np.pi / 360  # 霍夫角度移动步长  
5.	    hof_threshold = 20  # 霍夫平面累加阈值threshold  
6.	    min_line_len = 30  # 线段最小长度  
7.	    max_line_gap = 60  # 最大允许断裂长度  
8.	  
9.	    lines = cv2.HoughLinesP(img, rho, theta, hof_threshold, np.array([]), minLineLength=min_line_len, maxLineGap=max_line_gap)  
10.	    return lines  

如下是返回结果示例图4-6,函数return 的是检测到的直线集合:
在这里插入图片描述

图4-6 Hough变换返回结果

4.2.2 直线检测

  在了解cv.HoughLinesP函数参数与解释后,使用其在本任务中进行直线检测,返回直线坐标。其中根据我的需求调整参数之后,在本实验中两组较优参数如下:

1.	lines = cv2.HoughLinesP(img_canny, 0.5, np.pi / 180, 20, np.array([]), minLineLength=30,  
2.	                             maxLineGap=10)  # test1较优参数   
3.	lines = cv2.HoughLinesP(img_canny, 1, np.pi / 180, 100, np.array([]), minLineLength=100,  
4.	                            maxLineGap=8)  # ta参  

  调整参数的过程中也要注意切忌过拟合,否则会降低代码(模型)的泛化能力,在后续过程还可以进行直线的分类与判断确定所需的目标直线。

4.2.3 直线绘制

  在4.2.2子章节中利用Hough变换返回了检测到的直线的坐标,在本部分进行直线的绘制,绘制函数为opencv库中的cv2.line(image, (x1, y1), (x2, y2), color, pixel),代码及解释如下:

🔥 绘制直线——版本1
🌟 算法描述:
  函数的输入参数包括一个图像image和检测到的直线列表lines,以及绘制线条颜色color和线条粗细thickness。对于每条直线,获取直线的起点(x1, y1) 和终点(x2, y2),然后使用cv2.line函数在line_image上绘制直线,将直线的起点和终点连接起来。

1.	# 绘制路沿线,将经过hough变换之后检测到的直线绘制出来  
2.	def draw_luyan_lines(image, lines, color=[0, 0, 255], thickness=2):  
3.	    line_image = np.zeros_like(image)  # 创建与原始图像大小一致的全零图像  
4.	    
5.	    # 遍历所有直线  
6.	    for line in lines:  
7.	        x1, y1, x2, y2 = line[0]  
8.	        cv2.line(line_image, (x1, y1), (x2, y2), color, thickness=thickness)  
9.	  
10.	    return line_image  

绘制样例(无mask掩模)如图4-7所示:

在这里插入图片描述

图4-7 直线绘制样例(无mask掩模)

   分析:如图4-7所示,直接Hough变换存在过度检测、过度判断等问题,故对于目标直线定义一个合理的判断逻辑也至关重要,因此在直线绘制过程中加入如下判断:
(1)直线的几何特征与空间的结构特征判断
  对于过度检测的情况,可以利用直线的几何特征或空间的结构特征对直线进行筛选。
  空间结构如mask掩模,上文有详述。其对于直线的筛选样例如图4-8所示。

在这里插入图片描述

图4-8 空间的结构特征判断筛选直线样例

  几何特征如直线斜率,观察本实验数据不难分析,目标路沿斜率变化不大且在某个区间,故利用python进行统计分析,在mask掩模输出相关直线斜率信息。
  由于我未能找到相关路沿的视频数据集,因此只能依靠现有的一些路沿图片进行分析,存在一定的误差,可能只符合我这个实验,样例如表4-2所示:

表4-2 直线斜率分析样例

在这里插入图片描述
  分析:对于表4-2及其他汇总数据进行统计分析,可知本实验中路沿斜率一般为2+,故在本实验中设置低阈值为2,高阈值为3.5。在直线绘制时时加入判断,其对直线的筛选样例如图4-9所示:在这里插入图片描述

图4-9 直线的几何特征判断筛选直线样例

  可以看出路边的砖块由于斜率相近,被识别的进来,但是通过观察看到这些直线的长度明显小于路沿的长度,因此这里我们可以简单修改一下Hough判断的参数,增加识别直线的最小长度,经过调整参数得到了我们期望的直线,如图4-10:

在这里插入图片描述

图4-10 调整Hough变换的参数得到期望的直线

🔥 绘制直线代码——版本2——根据直线的几何特征进行了判断筛选
🌟 算法描述:
  跟版本1相比,新增了计算直线斜率和判断斜率范围这两步,根据以上分析,当直线斜率在(2,3.5)的范围之内时,才绘制直线,虽然存在一定误差,但是能够去除大部分不相关的直线。

1.	# 绘制路沿线,将经过hough变换之后检测到的直线绘制出来  
2.	def draw_luyan_lines(image, lines, color=[0, 0, 255], thickness=2):  
3.	    line_image = np.zeros_like(image)  # 生成与原始图像大小一致的zeros矩,具有相同的行数、列数和通道数  
4.	    # 定义点的颜色  
5.	    # color = (0, 0, 255)  # 红色,以 (B, G, R) 的顺序表示  
6.	  
7.	    slope_threshold_low = 2     # 低阈值  
8.	    slope_threshold_high = 3.5  # 高阈值  
9.	  
10.	    # 在图片上绘制直线  
11.	    for line in lines:  
12.	        x1, y1, x2, y2 = line[0]  
13.	        # 计算直线斜率  
14.	        slope = (y2 - y1) / (x2 - x1)  
15.	        # 判断直线斜率是否在阈值范围内  
16.	        if slope_threshold_low <= abs(slope) <= slope_threshold_high:  
17.	            cv2.line(line_image, (x1, y1), (x2, y2), color, thickness=thickness)  
18.	        # 绘制点  
19.	        # cv2.circle(line_image, (x1, y1), radius=2, color=color, thickness=-1)  
20.	        # cv2.circle(line_image, (x2, y2), radius=2, color=color, thickness=-1)  
21.	  
22.	    return line_image  

(2)路沿直线的确定
  在经过直线的几何特征与空间的结构特征判断后,仍存在不止一条直线,在其中要确定我们的目标直线(路沿),还需要增加一定的限制条件。在本实验中,确定路沿线以如下三个特点为例:
  特点1——直线位置:本实验中路沿直线一般在筛选后直线中间,可以使用拟合后直线x轴坐标用于排除两边直线。
  特点2——斜率聚类:对图像几何特征进行分析,图像从左至右直线斜率不断增大,可利用斜率对于筛选后直线聚类,根据特点1,选择中部斜率类进行保留。
  特点3——x轴差值:根据特点2,可根据x轴差值(拟合直线在本图像中的最大x差值)确定路沿直线,利用python进行统计分析,输出相关直线x轴差值信息,样例如表4-3所示:

在这里插入图片描述  分析:对于表11及其他汇总数据进行统计分析,可知本实验图片的路沿x轴差值区间为大致[80,130]。因此我在直线绘制前时加入如上三大特点,其优化代码如下:

🔥 绘制直线代码——版本3——根据目标直线(路沿)增加限制条件
🌟 算法描述:
  在修改后的代码中,新增了以下部分:
  (1)添加了变量target_line和target_line_x_diff,用于保存中间路沿直线的相关信息;
  (2)在遍历所有直线的过程中,首先计算直线的中点坐标 mid_x;
  (3)判断直线是否在中间位置,即判断 mid_x 是否位于图像宽度的 1/3 到 2/3 范围内;
  (4)如果直线满足中间位置的条件,则更新路沿直线为具有最大x轴差值的直线;
  (5)最后,只绘制路沿直线 target_line;
  总体的算法流程图如下图4-11:

在这里插入图片描述

图4-11 直线绘制算法流程图

  这样,函数将根据特点1和特点3筛选出中间位置且具有最大 x 轴差值的路沿直线进行绘制。

1.	# 绘制路沿线,将经过hough变换之后检测到的直线绘制出来  
2.	def draw_luyan_lines(image, lines, color=[0, 0, 255], thickness=2):  
3.	    line_image = np.zeros_like(image)  # 创建与原始图像大小一致的全零图像  
4.	  
5.	    # 保存中间路沿直线的相关信息  
6.	    target_line = None  
7.	    target_line_x_diff = 0  
8.	  
9.	    # 遍历所有直线  
10.	    for line in lines:  
11.	        x1, y1, x2, y2 = line[0]  
12.	  
13.	        # 计算直线斜率  
14.	        slope = (y2 - y1) / (x2 - x1)  
15.	  
16.	        # 计算直线的中点坐标  
17.	        mid_x = (x1 + x2) // 2  
18.	  
19.	        # 判断直线是否在中间位置,即判断 mid_x 是否位于图像宽度的 1/3 到 2/3 范围  
20.	        if mid_x > image.shape[1] // 3 and mid_x < (2 * image.shape[1] // 3):  
21.	  
22.	            # 如果直线满足中间位置的条件,则更新路沿直线为具有最大 x 轴差值的直线  
23.	            x_diff = abs(x2 - x1)  
24.	            if target_line is None or x_diff > target_line_x_diff:  
25.	                target_line = line  
26.	                target_line_x_diff = x_diff  
27.	  
28.	    # 绘制路沿直线  
29.	    if target_line is not None:  
30.	        x1, y1, x2, y2 = target_line[0]  
31.	        cv2.line(line_image, (x1, y1), (x2, y2), color, thickness=thickness)  
32.	  
33.	    return line_image  

效果样例如图4-12所示:
在这里插入图片描述

图4-12 路沿直线确定优化效果

  不同任务下待检测直线的确定特点有所不同,故路沿(目标)直线确定这一步添加需谨慎,需针对不同实际任务需求做修改。直接迁移有可能起到适得其反的效果,降低模型的泛化能力。在简单任务中也可直接使用直线的几何特征与空间的结构特征判断+后续修正即可,可以确定与目标直线相关的几条直线。

五、车道检测(白色与黄色车道)

  除开路沿检测,在车道检测中,通常会有黄色车道和白色车道之分,如果不区分车道类别,只为了检测出车道,那么我们只需要在图像处理之后进行适当的边缘提取操作即可,但是在现实生活中,我们通常更希望能够识别出车道的类别,因此在本章我提供了白色车道和黄色车道识别的算法。

5.1 车道检测实现流程

  本章的核心任务为车道检测(检测出白色车道和黄色车道),其实现流程如图5-1所示:

在这里插入图片描述

图5-1 车道检测实现流程

  在色彩切片、边界提取处,可以选用许多不同的方法来实现,在下面章节我将详细描述我使用的方法,同时为了更清楚的了解整体算法框架,我将上述流程图细分为了更加详细的实现过程,如图5-2所示:

在这里插入图片描述

图5-2 详细的流程

5.2 高斯滤波

  在车道线检测任务中,高斯模糊作为图像预处理的一步是有一定的必要性的。以下是几个原因:
  1. 噪声抑制:图像中可能存在不同类型的噪声,如传感器噪声、图像采集噪声、图像传输噪声等。这些噪声会干扰车道线的检测和分割。
  2. 平滑图像:车道线检测通常基于边缘检测或颜色过滤等技术。高斯模糊可以模糊图像,平滑不必要的细节,使车道线成为更连续、更稳定的特征。
  3. 连接断裂部分:在某些情况下,车道线可能会出现间断或断裂,这可能是由于噪声、遮挡或其他因素引起的。高斯模糊可通过平滑图像来连接车道线的断裂部分,填充空白区域,使车道线成为更连续的线段。
  4. 改善车道线检测算法的稳定性:高斯模糊可以降低图像中的高频细节,从而减少噪声对车道线检测算法的干扰。这有助于提高车道线检测算法的稳定性和鲁棒性,减少误检和漏检的情况。
  在第三章路沿检测中我也详细描述过为什么需要高斯滤波,并且有详细的算法描述,而且本章的重点也并不是高斯滤波,因此就不再过多描述。
  需要注意的是,我需要出检测黄色和白色车道,因此图像预处理中不宜使用灰度化,在后续我会说明为什么灰度化不合适,这里的图像预处理仅进行高斯模糊即可。

5.3 色彩切片

  色彩切片处是希望通过车道线的颜色特征:白色或者黄色来提取车道线,白色和黄色单独提取后两者图像相加便得到了同时含有白色车道和黄色车道的图像。
  色彩切片步骤的关键在于如何获得较为纯净的车道线,以让后续步骤更容易判断直线的位置。

5.3.1 白色车道线切片

  白色车道线选择使用球状的RGB色彩切片:如果一个点的RGB值与给定的一个颜色的RGB值的“空间坐标系距离”在一个区间内,则将其切片。
  但是这有一个问题:因为不同图片的光照情况不同,因此很难选取一个作为基准的白色值——因为一个光照条件好的图像的地面的“黑色”的RGB值很有可能会相当于另一光照较差的图像的“白色”。
  最初我想使用对所有图片都进行直方图均衡,再使用一个统一的白色的RGB值对他们切片。但是将这种方法应用于部分对比度较差的图片,会获得很不理想的效果,如图5-3所示:
在这里插入图片描述

图5-3 对比度较差下使用直方图均衡化

  例如上面这张图,假如对它的每个通道都进行单独的直方图均衡,则每一个通道都会出现类似上图的效果:白线反而更难以辨别了。

🔥 直方图均衡化
🌟 算法描述:
  直方图均衡化是图像处理领域中利用图像直方图对对比度进行调整的方法。这种方法通常用来增加许多图像的全局对比度,尤其是当图像的有用数据的对比度相当接近的时候。这里我调用库函数实现。

1.	# 直方图均衡化  
2.	def histogram_equalization(img):  
3.	    (b, g, r) = cv2.split(img)  # 通道分解  
4.	    bH = cv2.equalizeHist(b)  
5.	    gH = cv2.equalizeHist(g)  
6.	    rH = cv2.equalizeHist(r)  
7.	    result = cv2.merge((bH, gH, rH), )  # 通道合成  
8.	    # res = np.hstack((img, result))  
9.	    return result  
10.	  
11.	  
12.	# 原始图片  
13.	    img = cv2.imread(other_img_save_path + '22.jpg')  
14.	    img = cv2.resize(img, (window_width, window_height))  
15.	    cv2.imshow('img'+logo, img)  
16.	  
17.	    # 直方图均衡化的图片  
18.	    img_after_histogram_equalization = dd.histogram_equalization(img)  
19.	    cv2.imshow('img_after_histogram_equalization'+logo, img_after_histogram_equalization)  
20.	  
21.	    cv2.waitKey(0)  
22.	    cv2.destroyAllWindows()  

  解决办法:我选用了另一种方法,通过在图像的中下部搜索RGB强度最大的像素点,以此RGB值作为这张图像的白色值,对这个白色值进行变换后再对其进行球形切片,如下图5-4所示:
在这里插入图片描述

图5-4 白色车道线切片

  右图:对左图使用我的方法进行切片后获得的白色车道线(同时我将每个识别出来的白色车道线的RGB值都更新为了(255,255,255))。
  由于这个方法为每一张图像动态地选取了不同的白色值,因此对几乎所有的图都有较好的效果。
  代码实现过程如下:

(1)在图像的中下部搜索RGB强度最大的像素点
🔥 搜索图片中下部RGB强度最大的像素点作为白色判断
🌟 算法描述:
  循环遍历图像的中下部分区域的行和列,对于每个像素位置 (i, j),使用内部循环遍历其RGB通道的值。在遍历过程中,记录当前位置RGB通道的最小值t_min,如果t_min大于max_val,则更新max_val 的值为t_min,最后,函数返回最大的白色值max_val,即为白色车道线的最大值。

1.	# 在图像的中下部获得白色的车道线的最大值  
2.	def Get_White_Max_Value(img):  
3.	    max_val = 0  
4.	  
5.	    imshape = img.shape  # 获取图像大小  
6.	  
7.	    # 使用两个嵌套的循环遍历车道附近的行和列  
8.	    for i in range(int(img.shape[0]/2), img.shape[0]):  
9.	        for j in range(int(img.shape[1] * 0.2), int(img.shape[1] * 0.8)):  
10.	            t_min = 255  
11.	            # 每个像素位置,遍历颜色通道  
12.	            for c in range(3):  
13.	                val = img[i, j, c]  
14.	                if val < t_min:  
15.	                    t_min = val  
16.	            if t_min > max_val:  
17.	                max_val = t_min  
18.	    # 返回最大的白色值  
19.	    return max_val  

(2)计算空间坐标系距离
🔥 计算空间坐标系距离
🌟 算法描述:
  使用立体空间坐标系的公式算出距离判断条件后,决定是否将原图某像素点的色彩值留下。如果差值的平方和大于颜色半径的平方,则说明该像素点与给定颜色阈值的差异较大,将该像素点的RGB分量设置为0,即将其切割掉,说明该像素点的色彩值在范围之外,就可以置为黑色排除。

1.	# RGB_Value 是一个长度为3的整型数组,表示颜色阈值;radius 是颜色半径  
2.	# 做颜色切割  
3.	def do_color_slicing(image, rgb_value, radius):  
4.	    row_size, col_size, _ = image.shape  
5.	    # 嵌套的循环遍历图像的每个像素点  
6.	    for i in range(row_size):  
7.	        for j in range(col_size):  
8.	            # 计算当前像素点与给定颜色阈值之间的差值  
9.	            l_r = int(image[i, j, 2]) - rgb_value[0]  
10.	            l_g = int(image[i, j, 1]) - rgb_value[1]  
11.	            l_b = int(image[i, j, 0]) - rgb_value[2]  
12.	            # 如果平方和大于颜色半径的平方,则将该像素点的RGB分量设置为0,即将其切割掉  
13.	            if (l_r * l_r + l_g * l_g + l_b * l_b > radius * radius):  
14.	                image[i, j] = [0, 0, 0]  
15.	    return image  

(3)获得白色掩膜
🔥 提取白色车道
🌟 算法描述:
  调用函数Get_White_Max_Value(input_image)获取输入图像中白色线的最大值,并将其乘以0.9,得到一个白色线的阈值white_max,然后,创建一个长度为3的 rgb_white 数组,将 white_max 的值赋给每个通道,用于表示白色的阈值。接着调用函数do_color_slicin对输入图像进行颜色切割操作,使用rgb_white作为阈值,color_radius作为颜色半径。
  最后,将颜色切割得到的白色掩膜white_mask返回,就得到了我们期望的白色车道。

1.	# 获得白色掩膜  
2.	def Get_White_Line(input_image):  
3.	    # 2.获取 img_after_gaussian 的白色线的最大值  
4.	    white_max = Get_White_Max_Value(input_image)  
5.	    white_max *= 0.9  
6.	  
7.	    rgb_white = [white_max, white_max, white_max]  
8.	    color_radius = 70  
9.	    # 3.对 img_after_gaussian 进行颜色切割,阈值为 rgb_white ,颜色半径为 color_radius  
10.	    white_mask = do_color_slicing(input_image, rgb_white, color_radius)  
11.	  
12.	    return white_mask  

  经过上述三个步骤得到的白色车道如下图所示,可以看到图像上方的天空也被识别进来了,后续我们将进行其他操作,去除干扰和不需要的元素。具体操作将在以下内容详细叙述:

在这里插入图片描述

图5-4 白色车道线切片

5.3.2 黄色车道线切片

  在本子章节我将根据我此次实验的数据介绍一种识别黄色车道的方法,在第六章的6.3子章节我将扩展另一种方法,两种方法原理相似,只是实现方式上略有不同。

(1)原理分析
  I:黄色车道的H值(色度)的特殊性
黄色车道线切片选择使用了在HSL色彩空间而不是RGB色彩空间下切片。
  由于光照等因素的影响,不同图像的对比度差异较大,这会导致黄色的车道线的RGB值有较大的波动,会有如下图5-5所示的情况出现。如果使用RGB切片并选取较大的切片范围的话可能有较差的结果。而在HSI色彩空间下的黄色车道线的H值(色度)却不会有较大变化,因此可以有更好的效果。

在这里插入图片描述

图5-5 不同光照下的黄色车道的色度变化

  通过观察,对于我此处实验的数据集,我最后选择了使用色度45±3作为黄色车道线的色度进行切片。

(2)黄色车道检测算法实现
  通过上述分析,我们筛选黄色车道的主要方向是在HSV色彩空间,因此在处理图片时就需要将RGB转成HSV,这也是为什么本次图像预处理不使用灰度化的原因,因为灰度化之后的图片HSV色彩空间色度H和饱和度S属性全部为零,如图5-6所示,就无法满足我们判断的需求。
在这里插入图片描述

图5-6 灰度化图片的HSL属性

根据以上描述,设计检测黄色车道线的算法如下:

🔥 获取黄色车道线
🌟 算法描述:
  通过循环遍历输出图像的每个像素,获取当前像素的RGB值,对于每个像素,调用get_hsi_h和get_hsi_s函数计算其在HSI模型中的色调H和饱和度S。根据设定的阈值和范围值,判断当前像素的色调H是否满足黄色线的条件(色调H在(42,48)之间),如果不满足条件,则将当前像素设为黑色(R、G、B均为0)。

1.	# 通过HSI模型得到满足要求的黄色线  
2.	def Get_Yellow_Line(input_image):  
3.	    output_image = np.copy(input_image)  
4.	  
5.	    for i in range(output_image.shape[0]):  
6.	        for j in range(output_image.shape[1]):  
7.	            # 获取当前像素的 RGB 值  
8.	            R = output_image[i, j, 2]  
9.	            G = output_image[i, j, 1]  
10.	            B = output_image[i, j, 0]  
11.	  
12.	            if(R == 0 and G == 0 and B == 0):  
13.	                # 纯黑色直接pass  
14.	                pass  
15.	            else:  
16.	                # 将 RGB 值转换为 HSI 模型中的 H(色调)和 S(饱和度)  
17.	                H = get_hsi_h(R, G, B)  
18.	                S = get_hsi_s(R, G, B)  
19.	  
20.	                # 设置色调范围和饱和度阈值  
21.	                range_value = 3  
22.	                value = 45  
23.	                threshold = 0.40  
24.	  
25.	                # 判断当前像素 的 色滴Hue 是否满足黄色线的条件  
26.	                if (S >= threshold and  
27.	                         # 42 48  
28.	                         (H >= value - range_value and H <= value + range_value)):  
29.	                    # 符合条件的保留,不做处理  
30.	                    pass  
31.	                else:  
32.	                    # 不符合条件的像素设为黑色  
33.	                    output_image[i, j] = [0, 0, 0]  
34.	  
35.	    return output_image  
36.	  
37.	# 获取 HSI 模型中的色调 H  
38.	def get_hsi_h(R, G, B):  
39.	    CMax = max(R, G, B)  
40.	    CMin = min(R, G, B)  
41.	  
42.	    delta = CMax - CMin  
43.	    rt_val = 0  
44.	  
45.	    if delta == 0:  
46.	        return 0  
47.	    elif CMax == R:  
48.	        GB = (int(G) - int(B)) / delta  
49.	        rt_val = GB * 60  
50.	        if G < B:  
51.	            rt_val += 360  
52.	    elif CMax == G:  
53.	        BR = (int(B) - int(R)) / delta  
54.	        rt_val = BR * 60 + 120  
55.	    elif CMax == B:  
56.	        RG = (int(R) - int(G)) / delta  
57.	        rt_val = RG * 60 + 240  
58.	  
59.	    return rt_val  
60.	  
61.	  
62.	# 获取 HSI 模型中的饱和度 S  
63.	def get_hsi_s(R, G, B):  
64.	    # 将其范围映射到 [0, 1]  
65.	    CMax = R / 255.0  
66.	    CMin = R / 255.0  
67.	    g = G / 255.0  
68.	    b = B / 255.0  
69.	  
70.	    if g > CMax:  
71.	        CMax = g  
72.	    if b > CMax:  
73.	        CMax = b  
74.	    if g < CMin:  
75.	        CMin = g  
76.	    if b < CMin:  
77.	        CMin = b  
78.	  
79.	    L = (CMax + CMin) / 2  
80.	  
81.	    if CMax == CMin:  
82.	        return 0  
83.	    elif L >= 0.5:  
84.	        return (CMax - CMin) / (2.0 - CMax - CMin)  
85.	    else:  
86.	        return (CMax - CMin) / (CMax + CMin) 

黄色车道线切片绘制样例(无mask掩模)如图5-7所示:
在这里插入图片描述

图5-7 黄色车道分割样例

5.3.3 切片后图像相加

  切片操作完成之后,将黄色的车道线+白色车道线和起来才是完整的、需要检测的车道线,因此我使用了图像相加,代码如下:

🔥 切片后图像相加
🌟 算法描述:
  遍历输出图像的每个像素,并获取两幅输入图像在当前位置的RGB值,计算两幅图像在当前位置RGB通道上的值相加得到的结果,判断输出图像的RGB通道值是否大于255,如果大于255,则将其截断为255,将计算得到的RGB通道值赋值给输出图像的对应位置。就得到了两幅图像相加的结果

1.	# 把两幅图像相加  
2.	def tow_img_overlay(image_white, image_yellow):  
3.	    out_img = np.zeros_like(image_white)  # 创建与 image1 相同大小的零矩阵作为结果  
4.	  
5.	    for i in range(out_img.shape[0]):  
6.	        for j in range(out_img.shape[1]):  
7.	            # 白色掩膜的 rgb  
8.	            w_R = image_white[i, j, 2]  
9.	            w_G = image_white[i, j, 1]  
10.	            w_B = image_white[i, j, 0]  
11.	            # 黄色掩膜的 rgb  
12.	            y_R = image_yellow[i, j, 2]  
13.	            y_G = image_yellow[i, j, 1]  
14.	            y_B = image_yellow[i, j, 0]  
15.	            rs_R = (w_R + y_R)  
16.	            rs_G = (w_G + y_G)  
17.	            rs_B = (w_B + y_B)  
18.	            # 判断输出图像的rgb是否大于255  
19.	            rs_R = 255 if rs_R > 255 else rs_R  
20.	            rs_G = 255 if rs_G > 255 else rs_G  
21.	            rs_B = 255 if rs_B > 255 else rs_B  
22.	            # 赋值  
23.	            out_img[i, j, 2] = rs_R  
24.	            out_img[i, j, 1] = rs_G  
25.	            out_img[i, j, 0] = rs_B  
26.	  
27.	    return out_img  

示例结果如下图5-8:
在这里插入图片描述

图5-8 车道相加

5.4 删除天空(ROI提取)

  部分图像的天空会有大片的白色或者较亮的蓝色,容易在白色切片时被引入。而天上的信息是我们并不需要的,因此我们需要选用一种方法排除天空对图像纯净度的印象,删掉天空以让图片尽可能的只保留有车道线,也即是ROI提取。
  我通过直角坐标系计算与调整观察,初步选择了如下图所示的一个椭圆+长方形来作为图像保留的部分。在大部分图像上——包括上坡、平地、下坡等地平线发生变动的图像,这个范围都能较好的划分车道线与天空。图像外的天空会被删去。
在这里插入图片描述

图5-9 提取图片中下部区域

🔥 删除天空
🌟 算法描述:
同理类似第二章的ROI提取,需要我们动态调整顶点vertices的参数:

1.	# 车道检测  
2.	# 根据车的方向,构建一个梯形和三角形区域,消除四周的背景干扰  
3.	def roi_trapezoid(image):  
4.	    # 生成感兴趣区域即Mask掩模  
5.	    mask = np.zeros_like(image)  # 生成图像大小一致的mask矩阵  
6.	  
7.	    row = image.shape[0]  # 行 y  height  
8.	    line = image.shape[1]  # 列 x  width  
9.	    print('row = ', row)  
10.	    print('line = ', line)  
11.	  
12.	    # 填充顶点vertices中间区域  
13.	    if len(image.shape) > 2:  
14.	        channel_count = image.shape[2]  
15.	        ignore_mask_color = (255,) * channel_count  
16.	    else:  
17.	        ignore_mask_color = 255  
18.	  
19.	    # 定义多边形的顶点坐标 1280*720  
20.	    # 缩放为 640*360  
21.	    # 1   4  
22.	    # 2   3                         1          2          3           4  
23.	    # vertices = np.array([[250, 200], [60,355], [600, 355], [400, 200]], np.int32)  
24.	  
25.	    # 定义多边形的顶点坐标 1280*720  
26.	    # 缩放为 640*360  
27.	    # 1   4  
28.	    # 2   3                           1          2                3           4  
29.	    vertices = np.array([[0, row*0.55], [0, row], [line, row], [line, row*0.55]], np.int32)  
30.	  
31.	    # 将多边形的顶点数组组合成列表  
32.	    polygons = [vertices]  
33.	  
34.	    # 填充梯形区域  
35.	    cv2.fillPoly(mask, polygons, ignore_mask_color)  
36.	    # 目标区域提取:逻辑与  
37.	    masked_image = cv2.bitwise_and(image, mask)  
38.	    return masked_image  

天空删去前后比较示例图如下图5-10:

在这里插入图片描述

图5-10 天空删去前后对比

5.5 形态学运算

  由于某些线在切片后并不是完整的,可能出现如下图5-11所出现的“空心”情况,因此我还在对其进行了形态学运算将其进行“补全”——使用闭运算平滑直线内部黑色的细线,再使用膨胀运算填充直线。

在这里插入图片描述

🔥 形态学运算(闭运算)
🌟 算法描述:
  该函数接受一个输入图像img,然后定义了一个卷积核的大小kernel_size,并创建了一个由全1构成的卷积核kernel,用于进行膨胀和腐蚀操作。
  闭运算是指先进行膨胀操作,然后再进行腐蚀操作。它可以用来填充图像中的小孔洞、平滑边界、消除噪声等。
  因此先使用cv2.dilate函数对输入图像进行膨胀操作,参数 iterations=1 表示进行一次膨胀操作。膨胀操作会使图像中的白色区域扩张,再使用 cv2.erode 函数对膨胀后的图像 dilation1 进行腐蚀操作,参数 iterations=1 表示进行一次腐蚀操作。腐蚀操作会使图像中的白色区域收缩,最后返回经过闭运算处理后的图像。

1.	# 闭运算  
2.	def img_close_operation(img):  
3.	    kernel_size = 6  
4.	    kernel = np.ones((kernel_size, kernel_size), np.uint8)  
5.	  
6.	    # 膨胀操作  
7.	    dilation1 = cv2.dilate(img, kernel, iterations=1)  
8.	  
9.	    # 腐蚀操作  
10.	    erosion = cv2.erode(dilation1, kernel, iterations=1)  
11.	  
12.	    # # 膨胀操作  
13.	    # kernel_size = 8  
14.	    # kernel = np.ones((kernel_size, kernel_size), np.uint8)  
15.	    # dilation2 = cv2.dilate(erosion, kernel, iterations=1)  
16.	  
17.	    return erosion  

实现效果如下图5-12:
在这里插入图片描述

图5-12 使用闭运算修复前后

六、基于Hough变换的车道检测

  车道线的线形主要有虚线和实线,但大多数车道线检测算法对这两者并不进行区分。然而在对道路环境进行感知中,虚线和实线的区分具有非常重要的作用,区分两者有利于车辆进一步推断自身所在环境,比如在车道保持算法中对虚线和实线的偏离预警应有不同的策略。
  而我在车道线的检测过程中基于虚线与实线的几何特征对这二者进行了区分。因为使用霍夫变换算法检测到的车道线返回的值是车道线的起点与终点的坐标,根据实际情况可知实线的长度要大于虚线的长度,因而推断出可计算检测到的车道线的长度对实线与虚线进行区分。但是为了减少计算量,我选择了计算检测到的车道线的起点与终点坐标在 y 方向上的差值以替换长度值来实现区分。
  假设在一帧中检测到的某条车道线的起点和终点坐标分别为 (x1,y1) 和 (x2,y2),经过这两个坐标的直线的斜率为 slope,具体算法过程如下:
  (1)根据直线斜率 slope 的正负判断检测的车道为左车道还是右车道。
  (2)对于左右 2 条车道,分别计算y1 与 y2 差值的绝对值,并将该长度与阈值 Threshold(我给定的是60)进行比较,若大于该阈值,则判定为实线;否则为虚线。
  根据以上算法描述,代码如下:

6.1 虚实线检测

  车道线的线形主要有虚线和实线,但大多数车道线检测算法对这两者并不进行区分。然而在对道路环境进行感知中,虚线和实线的区分具有非常重要的作用,区分两者有利于车辆进一步推断自身所在环境,比如在车道保持算法中对虚线和实线的偏离预警应有不同的策略。
  而我在车道线的检测过程中基于虚线与实线的几何特征对这二者进行了区分。因为使用霍夫变换算法检测到的车道线返回的值是车道线的起点与终点的坐标,根据实际情况可知实线的长度要大于虚线的长度,因而推断出可计算检测到的车道线的长度对实线与虚线进行区分。但是为了减少计算量,我选择了计算检测到的车道线的起点与终点坐标在 y 方向上的差值以替换长度值来实现区分。
  假设在一帧中检测到的某条车道线的起点和终点坐标分别为 (x1,y1) 和 (x2,y2),经过这两个坐标的直线的斜率为 slope,具体算法过程如下:
  (1)根据直线斜率 slope 的正负判断检测的车道为左车道还是右车道。
  (2)对于左右 2 条车道,分别计算y1 与 y2 差值的绝对值,并将该长度与阈值 Threshold(我给定的是60)进行比较,若大于该阈值,则判定为实线;否则为虚线。
  根据以上算法描述,代码如下:

🔥 虚实线检测
🌟 算法描述:
  给定判断线段长度的阈值threshold,然后对传入的图像img和线段列表lines进行处理,通过对每条直线进行遍历,获取其斜率和垂直方向的距离。
  根据线段的斜率判断左右车道,根据线段长度判断虚实线,如果长度大于等于阈值threshold,则判断为实线,设置文字"Right solid"或"Right solid",否则为虚线,设置文字为"Right Dotted"或"Left Dotted"

1.	# 实线和虚线判断  
2.	def solid_dotted_judge(frame, lines):  
3.	    if len(lines) > 0:  
4.	        for line in lines:  
5.	            x1, y1, x2, y2 = line[0]  
6.	            fit = np.polyfit((x1, x2), (y1, y2), 1)  
7.	            slope = fit[0]  
8.	            k = abs(y2 - y1)  
9.	            font = cv2.FONT_HERSHEY_SIMPLEX  
10.	            # 右车道线  
11.	            if slope > 0:  
12.	                if k >= 60:  
13.	                    cv2.putText(frame, 'Right Solid', (int((x1 + x2) / 2), int((y1 + y2) / 2)), font, 0.5, (0, 255, 0), 2)  
14.	                else:  
15.	                    cv2.putText(frame, 'Right Dotted', (int((x1 + x2) / 2), int((y1 + y2) / 2)), font, 0.5, (0, 255, 0), 2)  
16.	            # 左车道线  
17.	            else:  
18.	                if k >= 60:  
19.	                    cv2.putText(frame, 'Left Solid', (int((x1 + x2) / 2), int((y1 + y2) / 2)), font, 0.5, (0, 255, 0), 2)  
20.	                else:  
21.	                    cv2.putText(frame, 'Left Dotted', (int((x1 + x2) / 2), int((y1 + y2) / 2)), font, 0.5, (0, 255, 0), 2)  

实现效果的样例图如图6-1所示:

在这里插入图片描述

图6-1 虚实线车道识别

  右图看出已经自动识别出来了虚实线,如左侧的黄色实线车道识别为“Left Solid”,识别正确,右侧的白色虚线车道也分别被识别出了“Right Dotted”,也识别正确。
  右图可以看出有些车道出现两次识别结果,这是因为在判断时是根据Hough变化的“骨架”来识别的,有些车道较宽,因此两侧的车道线都被识别了出来,如上图标注的1,2所示,而有些车道因为距离比较远,骨架比较“纤细”,因此只被识别了一次,如上图标注的3所示。

6.2 黄白线的检测

  在第五章的5.3.2子章节中,我根据黄色的H(色调)的特殊性介绍了一种检测黄色的方法,在本章,我将继续介绍另一种检测黄色的方法。
(1)原理分析
  II:黄色车道的S值(饱和度)的特殊性:
  通过查阅HSV颜色空间各个颜色参数值的汇总表,汇总表如图6-2所示:

在这里插入图片描述
图6-2 HSV颜色空间各个颜色参数值
  从上表可以得知,将颜色空间从 RGB 转成 HSV 后,白色的 S (饱和度)参数值介于 [0,30] 之间,而黄色的 S 参数介于 [43,255] 之间,两者存在着清晰的界限。
  那么在假定只有黄白车道线的前提下,只要判定某一像素点的 S 参数值大于 40,那么就可以认为该点为黄点。进而,在检测出的车道线上取若干(以 11 为例)个点分别判断 S 值,如果黄点的个数超过阈值(以6为例),则判定该线为黄线,否则判断为白线。
(2)黄色车道检测算法实现
  根据以上描述,设计检测黄色车道线的算法如下:

🔥 黄色车道检测算法
🌟 算法描述:
  将原彩色图像转换为HSV颜色空间,然后根据直线的起点和终点坐标计算出直线上的11个坐标点,并将这些点保存在列表points中,以便后续统计每个点的颜色。
  遍历列表points中的每个点,获取其坐标,并从HSV图像中获取该点的像素值(h, s, v)。其中,h表示色调,s表示饱和度,v表示亮度。
接下来,函数根据饱和度值s将像素点分为黄色和白色两类。如果饱和度s大于阈值 45,则将该点视为黄色像素点;如果饱和度 s 小于等于阈值 30,则  将该点视为白色像素点。通过统计黄色和白色像素点的数量,得到直线上黄色和白色像素点的个数。
  最后根据黄色和白色像素点的数量进行判断,如果黄色像素点数大于6个,则判断该直线为黄色车道线;如果白色像素点数大于6个,则判断该直线为白色车道线;否则,无法确定直线的颜色,返回 “Unknown”。

1.	# 检测车道的颜色  
2.	def detect_lane_color(image, line):  
3.	    # 将图像转换为HSV颜色空间  
4.	    hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)  
5.	  
6.	    # 提取直线上的像素点坐标  
7.	    x1, y1, x2, y2 = line  
8.	    points = []  
9.	    for i in range(11):  
10.	        x = int(x1 + (x2 - x1) * i / 11)  
11.	        y = int(y1 + (y2 - y1) * i / 11)  
12.	        points.append((x, y))  
13.	  
14.	    # 统计直线上黄色和白色像素点的数量  
15.	    yellow_count = 0  
16.	    white_count = 0  
17.	    for point in points:  
18.	        x, y = point  
19.	        h, s, v = hsv_image[y, x]  
20.	        if s > 45:  
21.	            yellow_count += 1  
22.	        elif s <= 30:  
23.	            white_count += 1  
24.	  
25.	    # 判断直线的颜色  
26.	    if yellow_count > 6:  
27.	        return yellow  
28.	    elif white_count > 6:  
29.	        return white  
30.	    else:  
31.	        return "Unknown"  

🔥 将黄色车道检测算法扩展到虚实线检测部分,在图像上显示文字
🌟 算法描述:
  根据上面检测车道颜色的代码,计算出当前车道的颜色之后,使用三目表达式判断即可。

1.	# 实线和虚线判断,扩展了颜色识别,用于 6.3子章节代码  
2.	def solid_dotted_judge(img, lines):  
3.	    threshold = 60  # 判断线段长度  
4.	    font_size = 1   # 字体粗细  
5.	    if len(lines) > 0:  
6.	        for line in lines:  
7.	            x1, y1, x2, y2 = line[0]  
8.	            fit = np.polyfit((x1, x2), (y1, y2), 1)  
9.	            slope = fit[0]  
10.	            k = abs(y2 - y1)  
11.	            font = cv2.FONT_HERSHEY_SIMPLEX  
12.	  
13.	            # 判断出来的车道颜色  
14.	            line_color = detect_lane_color(img, line[0])  
15.	            if line_color == 'Unknown':  
16.	                continue  
17.	  
18.	            # 字体颜色  
19.	            font_yellow_color = (255, 239, 59)  
20.	            font_white_color = (252, 253, 237)  
21.	  
22.	            # 文字位置  
23.	            text_position_x = int((x1 + x2) / 2 - 55)  
24.	            text_position_y = int((y1 + y2) / 2)  
25.	            text_position = (text_position_x, text_position_y)  
26.	  
27.	            print('slope = ', slope, 'line_color= ', line_color, ' text_position=', text_position)  
28.	  
29.	  
30.	            # 右车道线  
31.	            if slope > 0:  
32.	                if k >= threshold:  
33.	                    text = line_color+' Right solid'  
34.	                    cv2.putText(img, text,  
35.	                                text_position, font, 0.5,  
36.	                                font_white_color if line_color == 'White' else font_yellow_color,  
37.	                                font_size)  
38.	                else:  
39.	                    text = line_color+' Right Dotted'  
40.	                    cv2.putText(img, text,  
41.	                                text_position, font, 0.5,  
42.	                                font_white_color if line_color == white else font_yellow_color,  
43.	                                font_size)  
44.	            # 左车道线  
45.	            else:  
46.	                if k >= threshold:  
47.	                    text = line_color+' Left Solid'  
48.	                    cv2.putText(img, text,  
49.	                                text_position, font, 0.5,  
50.	                                font_white_color if line_color == white else font_yellow_color,  
51.	                                font_size)  
52.	                else:  
53.	                    text = line_color+' Left Dotted'  
54.	                    cv2.putText(img, text,  
55.	                                text_position, font, 0.5,  
56.	                                font_white_color if line_color == white else font_yellow_color,  
57.	                                font_size)  

实验示例图如下图6-3所示:

在这里插入图片描述

图6-3 检测黄白车道并在图像上显示

  对比左侧虚实线检测的图片可以看出,右侧图片新增了车道颜色判断,并且不同的车道绘制不同颜色的字符串,这样更加直观显示车道的种类。

6.3 Canny边缘检测

  我们这一步需要用到opencv库中的cv.Canny函数,其中使用到的参数为:src——输入图像,low_threshold ——低阈值,high_threshold——高阈值,具体代码如下:

🔥 形态学运算(闭运算)
🌟 算法描述:
实现原理和算法描述在第三章的3.4子章节和3.5子章节已经详细描述过

1.	# Canny边缘检测  
2.	def canny(image):  
3.	    # canny算子提取边缘  
4.	    # 低阈值  
5.	    low_threshold = 75  
6.	    # 高阈值  
7.	    high_threshold = 225  
8.	    return cv2.Canny(image, low_threshold, high_threshold)  

实现效果如下图6-4所示:

在这里插入图片描述

图6-4 边缘提取前后对比图

6.4 绘制车道线(解决霍夫变换检测到多条直线问题)

  考虑到使用边界提取可能会出现的左右“双线”的情况,我设计了一种算法来“模糊”地得到霍夫变换后对应的直线的参数。
  对粗的、是双线的直线进行霍夫变换可能出现下图6-5所示情况。这个问题在需要选取多条车道线的本题会体现的更加严重,因此需要设法“归并”这些相似的直线。在这里插入图片描述

图6-5 霍夫变换检测到多条直线

  在经过变换后的霍夫坐标系空间,每一个(theta,radius)坐标都有一个值——值较大那点是原图直线的概率较大。
在这里插入图片描述

图6-6 霍夫空间

  如上图6-6所示的霍夫空间(图片来自网络),假设我们需要选取的两根直线是A和B,但是由于车道线分布的不均匀性——即有些车道可能是完整的一个线,而有些车道线只是一些点,这会导致可能A附近的某另外一个点A2的值会大过B,这就导致了我们不能成功的检测到我们需要的两个直线。
  而我们所期望的是单车道线检测。为了直观体验与后续处理,需要对上一步检测到的直线进行进一步的预处理,因此我想出了一个算法来对相似的线进行归并,筛选出差异较大的直线,处理的方法具体为:
  • 对每条直线求取斜率,分别归为左右列表中
  • 对得到的列表进行斜率平均值m0的计算和最高点A的计算
  • 根据平均斜率m_{0}、最高点计算线段、图像下方的交点B坐标
  • 连接最高点A与交点B
  算法流程图如下图6-7:

在这里插入图片描述

图6-7 绘制车道线的算法流程图

根据以上算法原理和实现过程,我编写代码如下:

🔥 绘制车道线,将经过hough变换之后检测到的直线绘制出来
🌟 算法描述:
  函数输入参数包括源图像image、检测到的直线集合lines和线段的厚度thickness。函数先定义了用于存储右侧车道线和左侧车道线相关信息的列表。然后,设定了斜率的阈值范围slope_min和slope_max,以及图像中线的x坐标middle_x和最大 y 坐标max_y。
  在具体实现中,函数迭代遍历检测到的直线集合lines,对每条直线的端点坐标(x1, y1, x2, y2)进行处理,得到拟合直线的斜率slope。
然后,通过比较斜率的绝对值是否在阈值范围内,判断直线是属于左侧车道线还是右侧车道线。如果斜率大于0且线段的x坐标都在图像中线右边,将该点认为是右侧车道线的点,分别将其y坐标和x坐标添加到对应的列表中。如果斜率小于0,将该点认为是左侧车道线的点,同样将其 y 坐标和 x 坐标添加到对应的列表中。遍历结束之后,对左侧车道线和右侧车道线进行绘制。
  对于左侧车道线,判断左侧车道线的y坐标列表left_y_set是否非空,如果非空,找到列表中y坐标最小的点的索引lindex,然后获取对应的x坐标left_x_top和y坐标left_y_top。计算左侧车道线的平均斜率lslope。根据斜率,计算车道线与图像底部的交点的x坐标left_x_bottom。最绘制左侧车道线的线段,起点为(left_x_bottom, max_y),终点为(left_x_top, left_y_top)。
  对于右侧车道线,同理和左侧车道一样进行处理。
  这样就完成了对检测到的直线进行车道线绘制的过程。

1.	# 绘制车道线,将经过hough变换之后检测到的直线绘制出来  
2.	def draw_chedao_lines(image, lines, thickness):  
3.	    # 分别用于存储右侧车道线的y坐标、x坐标和斜率  
4.	    right_y_set = []  
5.	    right_x_set = []  
6.	    right_slope_set = []  
7.	    # 存储左侧车道线的相关信息  
8.	    left_y_set = []  
9.	    left_x_set = []  
10.	    left_slope_set = []  
11.	  
12.	    # slope_min = .25  # 斜率低阈值  
13.	    # slope_max = .90  # 斜率高阈值  
14.	    slope_min = 0.5  # 斜率低阈值  
15.	    slope_max = 1.2  # 斜率高阈值  
16.	    middle_x = image.shape[1] / 2  # 图像中线x坐标   image.shape[1]是图像的列数  
17.	    max_y = image.shape[0]  # 最大y坐标  image.shape[0]是图像的行数  
18.	  
19.	    for line in lines:       # 迭代检测到的直线集合lines  
20.	        for x1, y1, x2, y2 in line:     # 遍历每条直线的端点坐标 (x1, y1, x2, y2)  
21.	            fit = np.polyfit((x1, x2), (y1, y2), 1)  # 拟合成直线  
22.	            slope = fit[0]  # 斜率  
23.	  
24.	            # print('x1 = ', x1, ' x2 = ', x2, ' y1 = ', y1, ' y2 = ', y2 ,'\n')  
25.	            print('slope = ', slope)  
26.	            # 比较斜率是否在阈值范围内,判断直线是属于左侧车道线还是右侧车道线  
27.	            if slope_min < np.absolute(slope) <= slope_max:  
28.	                # 将斜率大于0且线段X坐标在图像中线右边的点存为右边车道线  
29.	                if slope > 0 and x1 > middle_x and x2 > middle_x:  
30.	                    right_y_set.append(y1)  
31.	                    right_y_set.append(y2)  
32.	                    right_x_set.append(x1)  
33.	                    right_x_set.append(x2)  
34.	                    right_slope_set.append(slope)  
35.	                # 将斜率小于0且线段X坐标在图像中线左边的点存为左边车道线  
36.	                # elif slope < 0 and x1 < middle_x and x2 < middle_x:  
37.	                elif slope < 0:  
38.	                    left_y_set.append(y1)  
39.	                    left_y_set.append(y2)  
40.	                    left_x_set.append(x1)  
41.	                    left_x_set.append(x2)  
42.	                    left_slope_set.append(slope)  
43.	  
44.	    left_line_color = (1, 255, 0)   # 左车道颜色 绿色  
45.	    right_line_color = (30, 117, 248)  # 右车道颜色  蓝色  
46.	  
47.	    # 绘制左车道线  
48.	    if left_y_set:  
49.	        lindex = left_y_set.index(min(left_y_set))  # 在左侧车道线的y坐标集合中找到最高点的索引 lindex  
50.	        # 获取对应的x坐标 left_x_top 和y坐标 left_y_top  
51.	        left_x_top = left_x_set[lindex]  
52.	        left_y_top = left_y_set[lindex]  
53.	        lslope = np.median(left_slope_set)  # 计算左侧车道线的平均斜率 lslope  
54.	        # 根据斜率计算车道线与图片下方交点作为起点  
55.	        left_x_bottom = int(left_x_top + (max_y - left_y_top) / lslope)  
56.	        # 绘制线段  
57.	        cv2.line(image, (left_x_bottom, max_y), (left_x_top, left_y_top), left_line_color, thickness)  
58.	  
59.	    # 绘制右车道线  
60.	    if right_y_set:  
61.	        rindex = right_y_set.index(min(right_y_set))  # 最高点  
62.	        right_x_top = right_x_set[rindex]  
63.	        right_y_top = right_y_set[rindex]  
64.	        rslope = np.median(right_slope_set)  
65.	        # 根据斜率计算车道线与图片下方交点作为起点  
66.	        right_x_bottom = int(right_x_top + (max_y - right_y_top) / rslope)  
67.	        # 绘制线段  
68.	        cv2.line(image, (right_x_top, right_y_top), (right_x_bottom, max_y), right_line_color, thickness)  

  未经过任何处理,直接使用霍夫变化检测出的直线如图6-8所示,可以看到明显检测出了多条直线。

在这里插入图片描述

图6-8 未经处理的霍夫变换

经过处理之后实现效果图如图6-9所示:

在这里插入图片描述

图6-9 绘制车道线

  通过以上算法,可以看到,我们成功避免了霍夫变换出现了多条线段相互相邻的情况,只保留了中间的一条直线,即车道线的“骨架”。
  在面对一些有较多点的密集的粗线,该方法也能准确的识别出这部分区域对应的单线,而不是输出许多条线。

6.5 图像融合

  这一步是将原始彩色图像与我们刚绘制的车道线图像进行比例的融合,这里需要介绍一个函数cv.addWeighted,参数src1——原始图像或矩阵;alpha——其原始图像对应的权重;src2——第二幅图像;beta——第二副图像对应的权重;gamma——整体添加到数值,默认为0即可。
  这里我们将原图权重设为0.8,车道线图像设为1,则最后呈现的效果为车道线较为明显,可视化程度提高,具体代码如下:

🔥 图像融合
🌟 算法描述:
  函数根据给定的权重比例alpha和beta,将原图像和车道线图像进行线性组合,并返回融合后的图像blended_image

1.	# 原图像与车道线图像按照a:b比例融合  
2.	def weighted_img(img_after_gaussian, line_image):  
3.	    alpha = 0.8  # 原图像权重  
4.	    beta = 1  # 车道线图像权重  
5.	    lambda_ = 0  
6.	    blended_image = cv2.addWeighted(img_after_gaussian, alpha, line_image, beta, lambda_)  
7.	    return blended_image  

实现效果如下图6-10所示:
在这里插入图片描述

图6-10 图像融合

可以看出直线的拟合效果较好,最终效果图达到了我们的预期

6.6 视频合成

  在进行完直线检测+直线绘制+直线处理+图像融合后得到了彩色图,可以直接按第二章中的视频合成方法生成最终视频。
  具体流程即将视频按帧拆分成图片,遍历所有图片分别进行上述处理,得到最终图像融合后的图片之后,将所有图片合成视频,算法流程如下图6-11:
在这里插入图片描述

图6-11 合成视频的算法流程图

根据以上算法原理和实现流程图,代码如下:

🔥 视频合成
🌟 算法描述:
具体描述如图6-11: 合成视频的算法流程图

1.	def show_chedao_vedio():  
2.	    video_chedao_path = "D:\\AllCode\\python\\lane_dectection\\chedao\\vedio\\video_5s.mp4"  
3.	    img_chedao_save_path = "D:\AllCode\\python\\lane_dectection\\chedao\\origin\\"
4.	      img_chedao_final_handle_path = "D:\\AllCode\\python\\lane_dectection\\chedao\\final_handle\\"  
5.	    vedio_chedao_final_handle_path = "D:\\AllCode\\python\\lane_dectection\\chedao\\vedio\\"  
6.	    video_time_len = aa.get_duration_from_cv2(video_chedao_path)  
7.	    print("视频时长:", video_time_len, "s\n")  
8.	  
9.	    # 1.将视频保存成原始图片  
10.	    aa.Video_splitting(video_chedao_path, img_chedao_save_path)  
11.	    # 处理图片并保存  
12.	    for i in range(144):  
13.	        filename = img_chedao_save_path + 'img' + str(i) + '.jpg'  
14.	        img_origin = cv2.imread(filename)  
15.	        final_img = handle_chedao_img(img_origin)  
16.	        final_filename = img_chedao_final_handle_path + 'img_final' + str(i) + '.jpg'  
17.	        cv2.imwrite(final_filename, final_img)  
18.	        print(i)  
19.	  
20.	    # 图片合成视频  
21.	    videoname = vedio_chedao_final_handle_path + "video_final.mp4"  # 要创建的视频文件名称  
22.	    fourcc = cv2.VideoWriter_fourcc(*'mp4v')  # 编码器修改  
23.	    fps = 24  # 帧率(多少张图片为输出视频的一秒)  
24.	    size = (window_width, window_height)  
25.	    videoWrite = cv2.VideoWriter(videoname, fourcc, fps, size)    # 1.要创建的视频文件名称 2.编码器 3.帧率 4.size  
26.	    for i in range(144):    # 图片合成视频  
27.	        filename = img_chedao_final_handle_path + 'img_final' + str(i) + '.jpg'  
28.	        img = cv2.imread(filename)  
29.	        videoWrite.write(img)  # 写入  
30.	        print(i)  
31.	  
32.	def handle_chedao_img(img_origin):  
33.	    img_origin = cv2.resize(img_origin, (window_width, window_height))  
34.	    # 2.进行高斯模糊  
35.	    img_after_gaussian = dd.GaussianFilter(img_origin)  
36.	    # 3.获得黄色掩膜  
37.	    img_yellow_roi = bb.Get_Yellow_Line(img_after_gaussian)  
38.	    # 4.获得白色掩膜  
39.	    img_white_roi = bb.Get_White_Line(img_after_gaussian)  
40.	    # 5.黄色和白色两幅图像相加  
41.	    img_overlay = bb.tow_img_overlay(img_white_roi, img_yellow_roi)  
42.	    # 6.提取彩色 roi  
43.	    img_color_roi = cc.roi_trapezoid(img_overlay)  
44.	    # 7.将图像进行闭运算  
45.	    img_close_operation = bb.img_close_operation(img_color_roi)  
46.	    # 8.canny 边缘提取  
47.	    img_after_canny = ee.canny(img_close_operation)  
48.	    # 9.基于霍夫变换的直线检测  
49.	    lines = cv2.HoughLinesP(img_after_canny, 1, np.pi / 180, 20, np.array([]), minLineLength=30, maxLineGap=60)  # ta参  
50.	    #  10. 筛选直线 斜率在指定范围之内  
51.	    filtered_lines = ee.select_line(lines)  
52.	    # 11.霍夫变换提取的直线绘制到空白图像上,即绘制车道线段  
53.	    line_image = np.zeros_like(img_color_roi)  
54.	    ee.draw_chedao_lines(line_image, filtered_lines, 4)  
55.	    # 12.图像融合  
56.	    img_mix = ee.weighted_img(line_image, img_origin)  
57.	    return img_mix  

实现效果如下图6-12所示:

在这里插入图片描述
在这里插入图片描述

图6-12 视频合成

  分析:由图6-12可见,图像融合后对原彩图较好地还原,并在此绘制检测出的直线,有不错的效果。

七、总结与思考

7.1 路沿检测综述

  在本次路沿检测中,使用了Canny算子边缘检测+Hough变换直线提取的经典方法进行路沿(车道线)检测,在代码实现方面,参数调节、直线筛选与限制条件判断等条件上编写较麻烦;在检测效果上,由于传统方法的限制,直线检测偶有不稳定现象,且模型对于数据集与环境条件依赖过大,可迁移性不强。
  故我尝试查找相关资料,发现车道线检测领域是一个热门研究领域,与无人驾驶领域有很大的相关性,在对部分论文进行研读后,在此补充并综述一些其他路沿(车道线)检测方法,详情如下:
  传统图像方法综述
  如前文所述,传统图像方法通过边缘检测滤波等方式分割出车道线区域,然后结合Hough变换、直线聚类等算法进行车道线检测。这类算法需要人工手动去调滤波算子,根据算法所针对的街道场景特点手动调节参数曲线,工作量大且鲁棒性较差,当行车环境出现明显变化时,车道线的检测效果不佳。主流方式如表7-1:

表7-1 传统图像方法进行车道线检测方法综述

在这里插入图片描述
  缺点:应用场景受限;Hough变换检测方法准确但很难做弯道检测,拟合方法可以检测弯道但不稳定,仿射变换可以做多车道检测但在遮挡等情况下干扰严重。透视变换操作会对相机有一些具体的要求,在变换前需要调正图像,而且摄像机的安装和道路本身的倾斜都会影响变换效果。其次,这些方法都无法满足实时性要求。
  随着之后学习的深入,我还可以尝试深度学习等方法重新进行检测,由于目前实力有限,只能使用传统图像方法进行检测。

7.2 车道检测综述

  本次实验我编写的算法代码对于大多数平常车道检测效率较高,若经过参数调优或引入新的步骤可有望达到更高的准确率。
  本方法的关键是如何在色彩切片时得到纯净的车道线图像。因此,虽然本方法在大多数“正常”的图像中可以实现几乎100%的准确率,但是有如下特征的图像则无法得到较好效果:
  • 地面附近有白色色块
  • 车道附近有黄色沙滩

(1)白色切片异常
  有些图片附近由于颜色的相似性,它们的白色可能会被误判为车道线。如下图7-2:

在这里插入图片描述
在这里插入图片描述

图7-2 特殊情况

(2)黄色切片异常
  黄色沙滩产生影响的原因:有着和车道线相似的H值,处在车道线的30±5区间中

在这里插入图片描述

在这里插入图片描述

图 7-3 黄色切片异常

(3)异常图片分析
异常图片案例:
在这里插入图片描述
在这里插入图片描述

图7-5 白色影响

如果直接对这些图像按流程进行霍夫变换,则会出现以下效果:
在这里插入图片描述

图7-6 霍夫变换

但通过图像观察,可以得出这些图像共同的性质:
  • 产生不良影响的色块为大面积色块
  因此理论上可以使用傅里叶变换对其进行低频滤波来排除这些干扰,这样应该可在一定程度上提高识别准确率。
  但由于本人对傅里叶变换的理解不够深入,且傅里叶变换手工实现起来较为复杂,如果仅仅调用现场的库函数或者别人写好的包,实在是意义不大,因此后续的优化代码并未添加。也在此留个小边边,待以后学习深入了再回顾此次实验,争取更加完善。

7.3思考和体会

  在时间长达一个月的大作业实现期间,我想出了许多不同的方法来进行车道线检测和路沿检测,其本质差不多都是边缘提取+霍夫变换。
之前的课外学习中接触过 python 图像处理的有关库,因此对这门课很感兴趣。通过这门课,我了解到了图像处理的原理、算法,入门了一直想学的 python,还能深入体会其他专业课知识的具体应用,真的让我收获满满。
  做作业的过程比较长久,有空就跑一跑试一试,虽然觉得自己做的这个题目有点小儿科,但是还是通过自己的思考,解决了不少问题,这个过程成就感超强,毕竟浮于表面都是风光,沉下心来自有答案。
  写到这就算大作业结束了,过程中因为其他课程也有相应的大实验也消耗了蛮多精力,但是也学到了很多知识,能够运用大三下和之前学到的东西完成一件任务是件很值得我骄傲和高兴的事情。


参考

传统车道线检测-canny边缘检测-霍夫变换

传统车道线检测之黄白线、虚实车道线检测

计算机视觉——车道线(路沿)检测

图像处理基础:图像膨胀、腐蚀、开闭运算及梯度运算的Python实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/25929.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

软件游戏steam_api.dll丢失的解决方法,总结5种有效的方法

在玩电脑游戏时&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“游戏缺少steam_api.dll”。这个问题可能让很多玩家感到困惑和烦恼。那么&#xff0c;究竟是什么原因导致游戏缺少steam_api.dll呢&#xff1f;又该如何解决这个问题呢&#xff1f;本文将为大家…

ISO 19115-2:2019 第6章 获取和处理元数据

6 获取和处理元数据 6.1 获取和处理要求的元数据 ISO 19115-1 确定了描述数字地理资源所需的元数据。本文件扩展了 ISO 19115-1 中确定的元数据,并确定了描述地理资源获取和处理所需的附加元数据。 6.2 获取和处理元数据包及其依赖关系 ISO 地理信息系列标准使用一个或多个…

接口测试时, 数据Mock为何如此重要?

一、为什么要mock 工作中遇到以下问题&#xff0c;我们可以使用mock解决&#xff1a; 1、无法控制第三方系统某接口的返回&#xff0c;返回的数据不满足要求 2、某依赖系统还未开发完成&#xff0c;就需要对被测系统进行测试 3、有些系统不支持重复请求&#xff0c;或有访问…

Qt安装时出现无法下载存档,环境配置,main中自定义类编译不过问题

1. Qt安装时出现无法下载存档 进入Qt安装程序exe所在的文件目录&#xff0c;一般在下载文件夹&#xff0c;右键打开cmd。cmd输入&#xff1a;对应的exe镜像提速。 .\qt-online-installer-windows-x64-4.8.0.exe --mirror https://mirrors.cloud.tencent.com/qt/ 2. 环境配置 …

Jmeter压测 —— 1秒发送1次请求

场景&#xff1a;有时候测试场景需要设置请求频率为一秒一次&#xff08;或几秒一次&#xff09;实现方法一&#xff1a;1、首先需要在线程组下设置循环次数&#xff08;可以理解为请求的次数&#xff09; 次数设置为请求300次&#xff0c;其中线程数跟时间自行设置 2、在设置…

XSS(跨站脚本攻击)

1.什么是xss XSS全称&#xff08;Cross Site Scripting&#xff09;跨站脚本攻击&#xff0c;为了避免和CSS层叠样式表名称冲突&#xff0c;所以改为了 XSS&#xff0c;是最常见的Web应用程序安全漏洞之一,XSS是指攻击者在网页中嵌入客户端脚本&#xff0c;通常是JavaScript编写…

晨控CK-UR12-E01与欧姆龙NX/NJ系列EtherNet/IP通讯手册

晨控CK-UR12-E01与欧姆龙NX/NJ系列EtherNet/IP通讯手册 晨控CK-UR12-E01 是天线一体式超高频读写器头&#xff0c;工作频率默认为902MHz&#xff5e;928MHz&#xff0c;符合EPC Global Class l Gen 2&#xff0f;IS0-18000-6C 标准&#xff0c;最大输出功率 33dBm。读卡器同时…

vue3-使用富文本编辑器-wangEditor-文章发表1

最近在搞项目:我们组内几位成员正在搞一个网站搭建,以后更新会比较缓慢 引言:如果要网站要用的富文本编辑器的话,这边推荐用wangEditor 官网地址传送 : wangEditorhttps://www.wangeditor.com/ 我现在还在扩展我的写文章用的富文本编辑器 现在我将简单介绍一下其基本使用方…

#15松桑前端后花园周刊-Turborepo 2.0、ESLint v9.4.0、重新学习promise、CSS gap

⚡️行业动态 ESLint 推出了一个新的 ESLint 配置迁移器 ESLint 用户没有升级到 ESLint v9.x 的最大原因之一是迁移配置文件似乎很困难和复杂。因此 Eslint 推出eslint/migrate-config支持将. eslintrc 文件迁移到 eslint.config.js&#xff0c;以帮助用户提高配置文件的速度。…

Web 自动化测试(基于Pytest极简)

Pytest 初体验 在使用 Python 进行 Web UI 自动化测试时&#xff0c;我们除了使用 unittest 单元测试框架&#xff0c;还可以使用 pytest&#xff0c;本节实验就给大家简单的介绍一下 pytest。 环境配置 本系列实验我们借助 VS Code 工具编写代码&#xff0c;使用的 Python …

Elasticsearch-经纬度查询(8.x)

目录 一、开发环境 二、pom文件 三、ES配置文件 四、ES相关字段 五、ES半径查询 ES的字段类型:geo_point&#xff0c;可以实现以一个点为中心的半径查询(geo_distance query) ES 地里位置查询: 半径查询(geo_distance query)查询指定矩形内的数据(geo_bounding_box quer…

管理数据必备;侦听器watch用法详解,vue2与vue3中watch的变化与差异

目录 一、侦听器&#xff08;watch&#xff09;是什么&#xff1f; 二、Vue2中的watch&#xff08;Options API&#xff09; 2.1、函数式写法 2.2、对象式写法 ①对象式基础写法 ②回调函数handler ③deep属性 ④immediate属性 三、Vue3中的watch 3.1、向下兼容&#xff…

C++面向对象程序设计 - 输入输出流进一步研究

在C中&#xff0c;输入输出流&#xff08;I/O&#xff09;是一个强大的特性&#xff0c;它允许程序与各种输入/输出设备&#xff08;如键盘、显示器、文件等&#xff09;进行交互。C标准库中的<iostream>头文件定义了基本的输入输出流类&#xff0c;如std::cin&#xff0…

搭建RocketMQ主从异步集群

搭建RocketMQ主从异步集群 1、RocketMQ集群模式 为了追求更好的性能&#xff0c;RocketMQ的最佳实践方式都是在集群模式下完成的。RocketMQ官方提供了三种集群搭建方式&#xff1a; 2主2从异步通信方式&#xff1a;使用异步方式进行主从之间的数据复制。吞吐量大&#xff0c;…

665. 非递减数列(中等)

665. 非递减数列 1. 题目描述2.详细题解3.代码实现3.1 Python3.2 Java 1. 题目描述 题目中转&#xff1a;665. 非递减数列 2.详细题解 判断在最多改变 1 个元素的情况下&#xff0c;该数组能否变成一个非递减数列&#xff0c;一看到题目&#xff0c;不就是遍历判断有几处不…

现实转虚拟:Video2Game引领3D互动体验

在当今数字化时代&#xff0c;虚拟环境的创建对于游戏开发、虚拟现实应用和自动驾驶模拟器等多个领域至关重要。然而&#xff0c;传统的虚拟环境创建过程不仅复杂而且成本高昂&#xff0c;通常需要专业人员和专业软件开发工具的参与。例如&#xff0c;著名的《侠盗猎车手V》以其…

GUI编程02-布局管理器

流式布局 FlowLayout 东西南北中 BorderLayout 表格布局 GridLayout 流式布局 package YMP.GUI; ​ import java.awt.*; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; ​ public class TestFlowLayout {public static void main(String[] args…

QSlider样式示例

参考代码&#xff1a; /********************QSlider横向滑动条样式**********************/ QSlider {background-color: rgba(170, 255, 255, 100); /* 设置滑动条主体*/ }QSlider::groove:horizontal {border: 1px solid #999999;height: 8px; /* 默认…

猫头虎分享已解决Bug || Uncaught TypeError: Cannot set property ‘innerHTML‘ of null**

猫头虎分享已解决Bug || Uncaught TypeError: Cannot set property ‘innerHTML’ of null** 原创作者&#xff1a; 猫头虎 作者微信号&#xff1a; Libin9iOak 作者公众号&#xff1a; 猫头虎技术团队 更新日期&#xff1a; 2024年6月6日 博主猫头虎的技术世界 &#x…

【InternLM实战营第二期笔记】07:OpenCompass :是骡子是马,拉出来溜溜

文章目录 课程实操 课程 评测的意义是什么呢&#xff1f;我最近也在想。看到这节开头的内容后忽然有个顿悟&#xff1a;如果大模型最终也会变成一种基础工具&#xff08;类比软件&#xff09;&#xff0c;稳定或可预期的效果需要先于用户感知构建出来&#xff0c;评测 case 就…