ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

自动驾驶多类别交通检测实战:从COCO格式转换到YOLOv8训练与避坑

自动驾驶多类别交通检测实战:从COCO格式转换到YOLOv8训练与避坑 简介面向自动驾驶感知、智能交通监控与驾驶辅助训练提供一套多类别交通物体与交通标志检测数据集覆盖车辆、行人、路标、交通灯等同步检测需求。数据来自真实道路场景训练集656张、验证集188张、测试集94张共938张图片标注了限速、禁止通行、交叉路口等134种交通标志以及轿车、公交车、卡车、摩托车、自行车等交通工具并包含弯道、陡坡、红绿灯、斑马线、单行道、落石、动物出没等146个精细类别。资源共1878个文件含938张JPG道路图片、938个对应TXT标注文件、1个YAML配置和1个DOCX说明文档压缩包大小104.37MB。标注采用YOLO边界框格式可直接用于YOLOv5/v7/v8等主流框架训练支持多目标密集标注覆盖城市道路、高速公路、山区路段及不同光照条件。目前已有89人学习适合需要构建交通场景目标检测模型的研究者与开发者快速上手。1. 自动驾驶多类别交通物体与交通标志检测数据集一次把车辆、行人、骑手和标志牌凑齐开箱一个名为“自动驾驶多类别交通物体与交通标志检测数据集”的 zip 包等于拿到一批贴着边界框的交通场景素材车辆、行人、骑手、公交车这类交通物体是一组限速牌、禁令标志、指示标志、信号灯这类交通标志是另一组。落地路径很清晰——把压缩包解析成标准格式喂给 YOLOv8 或 MMDetection 这类检测框架训出一个多类别目标检测模型再谈实车部署或决策链路接入。数据包能不能支撑自动驾驶感知从来不取决于 zip 有多少 GB而取决于场景覆盖、标注一致性和类别分布均衡程度。下面从解压开始按一个完整训练项目的顺序把每一步怎么落、坑在哪里讲透。2. 解压之后先别急着训练目录结构、标注格式与类别分布怎么核查2.1 第一次解压用这三条命令确认数据集完整性与目录骨架拿到这个 zip我一般不会直接在图形界面里双击解压而是先看压缩包清单避免解到一半发现文件损坏或目录结构不对。Linux 下习惯这么操作# 1. 先列压缩包里的顶层目录确认是否带根目录 unzip -l 自动驾驶多类别交通物体与交通标志检测数据集.zip | head -40 # 2. 解压到指定目录保留原有目录层级 unzip -q 自动驾驶多类别交通物体与交通标志检测数据集.zip -d ./datasets/traffic # 3. 解压后统计文件总数和压缩包窗口里显示的数做对比 find ./datasets/traffic -type f | wc -l第一行unzip -l只列内容不解压能看出 zip 里有没有打包顶层目录这直接影响后面的路径配置。第二行把包解开到./datasets/traffic-q让日志安静一点。第三步统计文件总数和压缩包侧边栏或下载页给的文件数对比数字对不上就说明解压过程有文件被跳过多数是文件名编码问题或磁盘满了。解压完成后第一件事不是跑训练而是把目录骨架完整看一遍。交通场景数据集最常见的布局是images/和labels/两层各自下面再分train/、val/、test/也有部分数据集把标注合并成一个annotations/文件夹里面放一个大的 COCO JSON 文件。这两种布局对应完全不同的训练流程先查清楚再动手tree -L 3 ./datasets/traffic按我经手过的同类数据集经验images/train/xxx.jpg配labels/train/xxx.txt的是 YOLO 风格images/配annotations/instances_train.json的是 COCO 风格。同一个 zip 里两套都出现的情况也有但通常其中一套是附带工具生成的冗余文件训练前先确认你要用哪一套别让配置文件同时指向两个人。2.2 标注格式对比COCO 的 json 和 YOLO 的 txt 怎么识别、怎么转多类别交通检测数据集里标注格式几乎只有两大家族COCO 的 JSON 和 YOLO 的纯文本。先看 COCO 格式长什么样。一个典型的instances_train.json里有images、annotations、categories三个数组categories决定了类别 ID 排序{ categories: [ {id: 1, name: person}, {id: 2, name: bicycle}, {id: 3, name: car}, {id: 4, name: motorcycle}, {id: 5, name: bus}, {id: 6, name: traffic_light}, {id: 7, name: traffic_sign} ], annotations: [ {id: 1, image_id: 1, category_id: 3, bbox: [320, 240, 128, 96]}, {id: 2, image_id: 1, category_id: 7, bbox: [860, 120, 54, 38]} ] }注意 COCO 的bbox是[x, y, width, height]x、y 是框的左上角坐标。而 YOLO 的 txt 每行是class_id center_x center_y width height坐标值是相对于图片宽高的 0 到 1 浮点数。两套格式最大的坑在坐标换算COCO 转 YOLO 时每个值都要除以图宽图高而且类别 ID 不一定从 0 开始连续上面 COCO 里person是 1YOLO 分类规定必须从 0 开始所以转换时必须做一次重映射。我一般用一段 Python 脚本做 COCO 到 YOLO 的转换省得手算坐标import json import os coco_path annotations/instances_train.json out_dir labels/train os.makedirs(out_dir, exist_okTrue) with open(coco_path, r, encodingutf-8) as f: coco json.load(f) # 建 image_id - 文件名的映射必须和实际图片文件名一致 id2name {im[id]: im[file_name] for im in coco[images]} # 类别重映射COCO 的 id 可能不连续压成 0..N-1 的连续整数 old2new {cat[id]: i for i, cat in enumerate(coco[categories])} for ann in coco[annotations]: img_name id2name[ann[image_id]] img_info next(im for im in coco[images] if im[id] ann[image_id]) w, h img_info[width], img_info[height] x, y, bw, bh ann[bbox] # 左上角宽高 转 中心点宽高并归一化到 0..1 cx, cy x bw / 2, y bh / 2 cx, cy, bw, bh cx / w, cy / h, bw / w, bh / h cls_id old2new[ann[category_id]] txt_path os.path.join(out_dir, os.path.splitext(img_name)[0] .txt) with open(txt_path, a) as f: f.write(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n)这段逻辑有三个关键细节。其一os.path.splitext把img_name的后缀换成.txt要求图片名和标注名的前缀严格一致哪怕图片是.jpeg、标注是.txt也一样。其二类别重映射用enumerate把 COCO 的稀疏 ID 压成 0 开始的连续索引避免训练时类别数比实际多或者索引越界。其三坐标转换必须用同一张图的宽高归一化如果原图被旋转过、裁剪过或启用过 EXIF 旋转宽高就和标注基准不一致转出来的 YOLO 框全是歪的。提示转换之后随机抽三五张图用 OpenCV 把框叠上去看一眼不要轻信脚本输出。画框验证这一步比之后上 GPU 跑了 50 轮再发现问题省时得多。import cv2 import os img_path images/train/road01_000001.jpg txt_path labels/train/road01_000001.txt img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: cls, cx, cy, bw, bh map(float, line.split()) x1, y1 int((cx - bw / 2) * w), int((cy - bh / 2) * h) x2, y2 int((cx bw / 2) * w), int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) img_resized cv2.resize(img, (1280, 720)) cv2.imshow(check, img_resized) cv2.waitKey(0) cv2.destroyAllWindows()这段可视化脚本本身没什么高深内容但它能把标注框是整体偏移、宽高比不对、还是类别 ID 与目标物不符这些问题一次性暴露出来。我几乎在每一个数据集项目里都会跑一遍宁可多花三分钟也不想让问题藏到训练结束。2.3 类别统计与不平衡交通标志偏少、行人被遮挡训练前要有心理预期多类别交通数据集的痛点是类别不均衡。车辆类car、bus、truck往往占一半以上的标注框行人次之交通标志和信号灯因为目标面积小、出现频率低在整体标注框里占比通常低于 10%。这不奇怪——真实路面上车的数量本来就比标志牌多采样时又经常从车载摄像头连续抽帧长时间在同一条路、同一个路口标志牌反复出现在同一批画面里特征重复度高多样性反而低。在动手训练前用脚本统计一下每个类别的框数量python - EOF import os, glob label_dir labels/train cnt {} for txt in glob.glob(os.path.join(label_dir, *.txt)): with open(txt) as f: for line in f: if not line.strip(): continue cls line.split()[0] cnt[cls] cnt.get(cls, 0) 1 for k, v in sorted(cnt.items(), keylambda x: -x[1]): print(fclass {k}: {v}) EOF统计完你会直观看到哪些类别在拖后腿。如果交通标志这类占比很低训练时要么给分类损失加权重要么在数据增强时对含该类别的图像做上采样复制。更稳妥的做法是先看置信度阈值下的逐类召回率如果行人好检测而标志牌召回率只有六成那不是模型能力问题是样本本身不够。这套数据集的隐含价值在于它同时包含“交通物体”和“交通标志”两个语义层。前者是通用目标检测都覆盖的常见类别后者对检测器的泛化要求更高——标志的颜色、亮度和形变在不同时段、逆光和雨雾条件下差异很大正好是第 4 章避坑记录里要展开的内容。3. 用这个数据集训练一版多类别检测模型从划分到调参的完整流程3.1 按场景划分数据集别让同一条路的样本同时出现在训练集和验证集拿到 zip 直接跑训练的人通常第一步就栽在划分上。很多数据集包自带train/、val/目录看起来能用但划分方式未必合理。交通数据集最典型的问题是连续帧同一辆车出现在第 100 帧和第 101 帧如果第 100 帧在训练集、第 101 帧在验证集验证 mAP 会虚高因为模型其实“见过”这辆车了。我一般不管它自带的划分重新做一次场景级切分。所谓场景级就是把图片按时间戳、拍摄路段或 GPS 轨迹分组同一个场景下的所有帧要么全进训练集要么全进验证集。一个简单可操作的做法是按文件名前缀分组import os, shutil, random from collections import defaultdict src_img images/all # 原始图片目录 src_lbl labels/all # 原始标签目录 out_img data_reorg/images out_lbl data_reorg/labels os.makedirs(out_img /train, exist_okTrue) os.makedirs(out_img /val, exist_okTrue) os.makedirs(out_lbl /train, exist_okTrue) os.makedirs(out_lbl /val, exist_okTrue) # 按场景标识分组若文件名是 road01_000123.jpg就取 road01 作为场景组 groups defaultdict(list) for f in os.listdir(src_img): scene_id f.rsplit(_, 1)[0] groups[scene_id].append(f) # 场景级切分保证一个场景的所有帧只进一边 random.seed(42) all_scenes list(groups.keys()) random.shuffle(all_scenes) val_scenes set(all_scenes[: int(len(all_scenes) * 0.2)]) for scene, files in groups.items(): target_img out_img (/val if scene in val_scenes else /train) target_lbl out_lbl (/val if scene in val_scenes else /train) for f in files: shutil.copy(os.path.join(src_img, f), os.path.join(target_img, f)) lbl_name f.rsplit(., 1)[0] .txt lbl_src os.path.join(src_lbl, lbl_name) if os.path.exists(lbl_src): shutil.copy(lbl_src, os.path.join(target_lbl, lbl_name))这段代码里f.rsplit(_, 1)[0]假设文件名是road01_000123.jpg这样的格式去掉最后一截帧号后把road01当作场景组。如果你的文件名没有清晰前缀就得看有没有伴随的scene.csv或timestamp元数据如果都没有退而求其次按编号每间隔 N 帧抽一帧做验证。总之目标是让验证集和训练集在内容上解耦而不是随机打散文件这是交通视频类数据集和普通静态图片数据集最本质的区别。3.2 最小可运行训练流程Ultralytics YOLOv8 的 yaml 配置与启动命令划分好之后进入训练环节。目标检测框架选型我在交通数据场景下优先推荐 YOLOv8理由是文档全、坑少、一套 CLI 能跑通全流程。先建一个数据集描述文件traffic.yaml# traffic.yaml — 放在数据集根目录下 path: ./data_reorg # 上一步 reorg 后的根目录 train: images/train val: images/val names: 0: person 1: bicycle 2: car 3: motorcycle 4: bus 5: traffic_light 6: traffic_sign这里的names字典必须和标注文件里的class_id一一对应顺序从 0 开始不可随意调整。很多人在这一步把 COCO 的 id 直接搬过来比如person写成 1结果 YOLO 认为第 0 类是person但标注文件里根本没有 0训练直接报class index out of range属于最基础的低级错误。写好后启动训练yolo detect train \ datatraffic.yaml \ modelyolov8s.pt \ epochs80 \ imgsz1280 \ batch16 \ projectruns/traffic_bench \ nameexp1解释一下这几个参数在交通场景里的取舍。modelyolov8s.pt在精度和速度之间取平衡用 s 版本做第一次 baseline后面再切 m 或 l。imgsz1280交通标志是典型小目标640 输入下远处一块 30x30 像素的牌子可能只占十几个像素1280 能显著提升小目标召回代价是显存和训练时间翻倍显存不够就先从 960 开始。epochs80中小型数据集 80 轮足够收敛。前 20 轮观察 loss 趋势如果降到 2.0 以下可以放心跑完如果 20 轮还在 4 以上先查学习率和数据加载是否有错。batch16按 1280 输入、单卡 4090 估算这是稳妥值。batch 过大会让 BatchNorm 统计不稳定验证 loss 振荡。训练完成后runs/traffic_bench/exp1/weights/best.pt就是最优权重。验证输出会给出每个类别的mAP50和mAP50-95第一件事就是看交通标志这一类有没有明显掉队。3.3 调参的三种常见策略类别权重、多尺度训练、置信度阈值如果跑出来的逐类 mAP 差距很大需要针对性调参。我在交通检测项目里的调参顺序通常分三步。第一步是类别加权。YOLOv8 没有直接的cls_loss权重参数但可以通过采样变相实现统计每个类别的框数对稀有类别所在的图像复制几份再写入新目录。脚本逻辑不复杂就是把 labels 里含traffic_sign这类目标的 txt 找出来把对应的 jpg 和 txt 复制多份。注意复制后图片不能重名否则数据加载会串。第二步是多尺度训练。把imgsz改成[960, 1280]让模型在小图和大图之间交替训练。本质是在模拟摄像头在不同距离下的观察——近处的标志大、远处的小多尺度能缓解距离变化带来的尺度跨度问题。代价是训练时间增加约 20%但对交通标志这类尺度跨度极大的目标收益非常明显。第三步是推理阈值。训练完的模型默认置信度阈值 0.25在交通场景里不算最优。实车测试时如果发现远距离标志漏检把conf降到 0.1 或 0.15如果误检太多提到 0.4 以上。这个动作不影响权重只改推理参数但在实车感知体验上的差异往往比换更大的模型更直接。4. 避坑标注噪声、小目标丢失与格式转换的五条踩坑记录4.1 现象验证 mAP 到 0.78实车跑一段路却误检出一堆假标志牌原因训练集和验证集划分时把同一路口的连续帧拆开了模型对场景过拟合对没见过的路口泛化差。同时训练集缺少负样本——也就是完全没有交通标志的路面背景图。解决按 3.1 的场景划分重新切数据再额外找一批不含任何目标物的背景图放进images/train每张背景图配一个空 txt 文件。空标注文件不是 bug是 YOLO 训练里重要的负样本机制它能让模型学会“没有标志就是没有标志”而不是靠场景记忆硬猜。4.2 现象mAP50 还不错但 mAP50-95 一直上不去小目标召回只有三成原因训练分辨率太低。远处交通标志在 640x640 输入下只占十几个像素特征提取时几次下采样就把目标抹掉了。解决把imgsz从 640 提到 1280必要时结合多尺度训练。另外检查数据增强里的mosaic参数——mosaic 拼接时小目标容易被其他图块挤压甚至被裁掉如果发现打开 mosaic 后小目标召回掉得很厉害可以把mosaic概率降到 0.5 或直接关掉再对比一轮。4.3 现象把 COCO json 转成 YOLO txt 后训练时报class index out of range原因COCO 的category_id不一定从 0 开始很多数据集从 1 开始有的在中间跳号。转换脚本里只搬了 bbox 坐标没有做old2new重映射导致标注里出现 7 或 14 这类超出类别数的 ID。解决转换时先读取categories数组用enumerate生成新索引。转换完成后抽查几个 txt 文件确认所有 class_id 都落在 0 到 N-1 之间。# 抽查所有 txt 里的最大类别 ID超过 N-1 就要回头查映射 awk {print $1} labels/train/*.txt | sort -n | tail -14.4 现象解压后labels/train文件数比images/train少训练时一直提示找不到标签原因部分标注文件在打包时遇到非法文件名如中文冒号、首尾空格没有正确写入 zip或者在解压时被安全软件拦截。这是最麻烦的一类问题。解决解压后立刻用命令对比两个目录的文件名集合找出缺失列表。如果缺失数量少直接删除对应图片前提是剩余图片量足够训练如果缺失超过 5%建议重新下载原包。diff \ (ls images/train | sed s/.jpg// | sort) \ (ls labels/train | sed s/.txt// | sort) | head -20diff的输出会列出只在某一侧存在的文件名能精确定位哪张图缺标签、哪个标签缺图片。4.5 现象训练 Loss 正常下降但验证集上每一轮的准确率忽高忽低原因这不是数据集质量问题而是划分方式的问题。纯随机切分会让验证集里某一类目标偶尔整段消失或翻倍验证集图片数量少、场景集中时两个 epoch 之间落在验证集上的目标分布差异大指标自然震荡。解决把验证集锁死改成按场景分组的固定文件列表并且验证集中每个类别至少要有 150 个标注框。如果数据量实在少可以用 K 折交叉验证但每一折也要按场景切不能随机切。5. 从数据集到自动驾驶决策评估、延迟下调和结构化输出5.1 分类型评估只看总 mAP 会让交通标志问题被平均掉总 mAP 高不等于能上车交通场景里每个类别的失效模式不一样。用下面的命令跑一次带 JSON 输出的验证yolo detect val \ datatraffic.yaml \ modelruns/traffic_bench/exp1/weights/best.pt \ conf0.25 \ save_jsonTrue输出会生成predictions.json里面每个检测框带category_id和score。按类别汇总重点看traffic_light和traffic_sign在 0.25 置信度下的召回率。这两类低于 0.5基本没有上车资格先回去补样本而不是换模型。5.2 决策延迟32.8毫秒需要什么条件“决策延迟32.8毫秒”换算过来接近 30 FPS 的推理节奏想在嵌入式设备或车载工控机上达到通常要做三件事模型导出为 FP16 或 INT8 精度、用 TensorRT 做优化、把推理输入分辨率锁在 960 而不是 1280。32.8 毫秒指的是从图像进入推理引擎到输出检测结果的时间包含 NMS 后处理不是纯 GPU 前向时间。实测时我会用trtexec接口交替测多次取中位数而不是看训练框架里的日志因为后者会吞掉不少预处理开销。5.3 结构化输出检测框如何变成自动驾驶决策消息检测结果最终要交给融合模块或决策模块格式要标准化。我惯用一个简单的 JSON 结构作为中间消息{ timestamp: 1712372948123, objects: [ {class: car, box: [320, 240, 640, 480], score: 0.92, distance_m: 18.5}, {class: traffic_sign, box: [800, 100, 860, 180], score: 0.74, distance_m: 42.0} ] }distance_m由相机内参和检测框底边像素位置换算行业里通常接一个基于平面假设的单目测距模块精度不追厘米级0 到 60 米范围误差控制在 10% 以内就能满足大部分辅助决策场景。检测框坐标用像素值还是归一化值要和你下游模块对齐别在集成时才发现单位不一致。最后说一个老兵的习惯数据集的 zip 不只是训练素材。我会给每个版本的 zip 建一个校验和清单连同标注修改变动记录存进 git。将来模型在某个地区大规模误检时能回头定位是数据变了还是代码变了。这个习惯帮我省掉过不止一次“玄学翻车”的排查时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表