简介:面向目标检测与交通安全应用场景,该数据集收录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张VOC+YOLO双格式数据集,正是为这类场景准备的——图片直接从事故监控或行车记录仪画面里截取,标注已经整理成目标检测能直接消费的格式。它适合两类人:一是准备用YOLO系列做车辆、事故场景检测,需要一份真实基线数据验证流程的从业者;二是想练手VOC与YOLO两种标注格式互转、并走完训练闭环的新手。1740张的量级不算大,但事故场景本身获取成本高,用它跑通流程、暴露问题、找出补数据的方向,是它最务实的价值。
2. 数据集内部结构:VOC与YOLO两张皮怎么组织,先搞清楚你手里有什么
2.1 解压后先看这三个目录
第一次做目标检测数据集处理的时候,最容易犯的错就是拿到压缩包直接解压、看两眼图片就开始写训练脚本。事故类数据集尤其不能这么干,因为它的目录结构往往同时包含两套标注,先花十分钟把文件组织摸清楚,后面省下的时间远不止十分钟。
这个数据集的最大特点是同时提供VOC和YOLO两套标注。VOC格式适合用LabelImg、Roboflow等工具二次编辑,也方便做可视化检查;YOLO格式是训练时直接喂给ultralytics系列框架的格式。常见的打包目录结构如下:
traffic_accident/ ├── VOC/ │ ├── JPEGImages/ # 1740 张原图,jpg │ ├── Annotations/ # 同名 xml,VOC 标注 │ └── ImageSets/Main/ # 可选的 train/val 划分文件 └── YOLO/ ├── images/ # 与 VOC 相同的 1740 张原图 └── labels/ # 同名 txt,每行一个目标需要注意,不同版本的数据集打包习惯可能略有差异,有的会把两张皮合并成images/ + labels/ + annotations/三个平级目录,但核心规律不变:VOC那边一定有JPEGImages和Annotations,YOLO那边一定有images和labels。解压后先tree -d看目录,确认结构再往下走。
两套标注的对应关系是“同名不同后缀”:
| 路径 | 对应标注文件 | 内容 | 适合干什么 |
|---|---|---|---|
VOC/JPEGImages/*.jpg | VOC/Annotations/*.xml | 左上右下像素坐标 + 类别名 | 可视化、二次标注、格式转换 |
YOLO/images/*.jpg | YOLO/labels/*.txt | 类别id + 归一化中心点坐标 | 直接训练 |
2.2 事故场景截取的三个数据特征
标题里“事故场景截取”这五个字,决定了它和常规车辆检测数据集有本质差异。普通车辆数据集统计的是“路面上有多少辆车”,事故数据集统计的是“这辆事故车和普通车怎么区分、碎片在哪、行人有没有被遮挡”。我处理这类数据时通常会关注三个特征,它们直接决定后续训练参数怎么设。
第一个是小目标比例偏高。监控摄像头的架设高度决定了车辆在画面里占比不大,事故后的碎片、掉落物、甚至受伤倒地的人,在1080p画面里往往只有几十个像素。用默认的imgsz=640去训练,这些小目标会被缩到十几个像素,基本学不到有效特征。后面第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(parents=True, exist_ok=True) out_path = out_dir / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines), encoding="utf-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"被当成两种类别。
(xmin+xmax)/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(f"images/{label_path.stem}.jpg\n") print(f"train: {len(train_files)}, val: {len(val_files)}")固定seed=42很关键,事故数据集本身规模不大,不同划分之间的方差可能超过模型优化带来的提升。固定种子让每次对比实验结果可复现,不至于把数据划分的随机性当成算法改进。生成的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.jpg,YOLO最终拼接成/data/traffic_accident/images/xxx.jpg。如果两边路径写法不一致,训练会直接报Dataset not found,这个错误在事故数据集的打包迁移里极其常见。
训练命令如下:
cd /data/traffic_accident yolo detect train \ data=traffic_accident.yaml \ model=yolov8s.pt \ imgsz=1280 \ batch=8 \ epochs=100 \ patience=15 \ lr0=0.001 \ lrf=0.01 \ warmup_epochs=5 \ mosaic=0.5 \ mixup=0.0 \ hsv_h=0.0 \ hsv_s=0.3 \ hsv_v=0.3 \ seed=42 \ device=0这是我在事故场景数据上验证过能稳定跑完的命令组合。第一次运行时如果网络条件不好,可以先把yolov8s.pt下载好放到当前目录,YOLO会优先读取本地权重文件,避免训练中途因为权重下载失败卡住。
4.2 三项参数为什么必须调:imgsz、mosaic、学习率
命令里大多数参数保持默认,但以下三个是事故场景必须手动干预的,理由都在数据特征里。
第一,imgsz=1280。这是最重要的一项。监控画面里的事故车碎片可能只占图片面积的0.5%以下,用默认的640分辨率训练,这个小目标会被压缩成几个像素,无论模型怎么学都不可能学好。1280会让显存占用约为640时代的4倍,如果显卡只有6GB显存,降级方案是imgsz=960配合batch=8,再不行就换yolov8n预训练权重。目标检测数据集处理到这个阶段,分辨率直接决定小目标的天花板,这个钱不能省。
第二,mosaic=0.5,mixup=0.0。事故场景的目标本来就稀疏,mosaic把四张图缩小拼接,小目标进一步缩小;而mixup把两张图叠出半透明目标,对遮挡严重的场景来说是无中生有的噪声。我把mosaic保留在0.5而不是完全关闭,是保留一部分多尺度学习能力;关闭mixup则是因为事故车和普通车在视觉上高度相似,叠加增强会削弱类别边界。
第三,lr0=0.001而不是默认的0.01。1740张数据属于典型的小数据集,微调预训练权重时学习率太大会让骨干网络的权重被大步长更新冲垮。我见过从预训练权重直接开训、第一个epoch loss直接飙到几位数然后nan的情况,把初始学习率降到0.001并配合warmup_epochs=5,基本可以避免。
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默认的patience=15等的是验证集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不错,换一个路口漏检一半——场景过拟合
你可能遇到这种情况:验证集上mAP@0.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(f"YOLO/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], outline="red", width=3) draw.text((x1, y1 - 12), label_map[int(cls_id)], fill="red") img.save(f"check/{label_path.stem}.jpg")这段代码不参与训练,但它能在两分钟内告诉你坐标转换是否翻车。如果框和目标总是对不齐,优先怀疑get_image_size里读的尺寸和实际图片不一致。
5.4 现象:开启mosaic后碎片类精度不升反降——增强强度反噬
默认配置下YOLOv8会开启mosaic=1.0,即每张训练图都有概率由四张图拼接而成。在常规车辆数据集上这个增强能显著提升精度,但在事故碎片这类小目标上可能适得其反。
原因在于mosaic的原理是先把四张图缩小再拼成大图,缩小后的碎片可能从50像素缩到12像素,特征几乎完全消失。模型被强迫在几乎没有有效信息的情况下学习“识别碎片”,反而削弱了对原始尺度目标的敏感度。
我的做法是:mosaic=0.5保留一定概率的多尺度上下文,copy_paste=0.2模拟目标重叠,mixup=0.0。如果你发现碎片类AP仍然很低,还可以把碎片图片单独做两倍复制放进训练集——不改变标注的复制粘贴增强,让模型有机会多看几遍这些小目标。
5.5 现象:从预训练权重微调第一轮就nan——学习率太大了
在1740张小数据集上微调yolov8s.pt,最忌讳的是直接用超参数默认值。YOLOv8默认初始学习率0.01是针对完整数据集从头训练设计的,预训练权重已经收敛过了,大步长更新会让骨干网络的统计量被破坏,表现为第一个epoch的loss值异常高,接着很快跑出nan。
如果你已经遇到这个问题,不要怀疑数据或代码,先做三件事:把lr0改为0.001,lrf改为0.01,warmup_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, imgsz=1280, conf=0.25, verbose=False, device=0)[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,统计完每类目标数再动训练脚本——顺序一旦搞反,后面就全是调参玄学。这套流程走下来,至少能让你少走一遍我走过的弯路,希望帮到你。
本文还有配套的精品资源,点击获取