底特律街景六分类数据集:YOLO训练前的数据校验与预处理指南
简介底特律街景目标检测数据集面向计算机视觉与目标检测初学者及算法工程师提供已按YOLO标注格式整理好的街景数据共6个类别汽车、交通标志、车道线、行人、摩托车手与骑行者。数据集划分为训练集、验证集和测试集无需清洗或格式转换即可直接投入模型训练与评测。资源包共有2000个文件其中标签txt文件1999个另有1个可视化脚本show.py可对单张图片绘制边界框并保存方便快速检查标注效果整体压缩包约472.5MB。已有143人浏览学习。除分类与划分信息外资源还附带类别txt文件便于配置模型类别映射。下载后既能获得标准格式的监督数据也能借助脚本直观验证标注质量适合用于街景场景下的目标检测实验、课程设计或算法对比。1. 底特律街景六分类数据集先看到 txt 里的坑再谈 YOLO 训练拿到一份“底特律街景目标检测数据集”多数人第一反应是直接丢给 YOLOv8 开练。实际拆开看六个类别里既有宽度只有几像素的车道线又有摩托车手与骑行者的细粒度区分这种类别组合让训练时的锚框分配和损失计算比常见的 80 类 COCO 更敏感。项目本身给的是 YOLO 格式 txt不需要像 VOC 那样做标注转换但正因为“看似不需要额外处理”类别顺序、归一化坐标、train/val/test 拆分这三件事才更容易被当作已就绪而跳过。这篇博文用可复现的代码和数据校验思路把这条链路过一遍适合拿它跑通检测流程、做数据质量审计的工程师也适合想了解街景多类别互相遮挡场景的算法新人。2. 标签结构与 YOLO 坐标体系解析 txt 之前先统一数据口径2.1 六分类的类别规划细粒度类别增加彼此遮挡难度底特律街景数据集的类别清单是汽车、交通标志、车道线、行人、摩托车手、骑行者。前两类是常规街景目标后面四类的问题在第 2.3 节会体现出来行人与摩托车手、骑行者经常同时出现在同一路面区域边界框的交叠比例很高车道线又是长条形的细线目标在 YOLO 这类基于矩形锚框的检测器里width 和 height 的比值与其他类别相差一个数量级。如果项目里带一份classes.txt建议第一件事就是把它读出来按行号和训练配置里的names对一下。我一般会执行cat classes.txt看原始顺序而不是想当然认为“摩托车手”排在第 4 位。YOLO 的 txt 标签第一列就是类别 id从 0 开始改顺序等于把所有标签重新映射一遍这是最容易引入隐性错误的地方。下面的表格是这份数据集常见的一种类别顺序实际以你拿到的classes.txt为准。class id标签原文中文含义直观几何特征0car汽车矩形宽高比约 1.2~2.01traffic_sign交通标志小目标通常 64×642lane_line车道线细长w/h 可能大于 103pedestrian行人竖长条宽高比约 0.3~0.54motorcyclist摩托车手中等尺寸常与车混叠5rider骑行者与行人尺寸接近难区分2.2 用 Python 快速解析单条 txt坐标是否越界一眼可见YOLO 每行标签的格式是class_id x_center y_center width height坐标全部归一化到 0~1。所谓“无需额外处理”只代表不需要转成 COCO 或 VOC但不代表不需要校验。拿项目里的0453.txt这类文件我会先用下面这段代码看前几条import sys def inspect_label(txt_path: str, classes_path: str classes.txt) - None: with open(classes_path, r, encodingutf-8) as f: classes [line.strip() for line in f if line.strip()] with open(txt_path, r, encodingutf-8) as f: lines [line.strip().split() for line in f if line.strip()] print(f共检测到 {len(lines)} 个目标) for idx, item in enumerate(lines[:5]): cls_id int(float(item[0])) x_c float(item[1]) y_c float(item[2]) w float(item[3]) h float(item[4]) print(fid{cls_id:2d} cls{classes[cls_id]:14s} fcenter({x_c:.3f},{y_c:.3f}) fsize({w:.3f},{h:.3f})) if not (0 x_c 1 and 0 y_c 1 and w 0 and h 0): print( - 坐标异常标签需要检查) if __name__ __main__: inspect_label(sys.argv[1])这段代码先把类别名称读成列表再按行切分标签内容。之所以cls_id用int(float(item[0]))是因为有些标注工具会导出1.000000这种带小数点格式直接强转会抛ValueError。classes[cls_id]用来把数字 id 还原成可读文本避免可视化时只画出一个不知道是什么类别的框。2.3 坐标合法性检查先批量扫描再相信训练流程单张 txt 检查不能代表整个数据集。底特律街景这类由视频帧抽帧得到的数据容易出现三类问题抽帧时重复帧导致同一目标被标注两次边缘目标被裁成极小框少数 txt 文件行末混入不可见字符。处理方式是在第 2.2 节基础上加一个批量扫描器统计尺度异常与越界框。检查项正常范围建议处理class_id0 到类别数-1超范围直接丢弃该行x_center / y_center0~1越界时保留但需人工复核width / height 0 且不超过 1等于 0 的行删掉w/h 比值车道线可能 10检查是否为标注重叠导致文件名重复唯一train 与 val 不能出现同名图片这一轮的常见误用是直接跑yolov8 train训练脚本会把非法标签报成 warning但大量 warning 会被日志淹没。与其事后在训练曲线里猜不如用 5 行代码提前把异常数量算清楚。等批量扫描通过再进入下一节的目录整理和训练列表生成。3. 1575/450/225 三份拆分的工程化让 YOLO 训练直接吃到数据3.1 从平铺文件到标准目录images 与 labels 的镜像关系该项目给出的结构是图片和同名 txt 共同存在训练集 1575 张、验证集 450 张、测试集 225 张。很多 YOLO 训练脚本默认从images/train读图再从同路径替换后缀找标签如果直接把 txt 和图片堆在同一个目录需要临时写脚本把路径映射出来。我建议先按下面的目录结构整理detroit_street/ ├── images/ │ ├── train/ # 1575 张图片 │ ├── val/ # 450 张图片 │ └── test/ # 225 张图片 ├── labels/ │ ├── train/ # 1575 个 txt │ ├── val/ # 450 个 txt │ └── test/ # 225 个 txt ├── classes.txt └── data.yaml假设原来已经按train/val/test分好目录只是混着图片和 txt可以用脚本按扩展名分开。这里的关键点是labels 子目录必须与 images 子目录保持同名YOLO 训练时在data.yaml里只写 images 路径标签路径由训练器自动推断目录名不一致会直接导致 0 张图片参与训练。3.2 生成训练列表把绝对路径写进 txt 更省心训练和验证图片路径可以写在一个.txt清单文件里。用 Python 生成最稳妥命令如下python make_path_list.py \ --root /data/detroit_street \ --split train,val,test对应的make_path_list.py核心部分import glob import os def build_lists(root: str, splits: list[str]) - None: for split in splits: image_dir os.path.join(root, images, split) image_paths sorted(glob.glob(os.path.join(image_dir, *.jpg)) glob.glob(os.path.join(image_dir, *.png))) list_path os.path.join(root, f{split}.txt) with open(list_path, w, encodingutf-8) as f: f.writelines(p \n for p in image_paths) print(f{split}: {len(image_paths)} images - {list_path}) if __name__ __main__: import argparse parser argparse.ArgumentParser() parser.add_argument(--root, typestr, requiredTrue) parser.add_argument(--splits, typestr, defaulttrain,val) args parser.parse_args() build_lists(args.root, args.splits.split(,))glob同时匹配 jpg 和 png避免漏掉不同后缀的街景帧图。sorted是为了让同一段连续帧在列表里相邻便于后续抽样检查。生成的train.txt、val.txt、test.txt可以直接供 YOLOv5 训练脚本使用也可以作为划分依据再喂给 PyTorch 的Dataset。3.3 用统计来验证拆分比例总数不能只等于 2250摘要给出训练、验证、测试图片数总和是 2250 张总大小 475MB平均每张图约 210KB这个量级跟行车记录仪或监控抽帧的 JPG 接近。如果整理完目录后统计数对不上常见原因是有损坏图片或者 txt 与图片不同名。import os def check_split(root: str, split: str) - None: img_dir os.path.join(root, images, split) lab_dir os.path.join(root, labels, split) imgs {os.path.splitext(f)[0] for f in os.listdir(img_dir)} labs {os.path.splitext(f)[0] for f in os.listdir(lab_dir)} print(f{split}: images{len(imgs)} labels{len(labs)} fmissing_label{len(imgs - labs)}) for s in [train, val, test]: check_split(/data/detroit_street, s)这个脚本统计的是“无标签图片数”。如果missing_label不为 0建议先把这些图片筛出去而不是让训练脚本自动跳过。训练脚本确实能跳过错漏但跳过的图片会让同一轮 epoch 的实际 batch 数不稳定影响学习率调度策略。4. show.py 的框可视化逻辑从归一化坐标到像素边界框4.1 单张可视化脚本的通用实现输入图片、读取同名 txt、保存结果项目提供一个show.py用法是“传入一张图片即可绘制边界框并保存在当前目录”。很多街景数据集的上手验证都靠这类脚本它的核心逻辑只有三件事读取图片尺寸把归一化坐标换算成像素用 OpenCV 画矩形。python show.py /data/detroit_street/images/val/0453.jpg下面是我认为一个可读性更好的show.py实现import os import sys import cv2 def visualize(image_path: str, classes_path: str classes.txt) - str: label_path os.path.splitext(image_path)[0] .txt if not os.path.exists(label_path): raise FileNotFoundError(f找不到标签: {label_path}) with open(classes_path, r, encodingutf-8) as f: classes [line.strip() for line in f if line.strip()] img cv2.imread(image_path) if img is None: raise ValueError(fOpenCV 无法读取图片: {image_path}) h, w img.shape[:2] with open(label_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(float(parts[0])) x_c, y_c, box_w, box_h map(float, parts[1:]) x1 int((x_c - box_w / 2) * w) y1 int((y_c - box_h / 2) * h) x2 int((x_c box_w / 2) * w) y2 int((y_c box_h / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) label classes[cls_id] if cls_id len(classes) else str(cls_id) cv2.putText(img, label, (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) output os.path.join(os.getcwd(), fannotated_{os.path.basename(image_path)}) cv2.imwrite(output, img) print(f可视化结果: {output}) return output if __name__ __main__: visualize(sys.argv[1])这段代码里的坐标换算公式是关键x1 (x_c - box_w / 2) * w是把归一化中心点转成像素左上角而不是直接拿x_c * w当左上角。最容易画错的地方就是忘记除以 2 或忘记乘以图片真实宽高导致框整体偏移半个目标。4.2 可视化时不显示任何框八成不是绘图问题是标签路径问题实操中遇到“图能打开但没有任何框”的情况我通常按下面这个顺序排查。现象可能原因验证方法输出图片存在但全空白txt 路径拼接错误手动打印label_path部分图片无框该图确实没有目标检查 txt 文件字节数是否为 0框描在错误位置图片与 label 不是同一帧用第 2.2 节的inspect_label输出坐标颜色完全不可见图片底色和绿色相近临时把矩形颜色改成(0,0,255)每次可视化前都检查label_path是否存在是成本最低的防御。尤其项目文件名是0453.txt、0452.txt这种数字命名如果图片是0453.jpg可以直接os.path.splitext替换后缀如果图片是0453.jpeg但标签是0453.txt同一套splitext也能正确工作。4.3 从单张到批量抽看 val 集 16 张图判断标注一致性单张可视化可以验证单个文件但无法发现整体标注风格问题。批量思路是随机抽一个 split 里的图片把可视化结果归档到一个目录再逐个看。import os import random from show import visualize random.seed(42) # 固定随机种子保证抽查结果可复现 val_images os.listdir(/data/detroit_street/images/val) sample random.sample(val_images, 16) for name in sample: visualize(os.path.join(/data/detroit_street/images/val, name))固定种子是工程里的习惯。后面如果重新做数据清洗只要保持random.seed(42)就能复现同一批抽查图方便比较清洗前后的标注差异。16 张图虽然样本量不大但足够看出边界框是否紧贴目标轮廓尤其是车道线的框是不是被拉得过长、是否把一段虚线车道线标成一整条。5. 训练前最后一步类别分布统计与基于数据集的 YAML 配置5.1 把六分类写进 YOLO 训练配置数据文件的正确姿势YOLOv8 或者 YOLOv5 都通过一个瘦 YAML 文件描述数据路径和类别名这里需要让names的顺序和classes.txt完全一致。底特律街景数据集的配置可以写成train: /data/detroit_street/train.txt val: /data/detroit_street/val.txt test: /data/detroit_street/test.txt nc: 6 names: 0: car 1: traffic_sign 2: lane_line 3: pedestrian 4: motorcyclist 5: rider训练命令常见做法是yolo detect train \ data/data/detroit_street/data.yaml \ modelyolov8s.pt \ imgsz640 \ epochs100 \ batch16这里imgsz640对大部分街景目标够用但如果统计发现交通标志这类小目标占比很高我会把输入尺寸提到 960 或 1280。提到 1280 直观上能提升 mAP代价是训练显存和推理延时同时上升所以要先用第 2.3 节统计小目标占比再做决定。5.2 类别不平衡在训练损失里的表现不只是采样问题用上面的配置直接训练loss 下降曲线通常会显得“正常”因为背景占绝大多数检测 loss 被大目标主导。这个数据集特别的地方在于lane_line的目标宽度常常只有 3~10 像素归一化后的面积可能不足 0.001而motorcyclist和rider在空间上高度重合一个骑手旁边几乎必然有一辆摩托车或不远处有一辆汽车。类别建议关注指标可能出现的现象caroverall mAP最容易训练收敛快traffic_signsmall object mAP漏检多需要更高输入分辨率lane_linerecall框的 IoU 对位置极其敏感pedestrianprecision容易与骑行者混淆motorcyclistconflict with rider两个类别相互抢占正样本针对这个结构我一般会先把mosaic留在训练中段开启前 10 个 epoch 用小 mosaic 让模型稳定学到车和行人的基础特征后 90 个 epoch 再切默认增强。这样处理比直接调loss_scale更可控也能避免车道线类细长目标在随机裁剪中被切成无意义的短线段。5.3 一个实用技巧用标签统计脚本生成类别分布报告把类别分布打印成报告后你会在训练前就知道瓶颈。下面的脚本统计所有 split 的类别数量并输出每个类别的占比import glob import os from collections import Counter def category_report(labels_root: str, classes_path: str) - None: with open(classes_path, r, encodingutf-8) as f: classes [line.strip() for line in f if line.strip()] counter Counter() total_boxes 0 for txt_path in glob.glob(os.path.join(labels_root, **, *.txt), recursiveTrue): with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 5: continue counter[int(float(parts[0]))] 1 total_boxes 1 print(f总框数: {total_boxes}) for cid, count in counter.most_common(): name classes[cid] if cid len(classes) else funknown_{cid} print(f{cid:2d} {name:14s} {count:6d} {count / total_boxes * 100:5.1f}%) category_report(/data/detroit_street/labels, classes.txt)看到占比后如果traffic_sign只有几百个框就不值得单独换大模型先把该类别在训练集中重复采样几轮更实际。等报告里出现占比特别低的类别再回头核对这类框是不是被标注成了临近类别。这一步做完数据本身的可用性就完全掌握在你手里后面的 mAP 指标波动也更容易定位到模型而不是数据。本文还有配套的精品资源点击获取