YOLOv8+KITTI自动驾驶目标检测实战:从数据转换到小目标优化 很多刚开始做自动驾驶视觉实验的朋友上来就追最新的检测模型结果数据集没整理明白训练出来的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、Tram1行人Pedestrian、Person_sitting、Cyclist2红绿灯补充标注的交通信号灯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, encodingutf-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 \ datadata.yaml \ modelyolov8s.yaml \ epochs100 \ imgsz640 \ batch16 \ workers8 \ patience15 \ optimizerauto \ device0这里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 损失曲线和训练过程的监控手段训练过程中不要只看mAPmAP是训练结束后的汇总指标没办法帮你判断到底哪一步学坏了。我在每个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挺高一打开预测图红绿灯全被漏掉。视觉项目的每个环节都值得慢下来耐心看一遍。建议你不要急着追求更复杂的模型先把现在这套流程里的数据坑、类别坑、后处理坑踩踏实再去做模型结构上的改动收益会大得多。