【学习笔记】车道线识别——图像处理方法
一、图像基本知识1. HLS色相亮度饱和度色相通道确定颜色亮度通道亮度信息饱和度通道饱和度信息对于颜色区分鲜艳程度很关键。二、视频读取示例import cv2 if __name__ __main__: video cv2.VideoCapture(./img/img/up.mp4) # 读取帧率 fps video.get(cv2.CAP_PROP_FPS) # success代表是否成功读取,frame是视频的第一帧, success, frame video.read() # 如果第一帧读取成功 while success: cv2.imshow(frame, frame) success, frame video.read() # 视频第一帧之后的每一帧每一次success, frame video.read()只读一帧,这里循环调用成了每一帧 # 按键退出 # 按下空格关闭视频 if cv2.waitKey(1) ord( ): # cv2.waitKey(1) 函数会等待1毫秒检查是否有按键被按下。ord( ) 是一个Python内置函数它返回字符的ASCII码值。在这个例子中ord( ) 返回空格键的ASCII码值即32 break # 1000ms 1s 1000/fps 计算每一帧的时间 cv2.waitKey(int(1000 / int(fps))) # 防止出现非整数 print(int(1000 / int(fps))) 按下空格退出视频很慢,增加释放资源功能提高程序运行效率 # 释放资源 video.release() # 关闭所有窗口 cv2.destroyAllWindows()2、RGB红、绿、蓝三、图像边缘提取1. sobel算子本质上是卷积在图像处理领域梯度不是传统意义上的梯度只是用卷积算出了变化幅度本质上类似于梯度1.1 卷积核Gx [-1 0 1,-2 0 2,-1 0 1]Gy [-1 -2 -1,0 0 0,1 2 1]之所以选择这个卷积核是因为当出现边缘时左右或者上下的差值大-的或者的一边会大于另一边绝对值就会大如果与卷积核进行卷积的地方数值差不多最后结果则接近于0。这样便能区分出非边缘区域和边缘区域有2的原因时因为sobel认为离得近的像素点更重要即类似于加权1.2 Sobel算子的工作原理它使用两个卷积内核一个用于计算x方向上的梯度另一个用于计算y方向上的梯度。每个内核都考虑了像素及其邻域的值并根据这些值计算梯度。通过将这两个梯度分量结合起来通常使用平方和的平方根我们可以得到梯度的幅值它表示了图像中每个像素点处的强度变化速率。1.3 opencv的sobel函数参数由于sobel计算得到的边缘信息可能存在负数(通过导数计算得到的梯度信息)超出图像像素范围 0-255 的无符号数的8位整数类型需要用绝对值的形式来取值以便后续操作sobel_x参数1.src类型numpy.ndarray图像的输入, 必须是灰度图、单通道2.ddepth:类型int输出图像的深度(数据类型)指定了输出图像的位深度。常用的值-1输出图像与输入图像深度相同。其他值cv2.cv_8U8位无符号整数、cv2.cv_16S16位有符号数、cv2.cv_32F32位浮点数cv2.cv_64F64位浮点数。选择合适的输出图像深度对于计算结果的精度和表示的范围有影响。3.dx、dy类型int某个方向的阶数, 表示图像在x、y轴的求导次数1表示计算y、x方向的一阶导数0不计算x, y的导数。大于1表示更高阶的导数极少用4.ksize类型intsobel算子的大小, 通常取奇数, 表示计算梯度时所使用的卷积核大小, 常见的大小有3 * 35 * 57 * 7ksize越大, 算子对图像的平滑效果越强, 但是可能会丢失细节。5.scale类型float可选的缩放因子, 默认值1计算梯度结果比例缩放6.delta类型folat可选的偏移量, m默认值0, 在计算梯度时,.通常用于调整最终图像中的亮度或者对比度7.borderType类型int边缘像素处理方式, 邻域参数决定了怎么处理图像的边界cv2.BORDER_CONSTANT使用常数值来填充边界外的像素cv2.BORDER_REFLECT边界外的像素值的镜像反射。cv2.BORDER_FEPLTCATE边界外的像素值的镜像复制。CV2.BORDER_DEFALUT默认方式填充边界:使用实例sobel_x cv2.Sobel(channels_l, -1, 1, 0) sobel_x_2 cv2.Sobel(channels_l, -1, 1, 0, borderTypecv2.BORDER_REPLICATE) # sobel_x 变量将存储所有检测的边缘信息, # -1表示输出图像的数据类型与输入图像的数据类型保持一致 # 1 表示 在x轴的方向求一阶导数(检测横向边缘) # 0 在y轴的方向不求导数,(不用纵向检测) 由于sobel计算得到的边缘信息可能存在负数(通过导数计算得到的梯度信息) 而图像像素范围 0-255 的无符号数的8位整数类型 一定要用绝对值的形式来取值以便后续操作 abs_sobel_x np.absolute(sobel_x) abs_sobel_x_2 np.absolute(sobel_x_2) print(abs_sobel_x.shape) print(abs_sobel_x_2.shape)2. 归一化和二值化2.1 归一化2.2 二值化忽略不重要的地方,单独取出车道线,事实上就是取出轮廓最明显(梯度最大)的使用实例 # 使用比较高效的布尔索引进行二值化 # 将170作为下限阈值,255为上限阈值,基于车道线在图像中的亮度特征的分析以及多次实验 # 同时满足170和255条件的为True # 归一化结果进一步二值化 sx_binary np.zeros_like(scaled_sobel) sx_binary[(170 scaled_sobel) (scaled_sobel 255)] 255 # 先是取出170和255的元素,取出的均为布尔值,再根据布尔值为True的填充为255(在灰度图中即为白色) s_binary np.zeros_like(channels_s) s_binary[(100 channels_s) (channels_s 255)] 255 # 饱和度二值化 color_binary (sx_binary | s_binary) cv2.imshow(color, color_binary) cv2.waitKey(0) cv2.imwrite(panel12.png, color_binary)2.3 图片保存cv2.imwrite(panel12.png, color_binary)四、仿射变换import cv2 import numpy as np # 原图片 img cv2.imread(./img/img/up01.png) # 处理后的图片 color_binary cv2.imread(./panel11.png) # 灰度化 color_binary cv2.cvtColor(color_binary, cv2.COLOR_BGR2GRAY) img_shape color_binary.shape print(img_shape) ----------------------------------透视变换--------------------------------------------------- 汽车进入车道的角度会变化为了统一使用透视变换使视角统一 仿射变换是一种几何变换它保持了图像中的直线性和平行性。仿射变换包括平移、缩放、旋转和剪切shear等操作。 本质上是一种矩阵计算矩阵本身就是一种空间变换 这些变换可以组合使用以实现复杂的图像变换。 1、偏移量设置 offset_x 160 和 offset_y 0这些偏移量用于调整透视变换后目标图像的位置。 偏移量的选择通常基于实验和图像的具体需求以确保变换后的图像能够正确地表示车道线. offset_x 160 offset_y 0 2、定义原始图像上的透视变换点 pts1这些点定义了原始图像上进行透视变换的四个关键坐标点。定义了需要进行变换的区域 坐标是以图像宽度和高度的比例来表示的数据类型是np.float32类型的数组。这些点的选择基于图像内容和所需的变换效果。 pts1 np.float32([ [img_shape[1] * 0.4, img_shape[0] * 0.7], # 第一个坐标点,横坐标的图像宽度的0.4倍纵坐标的图像0.7倍该例结果为512.0和503.99999999999994 [img_shape[1] * 0.6, img_shape[0] * 0.7], # 第二个坐标点,横坐标的图像宽度的0.4倍纵坐标的图像0.7倍 [img_shape[1] * 1 / 8, img_shape[0]], # 第三个坐标点,横坐标的图像宽度的1/8倍纵坐标就是图像高度 [img_shape[1] * 7 / 8, img_shape[0]], # 第四个坐标点,横坐标的图像宽度的7/8倍纵坐标就是图像高度 ]) print(f放射变换的参数{img_shape[1] * 0.4, img_shape[0] * 0.7}) 3、定义目标图像上的透视变换点 pts2这些点定义了透视变换后目标图像上的四个点。这些点的坐标根据图像的宽度、高度和偏移量进行计算。 目标点的选择是为了将原始图像的特定区域映射到目标图像的特定位置以便于后续处理。 pts2 np.float32([ [offset_x, offset_y], # 第一个点变换后的坐标按照偏移量来定位 [img_shape[1] - offset_x, offset_y], # 横坐标根据图片的宽度和偏移量进行计算纵坐标按照偏移量设置0 [offset_x, img_shape[0] - offset_y], # 横坐标按照偏移量纵坐标根据图像高度和偏移量确定 [img_shape[1] - offset_x, img_shape[0] - offset_y], ]) 4、计算透视变换矩阵 使用cv2.getPerspectiveTransform(pts1, pts2)函数根据给定的源点集pts1和目标点集pts2计算透视变换矩阵pts。 这个矩阵用于将原始图像透视变换为目标图像。 这里的矩阵就是进行仿射变换的关键仿射变换是一种矩阵运算。 pts cv2.getPerspectiveTransform(pts1, pts2) 5、应用透视变换 使用cv2.warpPerspective(color_binary, pts, (img_shape[1], img_shape[0]))函数对原始图像color_binary进行透视变换 得到校正后的图像correct_image。变换后的图像与原始图像的宽高保持一致。 correct_image cv2.warpPerspective(color_binary, pts, (img_shape[1], img_shape[0])) # img_shape[0]和img_shape[1]是元组 6、绘制填充矩形 在correct_image上绘制一个填充矩形用于标记或处理图像的特定区域。矩形的坐标根据图像的宽度和高度比例计算得出。 cv2.rectangle(correct_image, [int(img_shape[1] * 0.4 20), int(img_shape[0] * 0.7)], [int(img_shape[1] * 0.6 20), int(img_shape[0])], color(0, 0, 0), thicknesscv2.FILLED ) cv2.imshow(correct_image, correct_image) cv2.waitKey(0) # correct_image是一个图片类型保存的图片命名要跟上后缀用于确定保存格式 # cv2.imwrite(panel2.png, correct_image) # cv2.waitKey(0)五、开运算与闭运算形态学的腐蚀与膨胀1. 开运算:先腐蚀后膨胀,用于去除图形中的小噪点、孤立的小点,腐蚀多余的像素点原理:腐蚀操作阶段使用一个结构元素(矩形、圆形、其他形状)逐个滑动。当遇到不符合物体(通常是白色)状态时,就会腐蚀掉(背景像素、通常时黑色的)就可以去除小的噪点、和微小的物体。膨胀操作阶段使用一个结构元素(矩形、圆形、其他形状)逐个滑动。当遇到有一个像素是目标像素时,就会膨胀其结构元素中心点的像素不会回复之前已经腐蚀的像素点应用:图像去噪、物体分离2. 闭运算:先膨胀后腐蚀。填充图像中的小孔、小裂缝。膨胀特点的元素.原理膨胀阶段使用一个结构元素(矩形、圆形、其他形状)逐个滑动。当遇到有一个像素是目标像素时,就会膨胀其结构元素中心点的像素不会回复之前已经腐蚀的像素点腐蚀阶段使用一个结构元素(矩形、圆形、其他形状)逐个滑动。当遇到不符合物体(通常是白色)状态时,就会腐蚀掉中心像素点(背景像素、通常时黑色的)就可以去除小的噪点、和微小的物体。避免过度膨胀。应用:图像修复物体轮廓修复3. 腐蚀与膨胀原理腐蚀定义:形态学操作,可以是图像中目标物体(白的或者比较亮的物体)经过一定的收缩在二进制图像中,(只有 0 黑色 1 白色)会将目标物体(白色区域)的边界像素根据一定的规则来变为背景颜色(黑色)原理:结构元素的小矩阵进行滑动,,对于每个像素位置,,当结构元素所覆盖的像素与图像中心点的元素的不完全匹配时就要进行覆盖开运算效果图闭运算效果图图片转载自形态学应用——图像开运算与闭运算_图像开运算和闭运算-CSDN博客开运算与闭运算示例# MORPH_RECT 表示创建矩形结构元素 # MORPH_ELLIPSE 表示创建圆形结构元素 # MORPH_CROSS 表示创建十字结构元素 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) print(fkernel is:{kernel}) 2、闭运算 # 先进行膨胀操作 # dilate 使用定义好的元素结构对图像correct_image进行膨胀 # 膨胀操作会让图像中白色(较亮)区域扩大 dilate_image cv2.dilate(correct_image, kernel) # 再进行腐蚀操作 # erode 使用定义好的元素结构kernel对图像correct_image进行腐蚀 erode_image cv2.erode(dilate_image, kernel) # 显示经过一次闭运算之后的腐蚀图像 # cv2.imshow(erode_image, erode_image) # cv2.waitKey(0) 3、开运算 # 先进行腐蚀操作 # erode 使用定义好的元素结构kernel对图像correct_image进行腐蚀 erode_image cv2.erode(erode_image, kernel) # 显示经过一次闭运算之后的腐蚀图像 # 再进行膨胀操作 # dilate 使用定义好的元素结构对图像correct_image进行膨胀 # 膨胀操作会让图像中白色(较亮)区域扩大 dilate_image cv2.dilate(erode_image, kernel) # cv2.imshow(erode_image, erode_image) # cv2.waitKey(0) 3、直方图 # np.sum(...,axis0) 表示沿着列方向,将图像中矩阵的数值加到一个新的矩阵中 histogram np.sum(dilate_image[:, :], axis0) # 类似于横向压缩为一个矩阵 print(pd.DataFrame(histogram)) # 使用matplotlib绘制直方图 # 横坐标 # 是图像的列索引(范围从0开始 到图像的长度通过np.arrange(0,len(histogram))获得,(例子结果是1280) # 纵坐标 # 对应的列像素总和,histogram # r red g green blue y yellow # - 虚线 .-点虚线 ................... plt.plot(np.arange(0, len(histogram)), histogram, r-) plt.show() # 这个直方图揭示了当纵坐标为多少时横坐标是非0的六、车道线可视化(示例)import cv2 import numpy as np import matplotlib.pyplot as plt import pandas as pd correct_image cv2.imread(./panel2.png) # 灰度处理 correct_image cv2.cvtColor(correct_image, cv2.COLOR_BGR2GRAY) # 创建矩形结构元素。 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) 1.膨胀物体边界扩展可以链接断开的部分 2.对膨胀的图像进行腐蚀消除噪点 3.再次腐蚀进一步细化图像消除不需要的部分 4.再次膨胀相互配合调整图像形态 5.又一次膨胀 6.又一次腐蚀 具体以实际情况为主 # 膨胀 dilate_image cv2.dilate(correct_image, kernel) # 腐蚀 erode_image cv2.erode(dilate_image, kernel) # 腐蚀 erode_image cv2.erode(erode_image, kernel) # 膨胀 dilate_image cv2.dilate(erode_image, kernel) # 膨胀 dilate_image cv2.dilate(dilate_image, kernel) # 腐蚀 erode_image cv2.erode(dilate_image, kernel) # 膨胀 dilate_image cv2.dilate(erode_image, kernel) # 腐蚀 erode_image cv2.erode(dilate_image, kernel) # cv2.imshow(erode_image, erode_image) # cv2.waitKey(0) 一、获取车道线的初始位置 1.1 找到两条车道线 # 直方图图像会有两个凸点代表了车道线因为数组的其他地方为0有车道线的地方才有数字每列相加为一维数组后只有有车道线的那一块会有数值。 histogram np.sum(dilate_image[:, :], axis0) # 类似于纵向压缩为一个矩阵 plt.plot(np.arange(0, len(histogram)), histogram, r-) plt.show() 1.2 计算两条车道线的中心线并以此划分左右车道的搜索范围 # 计算直方图(每列像素值相加数组)中的中点位置用于分割划分左右车道的搜索范围 midpoint np.array(histogram.shape[0] / 2, dtypenp.int32) # 在直方图的左半部分,寻找像素点累加的最大位置这个位置作为左车道线初始搜索的大致横坐标的起点。 left_x_base np.argmax(histogram[:midpoint]) # 在直方图的右半部分,寻找像素点累加的最大位置中点位置的偏移量这个位置作为右车道线初始搜索的大致横坐标的终点。 right_x_base np.argmax(histogram[midpoint:]) midpoint # histogram[midpoint:]只是中点到右车道线的距离 # 根据上面的计算初始化车道线 # 车道检测当前位置初始化左车道线的当前横坐标 left_x_current left_x_base right_x_current right_x_base 二、创建滑动窗口用于检测车道线并为正式滑动作准备 2.1 设置滑动窗口的数量、高度、检测的水平范围、最小像素点阈值 # 设置滑动窗口的数量数量可以决定在图像的垂直方向划分多个区域来搜索车道线 m_windows 9 # 计算每个滑动窗口的高度通过图像的总高度/窗口数量 window_height int(erode_image.shape[0] / m_windows) # 设置x的检测范围这里是滑动窗口宽度的一半手动指定一个值_____可以确定每个滑动窗口内左右车道线可能出现的水平范围 margin 100 # 6.设置最小像素点阈值用于统计每个滑动区域的非0像素个数当窗口内的非0像素个数小于阈值是就说明可能不是车道线对中心点位置进行更新 minpix 50 2.2 找到整个图像中不为零的像素点的坐标 # 获取图像中像素值不为0的坐标,nonzero()返回值是两个数组非零点的纵坐标(行)非0点的纵坐标(行索引)和横坐标(列索引) non_zero erode_image.nonzero() print(non_zero) # 将此数组的纵、坐标提取出来并且进行类型转换为numpy non_zero_y np.array(non_zero[0]) # 纵坐标 non_zero_x np.array(non_zero[1]) # 横坐标 2.3 初始化两个索引分别用于记录那些在左、右车道线搜索中找到的非0数值 # 用于记录搜索窗口的左右车道线的非0数值在nonzero_y和x的索引。初始化为空。 left_lane_inds [] right_lane_inds [] 三、 开始用滑动窗口搜索车道线,遍历该图中的每一个窗口从底部窗口开始向上遍历 # m_windows是滑动窗口个数 for window in range(m_windows): 3.1 窗口纵坐标范围 # 设置窗口的y的检测范围(纵坐标范围) win_y_low erode_image.shape[0] - (window 1) * window_height win_y_high erode_image.shape[0] - window * window_height 3.2 窗口横坐标范围 # 左车道线x的范围根据当前左车道的横坐标位置与设置margin(检测范围100)来确定当前车道线坑出现的水平范围 win_x_left_low left_x_current - margin win_x_left_high left_x_current margin # 右车道线x的范围根据当前右车道的横坐标位置与设置margin(检测范围100)来确定当前车道线坑出现的水平范围 win_x_right_low right_x_current - margin win_x_right_high right_x_current margin 3.3 收集在当前滑动窗口中的非零像素点的索引 # good_left_inds和good_right_inds是一个数组它包含了在当前滑动窗口中被识别为属于左右车道线的非零像素点的索引。这些索引指向non_zero_x和non_zero_y数组中的位置即它们对应于图像中非零像素点的横坐标 good_left_inds ((non_zero_y win_y_low) (non_zero_y win_y_high) # 表示处于窗口纵坐标范围内的非0值的索引 (non_zero_x win_x_left_low) ( non_zero_x win_x_left_high)).nonzero() # 表示处于窗口左车道横坐标范围的非0值的索引 good_right_inds ((non_zero_y win_y_low) (non_zero_y win_y_high) # 表示处于窗口纵坐标范围内的非0值的索引 (non_zero_x win_x_right_low) ( non_zero_x win_x_right_high)).nonzero() # 表示处于窗口右车道横坐标范围的非0值的索引 3.4 将收集到的索引添加到分为左右车道分别添加到列表中 # 将在车道线搜索窗口内的非0点的索引添加到记录在车道索引的列表中 left_lane_inds.append(good_left_inds) right_lane_inds.append(good_right_inds) 3.5 通过阈值检验车道是否在窗口内如果不在就更新横坐标(初始横坐标是根据上面的一维纵坐标相加矩阵) # 不直接用non_zero_x和non_zero_y原因就是good_left_inds和good_right_inds是经过窗口的阈值(minpix)检验的更能说明是车道线而non_zero_x和non_zero_y包含大量非车道线数值 # 如果获取的左车道线搜索窗口内的个数小于最小个数(minpix),则利用这些点的横坐标平均值来进行更新滑动窗口在x轴的车道线 if len(good_left_inds) minpix: left_x_current np.mean(non_zero_x[good_left_inds]).astype(dtypenp.int32) # 如果获取的右车道线搜索窗口内的个数小于最小个数(minpix),则利用这些点的横坐标平均值来进行更新滑动窗口在x轴的车道线 if len(good_right_inds) minpix: right_x_current np.mean(non_zero_x[good_right_inds]).astype(dtypenp.int32) 四、循环结束提取获得到的车道线的索引 # 将检测处左右车道点的索引列表合成一个numpy数组为了统一处理, axis1————按列方向 4.1 转为numpy数组 left_lane_inds np.concatenate(left_lane_inds, axis1) right_lane_inds np.concatenate(right_lane_inds, axis1) 4.2 获取左右车道的横纵坐标 left_x non_zero_x[left_lane_inds] left_y non_zero_y[left_lane_inds] right_x non_zero_x[right_lane_inds] right_y non_zero_y[right_lane_inds] ------------------------------------------------程序执行至此已经获取了所有车道线的坐标------------------------------------------------------------------- 五、曲线拟合 # 就是类似于回归拟合一样的东西用二次项的方式去拟合车道线 # 3.用于曲线拟合检测出的点二次多项式拟合返回结果是二次项的系数(a,b,c),拟合车道线检测出的点,拟合x ay left_fit np.polyfit(left_y[0], left_x[0], 2) right_fit np.polyfit(right_y[0], right_x[0], 2) 六、进行车道线可视化 6.1 获取图像行数 y_max erode_image.shape[0] 62 创建一个处理后的图像从灰度图重新转为彩色图 # np.dstack 是 NumPy 库中的一个函数用于沿深度方向第三维堆叠数组。 # 在这里是从单通道灰度图转为三通道灰度图三通道意味着虽然本身暂时还没有颜色但是具备了显示彩色的能力 out_img np.dstack( (erode_image, erode_image, erode_image)) * 255 # *255的原因是灰度图是二值图像只有0和1回到彩色图要变成0-255的范围白的就是255黑的仍然是0 6.3 获得根据车道线进行曲线拟合生成的坐标点 # 在拟合曲线中获取左、右车道线的像素点通过垂直方向的每一个坐标点y代入拟合的二次多项式公式进行计算横坐标从而生成一系列的坐标点 left_points [[left_fit[0] * y ** 2 left_fit[1] * y left_fit[2], y] for y in range(y_max)] right_points [[right_fit[0] * y ** 2 right_fit[1] * y right_fit[2], y] for y in range(y_max)] # 左右车道线的像素点进行合并。形成一个总的坐标点。 line_points np.vstack((right_points, left_points)) 6.4 优化 # 对合并后的坐标点进行随机打乱更加均匀地展示 np.random.shuffle(line_points) # 线条区分需要更加明显时使用。 6.5 根据左右车道线的像素位置绘制多边形。效果是看起来像一整个车道 # cv2.fillPoly(out_img, [np.array(line_points, dtypenp.int32)], (0, 255, 0)) 6.6 绘制拟合的车道线 # 遍历每个车道线像素点,在输出图像上以原型绘制 for point in line_points.astype(dtypenp.int32): cv2.circle(out_img, point, 10, (0, 255, 0), thickness5) 6.7 显示 # 显示绘制好的车道线的图像 cv2.imshow(Output, out_img) cv2.waitKey(0)