YOLO数据集实用指南)
简介这是专为夜间场景目标检测设计的YOLO格式数据集覆盖行人、自行车、汽车、狗四个类别适合需要直接上手YOLOv5训练与改进实验的开发者。数据集已按照YOLOv5目录结构划分完成训练集有8410张图片和对应标签验证集有1457张图片和对应标签全部采用YOLO相对坐标标注并附有类别class文件可直接加载使用无需再做数据整理。整个压缩包共2000个文件主体为1999个txt标注文件另含1个Python可视化脚本7z格式总大小约507.97MB目录规整便于接入项目。目前已有627人学习下载。配套的可视化脚本可随机读取一张图片自动绘制边界框并保存无需改动即可运行有利于快速核对标注质量并理解x_centre、y_centre、w、h的坐标含义免去了自行标注、划分数据集和编写检验脚本的麻烦为夜间行人车辆检测研究提供了实用的数据基础。1. 夜间车辆行人检测4类一份划分好的数据集能省下什么一份划分好的 YOLO 数据集最值钱的往往不是图片本身而是藏在目录结构里的划分逻辑和标签约定。夜间车辆、行人检测4类这个标题里带上了类别 class 文件和可视化脚本它正好把从业者最容易卡住的三个环节一次性补齐可直接被 YOLO 读目录、写对的类别定义、以及用来核对标注质量的可视化手段。它适合正在做车载摄像头、路口安防或交通流统计的工程师也适合想用 YOLOv8 训练自己的数据、又不愿意从零标注一万张夜图的初学者。目标是让数据从解压到跑通训练控制在半天内而不是把时间耗在整理文件格式上。2. 4类目标与夜间难点为什么白天准的模型到晚上就失灵2.1 四类目标怎么定义car、truck、bus、person 的标签边界标着“4类”的数据集最常见的划分是 car、truck、bus、person 四类。car 通常覆盖轿车、SUV、MPV 和各种小型面包车truck 指中型以上货车、厢式货车和挂车bus 指公交、大巴和通勤客车person 则是行人骑自行车或电动车的人在多数交通数据集里也会视标注规范归入 person避免第5类出现。这样划分的好处是类别槽位与 COCO 前几类兼容可以直接用公开预训练权重初始化不用动检测头。坏处也明显类别太粗如果项目里需要区分“大型货车”和“小型货车”这份数据就不够用。拿到手先看 class 文件确认这4类的定义跟你业务里说的“车”是不是同一个东西否则后面训练和部署全对不上。夜间数据和白天数据的最大差别不在模型而在标签的可信度。白天一张图里什么车、什么位置的边界清清楚楚晚上同样的目标可能只露出一个轮廓或者被灯光完全吞掉。这就是为什么同等标注量下夜间的标注质量普遍比白天低模型训练完也更容易出现“白天能用、晚上失灵”的现象。所以标题里特意强调“夜间”两个字的包往往已经在数据收集侧把光照差异当作已知条件处理过了而不是简单拿白天数据充数。2.2 夜间数据到底难在哪眩光、过曝与远距离小目标夜间场景有三个高发问题会让检测模型集体翻车。第一个是眩光和光晕车灯、路灯直射下目标周围的对比度塌陷检测框要么框住半个光晕要么干脆丢掉远处的车。第二个是过曝相机自动增益为了把黑暗区域提亮会把近处车辆的轮廓直接打成一片白板原本清晰的边缘没了特征。第三个是远距离小目标行人离摄像头超过30米时夜间像素可能只有十几个白天能靠轮廓识别的信息晚上全混在噪点里。这三个问题属于图像本身的物理限制任何后处理都只能缓解不能解决。因此一份夜间数据集是否可靠要看标注者怎么处理这些模糊边界是宁可框大一点还是必须紧贴物体边缘。如果你拿到的可视化图片里发现同一类目标在不同图上紧松不一致那训练时 loss 会长时间降不下去别急着调模型先回来看标签。2.3 划分好的数据集结构一个能被 YOLO 直接读取的目录模板用 YOLO 系的框架训练时数据目录最好是 images 和 labels 平行放置train/val/test 各自独立。下面这个结构是常见做法也是检查一份划分好数据集的第一条标准datasets/ ├── train/ │ ├── images/ │ │ ├── night_001.jpg │ │ └── night_002.jpg │ └── labels/ │ ├── night_001.txt │ └── night_002.txt ├── val/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/图片和标签必须同名同前缀后缀可以一个 .jpg 一个 .txt但文件名主体不能差一个字符。标签文件里每一行对应一个目标格式是class_id x_center y_center width height五个值全部归一化到 01 之间坐标基于图片宽高计算。只要看到某一行有 6 个数字或者坐标明显大于 1基本都是坏标签。配合这套目录训练配置 data.yaml 长这样# data.yaml 与 class 文件共用同一套类别顺序 train: datasets/train/images val: datasets/val/images test: datasets/test/images nc: 4 names: 0: car 1: truck 2: bus 3: person注意 YOLO 只要求 train 和 val 存在test 是可选项如果数据包里没有单独的 test 目录训练时可以直接把 val 当验证集用但这样会略微高估指标。解析这份 yaml 时names 的 key 用 0 开始的整数而不是随意定的编号这点和 class 文件的行号是一套逻辑后面会细说。3. 写好 class 文件与 data.yaml标签顺序错一位整个训练白费3.1 class 文件的约定行号就是索引顺序不能错YOLO 数据集的 labels 里存的是数字编号真正的人类可读类名放在 class 文件里。对这个格式行号就是类别索引第一行对应 0第二行对应 1以此类推。所以classes.txt里写# classes.txt 每行一个类名行号就是标签索引 mkdir -p datasets cat datasets/classes.txt EOF car truck bus person EOF这里最常见的翻车是把 person 写在第一行标签里原来的 0 还是 person但 data.yaml 里 0 却对应 car模型训练等于把行人当成车来学。因为类名是人类读的编号是模型读的二者靠顺序绑定顺序错位不会报错只会让验证时的混淆矩阵看起来乱成一团。检查一份数据包时先做三件事打开 classes.txt 看行数是不是 4随机抽一个 labels 里的 txt看每行第一个数字是否都在 03 之间再看 data.yaml 的 names 顺序和 classes.txt 是否完全一致。三步没问题才轮到跑训练。3.2 标签合法性检查空文件、越界与错位一次查清光靠肉眼抽查不够划分好的数据集里也可能混入空标签和越界框。常见的坏标签有四种txt 文件是 0 字节某一行不足 5 个数类别编号超出 4 类框的归一化坐标越界。下面这个脚本直接对 train 标签目录跑一遍输出问题文件列表和类别分布。# check_yolo_labels.py # 用法: python check_yolo_labels.py --label_dir datasets/train/labels --img_dir datasets/train/images --classes 4 import os import argparse from collections import Counter import cv2 def main(): parser argparse.ArgumentParser() parser.add_argument(--label_dir, requiredTrue) parser.add_argument(--img_dir, requiredTrue, help图片目录用于换算像素坐标查越界) parser.add_argument(--classes, typeint, default4) args parser.parse_args() class_counter Counter() empty_files [] invalid_lines [] out_of_range [] for label_name in os.listdir(args.label_dir): if not label_name.endswith(.txt): continue label_path os.path.join(args.label_dir, label_name) if os.path.getsize(label_path) 0: empty_files.append(label_name) continue img_path None base label_name[:-4] for ext in [.jpg, .jpeg, .png, .bmp]: candidate os.path.join(args.img_dir, base ext) if os.path.exists(candidate): img_path candidate break if img_path is None: invalid_lines.append((label_name, image not found)) continue h, w cv2.imread(img_path).shape[:2] for line in open(label_path, r, encodingutf-8): parts line.strip().split() if len(parts) ! 5: invalid_lines.append((label_name, fbad line: {line})) continue cls int(parts[0]) cxp, cyp, wp, hp map(float, parts[1:]) if cls 0 or cls args.classes: invalid_lines.append((label_name, fclass {cls} out of range)) continue if wp 0 or hp 0 or not (0 cxp 1 and 0 cyp 1): invalid_lines.append((label_name, fbad box: {parts[1:]})) continue x1 (cxp - wp / 2) * w x2 (cxp wp / 2) * w y1 (cyp - hp / 2) * h y2 (cyp hp / 2) * h if x1 -1 or y1 -1 or x2 w 1 or y2 h 1: out_of_range.append((label_name, (round(x1), round(y1), round(x2), round(y2)))) class_counter[cls] 1 print(empty files:, len(empty_files)) for name in empty_files[:20]: print( , name) print(invalid lines:, len(invalid_lines)) for name, msg in invalid_lines[:20]: print( , name, msg) print(out of range:, len(out_of_range)) for name, box in out_of_range[:20]: print( , name, box) print(class distribution:, dict(sorted(class_counter.items()))) if __name__ __main__: main()这段脚本的逻辑是把归一化坐标乘回图片宽高允许 1 像素的浮点误差超得太多就计入 out_of_range。--img_dir参数必须给因为不查图片宽高就没法判断越界。实际跑的时候如果空文件只有一两个可以直接删除对应图片如果 invalid_lines 比例超过 1%说明这份数据包质量不行最好先联系提供方或补标而不是硬着头皮训练。3.3 data.yaml 与 class 文件的对应换模型不改类名顺序同一个数据集在 YOLOv5、YOLOv8 和 YOLO11 之间切换时data.yaml 的写法略有差异但 names 顺序必须原样保留。比如从 YOLOv8 切到 YOLO11只改模型配置data.yaml 直接复用。如果某些框架要求 names 写成列表形式就按names: [car, truck, bus, person]传框架内部会把这个列表的第 0 位当成类别 0。还有一个常被忽略的点预训练权重里的类别顺序和你的 data.yaml 不一致时加载权重会出错或只能加载 backbone 部分。COCO 预训练权重前四类是 person、bicycle、car、motorcycle和这里的 car、truck、bus、person 不对齐。常见做法是忽略预训练权重里类别不匹配的输出层只加载前面特征提取层的参数让新的检测头从头训。这个逻辑在 YOLOv8 里由框架自动处理但如果你用的是自己改过的模型就得手动检查加载日志里到底加载了多少层。4. 数据可视化脚本夜间图像看不清先提亮再画框4.1 直接把框画在夜间图上画了等于没画做数据可视化最直接的写法是读图、读标签、用 OpenCV 画矩形框但对夜间图片画完的预览图常常是一张黑底加几个绿框框里的目标轮廓根本看不出来。这是因为暗部像素集中在很窄的灰度区间人眼需要亮度和对比度才能分辨目标。直接把原始像素输出等于把标注质量检查变成一场视力测试。所以被标题里“数据可视化脚本”吸引来的人最需要的是一个能兼顾“真实显示”和“能看清目标”的预览工具而不是一个只画框的程序。做法是先做 gamma 提亮或 CLAHE 增强用于显示然后把框画在增强后的图上同时保留原图作为对照。这样既能看到标签位置是否合理又能凭原始曝光判断目标是否肉眼可辨。4.2 一个可复现的标注预览脚本gamma 提亮预览下面这个脚本按指定目录读取标签和图片对图像做 gamma 亮度调整再画类别框和类名最后输出到 preview 文件夹。gamma 小于 1 时暗部变亮适合夜间预览。# plot_yolo_labels.py # 用法: python plot_yolo_labels.py --img_dir datasets/train/images --label_dir datasets/train/labels --classes 4 --names car truck bus person import os import argparse import cv2 import numpy as np def gamma_adjust(img, gamma): # 构建 gamma 查找表gamma1 时整体变亮 lut np.array([(i / 255.0) ** gamma * 255 for i in range(256)], dtypenp.uint8) return cv2.LUT(img, lut) def main(): parser argparse.ArgumentParser() parser.add_argument(--img_dir, requiredTrue) parser.add_argument(--label_dir, requiredTrue) parser.add_argument(--classes, typeint, default4) parser.add_argument(--names, nargs, default[car, truck, bus, person]) parser.add_argument(--max_imgs, typeint, default50) parser.add_argument(--gamma, typefloat, default0.5, help预览提亮参数训练时不要对原图做同样操作) parser.add_argument(--out, defaultpreview) args parser.parse_args() os.makedirs(args.out, exist_okTrue) colors [(0, 255, 0), (0, 0, 255), (255, 0, 0), (255, 255, 0)] count 0 for label_name in sorted(os.listdir(args.label_dir)): if not label_name.endswith(.txt): continue if count args.max_imgs: break base label_name[:-4] img_path None for ext in [.jpg, .jpeg, .png, .bmp]: candidate os.path.join(args.img_dir, base ext) if os.path.exists(candidate): img_path candidate break if img_path is None: continue img cv2.imread(img_path) if img is None: continue h, w img.shape[:2] preview gamma_adjust(img, args.gamma) if args.gamma else img.copy() label_path os.path.join(args.label_dir, label_name) for line in open(label_path, r, encodingutf-8): parts line.strip().split() if len(parts) ! 5: continue cls int(parts[0]) if cls len(args.names): continue cxp, cyp, wp, hp map(float, parts[1:]) x1 int((cxp - wp / 2) * w) y1 int((cyp - hp / 2) * h) x2 int((cxp wp / 2) * w) y2 int((cyp hp / 2) * h) color colors[cls % len(colors)] cv2.rectangle(preview, (x1, y1), (x2, y2), color, 2) cv2.putText(preview, args.names[cls], (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) cv2.imwrite(os.path.join(args.out, base .jpg), preview) count 1 print(fsaved {count} preview images to {args.out}) if __name__ __main__: main()参数说明里最容易出错的是--gamma0.5 能明显提亮但会扭曲真实颜色所以脚本注释里写清楚它只服务于预览。--names的顺序必须和 class 文件一致否则画出来的框名字是错位的。--max_imgs建议第一次跑设 50循环里先看前50张图的整体标注密度不要一口气输出几千张再慢慢翻。中文类名在这个脚本里显示会乱码因为 OpenCV 的 putText 不支持中文字体遇到中文类名就先转成英文或拼音不用纠结字体问题。4.3 让脚本输出类别分布可视化不只是看框预览图看的是单张标注质量类别分布看的是整个数据集的结构问题。把上一节的脚本稍微扩展在循环里累计每个类别的框数最后打印占比就能提前知道是不是 car 占了 80%、person 只占 3%。这个问题直接决定训练时要不要做类别重采样。# 在 plot_yolo_labels.py 的循环里追加统计逻辑 from collections import Counter class_counter Counter() # ... 解析标签行的位置保持一样 ... # 解析到合法 cls 后: class_counter[cls] 1 # 主循环结束后: total sum(class_counter.values()) for c in sorted(class_counter): ratio class_counter[c] / total if total else 0 print(fclass {c} ({args.names[c]}): {class_counter[c]} instances, {ratio:.1%})这段追加代码不改变图片输出只在命令行打印分布。类别占比悬殊时后续训练要重点盯住召回率最低的那一类别只看整体 mAP。对于夜间行人这类占比低于 5% 的类别即使整体 mAP 到 0.85行人 AP 可能只有 0.4部署到现场根本不够用。5. 夜间数据集训练避坑bn 崩溃、混淆矩阵不为 100% 与类别失衡5.1 训练到一半 loss 变 NaNBN 崩溃在夜间数据上的表现现象训练开始时 loss 正常下降几十步之后突然出现 NaN或者 loss 变成负数随后 val 指标直接掉到 0。这种情况在夜间数据集上比白天更常见因为夜间图像方差大暗部噪点会让 BatchNorm 层的统计量在几个 batch 内剧烈抖动一旦某个 batch 的方差接近 0归一化分母爆炸训练立刻崩溃。原因往往是三个叠加学习率设置过高、batch 太小、标签文件里混入异常坐标。batch 小的时候 BN 统计噪声放大夜间图像又不稳定任何一个异常 batch 都可能触发崩溃。排查时先别急着换模型把标签检查脚本先跑一遍确认没有 NaN 坐标或 0 宽高的框再去调训练参数。解决的办法是先把学习率压到 1e-3 以下batch 至少 16显存不够就降 imgsz 而不是降 batch。也可以把优化器换成 AdamW它对 BN 崩溃的容忍度比 SGD 高一些。已经崩溃的训练进程不用留恋直接重启因为 NaN 一旦出现就会污染 BN 的 running_mean靠继续训练救不回来。5.2 混淆矩阵总和不是 100%这不是 bug现象训练完看验证集输出混淆矩阵里对角线的值加起来明显不是 100%有人会怀疑是不是类别统计写错了或者标签有问题。YOLOv8 的验证过程会额外保存归一化后的混淆矩阵它里面包含 background 行和列而且每一行是各自独立做行归一化的。原因有两条一是漏检目标会被归入 background 那一类二是同一个真实框可能命中多个预测框导致计数大于实际目标数。所以矩阵里所有格子的总和天然大于或小于 100%具体取决于归一化方向。这个“总和不是 100%”几乎是每个 YOLO 训练者都会困惑的点但它不影响模型评估。要看模型真实水平就回到 mAP50、mAP50-95、precision 和 recall 这几个指标。混淆矩阵只用来观察某个类被误判成了哪个类比如 person 大量落到 background说明漏检为主person 落到 car说明框位置偏移严重两者修的方向完全不同。5.3 数据增强把夜间变成了黄昏预处理越狠标签越脏现象有人拿到夜间数据先用 CLAHE 或 gamma 把整张训练集提亮训练后白天测试集指标不错一放到真实夜间现场就出现大量漏检。因为网络学到的是“提亮后的夜间”不是真正的夜间部署时摄像头输出的原始夜间图像和训练分布不一致特征直接错位。原因在于数据的预处理增强了图像的可看性但改变了目标与环境的光照关系。夜间目标的可用特征本来就少增强会进一步抹掉暗部细节。正确做法是训练时只对原图做几何增强和色彩抖动不要在进网络前强制亮化。可视化脚本里的 gamma 只用于人工预览不要顺手把那段逻辑复制到训练管线的预处理里。5.4 类别不平衡行人在夜间更容易被漏检怎么办现象类别分布统计显示 person 只有 car 的 1/10训练后验证曲线里 person 的 recall 明显偏低。夜间行人对比度差、目标又小数量还少模型很快学会多在车辆那个类别上学行人成为长尾。解决路径按优先级排序先用第 4 章的分类统计确认差距有多大然后训练时把 imgsz 从 640 提到 960小目标在更大输入下能有更多像素可学仍然不够就在损失函数里提高 person 类的权重YOLOv8 的 class weights 参数可以单独配置。再往下就是数据层面操作对行人实例做复制粘贴增强或者专门采集深夜低光照的行人片段补充数据集。单靠调训练参数能挽回一部分但挽回不了标注质量本身的问题。6. 用预训练模型做最小验证先跑通再扩数据的三个习惯第一次用 YOLOv8 训练自己的数据集时不要一上来就把 epochs 设到 300。常见的做法是先下一份官方预训练权重把 data.yaml 指到这份划分好的夜间数据集上跑一个极小配方看数据流动是否正确。yolo detect train \ modelyolov8n.pt \ datadatasets/data.yaml \ epochs30 \ imgsz640 \ batch8 \ lr00.001 \ cacheFalse这个命令里值得注意的参数是cacheFalse第一轮把图片读入内存排查数据路径问题时不加载缓存改完数据文件后不用手动清理。lr00.001是为了避开前面说的 BN 崩溃epochs30只为了验证 loss 能否稳定下降不追求最终精度。三十轮跑完如果 loss 曲线正常mAP50 在 0.3 以上再逐步加 epochs、调数据增强如果 loss 震荡或指标为 0大部分时候问题在数据而不是模型。第二个习惯是训练完必须回看预测图不能只看指标数字。夜间图片里真正有效的决策点往往在“它到底有没有看到那个站在车影里的行人”mAP 反映不了你现场对漏检的容忍度。第三个习惯是建立一条固定动作标签检查脚本 → 可视化预览 → 分类统计 → 最小训练验证每次拿到新的夜间数据集都按这套顺序来。我自己在处理夜间数据时最常踩的坑是拿到划分好的数据集就跳过了可视化检查后来发现 val 和 train 有部分重复图像验证指标虚高到 0.98换一版干净划分后掉到 0.89。这类数据归属问题不靠模型调参能解决只能靠检查流程兜住。希望帮到你。本文还有配套的精品资源点击获取