【快车道线检测项目】项目学习总结
一、项目需求与背景
1. 背景
车道线检测是自动驾驶和高级驾驶辅助系统(ADAS)的核心任务之一,旨在通过摄像头或其他传感器实时识别道路上的车道线标记,帮助车辆保持车道、避免偏离,并为路径规划提供关键信息。传统方法(如边缘检测、霍夫变换或滑动窗口)依赖手工设计特征,在光照变化、遮挡、复杂道路(如弯曲车道或模糊标线)等场景下表现受限。而基于机器学习(尤其是深度学习)的方法通过自动学习鲁棒特征,显著提升了检测精度和泛化能力,成为当前主流解决方案。
技术挑战包括:
-
环境多样性:不同天气(雨雪、雾霾)、光照(逆光、夜间)和道路条件(磨损、阴影)。
-
实时性要求:需在嵌入式设备(如车载芯片)上低延迟处理(通常需≥30 FPS)。
-
复杂场景:车道线弯曲、车辆遮挡、临时施工标线等。
2. 需求
功能性需求:
-
多场景适应:在不同天气、光照及道路类型(高速、城市道路)下稳定检测。
-
多车道识别:同时检测当前车道及相邻车道线,支持车道保持和变道辅助。
-
弯曲车道处理:准确拟合抛物线或曲线车道(如环岛、弯道)。
-
遮挡鲁棒性:在部分遮挡(前车、污渍)时仍能推断车道走向。
非功能性需求:
-
实时性:处理单帧图像时间≤30ms(满足实时视频流需求)。
-
准确性:在主流数据集(如TuSimple、CULane)上达到≥95%的检测精度。
-
轻量化:模型需适配边缘计算设备(如Jetson TX2),参数量≤5M。
-
可扩展性:支持多传感器融合(如激光雷达、雷达)以提升冗余性。
3. 概述
方法框架:
-
数据准备:使用公开数据集(如TuSimple、CULane)或自建数据集,包含多样场景标注(点集或掩模)。
-
模型选择:采用深度学习模型(如LaneNet、U-Net或SCNN),结合实例分割与曲线拟合。
-
关键技术:
-
特征提取:通过CNN主干网络(如ResNet、EfficientNet)提取多尺度特征。
-
后处理:通过多项式拟合或透视变换生成连续车道线,或采用注意力机制增强关键区域。
-
轻量化优化:使用模型压缩技术(如知识蒸馏、量化)提升推理速度。
-
流程示例:
-
输入:车载摄像头捕获RGB图像。
-
预处理:图像归一化、ROI裁剪、透视变换(逆透视映射,IPM)。
-
推理:模型输出车道线位置的概率图或关键点。
-
后处理:聚类、曲线拟合,生成车道线方程或可视化结果。
-
输出:车道线坐标及曲率信息,传递给车辆控制系统。
评估指标:
-
准确率:基于交并比(IoU)或点匹配精度。
-
F1分数:平衡误检(FP)与漏检(FN)。
-
实时性:帧率(FPS)与端到端延迟。
4. 应用与展望
车道线检测不仅是L2级自动驾驶(如特斯拉Autopilot)的基础功能,还可扩展至高精地图构建、车道偏离预警(LDW)等场景。未来趋势包括:
-
多模态融合:结合雷达、LiDAR提升复杂环境下的鲁棒性。
-
端到端架构:直接输出控制指令(如方向盘转角),减少模块化冗余。
-
自监督学习:利用未标注数据降低模型训练成本。
通过结合深度学习与嵌入式优化,车道线检测技术正推动自动驾驶向更安全、更可靠的方向发展。
5. 快车道线检测算法与传统车道线检测算法的对比
传统车道线检测基于边缘特征检测车道线,但是只能检测样本原有标注好的车道,且无法识别车道被阻挡或磨损等复杂场景的情况;例如语义分割对图像的每一个像素进行分类,即判断每个像素点是车道还是背景,但可能存在不是车道的地方被识别成车道的错误;并且缺点有计算量大,训练速度慢,且判断的是车道而非车道线;
而快车道线检测算法不用判断每一个像素,而是判断一个个点,然后把点连起来作为车道线进行检测;
快车道线模型可以解决之前语义分割+实例分割的两个问题
- 车道线完全被遮挡下,像素中感受野中上下文都i没有车道线学习,导致无法识别车道线
- 在端上速度慢,如何实时进行检测
训练后,网络可以根据是否存在车辆来判断被遮挡住的车道线;
二、论文算法
论文总结:《Ultra Fast Structure-aware Deep Lane Detection》
1. 背景与问题
作者提出了一种新颖的车道线检测方法,旨在解决现有方法在速度和复杂场景下的不足。传统方法依赖于像素级分割,计算量大且难以处理遮挡和极端光照条件。本文提出的方法将车道检测视为基于行的选择问题,利用全局特征和结构损失函数,显著提高了速度和准确性。
作者将车道检测过程视为使用全局特征的基于行锚点的选择问题。在基于行的选择的帮助下,该公式可以显著降低计算成本。在全局特征上使用大的感受野,也可以处理具有挑战性的场景。此外,基于该公式,作者还提出了一个结构损失来显式模拟车道的结构。
车道线检测是自动驾驶系统的核心任务,但传统像素级分割方法(如SCNN、SAD)存在两大问题:
-
计算量大:逐像素分类导致高延迟,难以满足实时性需求(如多摄像头输入场景)。
-
复杂场景鲁棒性差:在遮挡、极端光照或车道线模糊(无视觉线索,no-visual-clue)时表现受限。
2. 算法核心
2.1 图像分割
作者通过预定义的行锚点(row anchors)进行水平位置选择,减少了计算量。此外,结构损失函数考虑了车道的连续性和形状,进一步优化了检测结果。
具体来说,作者提出使用全局特征在图像的预定义行中选择车道的位置,而不是根据局部感受野分割车道的每个像素,这大大降低了计算成本。
如上图,在左侧和右侧车道上选择的图示。在右侧,详细显示了一行的选择。行锚点是预定义的行位置,我们的公式定义为在每个行锚点上水平选择。在图像的右侧,引入了一个背景网格单元格,以指示此行中没有车道。
2.2 分类损失函数
车道表示为预定义行(即行锚点)处的一系列水平位置。为了表示位置,第一步是网格化。在每个行锚点上,位置被划分为许多单元格。通过这种方式,泳道的检测可以描述为在预定义的行锚点上选择某些单元格;
假设最大车道数为 C,行锚点数为 h,网格化单元数为 w。假设 X 是全局图像特征,f ij 是用于在第 i 个泳道、第 j 行锚点上选择车道位置的分类器。那么 lanes 的预测可以写成:
其中 Pi,j是 (w + 1) 维向量,表示为第 i 条车道、第 j 行锚点选择 (w + 1) 个网格化单元格的概率。
可以看到,作者的方法根据全局特征预测每个行锚点上所有位置的概率分布。因此,可以根据概率分布选择正确的位置。
进一步优化公式:
其中 LCE 是交叉熵损失。作者使用额外的维度来表示没有车道,因此我们的公式由 (w + 1) 维而不是 w 维分类组成。
2.3 优势与改进
如下图所示。可以看出,作者的公式比常用的分割要简单得多。假设图像大小为 H × W 。通常,预定义行锚点的数量和网格大小远小于图像的大小,即 h H 和 w W 。这样,原来的分割公式需要进行 (C + 1) 维的 H × W 分类,而我们的公式只需要解决 (w + 1) 维的 C × h 分类问题。通过这种方式,计算规模可以大大减少,因为我们公式的计算成本是 C ×h×(w+1),而分割的计算成本是 H × W × (C + 1)。例如,使用 CULane 数据集 [22] 的常用设置,我们方法的理想计算成本是 1.7 × 104 次计算,而分割成本为 1.15×106 次计算。计算成本显著降低,我们的公式可以达到极快的速度。
作者的模型和常规分割如上图示。作者的公式是在行上选择位置(网格),而 segmentation 是对每个像素进行分类。用于分类的维度也不同,标记为红色。所提出的公式显著降低了计算成本。此外,所提出的公式使用全局特征作为输入,其感受野比分割具有更大的感受野,从而解决了无视觉线索问题
并且为了处理无视觉线索问题,利用来自其他位置的信息很重要,因为无视觉线索意味着目标位置没有信息。例如,一条车道被一辆汽车遮挡,但我们仍然可以根据来自其他车道、道路形状甚至汽车方向的信息来定位该车道。通过这种方式,利用来自其他位置的信息是解决无视觉线索问题的关键。
从感受野的角度来看,我们的公式有一个整个图像的感受野,这比分割方法大得多。来自图像其他位置的上下文信息和消息可用于解决无视觉线索问题。从学习的角度来看,车道的形状和方向等先验信息也可以使用基于我们公式的结构损失来学习。
另一个显著的好处是,这种公式以基于行的方式对车道位置进行建模,这使我们有机会显式地建立不同行之间的关系。可以弥合由低级像素建模和高级 lane 长线结构引起的原始语义差距。
2.4 损失函数
除了上面的分类损失函数之外,作者还提出了两个损失函数,旨在对车道点的位置关系进行建模。通过这种方式,可以鼓励对结构信息的学习。
(1)数据源源于车道是连续的,也就是说,相邻行锚点中的车道点应该彼此靠近。在我们的公式中,车道的位置由分类向量表示。因此,连续属性是通过约束分类向量在相邻行锚点上的分布来实现的。这样,相似度损失函数可以是:
其中 Pi,j是对第 j 行锚点的预测,‖·‖1 表示 L1 范数。
(2)另一个结构损失函数侧重于车道的形状。一般来说,大多数车道都是直的。即使对于曲线车道,由于透视效果,它的大部分仍然是直线的。在这项工作中,我们使用二阶差分方程来约束车道的形状,对于直线情况,该形状为零。
要考虑形状,需要计算车道在每个行锚点上的位置。直观的思路是通过查找最大响应峰来从分类预测中获取位置。对于任何车道索引 i 和行锚点索引 j,位置 Loci,j 可以表示为:
其中 k 是表示位置索引的整数。需要注意的是,我们不在后台网格单元格中计数,位置索引 k 的范围只有 1 到 w,而不是 w + 1。
但是,argmax 函数是不可微分的,不能与进一步的约束一起使用。此外,在分类公式中,类没有明显的顺序,并且很难在不同行锚点之间建立关系。为了解决这个问题,我们建议使用预测的期望作为位置的近似值。我们使用 softmax 函数来获取不同位置的概率:
其中 Pi,j,1:w 是 w 维向量,Probi,j,: 表示每个位置的概率。
出于与方程 4 相同的原因(我们不在后台网格单元格中计数,位置索引 k 的范围只有 1 到 w,而不是 w + 1),不包括背景网格单元,计算范围仅为 1 到 w。
然后,locations 的期望可以写成:
其中 Probi,j,k 是第 i 条车道、第 j 行锚点和第 k 个位置的概率。这种定位方法的好处是双重的。第一个是期望函数是可微的。另一个是此作恢复了具有离散随机变量的连续位置。
又根据上一个方程,二阶差分约束可以写成:
其中 Loci,j 是第 i 条车道上的位置,即第 j 行锚点。我们使用二阶差分而不是一阶差分的原因是,在大多数情况下,一阶差分不为零。所以网络需要额外的参数来学习车道位置的一阶差值的分布。此外,二阶差值的约束相对较弱于一阶差值的约束,因此当车道不直时,影响较小。
最后,整体结构损失函数是:
其中 λ 是损耗系数。
上面的损失设计主要关注车道的内部关系。在本节中,我们提出了一种对全局上下文和局部特征执行的辅助特征聚合方法。提出了一种利用多尺度特征的辅助分割任务来模拟局部特征。我们使用交叉熵作为辅助分割损失。这样,我们的方法的整体损失可以写成:
其中 Lseg 是分割损失,α 和 β 是损失系数。整体架构如图 4 所示。
总结:
总的损失函数 = 存在的损失函数+权重*空间的损失函数+权重*辅助分割的损失函数
2.5 整体架构
图 4.整体架构。辅助分支显示在上半部分,仅在训练时有效。特征提取器显示在蓝色框中。基于分类的预测和辅助分割任务分别在绿色和橙色框中表示。对每个行锚点进行组分类。
分为主干和辅助两个分支
- 辅助分支只在训练时使用
- 测试时主要走主干分支
主干分支:使用ResNet网络得到训练结果(卷积、降采样、全连接),映射到原始图像上(哪根线对应哪个格),得到训练后的离散的栅格点,通过聚类或车道线拟合的方式把一个个格拟合成车道线;
辅助分支:把车道线离散出来,识别出是第几个车道,目的是为了使训练效果更好;
Resblocks可以选择任意网络,例如ResNet,FCN的计算量有点大;
PS:
(1)需要注意的是,我们的方法在训练阶段只使用辅助分割任务,在测试阶段会去掉。这样,即使我们添加了额外的切分任务,我们方法的运行速度也不会受到影响。它与没有辅助分割任务的网络相同。
(2)同时注意最后完成的是分类任务,最后全连接层输出14472个特征,即14472=【201,18,4】;
-
- 其中【18,4】与标签对应;
- 则是分类概率(其中0-199表示位置类别,200表示不存在车道)(即200+1=201);
2.6 评估指标
对于 TuSimple 数据集,主要评估指标是准确性。准确性的计算公式为:
其中 Cclip 是正确预测的车道点数,Sclip 是每个剪辑中真实值总数。
关于 CULane 的评估指标,每个通道都被视为一条 30 像素宽的线。然后计算 ground truth 和 predictions 之间的交集与并集 (IoU)。IoUs 大于 0.5 的预测被视为真阳性。F1-measure 作为评估指标,其公式如下:
其中 Precision是准确度,Recall是召回率 ,T P 是真阳性,F P 是假阳性,F N 是假阴性。
2.7 图像数据处理
实现细节:对于TuSimple 和 CULane 两个数据集,我们使用数据集定义的行锚点。具体来说,Tusimple 数据集的行锚点(图像高度为 720)的范围为 160 到 710,步长为 10。CULane 数据集的对应范围从 260 到 530,与 Tusimple 的步骤相同。CULane 数据集的图像高度为 540。网格化单元的数量在 Tusimple 数据集上设置为 100,在 CULane 数据集上设置为 150。
并且在优化过程中,图像大小调整为 288×800。我们使用 Adam来训练我们的模型,使用用 4e-4 初始化的余弦衰减学习率策略。方程 8 和 9 中的损耗系数 λ、α 和 β 都设置为 1。批量大小设置为 32,TuSimple 数据集的训练时期总数设置为 100,CULane 数据集的训练纪元总数设置为 50。我们之所以选择如此大量的 epoch,是因为我们的结构保持数据增强需要长时间的学习。下面将讨论我们的数据增强方法的详细信息。所有模型都使用 pytorch和 nvidia GTX 1080Ti GPU 进行训练和测试。
根据模型需要,输出标签为【4,18】矩阵,表示4条车道线在18个位置上的具体点,其中【4,18】可以作为超参数进行调整;
数据增强:由于车道的固有结构,基于分类的网络很容易过度拟合训练集,并在验证集上表现出较差的性能。为了防止这种现象并获得泛化能力,采用了一种由旋转、垂直和水平偏移组成的增强方法。此外,为了保持车道结构,车道延伸到图像的边界。增强的结果图5所示。
图 5.增强的演示。右侧图像上的车道被延长以保持车道结构,该结构用红色椭圆标记。
2.8 结论与总结
训练结果展示
图 8.Tusimple 和 CULane 数据集上的可视化。前两行是 Tusimple 数据集上的结果,其余行是 CULane 数据集上的结果。从左到右,结果是 image、prediction 和 label。在图像中,预测标记为蓝色,地面实况标记为红色。由于我们的方法仅预测预定义的行锚点,因此图像和标签在垂直方向上的比例并不相同。
优势:
- 提出了一种新颖、简单但有效的车道检测公式,旨在实现极快的速度并解决无视觉线索问题。与深度分割方法相比,我们的方法是选择车道的位置,而不是分割每个像素,并在不同的维度上工作,这是超快的。此外,我们的方法使用全局特征进行预测,其感受野比分割公式大。通过这种方式,也可以解决无视觉线索问题。
- 基于所提出的公式,我们提出了一个结构性损失,它显式地利用了车道的先验信息。
- 所提出的方法在具有挑战性的 CULane 数据集上实现了最先进的准确性和速度性能。我们方法的轻量级版本甚至可以在相同的分辨率下以相当的性能实现 300+ FPS,这比以前最先进的方法至少快 4 倍。
作者总结:
在本文中,作者提出了一种具有结构损失的新型公式,并取得了显着的速度和准确性。所提出的公式将车道检测视为使用全局特征的基于行的选择问题。通过这种方式,可以解决速度和无视觉线索的问题。此外,还提出了用于车道先验信息显式建模的结构损失。我们的配方的有效性和结构损失通过定性和定量实验都得到了充分的证明。特别是,我们使用 Resnet-34 主干的模型可以达到最先进的精度和速度。我们方法的轻量级 Resnet-18 版本甚至可以达到 322.5 FPS,在相同分辨率下具有相当的性能。
个人总结:
作者提出一种基于行锚点的全局选择框架,结合结构化损失函数,实现高速与高精度车道检测:
-
创新框架:将车道检测建模为行锚点选择问题。
-
预定义行锚点(Row Anchors),在每行水平划分网格(Gridding Cells),仅需在行锚点上选择车道位置,而非全图逐像素分类。
-
计算量显著降低:例如,CULane数据集上,计算量从分割方法的1.15×10⁶降至1.7×10⁴。
-
全局特征感知:利用整图感受野,结合上下文信息解决遮挡和光照问题。
-
-
结构化损失函数:
-
连续性损失(L_sim):约束相邻行锚点的预测分布相似性。
-
形状损失(L_shp):通过二阶差分约束车道线平滑性。
-
-
辅助分割任务:训练时引入辅助分割分支增强局部特征,推理时移除以保持速度。
3. 实验结果
实验部分显示,该方法在两个主流数据集(TuSimple和CULane)上达到了最先进的性能,尤其是轻量级版本实现了超过300 FPS的速度,显著快于之前的方法。
在两个主流数据集上验证方法性能:
-
速度:
-
ResNet-18轻量版:在CULane数据集上达到322.5 FPS(输入分辨率288×800),比SCNN快43倍。
-
ResNet-34版:速度175.4 FPS,精度优于SAD等现有方法。
-
-
精度:
-
CULane数据集:综合F1分数72.3%,在夜间、弯道等复杂场景表现突出。
-
TuSimple数据集:准确率96.06%,接近最优分割方法(SCNN 96.53%),但速度快22.6倍。
-
4. 方法优势
-
高效性:行锚选择框架减少90%以上计算量,适合边缘设备部署。
-
鲁棒性:全局特征与结构化损失提升遮挡、极端光照下的检测能力。
-
灵活性:支持多车道检测,可扩展至其他结构化任务(如路标识别)。
5. 未来方向
-
多模态融合:结合LiDAR或雷达增强三维感知。
-
端到端优化:直接输出车辆控制指令,减少模块冗余。
-
自监督学习:利用未标注数据降低标注成本。
5. 总结
本文提出了一种基于行锚点选择的车道检测框架,通过全局特征与结构化损失平衡速度与精度,在复杂场景下显著优于传统分割方法。其轻量版300+ FPS的性能为实时自动驾驶系统提供了高效解决方案,是车道检测领域的重要突破。
常用的语义分割通过对每个像素进行判断,而快车道线检测是把图像分为一个个栅格,对每一个纵格选择一个行格,对栅格进行聚类分类,判断栅格里是否有车道线;即是检测的模型,而非语义分割了;
例如把1280*256的图像横向分为100个格,纵向分为64个格;栅格是紧挨着的;处理图像缩小,感受野变大,训练时间也变短;
三、项目框架
1. 项目架构
2. 项目流程
- 准备数据集(CULane)
- 图像预处理(归一化)(栅格化)
- 检测处理后的数据
- 生成输入的标签数据
- 配置文件
- 设置超参数
- lr选择
- DataLoader加载数据
- 确定网络结构(加载预训练模型)
- 选择优化函数(SGD、Adam)
- 分为两个分支(主干和辅助),得到训练后的结果
- 计算损失函数(给车道线像素加一个权重,避免背景和车道线的差异)
- 整合两个分支损失函数的结果
- 进行反向传播和优化
- 训练epoch时会保存模型,得到模型的测试指标
四、数据集准备
1. 数据集下载
本次项目出于学习便捷考虑,只使用CULane的部分数据集进行训练
CULane地址:https://xingangpan.github.io/projects/CULane.html
数据集目录如下:
- 训练集与验证集:driver_193_90frame
- 测试集:driver_37_30frame
- 训练/验证/测试/列表:list
2. label数据处理
label目录下数据标签图是黑色的,但是如果放大看,里面其实不明显的像素值,因此我们只需使用opencv的阈值过滤函数将大于0部分的像素值设置成255(白色)就可以了。
修改后如下:
单张图片修改:
import cv2 as cv import numpy as np import matplotlib.pyplot as plt import os img = cv.imread("F://study//CULane//laneseg_label_w16//laneseg_label_w16//driver_23_30frame//05151646_0421.MP4//01200.png",0) ret1, thresh1 = cv.threshold(img,0,255,cv.THRESH_BINARY) cv.imwrite("F://study//CULane//laneseg_label_w16//laneseg_label_w16//driver_23_30frame//05151646_0421.MP4//01200.png",thresh1) plt.imshow(thresh1)
批量修改:
# -*- coding:utf8 -*- import os import random import cv2 as cv import matplotlib.pyplot as plt path = 'E:/CULane/161/label' filelist = [] for file in os.listdir(path): filelist.append(file) for i in range(len(filelist)): p = os.path.join(path, filelist[i]) img = cv.imread(p) ret1, thresh1 = cv.threshold(img, 0, 255, cv.THRESH_BINARY) cv.imwrite(p, thresh1) print('完成')
3. frame目录
frame目录下都是视频的节帧
显示图片:
4. list数据标注
对应标注为:
图像对应.txt注释文件。
每两个数为一个像素坐标(x,y),纵向标注,每隔十个像素标注,标注图像的下半部分。
可视化结果:
图像对应.txt注释文件。每两个数为一个像素坐标(x,y),纵向标注。
五、项目配置
1. 数据配置
由于本项目只使用CULAne数据集,因此只需配置culane.py的内容即可
数据集目录使用相对路径
2. 超参数配置
设置epoch=50,batch_size=32,其它参数保持默认
- epoch是指模型在训练过程中遍历整个训练数据集一次的过程。(如果训练数据集包含 10,000 个样本,那么一个 Epoch 就是模型在这 10,000 个样本上完成一次 前向传播(forward pass) 和 反向传播(backward pass) 的过程)
- batch_size表示单次传递给程序用以训练的参数个数或数据样本个数;
- 优化器(
optimizer
)通过调整模型参数以最小化损失函数,决定了神经网络在给定数据上的学习效率和效果; - 权重衰减(weight decay)是最广泛使用的正则化的技术之一, 它通常也被称为L 2正则化。它通过函数与零的距离来衡量函数的复杂度;
- Momentum(动量)是机器学习中一种常用的优化算法,用于加快模型收敛速度并减少震荡。
设置神经网络网格数量=200;
· backbone:深度神经网络的主要部分,通常用于特征提取。骨干网络通过卷积操作提取图像中的低级和高级特征,为后续的特定任务(如分类、检测、分割等)提供丰富的信息。这些骨干网络一般是预训练的,即在大型数据集(如ImageNet)上进行训练,以便在各种下游任务中实现良好的性能。
设置输出日志的目录 ‘./log’
3. 指定命令行参数
把配置文件设置到运行命令中,在Pycharm的运行参数中进行设置
--config ./configs/culane.py
六、数据处理模块
1. 数据标签
根据所用数据集特点设置行标签
即数据图像中车道线所在位置
2. 数据转换
本模块自定义函数对样本进行图像设置(缩放、旋转等操作);
共分为以下几个类或函数:
-
class Compose2:图像组合类,把transforms中的信息赋值给img,mask,bbx,然后直接返回;
-
class FreeScale:自由缩放类,对img和mask进行缩放;
-
class FreeScaleMask:自由缩放mask类,单独对mask进行缩放;
-
class Scale:尺寸类,读取并计算图像的h、w并返回;
-
class RandomRotate:随机旋转类,生成一个随机角度并旋转;
-
class DeNormalize:非规范化类,链式操作等价于 t = t * s + m;
-
class MaskToTensor:Mask转换为Tensor;
-
def find_start_pos:找到车道线起始位置,使用二分查找法;
-
class RandomLROffsetLABEL:随机LR偏移设置标签(左右);
-
class RandomUDoffsetLABEL:随机UD偏移设置标签(上下);
示例代码:
#尺寸类 class Scale(object): # size包括长宽(h, w) def __init__(self, size): self.size = size def __call__(self, img, mask): #img和mask不相等则输出各自尺寸 if img.size != mask.size: print(img.size) print(mask.size) #断言img和mask相等 assert img.size == mask.size #获取w,h w, h = img.size #若属于正常情况返回img和mask if (w <= h and w == self.size) or (h <= w and h == self.size): return img, mask #当w<h,但w!=self.size if w < h: #取self.size计算新的w和h ow = self.size oh = int(self.size * h / w) # (w,h),双线性插值算法进行图像缩放 # (w,h),最近邻插值算法进行图像缩放 return img.resize((ow, oh), Image.BILINEAR), mask.resize((ow, oh), Image.NEAREST) # 当h<w,但h!=self.size else: # 取self.size计算新的w和h oh = self.size ow = int(self.size * w / h) # (w,h),双线性插值算法进行图像缩放 # (w,h),最近邻插值算法进行图像缩放 return img.resize((ow, oh), Image.BILINEAR), mask.resize((ow, oh), Image.NEAREST) #随机旋转类 class RandomRotate(object): """Crops the given PIL.Image at a random location to have a region of the given size. size can be a tuple (target_height, target_width) or an integer, in which case the target will be of a square shape (size, size) """ """ 裁剪给定的PIL。图像位于随机位置,具有以下区域给定的尺寸。 size可以是元组(target_height,target_width) 或者是整数,在这种情况下,目标将是shape (size, size) """ #角度 def __init__(self, angle): self.angle = angle def __call__(self, image, label): #断言标签是空的 或 图像尺寸=标签尺寸 assert label is None or image.size == label.size #生成一个随机角度 = 随机整数-本身角度 angle = random.randint(0, self.angle * 2) - self.angle #旋转标签和图像 #label最近邻插值算法进行旋转 #img双线性插值算法进行旋转 label = label.rotate(angle, resample=Image.NEAREST) image = image.rotate(angle, resample=Image.BILINEAR) #返回图像和标签 return image, label
#找到车道线起始位置 #(行样本,开始车道线) def find_start_pos(row_sample,start_line): # row_sample = row_sample.sort() # for i,r in enumerate(row_sample): # if r >= start_line: # return i l,r = 0,len(row_sample)-1 #二分查找row_sample中start_line的位置 while True: mid = int((l+r)/2) #已经最后一位 if r - l == 1: return r if row_sample[mid] < start_line: l = mid if row_sample[mid] > start_line: r = mid if row_sample[mid] == start_line: return mid #随机LR偏移设置标签(左右) class RandomLROffsetLABEL(object): #最大偏移 def __init__(self,max_offset): self.max_offset = max_offset def __call__(self,img,label): #随机生成(-max_offset,max_offset)范围内整数 offset = np.random.randint(-self.max_offset,self.max_offset) w, h = img.size # 将图像对象转换为NumPy数组 img = np.array(img) #正向平移 #将图像向右平移 offset 像素,左侧空出区域填充黑色(RGB=0),右侧超界部分被截断。 if offset > 0: img[:,offset:,:] = img[:,0:w-offset,:] img[:,:offset,:] = 0 # 反向平移 # 将图像向左平移 offset 像素,右侧空出区域填充黑色(RGB=0),左侧超界部分被截断。 if offset < 0: real_offset = -offset img[:,0:w-real_offset,:] = img[:,real_offset:,:] img[:,w-real_offset:,:] = 0 #标签数据同上 label = np.array(label) if offset > 0: label[:,offset:] = label[:,0:w-offset] label[:,:offset] = 0 if offset < 0: offset = -offset label[:,0:w-offset] = label[:,offset:] label[:,w-offset:] = 0 #将NumPy数组转换为PIL图像,并返回 return Image.fromarray(img),Image.fromarray(label) #img[h,w,dim] #随机UD偏移设置标签(上下) class RandomUDoffsetLABEL(object): # 最大偏移 def __init__(self,max_offset): self.max_offset = max_offset def __call__(self,img,label): # 随机生成(-max_offset,max_offset)范围内整数 offset = np.random.randint(-self.max_offset,self.max_offset) w, h = img.size # 将图像对象转换为NumPy数组 img = np.array(img) # 正向平移 # 将图像向下平移 offset 像素,上侧空出区域填充黑色(RGB=0),下侧超界部分被截断。 if offset > 0: img[offset:,:,:] = img[0:h-offset,:,:] img[:offset,:,:] = 0 # 反向平移 # 将图像向上平移 offset 像素,下侧空出区域填充黑色(RGB=0),上侧超界部分被截断。 if offset < 0: real_offset = -offset img[0:h-real_offset,:,:] = img[real_offset:,:,:] img[h-real_offset:,:,:] = 0 # 标签数据同上 label = np.array(label) if offset > 0: label[offset:,:] = label[0:h-offset,:] label[:offset,:] = 0 if offset < 0: offset = -offset label[0:h-offset,:] = label[offset:,:] label[h-offset:,:] = 0 # 将NumPy数组转换为PIL图像,并返回 return Image.fromarray(img),Image.fromarray(label)
PS:
- Mask是一种操作,用于屏蔽或选择特定元素,常用于构建张量的过滤器。它相当于在原始张量上覆盖一层掩膜,从而屏蔽或选择一些特定元素;
- PIL:是用于图像处理的第三方库,提供丰富的图像操作功能;主要用于图像存储、格式转换、像素处理及批量操作等。
- NumPy数组:机器学习模型处理的数据通常是数值型的,而图像本身在计算机中是像素点的集合,每个像素有RGB值。NumPy数组可以高效地存储和操作这些数值数据,这对于处理大量的图像数据非常重要。另外,NumPy的数组操作非常高效,支持向量化运算,能够加速数据处理流程。
- PIL处理后的图像可以转换为NumPy数组,这样数据就可以无缝地输入到机器学习框架中,比如TensorFlow或PyTorch。这些框架通常接受张量输入,而NumPy数组可以很容易地转换为张量。此外,NumPy数组支持多维数组结构,这对于处理彩色图像(高度、宽度、通道)非常合适。且NumPy底层是用C实现的,处理大规模数据时速度更快,而PIL在处理图像时也进行了优化,两者的结合可以在不损失太多性能的前提下完成复杂的图像处理任务。
- 张量(Tensor)是一个多维数组,它是向量和矩阵的推广。
- 一个图像可以表示为一个三维张量,其中两个维度表示图像的宽度和高度,第三个维度表示颜色通道(如RGB)。文本数据可以表示为二维张量,其中一行表示一个单词,列表示单词在文本中的出现次数。
- 一个图像识别任务的输入可能是一个四维张量,其中包含批次大小、图像高度、图像宽度和颜色通道。
- 在卷积神经网络(CNN)中,卷积层处理的是二维输入(图像)和二维权重,这些都可以表示为张量。
3. 数据处理
本模块自定义函数进行数据图像的预处理任务;
导入上一节的数据转换模块
3.1 数据处理流程
- 加载样本数据;
- 对先验样本,行锚点进行排序;
- 从列表文件中读取图像和标签路径;
- 图像数据预处理;
- 延长车道线,并返回车道线标签列表;
- 进行网格划分,把图像划分为一个个网格,得到车道线标签位于网格中的坐标,并返回,用于分类任务;
- 是否需要额外对图像数据做处理,如不需要则返回处理后的样本和标签数据;
3.2 延长车道线
""" 从标注图像中提取车道线的坐标,并通过数据增强扩展车道线,以提供更丰富的训练样本。 主要是考虑到在实际应用中可能遇到不完整或部分遮挡的车道线。 """ def _get_index(self, label): w, h = label.size # 尺寸自适应处理 # 动态适配不同分辨率标注 if h != 288: # 锚点位置等比缩放 # 定义函数scale_f:将原始坐标 x 从基准分辨率(如 288 像素高度)按比例缩放到当前目标分辨率 h scale_f = lambda x : int((x * 1.0/288) * h) #row_anchor 存储车道线在图像中的垂直位置参考点(如预设的行索引) #将row_anchor缩放为sample_tmp sample_tmp = list(map(scale_f,self.row_anchor)) #初始化all_idx矩阵,结构是(num_lanes, len(sample_tmp), 2) all_idx = np.zeros((self.num_lanes,len(sample_tmp),2)) """ 遍历每个行锚点r,提取对应行的标注数据label_r。 对于每个车道线编号,寻找该行中车道线的位置,如果存在,则记录其x坐标的平均值;否则标记为-1。 收集每个车道线在指定行锚点处的横向位置,即x坐标。 """ for i,r in enumerate(sample_tmp): #r四舍五入转为int #将label转换为NumPy数组,并取出第int(round(r))行的数据。这样就能得到该行所有列的车道线标签信息。 label_r = np.asarray(label)[int(round(r))] #遍历该行 """ 对于每个车道线编号,寻找该行中车道线的位置, 如果存在,则记录其x坐标的平均值;否则标记为-1。 收集每个车道线在指定行锚点处的x坐标。 """ # 遍历每条车道线(如 self.num_lanes=4 表示最多检测4条车道线) for lane_idx in range(1, self.num_lanes + 1): #获取当前行描点位置 #在本行数据列表 label_r 中找到属于当前车道线 lane_idx 的像素位置 pos = np.where(label_r == lane_idx)[0] # 当前行锚位置无车道点 if len(pos) == 0: # 记录行锚索引 r all_idx[lane_idx - 1, i, 0] = r # 横向坐标标记为无效(-1) all_idx[lane_idx - 1, i, 1] = -1 continue # 取横向坐标的平均值 pos = np.mean(pos) all_idx[lane_idx - 1, i, 0] = r all_idx[lane_idx - 1, i, 1] = pos # data augmentation: extend the lane to the boundary of image #数据增强:将车道延伸到图像边界 #复制坐标位置 all_idx_cp = all_idx.copy() #遍历4根车道线 for i in range(self.num_lanes): #如果某个车道线在所有行锚点上的横向坐标都是-1(即无效),则跳过 if np.all(all_idx_cp[i,:,1] == -1): continue # if there is no lane #获取所有有效车道线索引 valid = all_idx_cp[i,:,1] != -1 # get all valid lane points' index # 获取所有有效的车道位置 valid_idx = all_idx_cp[i,valid,:] # get all valid lane points if valid_idx[-1,0] == all_idx_cp[0,-1,0]: # if the last valid lane point's y-coordinate is already the last y-coordinate of all rows # this means this lane has reached the bottom boundary of the image # so we skip # 如果最后一个有效车道点的y坐标已经是所有行的最后一个y坐标 # 这意味着该车道已到达图像的底部边界 # 所以跳过 continue # 如果车道太短,无法延伸 if len(valid_idx) < 6: continue # if the lane is too short to extend """ 使用后半部分的有效点进行线性拟合(np.polyfit,deg=1),得到斜率p。 然后找到起始位置start_line,即最后一个有效点的行索引, 接着确定从哪个行锚点开始需要延伸(pos)。 用拟合的直线方程计算这些行锚点的横向坐标fitted,并将超出图像宽度范围的值设为-1,其余更新到all_idx_cp中。 """ valid_idx_half = valid_idx[len(valid_idx) // 2:,:] #最小二乘法进行线性拟合直线 p = np.polyfit(valid_idx_half[:,0], valid_idx_half[:,1],deg = 1) #起始行start_line,即最后一个有效点的行索引 start_line = valid_idx_half[-1,0] #找到起始坐标 pos = find_start_pos(all_idx_cp[i,:,0],start_line) + 1 #得到代入方程后计算出的值 fitted = np.polyval(p,all_idx_cp[i,pos:,0]) #将超出图像宽度范围的值设为-1,其余更新到all_idx_cp中。 fitted = np.array([-1 if y < 0 or y > w-1 else y for y in fitted]) #断言该点之后都不存在车道线 assert np.all(all_idx_cp[i,pos:,1] == -1) #设置坐标 all_idx_cp[i,pos:,1] = fitted #如果还是存在-1,则进行调试 if -1 in all_idx[:, :, 0]: pdb.set_trace() return all_idx_cp
3.3 网格划分
""" 进行网格划分 将连续的车道线位置离散化为网格中的点,用于模型训练。 特别是多项式拟合的部分,可能是为了扩展车道线到图像边界,增强数据。 _grid_pts方法的主要任务是将连续的车道线坐标转换为离散的网格分类标签。 将检测问题转化为分类问题,每个网格代表一个可能的车道线位置。 """ #pts是原始的车道线点坐标,来自标注数据。num_cols是横向网格数量(多少列),w是图像的宽度。 def _grid_pts(self, pts, num_cols, w): # pts : numlane,n,2 # 分割形状:多个车道线,每个车道线有n个点,每个点有2个坐标(x,y) num_lane, n, n2 = pts.shape #依照定义间隔生成均匀分布的数值序列 #起始0,结束w-1,w/num_cols为间隔,共num_cols个数据 #生成一个从0到w-1的等差数列col_sample,数量为num_cols。 #生成网格采样点:使用np.linspace生成横向的网格分界点,均匀分布在图像宽度范围内。 col_sample = np.linspace(0, w - 1, num_cols) assert n2 == 2 #创建一个形状为(n, num_lane)的零矩阵to_pts #to_pts的形状为(n, num_lane),n对应纵向的锚点数量,每个位置存储该车道线在该纵向位置的横向网格索引。 #例如to_pts = [18*4],代表4条车道线,每条车道线里有18个点 to_pts = np.zeros((n, num_lane)) """ 循环处理每个车道线,取每个点的y坐标(pti = pts[i, :, 1]), 然后根据col_sample的间隔(固定的间隔)将每个y值分配到对应的网格中,如果pt为-1则标记为num_cols, 最后返回转换为整型的to_pts。 """ for i in range(num_lane): # 遍历每个车道线:对于每个车道线,提取所有点的y坐标(pti) # 4条车道线 pti = pts[i, :, 1] #将输入转换为NumPy数组,同时保持数据的原始结构和数据类型 # 计算网格索引:对于每个车道线的y坐标,计算其属于哪个网格。 # 网格宽度由col_sample的间隔(col_sample[1] - col_sample[0])决定。 # -1时,表示该位置无车道线,标记为num_cols,作为不存在车道线处理。 #矩阵中的每一列 = 原来的y坐标 // 网格高度 = 车道线所在的网格位置 to_pts[:, i] = np.asarray( [int(pt // (col_sample[1] - col_sample[0])) if pt != -1 else num_cols for pt in pti]) #转换为整数类型的矩阵,作为分类标签。 #即车道线所在网格标签 return to_pts.astype(int)
4. 数据读取
使用PyTorch框架进行数据处理
张量的作用:PyTorch中的张量(Tensor)是进行深度学习和其他数值计算的核心数据结构。它们类似于NumPy中的数组,但提供了GPU加速的额外功能,使得它们在进行大规模数值计算时更加高效。
对张量进行标准化处理的作用:减少不同特征间的尺度差异;加速模型训练速度;加速模型训练速度,收敛速度更快;提高模型性能;增强算法稳定性;防止数值溢出;
4.1 创建训练数据加载器:获取训练数据
""" 负责配置数据集的数据预处理、增强,并根据数据集类型和是否分布式训练来创建对应的DataLoader,返回训练数据加载器和每个车道的分类数目。 """ #创建训练数据加载器:获取训练数据 def get_train_loader(batch_size, data_root, griding_num, dataset, use_aux, distributed): #transforms.Compose():将多个图像变换操作组成一个序列,从而简化图像预处理流水线。 #将分割掩码缩放到288×800像素,并转为张量 target_transform = transforms.Compose([ mytransforms.FreeScaleMask((288, 800)), mytransforms.MaskToTensor(), ]) #生成36×100的小尺寸分割掩码(可能用于辅助损失计算),并转为张量 segment_transform = transforms.Compose([ mytransforms.FreeScaleMask((36, 100)), mytransforms.MaskToTensor(), ]) # 统一图像尺寸为288×800 # 转换为张量格式 # 应用ImageNet标准归一化(均值0.485/0.456/0.406,方差0.229/0.224/0.225) img_transform = transforms.Compose([ transforms.Resize((288, 800)), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)), ]) """ 组合三种数据增强: ▪ 随机±6度旋转 ▪ 垂直方向随机偏移(最大100像素) ▪ 水平方向随机偏移(最大200像素) """ simu_transform = mytransforms.Compose2([ mytransforms.RandomRotate(6), mytransforms.RandomUDoffsetLABEL(100), mytransforms.RandomLROffsetLABEL(200) ]) """ 根据dataset参数创建不同数据集实例: CULane数据集: ▪ 使用culane_row_anchor定义行锚点 ▪ 每个车道线划分18个分类区间(cls_num_per_lane=18) Tusimple数据集: ▪ 使用tusimple_row_anchor ▪ 划分56个分类区间(cls_num_per_lane=56) 异常情况抛出未实现错误 """ if dataset == 'CULane': train_dataset = LaneClsDataset(data_root, os.path.join(data_root, 'list/train_gt.txt'), img_transform=img_transform, target_transform=target_transform, simu_transform = simu_transform, segment_transform=segment_transform, row_anchor = culane_row_anchor, griding_num=griding_num, use_aux=use_aux) #每条车道线18个点 cls_num_per_lane = 18 elif dataset == 'Tusimple': train_dataset = LaneClsDataset(data_root, os.path.join(data_root, 'train_gt.txt'), img_transform=img_transform, target_transform=target_transform, simu_transform = simu_transform, griding_num=griding_num, row_anchor = tusimple_row_anchor, segment_transform=segment_transform,use_aux=use_aux) # 每条车道线18个点 cls_num_per_lane = 56 else: raise NotImplementedError #根据distributed标志选择采样器 if distributed: #分布式模式使用DistributedSampler,实现多GPU数据划分 sampler = torch.utils.data.distributed.DistributedSampler(train_dataset) else: # 单机模式使用RandomSampler实现常规随机采样 sampler = torch.utils.data.RandomSampler(train_dataset) """ 创建DataLoader时配置: batch_size:控制每批数据量 sampler:使用上一步选择的采样器 num_workers=4:启用4个子进程加速数据加载1 未显式设置shuffle参数,因采样器已包含随机逻辑 """ train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, sampler = sampler, num_workers=4) #train_loader是经过预处理和增强的迭代器,cls_num_per_lane参数供后续模型定义使用(如输出层维度设定) return train_loader, cls_num_per_lane
4.2 构建测试阶段的数据加载器
""" 构建并返回适用于车道线检测任务测试阶段的数据加载器(DataLoader) batch_size 控制每次迭代返回的样本数 16, 32 data_root 数据集根目录路径 '/data/culane' dataset 指定数据集类型 'CULane', 'Tusimple' distributed 是否启用分布式训练 True, False """ def get_test_loader(batch_size, data_root,dataset, distributed): """ 使用transforms.Compose定义图像预处理操作: Resize((288, 800)):将输入图像统一缩放至指定尺寸(288×800),以适应模型输入要求12; ToTensor():将图像转换为PyTorch张量格式15; Normalize:对张量进行标准化处理(基于ImageNet均值和标准差) """ img_transforms = transforms.Compose([ transforms.Resize((288, 800)), transforms.ToTensor(), transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)), ]) #根据dataset参数选择不同车道线数据集(CULane或Tusimple),并实例化对应的LaneTestDataset测试集对象 #cls_num_per_lane参数根据不同数据集的标注规则设定(如CULane为18,Tusimple为56) if dataset == 'CULane': test_dataset = LaneTestDataset(data_root,os.path.join(data_root, 'list/test.txt'),img_transform = img_transforms) cls_num_per_lane = 18 elif dataset == 'Tusimple': test_dataset = LaneTestDataset(data_root,os.path.join(data_root, 'test.txt'), img_transform = img_transforms) cls_num_per_lane = 56 if distributed: # 若distributed=True,使用SeqDistributedSampler确保多进程分布式训练中数据分片不重叠且顺序固定 sampler = SeqDistributedSampler(test_dataset, shuffle = False) else: #若distributed=False,采用SequentialSampler按原始顺序加载数据 sampler = torch.utils.data.SequentialSampler(test_dataset) #封装数据集、采样器及参数(如batch_size、num_workers=4)生成最终数据加载器,支持批量读取和多线程加速 loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, sampler = sampler, num_workers=4) return loader
PS:
- transforms.Compose():将多个图像变换操作组成一个序列,从而简化图像预处理流水线;
- 张量:PyTorch中的张量(Tensor)是进行深度学习和其他数值计算的核心数据结构。它们类似于NumPy中的数组,但提供了GPU加速的额外功能,使得它们在进行大规模数值计算时更加高效,并且张量是构建和操作神经网络的基本元素。
七、网络结构模块
1. 残差网络(Resnet)
ResNet(Residual Network,残差网络)是深度学习领域中非常重要且具有影响力的一种卷积神经网络(CNN)架构。
backbone.py文件主要用于构建一个18层的resnet网络作为主干网络;
""" 继承Pytorch中的ResNet网络 重写初始化和前向传播函数 改变网络结构,去除全连接层 主要为了实现:多任务特征提取(如检测、分割) """ class resnet(torch.nn.Module): #通过 layers 参数动态加载 ResNet 变体 #使用 pretrained=True 加载 ImageNet 预训练权重,增强特征泛化能力 def __init__(self,layers,pretrained = False): super(resnet,self).__init__() if layers == '18': model = torchvision.models.resnet18(pretrained=pretrained) elif layers == '34': model = torchvision.models.resnet34(pretrained=pretrained) elif layers == '50': model = torchvision.models.resnet50(pretrained=pretrained) elif layers == '101': model = torchvision.models.resnet101(pretrained=pretrained) elif layers == '152': model = torchvision.models.resnet152(pretrained=pretrained) elif layers == '50next': model = torchvision.models.resnext50_32x4d(pretrained=pretrained) elif layers == '101next': model = torchvision.models.resnext101_32x8d(pretrained=pretrained) elif layers == '50wide': model = torchvision.models.wide_resnet50_2(pretrained=pretrained) elif layers == '101wide': model = torchvision.models.wide_resnet101_2(pretrained=pretrained) else: raise NotImplementedError self.conv1 = model.conv1 self.bn1 = model.bn1 self.relu = model.relu self.maxpool = model.maxpool self.layer1 = model.layer1 self.layer2 = model.layer2 self.layer3 = model.layer3 self.layer4 = model.layer4 """ 保留原始 ResNet 的 conv1、bn1、relu、maxpool 及前 4 个残差块(layer1~layer4),去除全连接层,仅作为特征提取器 前向传播返回layer2、layer3、layer4的输出(x2、x3、x4),提供多尺度特征图 即输入 x → conv1(卷积) → bn1(归一化) → ReLU(激活函数) → maxpool(最大池化) → layer1 → layer2 → layer3 → layer4 → 输出 (x2, x3, x4) """ def forward(self,x): x = self.conv1(x) x = self.bn1(x) x = self.relu(x) x = self.maxpool(x) x = self.layer1(x) x2 = self.layer2(x) x3 = self.layer3(x2) x4 = self.layer4(x3) return x2,x3,x4
2. 定义网络结构
model.py主要定义网络结构
实现了一个用于车道线检测的神经网络模型,结合了分类任务和辅助分割任务。
-
conv_bn_relu模块:
-
结构:Conv2d + BatchNorm2d + ReLU激活函数
-
作用:实现了一个标准卷积块,用于特征提取。通过组合卷积、批归一化和激活函数,提升模型训练稳定性和特征表达能力。
-
-
parsingNet主网络结构:
-
Backbone:使用ResNet作为特征提取主干网络(支持不同深度如18/34/50),输出三个层次的特征(x2, x3, fea)。
-
输入尺寸:默认288x800,经过ResNet下采样32倍后,最终特征图尺寸为9x25(假设ResNet输出层为1/32下采样)。
-
输出维度:cls_dim=(37,10,4),对应网格划分数、每个车道的行锚点数和车道数。总维度37104=1480。
-
-
辅助分割分支(use_aux):
-
结构:处理ResNet中间特征(x2, x3, fea),通过上采样和拼接融合多尺度特征。
-
输出:通道数为cls_dim[-1]+1(4车道+背景),用于像素级车道分割,辅助主任务训练。
-
-
主分类分支:
-
特征处理:通过1x1卷积降维(2048/512→8通道),展平后输入全连接层。
-
分类层:线性层将1800维特征映射到总维度(1480),最终reshape为(37,10,4)结构。
-
-
初始化方法:
-
卷积层使用He初始化(kaiming_normal),线性层使用小标准差正态分布,BN层初始化为单位权重。
-
确保各层参数合理初始化,加速模型收敛。
-
-
前向传播流程:
-
提取ResNet多级特征 → 辅助分支融合多尺度特征 → 主分支降维分类。
-
输出车道参数预测(group_cls)和可选的分割结果(aux_seg)。
-
-
设计特点:
-
多任务学习:通过辅助分割任务提升特征质量,增强模型鲁棒性。
-
多尺度融合:利用不同层次特征(空间细节+语义信息)提升检测精度。
-
参数化输出:将车道检测建模为网格化分类问题,降低回归难度。
-
输出结果分析
- 201——200个位置+1个不存在车道线
- 18——18行
- 4——4条车道线
""" Conv2d + BatchNorm2d + ReLU激活函数,用作基础卷积单元 实现了一个标准卷积块,用于特征提取。通过组合卷积、批归一化和激活函数,提升模型训练稳定性和特征表达能力。 """ class conv_bn_relu(torch.nn.Module): def __init__(self,in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1,bias=False): super(conv_bn_relu,self).__init__() #输入通道、输出通道、卷积核大小 #bias=False:由于后续接BN层,可省略卷积偏置参数以提高计算效率 #dilation:支持空洞卷积,可控制卷积核的间隔采样 self.conv = torch.nn.Conv2d(in_channels,out_channels, kernel_size, stride = stride, padding = padding, dilation = dilation,bias = bias) self.bn = torch.nn.BatchNorm2d(out_channels) self.relu = torch.nn.ReLU() #严格执行卷积→归一化→激活的顺序,避免梯度异常 def forward(self,x): x = self.conv(x) x = self.bn(x) x = self.relu(x) return x
""" parsingNet主网络结构: Backbone:使用ResNet作为特征提取主干网络(支持不同深度如18/34/50),输出三个层次的特征(x2, x3, fea)。 输入尺寸:默认288x800,经过ResNet下采样32倍后,最终特征图尺寸为9x25(假设ResNet输出层为1/32下采样)。 输出维度:cls_dim=(37,10,4),对应网格划分数、每个车道的行锚点数和车道数。总维度37*10*4=1480。 主干网络:基于ResNet提取多尺度特征(如x2, x3, fea),支持不同深度(如backbone='50')和预训练权重加载45。 辅助分支(use_aux=True时启用):通过多层级特征融合实现分割任务,提升模型鲁棒性45。 分类分支:将主干特征映射为车道线预测结果,输出维度由cls_dim定义 """ class parsingNet(torch.nn.Module): """ size;输入图像分辨率(宽×高),用于计算特征图尺寸和上采样比例,如288×800常用于自动驾驶场景。 pretrained:控制是否加载ResNet预训练权重,利用迁移学习加速收敛。 backbone:选择ResNet变体(如'50'、'34'),影响特征图通道数(如backbone='34'时使用较小通道数)。 cls_dim:定义分类维度:(num_gridding, num_cls_per_lane, num_of_lanes),对应车道线预测的网格化编码结构。 use_aux:是否启用辅助分割分支,通过多任务学习提升模型性能 """ def __init__(self, size=(288, 800), pretrained=True, backbone='50', cls_dim=(37, 10, 4), use_aux=False): super(parsingNet, self).__init__() self.size = size self.w = size[0] self.h = size[1] self.cls_dim = cls_dim # (num_gridding, num_cls_per_lane, num_of_lanes) # num_cls_per_lane is the number of row anchors self.use_aux = use_aux self.total_dim = np.prod(cls_dim) # input : nchw, # output: (w+1) * sample_rows * 4 #主干网络:调用resnet()函数构建ResNet,输出多尺度特征x2, x3, fea,用于辅助分支和分类分支的特征提取 self.model = resnet(backbone, pretrained=pretrained) """ 辅助分割分支(use_aux): 结构:处理ResNet中间特征(x2, x3, fea),通过上采样和拼接融合多尺度特征。 输出:通道数为cls_dim[-1]+1(4车道+背景),用于像素级车道分割,辅助主任务训练。 """ if self.use_aux: #层级特征融合:通过aux_header2、aux_header3、aux_header4处理不同层级的特征图,并上采样后拼接(torch.cat) self.aux_header2 = torch.nn.Sequential( conv_bn_relu(128, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(512, 128, kernel_size=3, stride=1, padding=1), conv_bn_relu(128,128,3,padding=1), conv_bn_relu(128,128,3,padding=1), conv_bn_relu(128,128,3,padding=1), ) self.aux_header3 = torch.nn.Sequential( conv_bn_relu(256, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(1024, 128, kernel_size=3, stride=1, padding=1), conv_bn_relu(128,128,3,padding=1), conv_bn_relu(128,128,3,padding=1), ) self.aux_header4 = torch.nn.Sequential( conv_bn_relu(512, 128, kernel_size=3, stride=1, padding=1) if backbone in ['34','18'] else conv_bn_relu(2048, 128, kernel_size=3, stride=1, padding=1), conv_bn_relu(128,128,3,padding=1), ) #空洞卷积:aux_combine使用dilation=2和dilation=4扩大感受野,增强分割精度 self.aux_combine = torch.nn.Sequential( conv_bn_relu(384, 256, 3,padding=2,dilation=2), conv_bn_relu(256, 128, 3,padding=2,dilation=2), conv_bn_relu(128, 128, 3,padding=2,dilation=2), conv_bn_relu(128, 128, 3,padding=4,dilation=4), #输出通道:cls_dim[-1] + 1,对应车道线类别数及背景类 torch.nn.Conv2d(128, cls_dim[-1] + 1,1) # output : n, num_of_lanes+1, h, w ) initialize_weights(self.aux_header2,self.aux_header3,self.aux_header4,self.aux_combine) """ 分类分支(self.cls) 特征池化:self.pool通过1×1卷积压缩通道数(如2048→8),减少计算量。 全连接层:将特征扁平化后映射到total_dim,最终输出形状为cls_dim的多维预测结果 """ self.cls = torch.nn.Sequential( torch.nn.Linear(1800, 2048), torch.nn.ReLU(), torch.nn.Linear(2048, self.total_dim), ) self.pool = torch.nn.Conv2d(512,8,1) if backbone in ['34','18'] else torch.nn.Conv2d(2048,8,1) # 1/32,2048 channel # 288,800 -> 9,40,2048 # (w+1) * sample_rows * 4 # 37 * 10 * 4 initialize_weights(self.cls) def forward(self, x): # n c h w - > n 2048 sh sw # -> n 2048 # 主干网络提取特征 x2,x3,fea = self.model(x) ## 辅助分支处理 if self.use_aux: x2 = self.aux_header2(x2) x3 = self.aux_header3(x3) x3 = torch.nn.functional.interpolate(x3,scale_factor = 2,mode='bilinear') x4 = self.aux_header4(fea) x4 = torch.nn.functional.interpolate(x4,scale_factor = 4,mode='bilinear') aux_seg = torch.cat([x2,x3,x4],dim=1) aux_seg = self.aux_combine(aux_seg) else: aux_seg = None ## 主分支分类 fea = self.pool(fea).view(-1, 1800) group_cls = self.cls(fea).view(-1, *self.cls_dim) if self.use_aux: return group_cls, aux_seg return group_cls
3. 预训练权重(权重初始化模块)
预训练权重,顾名思义,就是预先训练好的模型参数。在深度学习中,模型的参数通常以权重矩阵和偏置向量的形式存在,这些参数是通过反向传播算法从大量的训练数据中学习得到的。预训练权重则是在大规模数据集(如ImageNet、COCO等)上,经过长时间、高强度的训练后,得到的一组最优或接近最优的模型参数。
作用:
-
提高模型效果:预训练模型通常在大规模数据集上进行了充分的训练,已经学习到了许多有用的特征和表示。将这些预训练权重作为新任务的初始值,可以帮助模型更快地收敛到更好的性能,从而提升模型的效果。
-
缩短训练时间:利用预训练权重可以大大减少新任务训练所需的时间和计算资源。因为预训练模型已经具备了一定的泛化能力,所以在新任务上只需进行微调(Fine-Tuning)即可,无需从头开始训练。
-
改善泛化能力:预训练模型通常具有更强的泛化能力,可以更好地迁移到新的任务或数据集上。这意味着,即使在新任务的数据量有限的情况下,预训练权重也能帮助模型获得较好的表现。
-
解决数据不足问题:当训练数据有限时,利用预训练权重可以有效弥补数据不足的缺陷。通过在新任务上微调预训练模型,可以充分利用预训练模型学习到的知识和经验,提高模型在小数据集上的表现。
权重初始化:
初始化方法选择
- 卷积层:采用Kaiming初始化,适配ReLU激活函数的非线性特性,缓解梯度消失问题12。
- BN层:权重初始化为1,偏置为0,维持输入分布稳定性,避免训练初期数值震荡23。
- 全连接层:使用小标准差正态分布,平衡参数多样性和梯度传播效率23。
def initialize_weights(*models): for model in models: real_init_weights(model) """ nn.Conv2d(卷积层) kaiming_normal_ + constant_ 使用ReLU增益模式,偏置置零 nn.Linear(线性层) normal_(mean=0, std=0.01) 小标准差防止激活值爆炸 nn.BatchNorm2d(批量标准化层) constant_(weight=1, bias=0) 保持初始分布稳定 """ def real_init_weights(m): if isinstance(m, list): for mini_m in m: real_init_weights(mini_m) else: if isinstance(m, torch.nn.Conv2d): torch.nn.init.kaiming_normal_(m.weight, nonlinearity='relu') if m.bias is not None: torch.nn.init.constant_(m.bias, 0) elif isinstance(m, torch.nn.Linear): m.weight.data.normal_(0.0, std=0.01) elif isinstance(m, torch.nn.BatchNorm2d): torch.nn.init.constant_(m.weight, 1) torch.nn.init.constant_(m.bias, 0) elif isinstance(m,torch.nn.Module): for mini_m in m.children(): real_init_weights(mini_m) else: print('unkonwn module', m)
4. 1*1卷积
通道维度调整(降维/升维)
- 作用:通过改变输出通道数(即滤波器的数量),1×1卷积可以灵活调整特征图的通道维度。
- 示例:
- 降维:若输入为 [H, W, 256],使用64个1×1卷积核,输出变为 [H, W, 64],显著减少后续计算量(如Inception模块)。
- 升维:增加通道数以提取更复杂的特征组合。
- 优势:减少参数和计算量,提升模型效率。
跨通道信息交互
- 作用:1×1卷积将输入的所有通道进行线性组合,学习不同通道间的关联性。
- 示例:RGB图像(3通道)经1×1卷积生成新通道,每个新通道是原始3通道的加权组合。
- 意义:增强特征表达能力,促进多通道信息的融合。
引入非线性
- 操作:1×1卷积后通常接激活函数(如ReLU)。
- 效果:在保持空间尺寸不变的情况下,增加网络的非线性拟合能力。
- 对比:若不用1×1卷积,单纯通道线性组合无法引入非线性。
残差连接的通道对齐
- 问题:在残差网络(ResNet)中,输入和输出的通道数可能不一致,无法直接相加。
- 解决:使用1×1卷积调整通道数,使残差分支与主分支通道匹配。
- 示例:输入为64通道,残差块输出128通道,则1×1卷积将64→128通道。
数学视角
- 输入:尺寸为
[H, W, C_in]
的特征图。 - 1×1卷积核:每个核的尺寸为
[1, 1, C_in]
,共C_out
个核。 - 输出:尺寸为
[H, W, C_out]
,每个输出通道是输入通道的线性组合:
1×1卷积的核心价值在于高效调整通道维度、融合跨通道信息并引入非线性,同时保持空间结构不变。它是现代CNN中实现轻量化、模块化设计的关键组件之一。
例如,选择2个1x1大小的卷积核,特征图的深度可以从3变成2;如果使用4个1x1的卷积核,特征图的深度可以从3变成4。
5. 空洞卷积
空洞卷积与普通卷积的主要区别在于引入了“扩张率”这一参数。扩张率定义了卷积核处理数据时各值之间的间距。当扩张率为1时,空洞卷积退化为普通卷积;当扩张率大于1时,卷积核的元素之间会按照扩张率所指定的间隔进行采样,从而扩大感受野。
优势:
- 扩大感受野:在不增加参数数量的情况下,空洞卷积可以扩大感受野,有助于捕捉更广泛的上下文信息。
- 保持分辨率:在语义分割等任务中,空洞卷积可以在保持特征图分辨率的同时增大感受野,减少信息损失。
局限性:
- 网格效应:当扩张率过大时,可能会出现“网格效应”,导致一些像素点被重复采样,影响模型的性能。
- 训练难度:空洞卷积的参数设置对模型性能有较大影响,需要仔细调整扩张率以获得最佳效果。
八、损失函数模块
1. OhemCELoss(在线难例挖掘交叉熵损失)
功能:筛选训练困难样本,提升模型对难例的学习能力
实现要点:
-
阈值转换:将概率阈值转换为对数空间(
-log(thresh)
),与交叉熵的数学形式对齐 -
样本筛选:
-
计算逐像素交叉熵损失(
reduction='none'
) -
按损失值降序排序,保留前
n_min
个困难样本 -
动态阈值过滤:仅保留损失超过阈值的样本
-
-
应用场景:解决类别不平衡问题,特别适用于车道线像素占比小的场景
数学表达式:
其中 SS 为筛选后的困难样本集合
class OhemCELoss(nn.Module): """ thresh:概率阈值,用于过滤低损失样本(如thresh=0.7表示仅保留预测概率低于30%的样本)。 n_min:保证每次至少选择n_min个样本参与反向传播,防止有效样本不足。 ignore_lb:忽略特定标签(如255常用于语义分割的掩码背景) """ def __init__(self, thresh, n_min, ignore_lb=255, *args, **kwargs): super(OhemCELoss, self).__init__() self.thresh = -torch.log(torch.tensor(thresh, dtype=torch.float)).cuda() self.n_min = n_min self.ignore_lb = ignore_lb self.criteria = nn.CrossEntropyLoss(ignore_index=ignore_lb, reduction='none') """ 损失计算:使用CrossEntropyLoss逐像素计算未降维损失,保留每个样本的独立损失值。 样本筛选: 按损失值降序排序,优先选择预测误差大的样本。 动态判断阈值thresh与第n_min个样本损失的关系,决定保留样本范围。 梯度回传:仅对筛选后的高损失样本计算梯度均值,忽略简单样本的影响 """ def forward(self, logits, labels): N, C, H, W = logits.size() loss = self.criteria(logits, labels).view(-1) loss, _ = torch.sort(loss, descending=True) if loss[self.n_min] > self.thresh: loss = loss[loss>self.thresh] else: loss = loss[:self.n_min] return torch.mean(loss)
2、SoftmaxFocalLoss(焦点损失)
FocalLoss:Focal Loss 是一种处理类别不平衡问题的有效方法,通过引入焦点因子和调整样本权重,使得模型对难以分类的样本更加关注,从而提高分类性能。它特别适用于目标检测和其他类别不平衡的任务。
SoftmaxFocalLoss功能:降低易分类样本权重,聚焦困难样本
实现特点:
-
调制因子:引入 (1−p)γ(1−p)γ 动态缩放因子,随置信度增加衰减损失贡献
-
计算流程:
-
计算softmax概率 → 生成调制因子 → 调整log概率 → NLLLoss计算
-
-
优势:缓解极端类别不平衡(如背景vs车道像素),增强模型对困难边缘区域的关注
数学形式:
""" 结合 Softmax 和 Focal Loss 的自定义损失函数类,主要用于处理分类任务中的类别不均衡与难易样本不均衡问题。其核心设计通过调整难样本的权重增强模型对困难样本的学习能力 gamma:控制难样本的权重,值越大对困难样本的关注度越高(如gamma=2时显著抑制简单样本的梯度贡献) ignore_lb:忽略指定标签(如语义分割中的背景类),避免无效区域干扰训练 """ class SoftmaxFocalLoss(nn.Module): def __init__(self, gamma, ignore_lb=255, *args, **kwargs): super(SoftmaxFocalLoss, self).__init__() self.gamma = gamma self.nll = nn.NLLLoss(ignore_index=ignore_lb) def forward(self, logits, labels): scores = F.softmax(logits, dim=1) # 计算类别概率分布 factor = torch.pow(1.-scores, self.gamma) # 生成调制因子 (1 - p)^gamma log_score = F.log_softmax(logits, dim=1) # 计算对数概率 log_score = factor * log_score # 应用Focal Loss调制 loss = self.nll(log_score, labels.long()) # 计算NLL损失 return loss
3、ParsingRelationLoss(解析关系损失)
功能:约束车道线的纵向连续性
实现机制:
-
相邻行差异:计算特征图相邻行预测结果的L1差异
-
平滑约束:使用smooth L1 loss强制相邻行输出相似
-
物理意义:确保车道线在垂直方向的连续性,避免断裂
计算公式:
""" 实现了一种空间关系约束损失函数,通过强制相邻行(或空间维度)的预测结果保持连续性,优化模型在结构化预测任务(如车道线检测、语义分割)中的输出一致性 """ class ParsingRelationLoss(nn.Module): def __init__(self): super(ParsingRelationLoss, self).__init__() def forward(self,logits): n,c,h,w = logits.shape loss_all = [] #遍历高度方向(假设为垂直维度),计算相邻两行(i与i+1)的预测差异 for i in range(0,h-1): loss_all.append(logits[:,:,i,:] - logits[:,:,i+1,:]) #loss0 : n,c,w loss = torch.cat(loss_all) #使用平滑L1损失函数,强制相邻行预测差异趋近于零,实现空间连续性约束 return torch.nn.functional.smooth_l1_loss(loss,torch.zeros_like(loss))
4、ParsingRelationDis(解析关系距离损失)
功能:保持车道线位置变化的平滑性
创新设计:
-
位置编码:通过softmax期望计算行位置坐标
-
生成embedding向量表示类别位置
-
pos = sum(softmax(x) * embedding)
实现概率到连续坐标的转换
-
-
差异一致性:计算相邻行位置差,强制相邻差异保持相似
-
实现细节:
-
仅处理前50%行(假设车道主要出现在图像下半部分)
-
使用L1 Loss约束相邻差异的一致性
-
数学表达:
""" 实现了一种结构化关系判别损失,通过约束相邻行(或空间维度)的位置预测差异的平滑性,优化模型在结构化预测任务(如车道线检测、表格结构分析)中的输出稳定性 """ class ParsingRelationDis(nn.Module): def __init__(self): super(ParsingRelationDis, self).__init__() #使用L1Loss(平均绝对误差)计算差异损失,相比MSELoss对异常值更鲁棒 self.l1 = torch.nn.L1Loss() # self.l1 = torch.nn.MSELoss() def forward(self, x): #输入处理与概率归一化(Softmax归一化) n,dim,num_rows,num_cols = x.shape x = torch.nn.functional.softmax(x[:,:dim-1,:,:],dim=1) #位置编码与位置预测 embedding = torch.Tensor(np.arange(dim-1)).float().to(x.device).view(1,-1,1,1) pos = torch.sum(x*embedding,dim = 1) #相邻行差异计算 diff_list1 = [] for i in range(0,num_rows // 2): diff_list1.append(pos[:,i,:] - pos[:,i+1,:]) #平滑性损失计算 loss = 0 #通过L1Loss强制相邻差异值趋近,实现二阶平滑性(相邻行差异的变化幅度一致) for i in range(len(diff_list1)-1): loss += self.l1(diff_list1[i],diff_list1[i+1]) #损失归一化:对损失值取平均,确保不同行数的输入具有可比性 loss /= len(diff_list1) - 1 return loss
5、组合应用分析
典型训练策略:
total_loss = (main_loss + λ1*relation_loss + λ2*distance_loss)
-
主损失:OhemCELoss 或 SoftmaxFocalLoss
-
正则项:RelationLoss 约束局部连续性,DistanceLoss 保证全局平滑性
设计优势:
-
多目标优化:分类精度+空间连续性联合优化
-
物理约束:将车道线的先验知识(连续、平滑)编码到损失函数
-
难例挖掘:提升对模糊边界、遮挡场景的鲁棒性
整体损失可以写成:
其中 Lseg 是分割损失,α 和 β 是损失系数。
八、模型训练模块(Train)
1、代码结构概览
训练流程:
-
主控制流:初始化配置 → 准备数据 → 构建模型 → 训练循环 → 模型保存
-
核心模块:
-
train()
:训练流程控制 -
inference()
:前向计算 -
calc_loss()
:多任务损失计算 -
resolve_val_data()
:结果后处理
-
2、关键模块解析
2.1. 分布式训练初始化
if distributed: torch.cuda.set_device(args.local_rank) torch.distributed.init_process_group(...) net = DistributedDataParallel(net) # 多卡训练封装
-
技术要点:
-
使用NCCL后端初始化进程组
-
通过local_rank指定设备索引
-
采用数据并行策略
-
2.2. 数据加载与模型构建
train_loader = get_train_loader(...) # 定制数据加载 net = parsingNet(...) # 构建车道线解析网络
-
实现特点:
-
支持多种backbone(ResNet系列)
-
动态调整输入尺寸(cfg.griding_num)
-
可选辅助分割分支(use_aux)
-
2.3. 训练循环控制
for epoch in range(...): train(...) # 单epoch训练 save_model(...) # 模型保存
-
迭代控制:
-
典型epoch式训练
-
支持断点续训(resume机制)
-
学习率动态调整(scheduler)
-
3、训练流程分解
3.1. 训练模式初始化
net.train() # 设置模型为训练模式 progress_bar = dist_tqdm(train_loader) # 分布式进度条 t_data_0 = time.time() # 数据加载计时起点
关键点:
-
dist_tqdm
支持多GPU训练时的同步进度显示 -
计时变量用于性能监控
3.2. 批次训练循环
for b_idx, data_label in enumerate(progress_bar): # 数据加载时间计算 t_data_1 = time.time() reset_metrics(metric_dict) # 重置指标计算器 global_step = epoch * len(data_loader) + b_idx # 全局迭代次数
设计特点:
-
global_step
的跨epoch累计便于学习率调度 -
指标重置确保各批次独立统计
3.3. 前向推理阶段
t_net_0 = time.time()
results = inference(net, data_label, use_aux)
-
数据流:
graph LR A[原始图像] --> B[Backbone特征提取] B --> C[主分类头] B --> D{使用辅助分支?} D -- Yes --> E[分割头] C --> F[cls_out] E --> G[seg_out]
3.4. 损失计算阶段
loss = calc_loss(loss_dict, results, ...)
-
多任务损失组合:
损失类型 权重系数 计算方式 分类损失(OhemCE) λ1 在线难例挖掘 分割损失(Focal) λ2 焦点损失 关系损失 λ3 相邻行连续性约束 距离损失 λ4 位置平滑性约束
3.5. 反向传播优化
optimizer.zero_grad()
loss.backward() # 反向传播
optimizer.step() # 参数更新
scheduler.step(global_step) # 学习率调整
-
优化策略:
- 标准PyTorch梯度更新流程
- 按迭代次数调整学习率(需确认scheduler是否支持)
3.6. 指标计算与日志记录
results = resolve_val_data(results, use_aux) # 结果解析 update_metrics(metric_dict, results) # 更新指标 if global_step % 20 == 0: # 稀疏日志记录 for me_name, me_op in zip(...): logger.add_scalar('metric/'+me_name, me_op.get()) logger.add_scalar('meta/lr', ...) # 学习率记录
日志监控项:
日志类别 | 记录内容 | 记录频率 |
---|---|---|
loss/* | 各损失项数值 | 每20 step |
metric/* | 评估指标(acc/IoU等) | 每20 step |
meta/lr | 当前学习率 | 每个step |
3.7. 进度条更新
progress_bar.set_postfix( loss='%.3f'%float(loss), data_time='%.3f'%float(t_data_1 - t_data_0), # 数据加载耗时 net_time='%.3f'%float(t_net_1 - t_net_0), # 网络计算耗时 **kwargs # 各指标当前值 ) t_data_0 = time.time() # 重置计时器
性能监控要素:
-
数据时间:反映数据加载效率(I/O瓶颈)
-
网络时间:反映模型计算效率(GPU利用率)
-
指标实时显示:帮助快速判断收敛情况
九、模型测试模块(Test)
1. 模块功能概览
-
核心流程:环境配置 → 模型加载 → 分布式初始化 → 执行评估
-
设计目标:
-
支持多GPU分布式推理
-
兼容不同数据集(CULane/TuSimple)
-
处理训练与部署的模型兼容性问题
-
2、关键代码解析
2.1. 运行环境配置与分布式初始化
torch.backends.cudnn.benchmark = True # 启用cuDNN自动优化 args, cfg = merge_config() # 合并命令行参数与配置文件 distributed = check_world_size() # 检测分布式环境
if distributed:
torch.cuda.set_device(args.local_rank) # 绑定GPU设备
torch.distributed.init_process_group(
backend='nccl', # NVIDIA集体通信库
init_method='env://' # 从环境变量获取地址
)
2.2 模型构建与加载
net = parsingNet( pretrained=False, # 测试时不加载预训练权重 backbone=cfg.backbone, cls_dim=(cfg.griding_num+1, cls_num_per_lane, 4), use_aux=False # 禁用辅助分支 ).cuda() # 处理训练保存的权重格式 state_dict = torch.load(cfg.test_model, map_location='cpu')['model'] compatible_state_dict = strip_module_prefix(state_dict) # 移除"module."前缀 net.load_state_dict(compatible_state_dict, strict=False)
2.3 评估执行
eval_lane(net, cfg.dataset, cfg.data_root,
cfg.test_work_dir, cfg.griding_num,
False, distributed)
根据数据集选择不同,使用不同评估方法
-
CULane:基于F1分数的车道存在性评估
- Tusimple:基于准确率和召回率的车道连续性评估
def eval_lane(net, dataset, data_root, work_dir, griding_num, use_aux, distributed): net.eval() if dataset == 'CULane': run_test(net,data_root, 'culane_eval_tmp', work_dir, griding_num, use_aux, distributed) synchronize() # wait for all results if is_main_process(): res = call_culane_eval(data_root, 'culane_eval_tmp', work_dir) TP,FP,FN = 0,0,0 for k, v in res.items(): val = float(v['Fmeasure']) if 'nan' not in v['Fmeasure'] else 0 val_tp,val_fp,val_fn = int(v['tp']),int(v['fp']),int(v['fn']) TP += val_tp FP += val_fp FN += val_fn dist_print(k,val) P = TP * 1.0/(TP + FP) R = TP * 1.0/(TP + FN) F = 2*P*R/(P + R) dist_print(F) synchronize()
3.流程总结
-
加载测试数据集
-
运行已有模型
-
计算评估指标(如F1-score、准确率)
-
生成可视化结果
-
保存预测结果
十、项目部署与应用
1. 项目性能评估
该模块用于评估车道线检测模型 parsingNet 的推理性能,通过测量模型在固定输入尺寸下的推理速度(时间与FPS),验证其部署可行性,适用于部署前性能验证。
核心流程包括:
- 模型初始化
- 预热推理
- 计时测试
- 结果分析
""" 用于评估车道线检测模型 parsingNet 的推理性能,通过测量模型在固定输入尺寸下的推理速度(时间与FPS),验证其部署可行性。 核心流程包括模型初始化、预热推理、计时测试与结果分析,适用于结构化预测任务(如车道线检测)的部署前性能验证。 """ torch.backends.cudnn.benchmark = True # 启用CUDA加速优化(输入尺寸固定时生效):ml-citation{ref="7" data="citationList"} net = parsingNet(pretrained = False, backbone='18',cls_dim = (100+1,56,4),use_aux=False).cuda() # net = parsingNet(pretrained = False, backbone='18',cls_dim = (200+1,18,4),use_aux=False).cuda() # 切换为评估模式(禁用Dropout等训练层):ml-citation{ref="7" data="citationList"} net.eval() # 构造全1输入张量(模拟归一化后的图像) x = torch.zeros((1,3,288,800)).cuda() + 1 for i in range(10): y = net(x) # 预热10次,初始化CUDA内核并避免首次推理延迟:ml-citation{ref="7" data="citationList"} t_all = [] #推理性能测试 for i in range(100): t1 = time.time() y = net(x) t2 = time.time() t_all.append(t2 - t1) # 记录100次推理时间 print('average time:', np.mean(t_all) / 1) # 平均速度 print('average fps:',1 / np.mean(t_all)) # 平均帧率 print('fastest time:', min(t_all) / 1) # 最快速度 print('fastest fps:',1 / min(t_all)) # 最高性能(最小延迟) print('slowest time:', max(t_all) / 1) # 最慢速度 print('slowest fps:',1 / max(t_all)) # 最低性能(最大延迟)
2. 模拟运行
核心流程:配置加载 → 模型初始化 → 数据准备 → 逐帧推理 → 结果可视化 → 视频生成
坐标转换公式:
-
x = pred_loc * 网格宽度 * 原图宽度 / 800
-
y = row_anchor位置 * 原图高度 / 288
关键代码:
#数据预处理流水线 img_transforms = transforms.Compose([ transforms.Resize((288, 800)), # 固定输入尺寸 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], # ImageNet标准归一化 std=[0.229, 0.224, 0.225]) ]) # CULane场景划分 splits = [ 'test0_normal.txt', # 正常场景 'test1_crowd.txt', # 拥挤场景 'test2_hlight.txt', # 强光干扰 'test3_shadow.txt', # 阴影干扰 'test4_noline.txt', # 无标线道路 'test5_arrow.txt', # 箭头标识 'test6_curve.txt', # 弯道 'test7_cross.txt', # 交叉路口 'test8_night.txt' # 夜间场景 ] #关键后处理逻辑 col_sample = np.linspace(0, 800-1, cfg.griding_num) # 横向采样点(0~799) col_sample_w = col_sample[1] - col_sample[0] # 网格宽度(约4像素) prob = scipy.special.softmax(out_j[:-1, :, :], axis=0) # 类别概率 loc = np.sum(prob * np.arange(1, griding_num+1), axis=0) # 期望位置计算 # 坐标映射回原图 x_coord = loc[k, i] * col_sample_w * img_w / 800 # 横向坐标 y_coord = img_h * (row_anchor[cls_num_per_lane-1-k]/288) # 纵向坐标 # 视频生成配置 fourcc = cv2.VideoWriter_fourcc(*'MJPG') # M-JPEG编码 vout = cv2.VideoWriter( filename=split[:-3]+'avi', fourcc=fourcc, fps=30.0, # 帧率 frameSize=(img_w, img_h) # 分辨率(CULane:1640x590) ) #车道点绘制策略 if np.sum(out_j[:, i] != 0) > 2: # 至少3个有效点才视为有效车道 for k in range(out_j.shape[0]): if out_j[k, i] > 0: cv2.circle(vis, ppp, 5, (0,255,0), -1) # 绘制绿色实心圆点
PS:
- 在运行模块,无需进入辅助分支,走主干分支进行识别即可;
- 在视频或图像画图时,使用openCV实现;
十一、项目总结
1、项目背景与需求
项目背景
车道线检测是自动驾驶和高级驾驶辅助系统的核心任务之一,旨在通过摄像头或其他传感器实时识别道路上的车道线标记,帮助车辆保持车道、避免偏离,并为路径规划提供关键信息。
功能性需求
- 多场景适应:在不同天气、光照及道路类型(高速、城市道路)下稳定检测。
- 多车道识别:同时检测当前车道及相邻车道线,支持车道保持和变道辅助。
- 弯曲车道处理:准确拟合抛物线或曲线车道(如环岛、弯道)。
- 遮挡鲁棒性:在部分遮挡(前车、污渍)时仍能推断车道走向。
非功能性需求
- 实时性:处理单帧图像时间≤30ms。
- 准确性:在主流数据集上达到≥95%的检测精度。
- 轻量化:模型需适配边缘计算设备,参数量≤5M。
- 可扩展性:支持多传感器融合以提升冗余性。
2、方法框架
数据准备
- 使用公开数据集:CULane,包含多样场景标注(点集或掩模)。
模型选择
- 深度学习模型:如ResNet,结合实例分割与分类任务。
关键技术
- 特征提取:通过CNN主干网络(如ResNet)提取多尺度特征。
- 后处理:通过多项式拟合或透视变换生成连续车道线,或采用注意力机制增强关键区域。
- 轻量化优化:使用模型压缩技术(如知识蒸馏、量化)提升推理速度。
3、代码实现流程
1. 数据集准备
- 下载并解压CULane数据集。
- 对数据集进行预处理,包括图像归一化、ROI裁剪、透视变换等。
- 生成输入标签数据,对应车道线位置。
2. 项目配置
- 设置配置文件(如
culane.py
),包括数据集目录、训练轮次(epoch)、批次大小(batch_size)、优化器参数等。 - 确定网络结构,加载预训练模型(如ResNet-18)。
3. 数据处理模块
- 自定义函数进行数据图像的预处理任务,如图像裁剪、归一化等。
- 使用PyTorch框架进行数据处理,设置行标签等。
4. 网络结构模块
- 自定义网络结构,分为主干的分类分支,辅助的检测分割分支;
- 基本单元使用ResNet网络结构
- 主干网络:基于ResNet提取多尺度特征(如x2, x3, fea),支持不同深度(如backbone='50')和预训练权重加载。
- 辅助分支(use_aux=True时启用):通过多层级特征融合实现分割任务,提升模型鲁棒性。
- 分类分支:将主干特征映射为车道线预测结果,输出维度由cls_dim定义
5. 损失函数模块
- 根据需求自定义三个损失函数;
-
SoftmaxFocalLoss:分类损失函数,判断当前网格是否属于车道线,可以降低易分类样本权重,聚焦困难样本;
-
ParsingRelationLoss:约束车道线的纵向连续性,确保车道线在垂直方向的连续性,避免断裂,可以延申车道线;
-
ParsingRelationDis:保持车道线位置变化的平滑性,车道是连续的,也就是说,相邻行锚点中的车道点应该彼此靠近。
6. 模型训练
- 初始化模型,切换为训练模式。
- 使用DataLoader加载数据,
- 使用定义好的网络模型,调用GPU进行迭代训练。
- 计算损失函数,包括分类损失和结构损失。
- 进行反向传播和优化,更新模型参数。
- 保存模型权重和训练日志。
7. 模型推理与评估
- 初始化模型,切换为评估模式。
- 构造输入张量,进行预热推理(多次前向传播以稳定性能)。
- 计时测试,记录多次推理时间,计算平均FPS、最快FPS和最慢FPS。
- 输出评估结果,包括平均推理时间、平均FPS等。
8. 性能优化
- 根据评估结果,调整输入尺寸、模型结构等。
4、评估指标
- 准确率和召回率。
- F1分数:平衡误检(FP)与漏检(FN)。
- 实时性:帧率(FPS)与端到端延迟。
通过以上流程,可以完成车道线检测模型的训练、评估与优化,为自动驾驶系统提供高性能的车道线检测功能。
十二、常见问题
1、使用到的技术?
- numpy进行矩阵计算;
- pytorch作为深度学习框架,进行训练、搭建、前向传播、反向传播、损失函数计算、性能评估等一系列操作;
- openCV进行画图,输出训练和测试结果;
2、ResNet是什么?
ResNet(Residual Network,残差网络)是一种在深度学习领域中非常重要的卷积神经网络(CNN)架构。
ResNet的核心思想是残差学习,通过学习输入与输出的残差,简化优化任务。其关键结构是残差块,通过短连接(Skip Connection)直接跳过部分非线性层,将输入直接传递至输出,有效缓解梯度消失问题。
3、张量与向量的关系?
向量是一维的,可以看作是一阶张量,而张量是一个更广泛的概念,可以有多个维度。在深度学习中,张量是非常重要的数据结构,用于表示和处理多维数据。下图展示了张量在不同维度的形式:
4、分割算法的作用?
图像分割算法通过像素级解析为计算机视觉任务提供基础数据,其作用贯穿于感知、分析与决策的各个环节。
深度学习中的图像分割算法通过像素级分类实现对图像的精细化分析与理解,其核心作用包括以下方面:
- 精准识别物体边界:分割算法可为图像中的每个像素分配类别标签,精确划分不同对象的边界,例如区分医学影像中的病变组织与正常组织。
- 提取结构化信息:通过分割结果生成掩膜(Mask),为后续任务(如目标检测、三维重建)提供结构化数据支持
5、分割任务的类型?
- 语义分割:为所有像素分配类别标签(如区分“车”“人”“路”)
- 实例分割:区分同类物体的不同实例(如分离同一场景中的多辆汽车)
6、深度学习中的Mask是什么?
Mask在深度学习中扮演“像素级导航”角色,通过精准的区域标识,支持分割、检测、生成等任务。
Mask(掩膜) 是一个关键概念,尤其在图像分割、目标检测和生成任务中广泛应用。
它通常是一个与输入数据(如图像)同尺寸的二值矩阵或类别矩阵,用于标识特定区域或像素的归属(如属于目标、背景或类别)。
Mask的核心作用
- 区域标记:
Mask明确指示图像中哪些像素属于目标区域(如用1表示目标,0表示背景),或为每个像素分配类别标签(如语义分割中的多通道掩膜)。 - 信息约束:
在训练或推理中限制模型仅关注特定区域,例如在图像修复任务中,Mask标记缺失区域,模型仅生成该区域内容。
7、反向传播之前为什么要梯度清零?
梯度清零是为了防止多个批次的梯度叠加,确保每次参数更新仅基于当前批次的计算结果,避免训练不稳定或错误更新。
-
梯度清零的意义:确保每个批次的梯度独立计算,避免历史梯度干扰当前批次的学习方向。
-
默认行为:在PyTorch等框架中,梯度是累积的(
gradient accumulation
)。每次调用.backward()
计算梯度时,新的梯度会累加到之前的梯度上,而不是覆盖。 -
不清零的后果:如果不清零梯度,多个批次的梯度会叠加,导致参数更新时的梯度值过大,可能引发以下问题:
-
模型参数更新不稳定(震荡或爆炸);
-
训练过程无法收敛;
-
损失函数剧烈波动。
-
8、项目算法介绍一:网格划分
传统的语义分割要判断每一个像素点,该算法把图像分割成一个个的小网格;
例如原图是1280*720像素的图像,通过分割成一个个网格,把判断像素->判断小网格,把识别到的小网格连接起来,从而大大减小计算量,易于部署到移动端,且准确度高;
从而把语义分割问题->分类检测问题;
9、项目算法介绍一:网络结构
10、项目算法介绍一:损失函数
11、关于数据增强
在有限的数据集中,通过数据增强,达到提升识别准确度和解决图像损坏、位置被遮挡的问题。
一般常用前两种方法:空间转换和颜色扭曲
同时对数据样本进行下面的操作,模拟异常场景,也可以提升模型的整体识别能力和抗干扰能力;
- 进行图像擦除,从而提高图像的整体识别能力和抗干扰能力
- 进行图像融合,达到图像雾化模糊的效果,从而提高图像的整体识别能力和抗干扰能力
- 同理还有图像拼接
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构