☰
YOLOv8+KITTI自动驾驶目标检测实战:从数据转换到小目标优化
2026/10/21 11:59:28 网站建设 项目流程

很多刚开始做自动驾驶视觉实验的朋友,上来就追最新的检测模型,结果数据集没整理明白,训练出来的mAP飘得厉害。我自己的习惯是反着来:先选一套相对规范的公开数据集,把格式和样本分布吃透,再让模型和训练策略一层层配合上去。这次实验我选的是YOLOv8,数据源用公开自动驾驶数据集KITTI,目标锁定车辆、行人、红绿灯三类,做了一套从数据准备、格式转换、训练调参到精度优化的完整流程。一路走下来,踩了不少坑,也整理出不少实际可复用的技巧。如果你正准备做自动驾驶感知方向的目标检测项目,或者想把YOLOv8在一个真实路况数据集上跑顺,这篇文章可以直接当操作手册看。

1. 为什么是YOLOv8 + KITTI:这一组合到底解决什么问题

1.1 三类目标在图像里呈现完全不同的“脾气”

做自动驾驶目标检测,最基础也最关键的就是把车、人、红绿灯分开且找准。但真上手之后你会发现,这三类目标在图像里的表现差异非常大,不能只用一套固定思路去处理。

车辆类目标通常尺寸较大、形状规整,边缘清晰,比如轿车、卡车、公交车,即便是远距离出现,框也比较稳定。行人类目标则复杂得多,姿态千变万化,走路、骑车、打伞、蹲着、被遮挡,都可能让模型出现漏检或误检,尤其是密集人群场景,一个行人框把旁边人框进去的乌龙很常见。红绿灯则属于典型的小目标,占整张图的比例通常不到百分之一,类别之间有时只有颜色和位置上的细微差别,检测难点在于“看得见”而不是“认得出”。

所以这个任务真正考验的不是模型能不能检测目标,而是能不能在同一个场景里同时处理好大目标、形变目标和极小目标。YOLOv8在这类多尺度混合场景里表现不错,加上训练和推理效率高,非常适合先把流程搭建起来再逐步优化。

1.2 YOLOv8在工程实践里有几个很直接的优势

选择YOLOv8,不单是因为它新,而是它在工程落地上有几个很实际的优点值得说。

一是检测头设计变了,采用了anchor-free加解耦头的结构。旧版YOLO很多把分类和回归放在同一个分支里,训练时两者会互相干扰。YOLOv8把分类和框回归解耦成两条并行分支,小目标和密集场景下收敛更稳。二是特征融合部分继承了多尺度特征融合的结构,底层高分辨率特征和深层语义特征能组合起来使用,这对红绿灯这种小目标来说非常关键。三是训练配置和导出链路完善,从训练迭代到模型导出做得很顺,不用从零手写一堆工程脚本。

还有一点是生态成熟。YOLOv8相关资料多,网上各类问题基本都有案例可查,遇到异常也好排查。对于项目周期短、又要快速验证效果的场景,这很省时间。前几代版本也用过,但综合对比后,YOLOv8在精度和易用性之间的平衡更符合这次实验的预期。

1.3 KITTI数据集到底适合用来做什么

KITTI是自动驾驶领域非常有代表性的公开图像数据集,采集自真实驾驶环境,包含城市道路、乡村道路、多车道等不同场景。它的图像分辨率并不算高,原始图像宽约1242像素、高约375像素,但胜在场景真实、标注相对规范。

不过注意一点,KITTI原始标签里车辆和行人类别比较完整,红绿灯并没有像车和行人那样成体系地标注出来。我在实验里做了一步额外处理:保留KITTI里车辆、行人相关的全部标注,再从其他公开形态的补充图像里整理红绿灯数据,裁剪并统一成同一种标签规范后并入训练集。这样既保证了车辆、行人数据的规范性,也补上了红绿灯样本的空缺。实际训练效果证明,这种做法比强行把所有类别混在一个不匹配的标注体系里要靠谱得多。

选择KITTI还有一个好处是它的标注结构稳定。数据类别划分清楚,车辆细分为Car、Truck、Van等,行人细分为Pedestrian、Person_sitting等,标签信息里还包含遮挡、截断状态、3D框等信息,虽然本次实验只用2D框,但种数据结构方便以后扩展3D检测研究方向。

2. 数据准备阶段:KITTI标注转YOLO格式的完整过程

2.1 先把KITTI标签字段彻底拆一遍

KITTI标签文件是纯文本格式,每一行代表一个目标的完整标注信息。第一版转换脚本我写得很草率,跑完发现很多目标直接丢框,问题就出在只取了前几个字段。

KITTI每一行标注的基本格式包括:目标类别、截断程度、遮挡程度、观测角度、2D框坐标、3D框参数。其中2D框坐标是left、top、right、bottom,代表目标框在图像中的绝对像素位置,这个直接用来转YOLO格式最方便。3D框参数虽然在2D检测里用不到,但字段位置占住,不能因为不需要就跳过解析,否则后面的框坐标全部错位。

刚开始做的时候,建议先把几行原始标注打印出来人工读一遍,确认字段顺序一般不会太长。这个步骤看起来笨,但能直接避免后面整批数据转换完才发现坐标偏移的问题。

2.2 类别映射和二类别的归并策略

KITTI原始类别比较细,直接全部塞进YOLO训练容易出现类别样本不均衡。我在实验里做了归并:Car、Van、Truck、Tram统一归为车辆类;Pedestrian、Person_sitting、Cyclist统一归为行人;红绿灯单独作为一个类别。

这样一来,车辆和行人的正样本数量都上去了,模型收敛容易不少,类别之间的区分度也更明显。不要担心归并会丢失细粒度信息,如果以后真的需要细分车辆类型,完全可以在这个基础上单独再训一个分类网络做二次判断。

红绿灯样本则单独处理。我先把补充图像中的红绿灯区域全部裁剪出来,生成对应的分类标签,再放回图像原图坐标系生成完整的目标检测标注。整个过程做了一个小工具,自动检测裁剪区域和原图坐标的映射关系,比起手动标注节省了不少时间。

类别映射关系整理如下:

类别ID检测目标归并来源
0车辆Car、Van、Truck、Tram
1行人Pedestrian、Person_sitting、Cyclist
2红绿灯补充标注的交通信号灯

2.3 KITTI坐标转YOLO格式的代码实现

写转换脚本时,最需要注意的是坐标归一化。YOLO格式需要的不是绝对像素坐标,而是相对于图像宽高的归一化中心点坐标和宽高。很多第一次做转换的人会直接拿KITTI的left、top、right、bottom去减,忘记除以图像尺寸,结果训练时损失骤降但检测框全部偏移。

转换脚本核心逻辑参考如下:

import os def kitti_to_yolo(label_file, img_width, img_height): # 类别映射表 class_map = { 'Car': 0, 'Van': 0, 'Truck': 0, 'Tram': 0, 'Pedestrian': 1, 'Person_sitting': 1, 'Cyclist': 1, 'TrafficLight': 2 } yolo_lines = [] with open(label_file, 'r', encoding='utf-8') as f: for line in f: parts = line.strip().split() if len(parts) < 15: continue class_name = parts[0] # DontCare这类标注直接跳过,不参与训练 if class_name == 'DontCare': continue if class_name not in class_map: continue left = float(parts[4]) top = float(parts[5]) right = float(parts[6]) bottom = float(parts[7]) # 防止坐标越界 left = max(0, min(left, img_width - 1)) right = max(0, min(right, img_width - 1)) top = max(0, min(top, img_height - 1)) bottom = max(0, min(bottom, img_height - 1)) if right <= left or bottom <= top: continue center_x = ((left + right) / 2.0) / img_width center_y = ((top + bottom) / 2.0) / img_height width = (right - left) / img_width height = (bottom - top) / img_height yolo_lines.append( f"{class_map[class_name]} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}" ) return yolo_lines

这段代码里有两个细节要注意。第一是DontCare类一定要跳过,这类目标在KITTI里表示“这一坨东西不确定是什么”,如果强加进训练会让模型学会把背景区域也当成目标。第二是越界处理,KITTI某些标注框可能略超出图像边界,直接归一化后宽高会大于1,训练时要么报错要么学得乱七八糟,提前截断到边界内是稳妥做法。

2.4 训练集和验证集的划分不能太随便

KITTI数据来自连续视频序列,单纯随机划分训练集和验证集有个隐患:同一辆车可能出现在相邻好几帧里,随机切分容易造成同一目标的重复信息同时出现在训练集和验证集,导致验证指标虚高。

这里做了按序列分组的划分。先把所有数据按视频序列编号分组,整个序列要么整段进训练集,要么整段进验证集,不允许一个序列里部分帧训练、部分帧验证。这种划分方式更接近真实场景,模型在验证集上的表现也更有参考价值。

划分比例用了8比2。验证集不要太小,否则小类别红绿灯的样本数不够,mAP波动会很大。如果发现某类别在验证集里不足二三十个样本,建议调整分组逻辑,把包含该类别的序列尽量分配到验证集一部分。

3. 训练过程:模型参数、超参数和针对小目标的优化

3.1 基础模型选择和训练资源配置

模型选择上我先后试过YOLOv8n、YOLOv8s和YOLOv8m。最直观的感受是:规模越小,训练越快,但对红绿灯这类小目标的召回率明显下滑;规模越大,精度提升是有的,但训练时间和显存压力也是实实在在的。

最终采用了折中策略:以YOLOv8s作为主力训练模型,推理速度快,精度与YOLOv8m差距不大,但对红绿灯的召回率已经能压到一个比较正常的水平。如果你手里的显卡显存比较宽裕,用YOLOv8m跑会再稳一点,但对整体流程验证来说YOLOv8s完全够用。

训练配置参考如下:

yolo detect train \ data=data.yaml \ model=yolov8s.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ workers=8 \ patience=15 \ optimizer='auto' \ device=0

这里batch设为16而不是更大的32,不是显卡带不动,而是留出显存余量给多尺度采样。后面会提到,输入尺寸对红绿灯检测影响很大,尺寸一往上提,显存占用立刻上去,batch设得保守一点更稳。

3.2 输入分辨率需要分档尝试,红绿灯太小是核心矛盾

KITTI原图高度375像素,宽高比在3.3比1左右。直接用640x640正方形输入训练,长条图会被压缩得很厉害,远处目标缩小严重,红绿灯本来就只有几十像素,再一缩基本就没法看了。

我做了三组对比实验:

输入尺寸车辆mAP50行人mAP50红绿灯mAP50训练速度
640x640高中低快
896x896高高中中
1280x1280高高高慢

最终采用了1280x1280配合rect训练的配置,即同一batch内的图像按宽高比边缘内进行统一尺寸拉伸,避免细节被过度压缩。代价是训练速度明显下降,但红绿灯的召回率提升能够换来这个成本。如果你的实验里红绿灯类别不是重点,640或896就能满足需求,没必要盲目堆分辨率。

3.3 类别不均衡:红绿灯样本较少时怎么补

实验里车辆和行人的样本数量明显高于红绿灯,比例大概在几十比一。如果不做处理,模型会倾向于把所有目标都预测成车辆或行人,红绿灯类别基本学不出来。

平衡手段用了两种。第一种是样本增强,对红绿灯图像做随机裁剪放大、轻微旋转、亮度扰动,让模型见过更多不同姿态的信号灯。第二种是类别加权,在loss计算中给红绿灯类别适当提升权重,这个可以通过YOLOv8训练配置里的类别相关参数调整实现。

实际操作后,红绿灯的召回率提升了将近15%,且没有明显牺牲车辆和行人的精度。不过要注意,加权系数不宜调得过大,否则模型会出现一类把红灯区误判为红绿灯的反向过拟合。通常权重乘以1.5到3倍之间是一个比较安全的范围,具体还是要看loss曲线和混淆矩阵的表现。

3.4 损失曲线和训练过程的监控手段

训练过程中不要只看mAP,mAP是训练结束后的汇总指标,没办法帮你判断到底哪一步学坏了。我在每个epoch结束后都会记录三样东西:总loss曲线、每个类别的召回率、某个固定验证集上的可视化预测图。

loss曲线如果出现断崖式下降后长时间横盘,说明模型已经在收敛了,加大epoch收益很小。召回率曲线则最能暴露类别问题,如果车辆召回率一路高歌,红绿灯召回率却还在低位摩擦,基本可以断言是样本不均衡在捣乱,这时候再去调超参数才有方向。

可视化预测图是排查异常的利器。有时loss明明很低,打开预测图发现所有红绿灯都被框成了行人,说明类别混淆严重,光看指标根本看不出来。每次训练迭代后随机抽几十张图存下来,花五分钟扫一遍,比盯着tensorboard发呆有效得多。

4. 精度分析:mAP之外的细节和误检问题

4.1 指标看mAP50还是mAP50-95,要说清楚

YOLOv8训练结束后会输出mAP50和mAP50-95两组指标。很多新手只盯着mAP50,其实这反映的是“定位大致对得上”的水平。mAP50-95计算了从IoU 0.5到0.95各档位下的均值,对框的定位精度要求更高,更贴近真实工程中目标框要贴合物体轮廓的需求。

这次实验里,车辆类mAP50-95在40%上下,行人略低一些,红绿灯最低,这是预料之中的。因为信号灯本身小,真实框和预测框稍微偏移几个像素,IoU就会掉得很厉害。

给个结论:如果只是做算法演示,可以把mAP50作为主要对比指标;如果后续要接规划控制模块,对框的准确性要求高,还是要尽量提升mAP50-95,尤其是小目标类别。

4.2 混淆矩阵里最常见的误检模式

训练完必看混淆矩阵,这次实验暴露出的问题主要有三类。

第一类是行人误检成车辆。多发生在行人穿着宽大衣物、背部轮廓模糊的情况下。第二类是车辆背景误检成车辆。车尾、车窗反光、道路护栏在特定光照下容易被误当成车辆目标。第三类是红绿灯漏检,白天背光场景最典型,信号灯和天空背景对比度低,模型找不到。

这几类误检不能只靠调超参数解决,我的处理方法是把典型的误检图整理成负样本集,单独加到背景类别里参与训练。这个做法对减少车辆背景误检很有效,对红绿灯漏检的作用不大,它更像是特征不明显导致的问题,需要靠多尺度融合或更高分辨率推理来缓解。

4.3 遮挡和密集场景的处理

KITTI里行人经常处于遮挡状态,两个行人擦肩而过时,标注框会有大量重叠。此时NMS后处理容易把其中一个人给抑制掉。

针对重叠目标,我把NMS的IoU阈值从默认的0.45调到0.3,保留重叠场景中置信度较低的候选框。这一改动提高了行人在密集场景的召回率,但代价是车辆目标偶尔出现重复框。通过增加置信度阈值过滤低置信度的重复框可以抵消这个问题。

遮挡严重的大目标,比如被树挡了一半的货车,单帧检测在原理上就很难解决。我的处理是不强求,保留置信度较低的预测结果,把决策交给后续的时序模块去跟踪,或者利用前后帧信息补全框位置。这是工程上很务实的做法。

5. 部署优化和后处理:让模型从好指标落回好体验

5.1 模型压缩和加速的优先级

训练完模型体积不小,直接部署到车载或嵌入式设备会很吃力。YOLOv8自带导出功能,可以导出多种推理格式,在精度损失不大的前提下提升推理速度。

我的建议是先做精度裁剪,再看是否量化。裁剪剪掉一部分不那么重要的卷积通道,模型体积能下降约三成,mAP损失通常在1到2个百分点以内。之后再尝试将权重从浮点转成半精度或整型精度,体积进一步下降,但此时要特别关注小目标的精度损失。

红绿灯这种小目标对量化尤其敏感。量化后整型推理时,原本勉强能检测到的信号灯可能直接消失。我在实验里对比过,浮点模型和整型量化模型在红绿灯召回率上差了接近10个点。所以如果你的部署设备支持,建议保留半精度推理,不要直接降到整型;如果必须整型量化,针对红绿灯类别单独做校准数据集和阈值补偿。

5.2 推理侧降低误检的实用技巧

模型导出完成后,推理侧还能做不少优化。一个很实用的小技巧是降低置信度阈值。训练阶段常用0.25置信度,但实际测试中发现红绿灯目标置信度普遍偏低,保持在0.4以上的不多。把阈值降到0.15到0.2,再配合NMS阈值收紧,可以有效捞回红绿灯检测结果。

另一个技巧是对检测框做最小尺寸过滤。自动驾驶场景中,太小的检测框大概率是误检,而且实际意义不大。过滤掉长边小于5像素的框,能减少大量背景噪声和随机框。

还有一点是输入尺寸的动态适配。推理时不一定非要和训练尺寸保持一致,如果你用的是1280训练,推理时可以根据使用场景灵活切换640或1280,根据算力余量动态调整,保证帧率稳定的前提下追求最高召回。

5.3 后续还能扩展的方向

这次实验的下一步,我准备把检测结果和跟踪器做结合。单帧检测很难避免漏检和误检,但物体在时间序列上不会凭空消失,也不会瞬间瞬移。通过跟踪器将前后帧关联起来,漏检帧可以用上一帧的位置补上,误检帧也会因为轨迹不连续而被过滤掉。这是自动驾驶感知里面非常实用的降噪手段。

还有个方向是给红绿灯检测加一个颜色分类分支。当前实验只完成了信号灯位置的检测,没有区分红灯和绿灯。下一步准备在检测框内增加一个小型的分类头,或者直接级联一个轻量分类网络,把信号灯的颜色和状态识别补上,让检测结果真正可被决策模块使用。


最后分享一个实际体会:做目标检测项目,最花时间的往往不是跑模型那几十分钟,而是数据整理和结果分析。第一次转换标签脚本写完,我以为万事大吉,结果漏检一堆;第一次训练完看mAP挺高,一打开预测图,红绿灯全被漏掉。视觉项目的每个环节都值得慢下来耐心看一遍。建议你不要急着追求更复杂的模型,先把现在这套流程里的数据坑、类别坑、后处理坑踩踏实,再去做模型结构上的改动,收益会大得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询