简介:面向夜间车辆与行人检测的YOLO格式数据集,覆盖行人、自行车、汽车、狗四类目标,适用于YOLOv5及后续版本的训练、微调与算法改进。数据按YOLOv5目录结构存放,标签采用中心点坐标加宽高的归一化格式,训练集8410张图片及对应txt标注,验证集1457张图片及对应txt标注,可直接加载使用。压缩包共2000个文件,以txt标签与类别配置为主体,另附1个数据可视化py脚本,无需修改即可随机读取图片绘制边界框并保存,便于快速核对标注质量。整个7z资源包约508MB,已有627人学习下载。对需要夜间数据或想省去标注环节的开发者,是一份即取即用的检测数据集,同时适合目标检测入门练习与夜间视觉算法验证。
1. YOLO数据集不是“下载就能训”:夜间4类数据先过三道坎
YOLO数据集这东西,下载下来只是开始。我拿到一份夜间车辆、行人检测的4类数据集时,第一反应不是急着开训,而是先花半小时确认三件事:train/val/test划分是不是真的合理、class文件里的类别ID和标签对没对上、附带的可视化脚本能不能直接跑。夜间场景的数据和白天完全是两套打法,低光照、车灯过曝、远处行人只有几十个像素,这些都会让“白天好用的模型晚上翻车”。这份资源把图片、标注、类别文件、划分结果一次配齐,适合正在做交通监控、夜间安防目标检测,或者想跑通yolov8训练全流程的人。省掉的是整理数据的时间,留下的是验证数据质量的责任。
2. 先看数据再看模型:4类夜间标注的平衡性与划分合理性
2.1 目录结构与4类标注:拿到手先做三件事
先看目录结构,确认资源是不是“能直接喂给训练”的完整形态。典型的数据集会按 images 和 labels 分成两个大目录,各自再按 train/val/test 切好,根目录下放 classes.txt 和 data.yaml。结构大概长这样:
dataset/ ├── images/ │ ├── train/ # 约 2860 张 │ ├── val/ # 约 410 张 │ └── test/ # 约 430 张 ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt └── data.yamlYOLO 格式的标签是 txt 文件,每个目标占一行,格式固定为class_id x_center y_center width height,五个值全都是相对于图像宽高的归一化坐标。classes.txt 里每一行就是一个类别,行号就是类别 ID,这个行顺序一旦错了,后续所有训练和可视化都会跟着错。这份资源的 4 类一般是 car、person、truck、motorcycle 这个口径,具体以 classes.txt 实际内容为准。
拿到数据集第一件事,不是贴张图看看效果,而是统计每个类别的目标数量,判断类别均衡性。我一般会先跑这样一段脚本:
import os from collections import Counter label_dir = "labels/train" class_names = ["car", "person", "truck", "motorcycle"] # 必须与 classes.txt 顺序一致 counter = Counter() sample_count = 0 for name in os.listdir(label_dir): if not name.endswith(".txt"): continue sample_count += 1 with open(os.path.join(label_dir, name), encoding="utf-8") as fp: for line in fp: cls_id = int(line.split()[0]) counter[class_names[cls_id]] += 1 print(f"标注文件数: {sample_count}") print(counter)这段脚本的作用很直接:逐行读入每个标签文件,取每行第一个数字当作类别 ID,映射到 class_names 后计数。这里有个隐藏检查点——sample_count 统计的是有效 txt 的数量。如果它比 images/train 里的图片数量少,说明存在漏标或者空标签的图,后面要单独排查。
参数说明:class_names 的列表顺序必须和 classes.txt 一模一样,否则统计结果会错位。这在夜间数据集里尤其坑,因为夜间过曝环境下,标注员很容易把远处模糊的轿车标成 truck,或者把摩托车标成自行车,类别口径一旦不统一,统计出来的比例就没有参考价值。
划分比例方面,常见做法是 7:2:1 或 8:1:1。如果这份数据是从夜间监控视频里抽帧来的,随机划分会有“时间重叠泄露”的问题——相邻两帧画面几乎一样,被随机分到 train 和 val,验证指标会虚高得离谱。正确的做法是按时间段切分,保证 val 和 test 里的画面完全来自模型没见过的连续片段。
2.2 夜间标注的特殊性:低光、过曝与小目标
夜间标注比白天难三个量级:低照度下行人轮廓和路面背景融为一体;车灯区域过曝,导致车辆边界根本不是车的真实轮廓;远处目标在 1280×720 的画面里可能只占 20×20 像素。这三个因素直接决定了可视化检查时的判断标准必须和白天不一样。
我拿到夜间数据后,会先定一套标注口径,再拿可视化脚本逐张核对。比如:目标遮挡超过 50% 的标不标;车灯过曝到只看得见光斑、完全看不到车身轮廓的车算不算一个框;行人被车挡住一半要不要单独标。如果资源本身口径统一,可视化叠加框看起来会非常整齐,框的边缘和目标的可见轮廓基本贴合;反之框会忽大忽小、边缘参差,这类数据拿去训练,边界框回归损失永远降不到理想水平。
可视化脚本在这里的核心价值,就是把归一化坐标还原成像素框叠回原图。核心代码逻辑如下:
import cv2 import os img_dir = "images/train" label_dir = "labels/train" class_names = ["car", "person", "truck", "motorcycle"] colors = [(0, 255, 0), (0, 0, 255), (255, 0, 0), (0, 255, 255)] for name in sorted(os.listdir(img_dir))[:20]: img = cv2.imread(os.path.join(img_dir, name)) h, w = img.shape[:2] label_path = os.path.join(label_dir, name.replace(".jpg", ".txt")) if not os.path.exists(label_path): print(f"跳过无标签文件: {name}") continue with open(label_path, encoding="utf-8") as fp: for line in fp: cls_id, xc, yc, bw, bh = map(float, line.split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), colors[int(cls_id)], 2) cv2.putText(img, class_names[int(cls_id)], (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[int(cls_id)], 2) cv2.imwrite(f"check_{name}", img)逻辑说明:先把归一化的中心点坐标和宽高乘回图像的真实宽高,换算成左上角和右下角的像素坐标,再画矩形和类别名。逐行读 txt 时默认每行只有 5 列,如果发现某一行有第 6 列,说明标签被改过格式或混入了置信度字段,需要清洗。
参数说明:colors 列表和 class_names 一样,顺序必须和 classes.txt 对齐,否则会出现“车被画成行人颜色”的误导。name.replace(".jpg", ".txt")只处理 jpg 后缀,如果你的数据里有 png 或 bmp,这里要改成对应后缀,否则一半的图片会提示找不到标签。
抽检重点放在三类区域:远处的车灯光斑有没有被框住、穿深色衣服的行人有没有漏标、两辆车前后重叠时框是不是互相吞并。这三类问题在夜间数据里出现的概率远高于白天。
2.3 划分版本与时间重叠泄露
非视频抽帧的数据集可以不看这一节,但夜间车辆行人数据大概率是从监控视频或行车记录仪抽帧来的,时间重叠泄露就是必须提前堵上的洞。我一般会这样检查:把 train 和 val 的图片文件名按时间戳排序,如果 val 里某张图的相邻帧出现在 train 里,说明划分时没做时间切分。
这个问题的隐蔽性在于:指标照样好看,mAP50 甚至能到 0.9,但模型部署到新路段立刻掉点。因为 val 里都是“见过”的帧,模型记住了画面而不是学会了目标。常见做法是按日期或按摄像头 ID 切分,保证 val 和 test 完全没见过同一时间段。
另一个习惯是把三份划分的文件名单各自导出成 txt,训练前再统计一次数量,确认三份名单没有交集也没有遗漏。这一步可用最简单的集合运算完成:
import os train_files = set(os.listdir("images/train")) val_files = set(os.listdir("images/val")) test_files = set(os.listdir("images/test")) print(f"train: {len(train_files)}, val: {len(val_files)}, test: {len(test_files)}") print("train ∩ val:", len(train_files & val_files)) print("train ∩ test:", len(train_files & test_files)) print("val ∩ test:", len(val_files & test_files))逻辑说明:把三个目录的文件名读成集合,交集长度应该全部为 0。只要有一个交集大于 0,就说明划分脚本有 bug 或者数据放重了。这个检查 30 秒能跑完,却能把训练指标从“假的高分”拉回“真实的低分”,是我拿到任何数据集都会强制走一遍的步骤。
3. 把可视化脚本用起来:标注质量检查与两类典型错误
3.1 可视化脚本的入口参数与输出物
资源里自带的数据可视化脚本,我打开第一件事就是看入口参数。常见做法是用 argparse 管理,好处是换数据集时不用改代码,只改命令行参数:
import argparse def parse_args(): parser = argparse.ArgumentParser(description="YOLO数据集可视化检查") parser.add_argument("--img_dir", type=str, required=True, help="图片目录") parser.add_argument("--label_dir", type=str, required=True, help="标签目录") parser.add_argument("--save_dir", type=str, default="vis_output", help="结果输出目录") parser.add_argument("--num", type=int, default=20, help="随机抽样张数") parser.add_argument("--view", action="store_true", help="弹窗预览,无显示环境时别开") return parser.parse_args() if __name__ == "__main__": args = parse_args() print(f"图片目录: {args.img_dir}") print(f"标签目录: {args.label_dir}") print(f"抽样数量: {args.num}")逻辑说明:这是脚本入口骨架,实际项目会在 parse_args 之后调用画框函数,把生成的图像写到 save_dir。把参数独立出来的意义在于:数据集的版本更替、路径变化都不需要动代码,命令行重新指定一遍即可。
参数说明:--num控制抽样数量,夜间数据我一般抽 50 张起步,比白天多一倍。原因是低光下漏标和错标都更隐蔽,抽 20 张很可能恰好避开问题样本。--view依赖 matplotlib 或 OpenCV 的 GUI,服务器裸机环境下开了会直接报错,所以服务器上跑就加--save_dir输出到文件夹,本地有桌面环境才开--view。
跑完脚本会得到三类输出物:叠加标注的原图、按类别统计的柱状图、目标尺寸分布图。这三样合起来就是数据集的“体检报告”。柱状图看类别均衡性,尺寸分布图看是不是有一大批小于 32×32 的目标——如果是,后面训练时 imgsz 和模型尺寸都要特殊处理。
3.2 典型错误之一:归一化坐标越界与极小框
画框时如果出现 x2 < x1,或者矩形大半个落在图像外面,先别急着怪可视化脚本——去翻原始标签。常见原因有三个:标注工具导出时坐标换算错误、手工标注时把框拖出画布边缘、脚本转换时像素坐标和归一化坐标混用。
处理办法是清洗。我一般会先整体备份 labels 目录,然后做 clamp 和过滤:
import os def clean_label(label_path): kept = [] with open(label_path, encoding="utf-8") as fp: for line in fp: parts = line.split() if len(parts) != 5: continue # 列数不对的丢弃 cls_id, xc, yc, bw, bh = map(float, parts) xc = min(max(xc, 0.0), 1.0) yc = min(max(yc, 0.0), 1.0) bw = min(bw, 1.0 - xc) # 宽度不能超出右边界 bh = min(bh, 1.0 - yc) # 高度不能超出下边界 if bw < 0.001 or bh < 0.001: continue # 相对原图小于千分之一的框直接丢弃 kept.append(f"{int(cls_id)} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") with open(label_path, "w", encoding="utf-8") as fp: fp.write("\n".join(kept)) label_dir = "labels/train" for name in os.listdir(label_dir): if name.endswith(".txt"): clean_label(os.path.join(label_dir, name))逻辑说明:对归一化坐标做 clamp 到 0~1,再把宽高限制在不超过右边界和下边界,最后过滤掉宽或高小于 0.001 的框。0.001 这个阈值在 1280×720 的图像里意味着目标小于 1.28 像素,这种框不仅没有学习价值,还会给边界框损失函数制造噪声。
参数说明:清洗脚本不依赖真实图像宽高,因为标签本身就是归一化的。更严谨的做法是用cv2.imread读取同名图片的真实宽高,再做像素级别的合法性校验。另外这是血泪经验:清洗一定要先备份。原标签被覆盖后,后悔药只能靠 git 或者重新下载。
3.3 典型错误之二:类别ID与class文件错位
可视化里最坑的翻车现场是:车被画成了行人颜色,行人被画成 truck 标签。原因几乎只有一个——classes.txt 和脚本里 class_names 的顺序不一致。
YOLO 标签只存类别 ID,不存类别名,类名全靠 classes 文件的行顺序映射。如果你用 LabelImg 标注时类表顺序是“car, person, truck, motorcycle”,后来有人把 classes.txt 改成“person, car, truck, motorcycle”,那所有 ID=0 的标签都会从 car 变成 person,而且训练不会报任何错。
我处理这个问题的习惯是写一个独立的 classes.yaml,让训练、可视化、统计、清洗四个环节从同一个文件读类别名:
# classes.yaml names: 0: car 1: person 2: truck 3: motorcycle逻辑说明:用一个 yaml 作为类别真源,各脚本都从它加载,就不会出现“我记得 class_names 顺序是 xxx”这种记忆偏差。可视化脚本里改成读取这个 yaml 生成 class_names 和 colors,训练时 data.yaml 的 names 也从这里复制过去。
参数说明:注意 YAML 的缩进和键名。Ultralytics 的 data.yaml 里 names 也可以写成列表形式,但要保证列表顺序和这里的 ID 一致。如果数据集里混入了第 5 类标签(比如 ID=4),训练时不会直接报错,但 nc=4 会被突破,表现在混淆矩阵上就是多出来一行——这个问题下一章细说。
4. 训练配置一步到位:data.yaml、预训练权重与超参数细节
4.1 data.yaml 路径与 names 对齐
把数据集喂给 yolov8 或 yolov5 之前,第一步是写 data.yaml。这个文件决定了 train/val/test 指向哪里、有几类、类名是什么:
# data.yaml train: /home/user/dataset/images/train val: /home/user/dataset/images/val test: /home/user/dataset/images/test nc: 4 names: 0: car 1: person 2: truck 3: motorcycle逻辑说明:train/val/test 指向的是图片目录,不是标签目录。训练时框架会根据图片文件名去 labels 目录找同名 txt,所以图片和标签的文件名必须完全一致。图片是 .jpg,标签就找 .jpg 替换成 .txt;如果图片是 .png 而标签写成 .jpg.txt,会有一半样本找不到标签。
参数说明:路径建议写绝对路径。用相对路径时是相对于你执行 yolo 命令的工作目录,不是 data.yaml 所在目录,这点最容易踩。Windows 用户还要注意路径分隔符统一用正斜杠/,data.yaml 里出现反斜杠会被解析成转义字符,路径直接失效。
names 的写法有列表和字典两种,Ultralytics 两种都认。我个人习惯用字典,明确写出 ID,防止调整类别顺序时把 ID 弄错。顺序调整本身要极度谨慎,因为标签里的 ID 是跟着 classes.txt 走的,顺序一变,语义全变。
4.2 预训练权重选择:夜间场景从 yolov8s 起步
yolo预训练模型下载这块,Ultralytics 框架会在第一次运行时自动拉取对应权重。你写model=yolov8s.pt,本地没有就去官方地址下载,放到当前目录。离线环境就得手动把 yolov8s.pt 这类权重文件放到项目目录,再启动训练。
模型尺寸怎么选?夜间场景我一般从 s 起步,不用 n。yolov8n 速度快,但夜间小目标多,行人往往只有几十个像素,轻量模型的特征提取能力不够,远处行人很容易漏检。yolov8s 是精度和速度的折中,显存 8G 以上可以跑,6G 以下就退到 n。如果项目是夜间车流统计这种大目标场景,n 也够用;如果是行人检测,优先 s 或 m。
训练命令长这样:
yolo detect train \ data=data.yaml \ model=yolov8s.pt \ epochs=100 \ imgsz=640 \ batch=-1 \ patience=20 \ project=runs/detect \ name=night_v1参数说明:batch=-1让框架根据显存自动选 batch size;patience=20的意思是验证集指标连续 20 个 epoch 不提升就早停。夜间数据集类别不均衡,少数类(比如 motorcycle)收敛慢,patience 用默认值容易在它还没学好时提前停掉。
imgsz 多说一句:640 是通用基准,如果可视化时发现很多框小于 32×32,可以试 768 或 1280。imgsz 加大带来的副作用是显存占用上升、训练时间变长。我的习惯是先用 640 跑通流程,确认没有数据问题后再用 1280 精调。
4.3 训练日志与指标:夜间模型的分数怎么看
训练过程中终端会实时刷出 box_loss、cls_loss、dfl_loss 三条曲线,val 阶段能看到 precision、recall、mAP50、mAP50-95。夜间场景最常见的分数组合是 mAP50 不低、mAP50-95 偏低,因为夜间目标边界模糊,预测框和真实框的重合度很难达到 IoU 0.75 以上的门槛。
验证模型不要只盯着训练集指标,训练完单独跑一次测试集:
yolo detect val \ model=runs/detect/night_v1/weights/best.pt \ data=data.yaml \ split=test逻辑说明:split=test 强制用 test 目录做验证,和训练时的 val 指标分开。很多人的训练日志好看,但 test 一跑就掉点,说明过拟合到了 val 的泄漏样本上——上一章说的“时间重叠泄露”是常见元凶之一。
这个场景下各指标的价值,我总结成一张小表:
| 指标 | 夜间场景的关注价值 | 说明 |
|---|---|---|
| mAP50 | 中度 | 只要框大致位置对就算正例,容易虚高 |
| mAP50-95 | 高 | 要求框贴合才能得分,夜间目标模糊时最有参考性 |
| 混淆矩阵 | 高 | 看哪些类互相误检,比如把摩托车检成自行车 |
| F1-score | 高 | 夜间常用 0.25 置信度阈值,F1 比单独看 P/R 更均衡 |
置信度阈值越低,recall 越高但 precision 掉得快,具体调到多少取决于你是“漏检代价大”还是“误检代价大”。夜间安防场景一般宁可误检不可漏检,conf 设置在 0.2~0.25 之间比较常见。
5. 避坑与排查:夜间训练中最容易翻车的五个问题
5.1 指标类坑:混淆矩阵总和不为1、mAP50 虚高
踩坑记录1:混淆矩阵总和不为1。现象是把训练生成的混淆矩阵下载下来,把所有格子加一遍,发现总和不是 1,甚至差得挺多。原因有两层:一是混淆矩阵默认展示的是计数而不是概率,按行归一化的 Normalized 版本才会让每行加起来等于 1;二是类别样本量极不均衡时,少数类那一格接近 0,肉眼看就是一大块深色,数字上总和也对不上。解决方法是训练完成后在验证结果页面勾选 Normalized 混淆矩阵,或者直接看 validation 输出的 normalized 版本图,顺带看一眼每类 GT 数量,判断是不是样本太少导致的“消失的类”。
踩坑记录2:mAP50 很高,mAP50-95 很低。现象是 mAP50 到了 0.85,mAP50-95 只有 0.25 左右,看起来像是模型崩了,但训练曲线一切正常。原因是夜间目标边缘模糊,IoU=0.75 的判定对框的贴合度要求太高;另一个常见原因是标注框本身没贴紧目标——标注员夜间凭轮廓画框,往往比白天大一圈,学习目标本身就带偏差。解决方法是组合拳:把 imgsz 从 640 调到 768 或 1280;把训练轮数拉长;然后用上一章的清洗脚本重新检查标注质量,重点看边界类目标的框是否贴紧。这个问题的排查空间有限,大多时候就是标注贴合度的问题,没什么玄学余地。
5.2 训练崩溃类坑:loss 变 NaN、OOM、类别 ID 超范围
踩坑记录3:BN 崩了,loss 变成 nan。现象是训练到第 5~15 个 epoch,loss 突然变 nan,然后一路 nan 到底。常见原因有三个:学习率过大导致梯度爆炸;batch 里混进纯黑或纯白图片,BN 统计量异常;mosaic 拼接时某块区域没有目标,变成空图参与计算。解决顺序是这样:先把 lr 从 0.01 降到 0.005 甚至 0.001,重启训练;不行就检查数据里有没有全黑图、全白图、损坏图片,清掉再跑;还崩就关掉 mosaic 和 mixup 增强,用最朴素的加载方式跑 20 个 epoch 确认数据集本身没问题。另外注意微调崩了之后的恢复策略:不要从头重训,用最后一次正常保存的 epoch 权重恢复,配更低的学习率继续。
踩坑记录4:显存 OOM,训练直接退出。现象是 batch=32、imgsz=640 跑 yolov8s,6G 显存直接 OutOfMemory。原因是 batch 设得太盲目。解决方法是 batch=-1 让框架自动选可用 batch,或者手动从 16 开始递减;还不行就降 imgsz 到 480 跑通流程,最后再提回 640。小显存上跑大图的替代方案是梯度累积,等效 batch 不变但显存峰值降下来。
踩坑记录5:训练时某些类别一直不参与计算,或者报 class id 超范围。现象是日志里的 cls_loss 很高,但 val 里的 motorcycle 类 AP 始终是 0,打开标签发现竟然有 ID=5 的标签,而 classes.txt 只有 4 类。原因是数据集被标注工具或人工追加了类别,但 classes.txt 没更新,多余类别的样本全部变成噪声。解决方法是先跑一段检查代码,把超范围 ID 全部找出来:
import os label_dir = "labels/train" max_id = 3 # 改成 nc - 1,nc=4 时就是 3 for name in os.listdir(label_dir): if not name.endswith(".txt"): continue with open(os.path.join(label_dir, name), encoding="utf-8") as fp: for line in fp: cls_id = int(line.split()[0]) if cls_id > max_id: print(f"{name} 里出现超范围类别 ID={cls_id}")逻辑说明:遍历所有标签文件,找出超过 nc-1 的类别 ID。这比训练时的报错更直观,因为有些错误 ID 不会导致训练中止,只是让 loss 里混入噪声,指标慢慢变差还不容易察觉。
参数说明:max_id 是数据集中最大合法 ID,比如 nc=4 时就是 3。跑完如果发现超范围标签,要么删掉该行,要么人工判断后把它合并到最近的合法类别里——合类动作只能人工判断,不能闭眼按 ID 替换。
6. 最后一道验证:用训练好的权重跑夜间视频并输出标注结果
6.1 视频推理与置信度阈值调节
训练结束不是以 mAP 最高为终点,而是以视频实测为终点。我每次都会拿一段没进过训练集的夜间视频,用 best.pt 跑一遍,直接输出带框视频:
from ultralytics import YOLO model = YOLO("runs/detect/night_v1/weights/best.pt") results = model.predict( source="night_road.mp4", save=True, conf=0.25, imgsz=640, project="runs/detect", name="night_video_check", )逻辑说明:save=True 会把推理结果以视频形式保存到 project 指定的目录下,框、类别名、置信度全部画好。conf=0.25 是夜间场景常用的阈值,比白天的 0.5 低,因为夜里目标对比度差,模型给出的置信度普遍偏低。如果你在验证集上看到 F1 曲线在 0.25~0.3 之间最高,就把阈值定在那个范围。
参数说明:imgsz 必须和训练时一致,否则输入分辨率改变后小目标的行为会漂移。运动相机拍的夜间视频如果帧之间抖动严重,可以先传图片目录逐帧输出检测结果,再自行合成视频,比直接喂长视频更容易定位漏检帧。
不同阈值的选择可以参照这张表:
| conf | 适用场景 | 代价 |
|---|---|---|
| 0.5 | 白天或条件好的画面 | 漏检多,误检少 |
| 0.25 | 夜间监控/行车记录仪 | 误检变多,但漏检明显下降 |
| 0.15 | 远处小目标优先 | 噪声框大量增加,需要后处理过滤 |
低置信度带来的噪声框,可以用 NMS 和类别过滤结合,也可以按帧间跟踪做一次平滑,这一步就开始进入工程化改造了。
我从早期踩过的坑说起吧:那段时间只盯训练日志,mAP50 涨到 0.88 就觉得稳了,结果模型丢到一段实拍夜间视频里,路灯下的树影被反复框成行人,车灯过曝的车头反而漏检。从那以后,我每次训完,不管是白天数据还是夜间数据,都把“跑一段没见过的视频”当成强制收尾动作,视频里过一遍再谈上线。如果你在做的项目正缺一份划分好、带可视化脚本的夜间数据,这份资源值得拿过去跑一遍,应该能省下不少整理标注的时间。希望帮到你。
本文还有配套的精品资源,点击获取