ARTICLE DETAIL

资讯详情

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

1740张事故场景数据集:VOC转YOLO与YOLOv8训练全流程

1740张事故场景数据集:VOC转YOLO与YOLOv8训练全流程 简介面向目标检测与交通安全应用场景该数据集收录1740余张事故现场截图采用Pascal VOC与YOLO双格式标注可直接用于YOLO系列、Faster R-CNN等模型的交通事故检测训练与验证。压缩包共2000个文件以1741个xml标注文件、259个txt文件为主要构成xml记录矩形框位置与类别“Accident”txt提供YOLO归一化坐标原始jpg图片与标注一一对应省去格式转换与数据清洗环节。资源整体约99.91MB全部标注集中于1个类别共1933个事故框标注工具为labelImg格式统一便于划分训练集、验证集及做数据增强。已有402人学习下载适合算法学习者、竞赛选手及安防监控、自动驾驶研究者快速获取高质量事故样本用于模型效果评估、调优和落地验证无论是快速验证检测算法还是扩充事故场景训练样本均可在此基础上直接开展。1. 1740张事故场景截取图能把交通事故目标检测模型训到什么程度交通事故检测这个方向模型结构从来不是最大瓶颈真正卡人的是数据事故车、散落碎片、不规则遮挡、夜间监控画面这些目标既稀疏又不规整公开数据集里能直接落到训练场景的更少。这个1740张VOCYOLO双格式数据集正是为这类场景准备的——图片直接从事故监控或行车记录仪画面里截取标注已经整理成目标检测能直接消费的格式。它适合两类人一是准备用YOLO系列做车辆、事故场景检测需要一份真实基线数据验证流程的从业者二是想练手VOC与YOLO两种标注格式互转、并走完训练闭环的新手。1740张的量级不算大但事故场景本身获取成本高用它跑通流程、暴露问题、找出补数据的方向是它最务实的价值。2. 数据集内部结构VOC与YOLO两张皮怎么组织先搞清楚你手里有什么2.1 解压后先看这三个目录第一次做目标检测数据集处理的时候最容易犯的错就是拿到压缩包直接解压、看两眼图片就开始写训练脚本。事故类数据集尤其不能这么干因为它的目录结构往往同时包含两套标注先花十分钟把文件组织摸清楚后面省下的时间远不止十分钟。这个数据集的最大特点是同时提供VOC和YOLO两套标注。VOC格式适合用LabelImg、Roboflow等工具二次编辑也方便做可视化检查YOLO格式是训练时直接喂给ultralytics系列框架的格式。常见的打包目录结构如下traffic_accident/ ├── VOC/ │ ├── JPEGImages/ # 1740 张原图jpg │ ├── Annotations/ # 同名 xmlVOC 标注 │ └── ImageSets/Main/ # 可选的 train/val 划分文件 └── YOLO/ ├── images/ # 与 VOC 相同的 1740 张原图 └── labels/ # 同名 txt每行一个目标需要注意不同版本的数据集打包习惯可能略有差异有的会把两张皮合并成images/ labels/ annotations/三个平级目录但核心规律不变VOC那边一定有JPEGImages和AnnotationsYOLO那边一定有images和labels。解压后先tree -d看目录确认结构再往下走。两套标注的对应关系是“同名不同后缀”路径对应标注文件内容适合干什么VOC/JPEGImages/*.jpgVOC/Annotations/*.xml左上右下像素坐标 类别名可视化、二次标注、格式转换YOLO/images/*.jpgYOLO/labels/*.txt类别id 归一化中心点坐标直接训练2.2 事故场景截取的三个数据特征标题里“事故场景截取”这五个字决定了它和常规车辆检测数据集有本质差异。普通车辆数据集统计的是“路面上有多少辆车”事故数据集统计的是“这辆事故车和普通车怎么区分、碎片在哪、行人有没有被遮挡”。我处理这类数据时通常会关注三个特征它们直接决定后续训练参数怎么设。第一个是小目标比例偏高。监控摄像头的架设高度决定了车辆在画面里占比不大事故后的碎片、掉落物、甚至受伤倒地的人在1080p画面里往往只有几十个像素。用默认的imgsz640去训练这些小目标会被缩到十几个像素基本学不到有效特征。后面第4章里我会把imgsz提到1280就是为了照顾这部分目标。第二个是遮挡严重。事故发生后几辆车挤在一起前车挡住后车的半个车身是常态。VOC标注里这类目标往往是一个不准确的矩形框框内混着大量背景。模型训练时会被这些背景信息干扰所以后面的训练配置里我不会开太强的mosaic增强——把四张图拼一起会让遮挡关系变得更混乱。第三个是类别数量和分布严重不均。正常车辆可能占标注总量的一半以上而“碎片”“事故车”这类关键类别可能只有几百个框。这意味着仅用mAP评估容易被多数类绑架必须单独看稀有类别的召回率。2.3 标注质量预览脚本统计类别数量与框大小分布在动手转换格式之前先跑一个统计脚本看看你手里这份数据到底有多少个类别、每类多少框、大小目标占比如何。这一步不需要训练两分钟就能给出后面所有参数决策的依据。# inspect_data.py —— 统计每类目标数量与框大小分布 import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter class_counter Counter() size_counter Counter() for xml_path in Path(VOC/Annotations).glob(*.xml): root ET.parse(xml_path).getroot() img_w int(root.findtext(size/width)) img_h int(root.findtext(size/height)) for obj in root.iter(object): name obj.findtext(name).strip() box obj.find(bndbox) bw float(box.findtext(xmax)) - float(box.findtext(xmin)) bh float(box.findtext(ymax)) - float(box.findtext(ymin)) area (bw * bh) / (img_w * img_h) class_counter[name] 1 size_counter[小(1%) if area 0.01 else 中(1%-25%) if area 0.25 else 大(25%)] 1 for name, cnt in class_counter.most_common(): print(f{name}: {cnt}) print(\n框面积占比分布:, dict(size_counter))这段代码的逻辑很简单遍历每个XML读取对象名用bndbox的宽度和高度除以图片宽高得到面积占比按“小、中、大”三档归类。root.iter(object)比findall更稳妥能处理嵌套层级异常的情况。脚本输出的信息量很大。如果“碎片”这类关键类别只有两三百个框那你后续训练必须给小目标单独加权重或做离线增广如果97%的框都是大目标imgsz用640也没问题。我见过不止一个团队跳过这一步直接训练结果loss降得很漂亮一看混淆矩阵稀有小类全部漏检——这种翻车纯属可以避免。3. VOC转YOLO转换脚本坐标归一化的四个边界坑与可直接复用的代码3.1 VOC与YOLO的坐标表示差异一个像素坐标一个归一化中心点VOC标注文件里每个目标的坐标是bndbox节点下的xmin、ymin、xmax、ymax单位是像素原点在图片左上角。YOLO格式则完全不同每一行由类别id、x_center、y_center、width、height五个数值组成且后四位全部归一化到0到1之间中心点坐标系。转换公式写出来很直观x_center ((xmin xmax) / 2) / image_width y_center ((ymin ymax) / 2) / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height公式只有四行但落地时坑不少。第一个坑是除错分母——XML的size节点里存的宽高虽然看起来靠谱但有时和真实图片尺寸不一致特别是经过压缩或裁剪的数据集除出来的坐标整体偏移训练时模型看到的框和实际目标对不上。第二个坑是坐标退化——xmax xmin或ymax ymin的标注框会算出负宽度YOLO训练时会直接崩溃或产生NaN。第三个坑是目标被图片边缘截断——事故场景里车辆冲出画面很常见标注框可能超出图片边界归一化后坐标大于1虽然YOLO勉强能读但会引入脏数据。第四个坑是类别名不干净——XML里的name可能有首尾空格也可能同一个类在部分文件里叫accident_car、部分叫accident car转换时必须统一处理。3.2 voc2yolo.py转换脚本及参数说明下面这个脚本是转换的核心我在多个数据集上复用过大半年针对上面四个坑都做了兜底处理。# voc2yolo.py —— 批量转换VOC标注为YOLO格式 import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image # 类别映射表以你解压后看到的 classes.txt 或统计结果为准 LABEL_MAP { accident_car: 0, normal_car: 1, person: 2, debris: 3, } def get_image_size(xml_path: Path): 优先读XML里的size异常时回退到真实图片尺寸 root ET.parse(xml_path).getroot() size root.find(size) if size is not None: w int(size.findtext(width, 0)) h int(size.findtext(height, 0)) if w 0 and h 0: return w, h # 兜底用PIL打开真实图片读宽高 img_path xml_path.parent.parent / JPEGImages / (xml_path.stem .jpg) img Image.open(str(img_path)) return img.size # (width, height) def convert_one(xml_path: Path, out_dir: Path): root ET.parse(xml_path).getroot() img_w, img_h get_image_size(xml_path) lines [] for obj in root.iter(object): name obj.findtext(name, ).strip().replace( , _) if name not in LABEL_MAP: print(f[skip] {xml_path.name}: 未知类别 {name}) continue box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) # 边界1退化框直接丢弃 if xmax xmin or ymax ymin: print(f[drop] {xml_path.name}: 空框或负宽高) continue # 边界2截断到图片边界防止归一化后坐标溢出 xmin max(0.0, xmin) ymin max(0.0, ymin) xmax min(img_w, xmax) ymax min(img_h, ymax) cx ((xmin xmax) / 2) / img_w cy ((ymin ymax) / 2) / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h lines.append(f{LABEL_MAP[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) if lines: out_dir.mkdir(parentsTrue, exist_okTrue) out_path out_dir / (xml_path.stem .txt) out_path.write_text(\n.join(lines), encodingutf-8) if __name__ __main__: xmls sorted(Path(VOC/Annotations).glob(*.xml)) for xml_path in xmls: convert_one(xml_path, Path(YOLO/labels)) print(f转换完成共处理 {len(xmls)} 个XML)脚本里值得注意的参数逻辑get_image_size里我先尝试读取XML内的size节点再用真实图片兜底。实际使用中我把这段逻辑反过来用——先跑一遍纯XML转换再用PIL验证50张图如果发现偏移就把兜底路径强制打开优先用真实尺寸。LABEL_MAP的类别映射一定要先跑第2章的统计脚本把XML里实际出现的类别名列出来再填。分类别名里replace( , _)是为了防止accident car和accident_car被当成两种类别。(xminxmax)/2的除法全程使用浮点不要在中间步骤转成int否则小目标框的中心点可能偏移一两个像素影响不大但没必要。3.3 85/15拆分固定种子划分train.txt与val.txt转换完成后需要把图片路径列表拆成训练集和验证集。注意这个拆分必须在image级别做而不是XML级别因为模型读取的是图片路径。用85/15而不是更激进的90/10是因为1740张规模下留出约260张做验证足够看出模型是否过拟合验证集太少会让早停和模型选择都变成玄学。# split.py —— 按文件列表划分训练集与验证集 import random from pathlib import Path random.seed(42) # 固定随机种子保证每次划分结果一致 files sorted(Path(YOLO/labels).glob(*.txt)) random.shuffle(files) split_idx int(len(files) * 0.85) train_files, val_files files[:split_idx], files[split_idx:] for subset_name, subset in [(train.txt, train_files), (val.txt, val_files)]: with open(subset_name, w) as f: for label_path in subset: f.write(fimages/{label_path.stem}.jpg\n) print(ftrain: {len(train_files)}, val: {len(val_files)})固定seed42很关键事故数据集本身规模不大不同划分之间的方差可能超过模型优化带来的提升。固定种子让每次对比实验结果可复现不至于把数据划分的随机性当成算法改进。生成的train.txt和val.txt里写的是相对路径因为后面data.yaml会指定数据集根目录避免在代码和配置文件里出现大量绝对路径。4. 用YOLOv8训练自己的事故数据集能一次跑完的最小命令与三项必调设置4.1 data.yaml配置与一条训练命令训练前先写一份data.yaml告诉YOLO数据在哪、类别有几类。假如第2章统计出来的类别就是四个配置如下# traffic_accident.yaml path: /data/traffic_accident # 改成你本机的实际路径 train: train.txt val: val.txt names: 0: accident_car 1: normal_car 2: person 3: debristrain和val写的是相对路径相对于path指定的根目录。这里有个细节train.txt里的图片路径是相对path的所以train.txt中写images/xxx.jpgYOLO最终拼接成/data/traffic_accident/images/xxx.jpg。如果两边路径写法不一致训练会直接报Dataset not found这个错误在事故数据集的打包迁移里极其常见。训练命令如下cd /data/traffic_accident yolo detect train \ datatraffic_accident.yaml \ modelyolov8s.pt \ imgsz1280 \ batch8 \ epochs100 \ patience15 \ lr00.001 \ lrf0.01 \ warmup_epochs5 \ mosaic0.5 \ mixup0.0 \ hsv_h0.0 \ hsv_s0.3 \ hsv_v0.3 \ seed42 \ device0这是我在事故场景数据上验证过能稳定跑完的命令组合。第一次运行时如果网络条件不好可以先把yolov8s.pt下载好放到当前目录YOLO会优先读取本地权重文件避免训练中途因为权重下载失败卡住。4.2 三项参数为什么必须调imgsz、mosaic、学习率命令里大多数参数保持默认但以下三个是事故场景必须手动干预的理由都在数据特征里。第一imgsz1280。这是最重要的一项。监控画面里的事故车碎片可能只占图片面积的0.5%以下用默认的640分辨率训练这个小目标会被压缩成几个像素无论模型怎么学都不可能学好。1280会让显存占用约为640时代的4倍如果显卡只有6GB显存降级方案是imgsz960配合batch8再不行就换yolov8n预训练权重。目标检测数据集处理到这个阶段分辨率直接决定小目标的天花板这个钱不能省。第二mosaic0.5mixup0.0。事故场景的目标本来就稀疏mosaic把四张图缩小拼接小目标进一步缩小而mixup把两张图叠出半透明目标对遮挡严重的场景来说是无中生有的噪声。我把mosaic保留在0.5而不是完全关闭是保留一部分多尺度学习能力关闭mixup则是因为事故车和普通车在视觉上高度相似叠加增强会削弱类别边界。第三lr00.001而不是默认的0.01。1740张数据属于典型的小数据集微调预训练权重时学习率太大会让骨干网络的权重被大步长更新冲垮。我见过从预训练权重直接开训、第一个epoch loss直接飙到几位数然后nan的情况把初始学习率降到0.001并配合warmup_epochs5基本可以避免。4.3 训练收尾best.pt与last.pt怎么选早停怎么看训练结束后输出目录runs/detect/train/下有weights/best.pt和weights/last.pt两个文件。绝大多数情况下直接选best.pt它是验证集表现最好的权重训练过程中的早停机制也会确保它和最后一次权重不差太多。但这里有一个容易忽略的判断对比best.pt和last.pt的验证集loss曲线。如果两者差距很大说明训练后期过拟合这时候回看训练日志里的val/box_loss曲线。真正需要早停的节点不是train loss趋于平缓而是val loss开始上升的时刻。YOLO默认的patience15等的是验证集mAP但mAP在小数据集上波动大我一般会同时盯val/box_loss和val/cls_loss两个曲线任何一个连续5个epoch不降反升就该考虑停止。5. 事故数据集训练避坑Loss降了mAP却不涨的五个原因5.1 现象Loss降到0.05附近mAP仍在0.3徘徊——先查类别失衡这是训练事故数据最容易撞上的现象也是最典型的“目标检测模型微调崩了”场景。训练日志里cls_loss一路走低看起来一切正常但验证集里accident_car的AP接近0。原因通常不在模型而在数据分布正常车辆框基数太大模型把绝大多数学习能力用在了拟合多数类上碎片和事故车样本太少在随机梯度下降里每几个batch才能见到一次权重更新被正常车样本淹没。解决办法有三个层次最简单的是在训练命令里给稀有类开离线复制粘贴增广把碎片区域的像素随机贴到正常场景图里相当于人为扩充样本其次是训练前查看inspect_data.py的输出如果某个类别低于总数10%考虑做类别重采样最后才是改损失函数权重但不建议新手一上来就动这个因为YOLOv8的损失权重不是命令行参数需要改源码收益远不如前两种。5.2 现象验证集mAP不错换一个路口漏检一半——场景过拟合你可能遇到这种情况验证集上mAP0.5到0.78模型表现不错但拿一段新路口的视频去测正常车辆被漏检三分之一事故车几乎全丢。原因几乎可以确定是“场景过拟合”。事故数据集往往是从少数几个监控点位截取的同一视角、同一背景、同一时间段反复出现。如果第3章的划分方式是随机打乱同一个视频源的连续帧会同时出现在训练集和验证集里模型实际记住的是背景纹理而不是车辆特征验证集分数自然虚高。正确做法是按视频来源分组划分把属于同一段视频的连续帧视为一个整体整体划入训练集或验证集。如果你手里的数据集没有提供视频来源ID可以在解压后按文件修改时间或文件名前缀做粗分组。还有一个钝但有效的办法验证时直接用第6章的未见过视频做测试新视频mAP才是真实泛化水平。5.3 现象框全是对的转成YOLO后整体偏移——XML的size字段不可信第3章的转换脚本里我加了“优先读XML size、异常时回退真实图片尺寸”的逻辑这个坑我在真实数据集上踩过。某次转换后可视化训练样本发现所有框向右上方偏移了大约5%——排查半天发现XML里size写的是960 x 540而实际图片是1920 x 1080归一化时整个坐标系的尺度就错了。解决思路很简单转换后必须做可视化检查。脚本跑完用下面的代码把YOLO标注画回图片上随机看20张# visualize_check.py —— 随机抽20张图画框验证转换结果 from pathlib import Path import random from PIL import Image, ImageDraw label_map {0: accident_car, 1: normal_car, 2: person, 3: debris} files sorted(Path(YOLO/labels).glob(*.txt)) random.seed(1) random.shuffle(files) for label_path in files[:20]: img Image.open(fYOLO/images/{label_path.stem}.jpg) draw ImageDraw.Draw(img) w, h img.size for line in label_path.read_text().strip().splitlines(): cls_id, cx, cy, bw, bh map(float, line.split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) draw.rectangle([x1, y1, x2, y2], outlinered, width3) draw.text((x1, y1 - 12), label_map[int(cls_id)], fillred) img.save(fcheck/{label_path.stem}.jpg)这段代码不参与训练但它能在两分钟内告诉你坐标转换是否翻车。如果框和目标总是对不齐优先怀疑get_image_size里读的尺寸和实际图片不一致。5.4 现象开启mosaic后碎片类精度不升反降——增强强度反噬默认配置下YOLOv8会开启mosaic1.0即每张训练图都有概率由四张图拼接而成。在常规车辆数据集上这个增强能显著提升精度但在事故碎片这类小目标上可能适得其反。原因在于mosaic的原理是先把四张图缩小再拼成大图缩小后的碎片可能从50像素缩到12像素特征几乎完全消失。模型被强迫在几乎没有有效信息的情况下学习“识别碎片”反而削弱了对原始尺度目标的敏感度。我的做法是mosaic0.5保留一定概率的多尺度上下文copy_paste0.2模拟目标重叠mixup0.0。如果你发现碎片类AP仍然很低还可以把碎片图片单独做两倍复制放进训练集——不改变标注的复制粘贴增强让模型有机会多看几遍这些小目标。5.5 现象从预训练权重微调第一轮就nan——学习率太大了在1740张小数据集上微调yolov8s.pt最忌讳的是直接用超参数默认值。YOLOv8默认初始学习率0.01是针对完整数据集从头训练设计的预训练权重已经收敛过了大步长更新会让骨干网络的统计量被破坏表现为第一个epoch的loss值异常高接着很快跑出nan。如果你已经遇到这个问题不要怀疑数据或代码先做三件事把lr0改为0.001lrf改为0.01warmup_epochs加到5。然后看第一个epoch的loss数值正常情况下应该在前几个batch内明显下降而不是震荡。如果改完仍然nan检查数据里是否存在真空xml或全黑图片——YOLO对纯色输入同样可能产生梯度异常删掉这批样本再训。6. 验证的一小时清单置信度阈值、混淆矩阵与夜间片段测试训练完不等于能用事故检测模型尤其需要一套快速验收流程。我习惯花一小时按下面三个步骤做验证每一步都能发现不同性质的问题。先看混淆矩阵。runs/detect/train/confusion_matrix.png是第一手信息来源。很多人纠结矩阵每一行的总和为什么不是100%因为YOLO的矩阵是按类别单独归一化的行与行之间基数不同总和本来就不唯一这个不是bug。真正要看的是两个交叉格子accident_car被预测成background的比例——这是漏检background被预测成accident_car的数量——这是误检。事故场景里误检的代价可能也很大因为系统会把正常行驶的车当成事故车报警。再用一段没见过的视频做在线测试。从网上找一段夜间或雨天的行车记录仪视频跑下面的脚本统计每帧各类别出现次数# quick_video_test.py —— 统计新视频上各类别出现次数 from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) cap cv2.VideoCapture(test_clip.mp4) total {} while cap.isOpened(): ret, frame cap.read() if not ret: break res model.predict(frame, imgsz1280, conf0.25, verboseFalse, device0)[0] for box in res.boxes: cls_name res.names[int(box.cls)] total[cls_name] total.get(cls_name, 0) 1 cap.release() print(total)一个10秒的夜间事故片段如果碎片类别一帧都没出现说明小目标学习不足回到第4章把imgsz再往上提如果正常车被大量误报成事故车则要调高conf阈值从0.25提到0.4再看效果。最后一件事是固化验证集。我会把这份未参与训练的视频片段截取成200张图片单独存成hard_test/目录作为后续每次改模型、调参数的固定测试集。没有固定测试集任何一次参数调整的“看起来变好了”都可能是随机波动。我自己的习惯是拿到任何事故数据集先解压看classes.txt统计完每类目标数再动训练脚本——顺序一旦搞反后面就全是调参玄学。这套流程走下来至少能让你少走一遍我走过的弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表