ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

YOLO夜间目标检测实战:数据集检查、训练配置与避坑指南

YOLO夜间目标检测实战:数据集检查、训练配置与避坑指南 简介面向夜间车辆与行人检测的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), encodingutf-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, encodingutf-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(fcheck_{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(ftrain: {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(descriptionYOLO数据集可视化检查) parser.add_argument(--img_dir, typestr, requiredTrue, help图片目录) parser.add_argument(--label_dir, typestr, requiredTrue, help标签目录) parser.add_argument(--save_dir, typestr, defaultvis_output, help结果输出目录) parser.add_argument(--num, typeint, default20, help随机抽样张数) parser.add_argument(--view, actionstore_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, encodingutf-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, encodingutf-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”那所有 ID0 的标签都会从 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 类标签比如 ID4训练时不会直接报错但 nc4 会被突破表现在混淆矩阵上就是多出来一行——这个问题下一章细说。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 框架会在第一次运行时自动拉取对应权重。你写modelyolov8s.pt本地没有就去官方地址下载放到当前目录。离线环境就得手动把 yolov8s.pt 这类权重文件放到项目目录再启动训练。模型尺寸怎么选夜间场景我一般从 s 起步不用 n。yolov8n 速度快但夜间小目标多行人往往只有几十个像素轻量模型的特征提取能力不够远处行人很容易漏检。yolov8s 是精度和速度的折中显存 8G 以上可以跑6G 以下就退到 n。如果项目是夜间车流统计这种大目标场景n 也够用如果是行人检测优先 s 或 m。训练命令长这样yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch-1 \ patience20 \ projectruns/detect \ namenight_v1参数说明batch-1让框架根据显存自动选 batch sizepatience20的意思是验证集指标连续 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 \ modelruns/detect/night_v1/weights/best.pt \ datadata.yaml \ splittest逻辑说明splittest 强制用 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 数量判断是不是样本太少导致的“消失的类”。踩坑记录2mAP50 很高mAP50-95 很低。现象是 mAP50 到了 0.85mAP50-95 只有 0.25 左右看起来像是模型崩了但训练曲线一切正常。原因是夜间目标边缘模糊IoU0.75 的判定对框的贴合度要求太高另一个常见原因是标注框本身没贴紧目标——标注员夜间凭轮廓画框往往比白天大一圈学习目标本身就带偏差。解决方法是组合拳把 imgsz 从 640 调到 768 或 1280把训练轮数拉长然后用上一章的清洗脚本重新检查标注质量重点看边界类目标的框是否贴紧。这个问题的排查空间有限大多时候就是标注贴合度的问题没什么玄学余地。5.2 训练崩溃类坑loss 变 NaN、OOM、类别 ID 超范围踩坑记录3BN 崩了loss 变成 nan。现象是训练到第 5~15 个 epochloss 突然变 nan然后一路 nan 到底。常见原因有三个学习率过大导致梯度爆炸batch 里混进纯黑或纯白图片BN 统计量异常mosaic 拼接时某块区域没有目标变成空图参与计算。解决顺序是这样先把 lr 从 0.01 降到 0.005 甚至 0.001重启训练不行就检查数据里有没有全黑图、全白图、损坏图片清掉再跑还崩就关掉 mosaic 和 mixup 增强用最朴素的加载方式跑 20 个 epoch 确认数据集本身没问题。另外注意微调崩了之后的恢复策略不要从头重训用最后一次正常保存的 epoch 权重恢复配更低的学习率继续。踩坑记录4显存 OOM训练直接退出。现象是 batch32、imgsz640 跑 yolov8s6G 显存直接 OutOfMemory。原因是 batch 设得太盲目。解决方法是 batch-1 让框架自动选可用 batch或者手动从 16 开始递减还不行就降 imgsz 到 480 跑通流程最后再提回 640。小显存上跑大图的替代方案是梯度累积等效 batch 不变但显存峰值降下来。踩坑记录5训练时某些类别一直不参与计算或者报 class id 超范围。现象是日志里的 cls_loss 很高但 val 里的 motorcycle 类 AP 始终是 0打开标签发现竟然有 ID5 的标签而 classes.txt 只有 4 类。原因是数据集被标注工具或人工追加了类别但 classes.txt 没更新多余类别的样本全部变成噪声。解决方法是先跑一段检查代码把超范围 ID 全部找出来import os label_dir labels/train max_id 3 # 改成 nc - 1nc4 时就是 3 for name in os.listdir(label_dir): if not name.endswith(.txt): continue with open(os.path.join(label_dir, name), encodingutf-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比如 nc4 时就是 3。跑完如果发现超范围标签要么删掉该行要么人工判断后把它合并到最近的合法类别里——合类动作只能人工判断不能闭眼按 ID 替换。6. 最后一道验证用训练好的权重跑夜间视频并输出标注结果6.1 视频推理与置信度阈值调节训练结束不是以 mAP 最高为终点而是以视频实测为终点。我每次都会拿一段没进过训练集的夜间视频用 best.pt 跑一遍直接输出带框视频from ultralytics import YOLO model YOLO(runs/detect/night_v1/weights/best.pt) results model.predict( sourcenight_road.mp4, saveTrue, conf0.25, imgsz640, projectruns/detect, namenight_video_check, )逻辑说明saveTrue 会把推理结果以视频形式保存到 project 指定的目录下框、类别名、置信度全部画好。conf0.25 是夜间场景常用的阈值比白天的 0.5 低因为夜里目标对比度差模型给出的置信度普遍偏低。如果你在验证集上看到 F1 曲线在 0.25~0.3 之间最高就把阈值定在那个范围。参数说明imgsz 必须和训练时一致否则输入分辨率改变后小目标的行为会漂移。运动相机拍的夜间视频如果帧之间抖动严重可以先传图片目录逐帧输出检测结果再自行合成视频比直接喂长视频更容易定位漏检帧。不同阈值的选择可以参照这张表conf适用场景代价0.5白天或条件好的画面漏检多误检少0.25夜间监控/行车记录仪误检变多但漏检明显下降0.15远处小目标优先噪声框大量增加需要后处理过滤低置信度带来的噪声框可以用 NMS 和类别过滤结合也可以按帧间跟踪做一次平滑这一步就开始进入工程化改造了。我从早期踩过的坑说起吧那段时间只盯训练日志mAP50 涨到 0.88 就觉得稳了结果模型丢到一段实拍夜间视频里路灯下的树影被反复框成行人车灯过曝的车头反而漏检。从那以后我每次训完不管是白天数据还是夜间数据都把“跑一段没见过的视频”当成强制收尾动作视频里过一遍再谈上线。如果你在做的项目正缺一份划分好、带可视化脚本的夜间数据这份资源值得拿过去跑一遍应该能省下不少整理标注的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表