
简介目标检测在低光照场景中常因数据集质量不足而性能骤降夜间车辆行人检测更是依赖标注规范与数据划分的合理性。YOLO格式以纯文本存储归一化边框坐标通过classes.txt与data.yaml完成类别映射其目录结构和标签定义直接影响模型的收敛与mAP表现。理解类别边界、统一遮挡目标标注规则并利用可视化脚本校验数据质量是提升训练效率的关键前置步骤。在夜间安防、交通监控等场景下合理配置YOLOv8增强参数与训练策略可有效应对小目标、过曝与类别不平衡问题。围绕YOLO数据集的准备、清洗与验证本文提供了一套从数据到训练的可复用实践方法帮助开发者降低试错成本。1. 夜间车辆行人检测的 4 类 YOLO 数据集能省掉你最头疼的标注环节晚上跑车辆行人检测最容易翻车的不是模型结构而是数据集本身。夜间图像对比度低、运动模糊多、车灯过曝拿白天数据硬训mAP 可能掉 10 个点以上。这版 4 类 YOLO 数据集把夜间车辆、行人按固定类别划分好train/val/test 目录、class 文件、数据可视化脚本一次配齐拿到就能在 YOLOv8 里开训省掉最耗时的一张张标注和划分环节。适合两类人做夜间安防与交通监控、想快速验证流程的从业者以及手头只有白天或低光照素材、不知道怎么下手的初学者。下面按“先看懂类别定义再开训再用可视化脚本查质量”的顺序展开最后给出夜训场景里最常见的几个坑。2. 先看懂 4 类目标和 class 文件类别定义、txt 标签与 data.yaml 的关系2.1 这 4 类到底怎么标car、person、motorcycle、bus 的边界拿到数据集第一步不是开训而是打开 class 文件确认类别。常见做法是把 4 类定成car、person、motorcycle、bus。car 指小轿车和 SUVbus 指公交车和旅游大巴person 指行人motorcycle 指电动两轮车和摩托车。这个划分对应的是夜间路口和城市道路监控里最常出现的四类目标。类名看着简单边界才是问题。我一般按一套固定规则来确认标注口径骑在摩托车上的人只标 motorcycle不标 person坐在车里的人只标 car不标 person公交车统一进 bus哪怕是小型接驳巴士。这个口径直接影响后面混淆矩阵的表现。如果你拿到数据集后先自己看一眼 class 文件再看几张 labels 的 txt就能判断它的标注规范偏严还是偏松这比直接跑训练重要得多。另一个需要确认的点是这 4 类里有没有“车辆”的统称。有的数据集会把 car 和 bus 合并成 vehicle 一类再把行人和两轮车分开这样也是 4 类。所以不要想当然任何一份别人划分好的 YOLO 数据集第一步都该是“打开 classes.txt 数一下行数逐行读类名”再对照 2.2 的标签格式去核对一两个文件。2.2 标签 txt 的每一行在写什么class_id 和归一化框坐标YOLO 格式的标注文件不是 XML也不是 JSON就是纯文本每一行表示一个目标。这行里第一个数字是类别 id后面四个浮点数分别是归一化后的中心点 x、中心点 y、框宽 w、框高 h。所谓归一化就是除以图像的宽和高所以全部落在 0 到 1 之间。下面这是某个 train 样本里的真实结构# night4/labels/train/000123.txt每行一个目标 1 0.5310 0.4720 0.1230 0.0860 3 0.3100 0.5820 0.0450 0.0320 0 0.8500 0.4700 0.2800 0.1900第一行的 class_id 是 1对照 classes.txt 就是 person后面 0.531 和 0.472 表示这个人在图像横向 53.1%、纵向 47.2% 的位置0.123、0.086 是目标宽高占整图的比例。第二行 id 是 3对应 bus框更扁更长符合公交车视觉比例。第三行 id 是 0对应 car。读取这类文件时有三个细节要注意。第一txt 文件里的浮点数不要取整0.5310 和 0.53 在还原像素坐标时会差出几个像素小目标可能因此偏出半个身位。第二没有目标的图像对应一个空 txt这是合法的不要删标注工具通常就是这样生成。第三如果是从 VOC 或 COCO 转过来的数据txt 里的坐标很容易出现绝对值那说明没归一化训练前必须先转换否则框的位置会完全错乱。2.3 classes.txt 与 data.yamlclass 文件顺序为什么改不得class 文件在 YOLO 系列里有两种常见形态一种是纯文本 classes.txt一行一个类名另一种是 data.yaml里面挂 names 列表。两者功能一样都是把 class_id 映射回可读的类名。先看 classes.txt 的内容cat night4/classes.txt # car # person # motorcycle # bus四行对应四个 idcar0person1motorcycle2bus3。这个顺序一旦定下来标签 txt 里的第一个数字就全部按这个顺序写入。data.yaml 里的 names 也必须和它保持完全一致path: /home/user/night4 train: images/train val: images/val test: images/test names: 0: car 1: person 2: motorcycle 3: bus这里最关键的一条经验class 文件的顺序绝对不能动。我见过有人觉得“bus 应该排前面”把顺序改成 bus、car、person、motorcycle结果原来所有标签里的 0 从 car 变成了 busmAP 直接掉到没法看。这是因为 txt 里存的是 id不是名字。如果确实要换顺序正确的做法是写一个小脚本逐行读 txt把第一列数字按新旧映射表改掉而不是直接编辑 class 文件。2.4 夜间遮挡目标标注规则只标可见部分夜间场景里目标往往只有一半可见比如行人被路灯杆挡住、车辆进出画面只露出一角。标注规范如果不统一会让模型学到错误的边界。我自己用的规则是只标可见部分不凭想象补全。举例来说行人从画面左侧进入身体只有 40% 在画面内那就按这 40% 拉一个框不要试着把画面外的部分估算进去。车辆停在一堆障碍物后面只有车头可见同样只标车头。这样做的好处是框和图像内容严格对齐模型在计算 IoU 时不会因为“看不见的部分”产生大量低质量正样本。夜间图像里目标边缘本来就模糊如果标注框再带“猜测成分”训练出来的回归分支就会学得犹豫。这个规则也意味着同一个目标在不同帧里的框大小可能差异很大这是正常的。夜间连续帧里一辆车从远处驶近框从 12 像素宽长到 300 像素宽模型要学的正是这种连续变化而不是固定尺寸模板。3. 划分好的数据集直接开训目录结构、最小命令与 5 个训练参数3.1 images/labels 目录与 train、val、test 里放多少张合适一份能直接开训的数据集目录结构通常固定成 images 和 labels 两大块。images 下按 train、val、test 三个子目录放图labels 下按同样结构放 txt。图片和标签靠“同名不同后缀”对应。比如images/val/000123.jpg对应labels/val/000123.txt。整套结构如下night4/ ├── images/ │ ├── train/ # 训练图 │ ├── val/ # 验证图 │ └── test/ # 测试图 ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── classes.txt ├── data.yaml ├── visualize_boxes.py # 标注框可视化脚本 └── class_stats.py # 类别分布统计脚本见到这种目录结构先确认两个事。第一images 和 labels 下的文件名必须完全一致哪怕一张图对应一个空 txt 也不能缺缺失 label 的那张图训练时要么被跳过要么报错。第二val 和 test 的比例。我一般接受的比例是 train 占 70% 到 80%val 占 15% 到 20%test 留 5% 到 10%。夜间数据集如果总量只有一两千张val 留太少会让 mAP 波动非常大。还有一类容易踩的坑划分是按文件名随机打散的还是按摄像头来源划分的。前者省事但同一个摄像头同一段路的连续帧可能同时进 train 和 val造成“数据泄漏”验证分数虚高。所以在开训前花五分钟用可视化脚本看一眼 val 里的缩略图确认没有大量和 train 高度相似的连续帧这一步值得做。3.2 用 YOLOv8 跑第一轮最小训练命令和 5 个必调参数YOLOv8 是目前训练这类数据集最顺手的入口。我自己训练自己的数据集时最小命令长这样cd /home/user yolo detect train \ datanight4/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0这条命令把 5 个参数全部摆到了明面上。data指向 data.yaml是整个实验的入口modelyolov8s.pt表示从 COCO 预训练权重开始而不是从头训练这对千张量级的数据集影响很大能少跑很多轮epochs100是夜训的起点太少会欠拟合太多会过拟合到夜间噪声上imgsz640是速度和精度的平衡点batch16在单张 8G 显存的卡上比较稳妥。有几个参数我没写但新手经常问。project和name控制输出目录如果不指定会默认落到runs/detect/train。workers控制数据加载线程数Windows 上建议设为 0 到 2Linux 上设成 4 到 8 都行。patience是早停轮数默认 50意思是 50 轮没提升就停如果训练集很小这个值容易误杀后期才出现的提升我会保守地改成 80 或直接关掉。3.3 夜间增强参数一页复盘hsv_v、mosaic、imgsz 怎么调夜间数据和白天数据在增强参数上的差异主要体现在亮度扰动和 mosaic 上。YOLOv8 的默认增强参数偏向自然光场景直接用到夜间数据集上会让模型见过太多“被增强得发蓝发紫”的诡异画面。下面这组是我在夜间车辆行人检测上常用的起点yolo detect train \ datanight4/data.yaml \ modelyolov8s.pt \ epochs120 \ imgsz640 \ batch16 \ hsv_h0.02 \ hsv_s0.70 \ hsv_v0.60 \ mosaic0.80hsv_v是亮度通道的扰动强度默认 0.4夜间场景我调到 0.6。原因是夜间图像亮度分布两极分化有的暗到只能看清轮廓有的被车灯照得局部过曝模型需要适应这种亮度差所以亮度扰动加大有好处。hsv_h保持 0.02 就够了晚上光照本来就偏单一调太大容易把车灯颜色学歪。mosaic从 1.0 降到 0.8因为夜间低对比度下四张图拼接后目标边界更糊过度依赖 mosaic 会让小目标更难学。imgsz这里保留 640但如果 val 里大量小目标比如 20×20 像素以下的行人我会提到 768。代价是训练时间和显存都涨约四成所以先在 640 上跑通再决定要不要上 768这是最稳的节奏。3.4 换 YOLOv5 或其他 YOLO 变体目录与 data.yaml 不用动目录结构和 data.yaml 这套东西在 YOLO 生态里是通用的。换 YOLOv5 时只需要把训练入口换成它的脚本cd yolov5 python train.py \ --data /home/user/night4/data.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --img 640注意 YOLOv5 的--data参数同样指向这份 data.yaml它读的也是train、val、names这几个字段。不同的是 YOLOv5 对目录的要求更宽松你甚至可以不用images/train这种结构直接在 data.yaml 里写绝对路径指向你自己的图片文件夹它会自动找同名 txt。YOLOv8 则更习惯固定的 images/labels 布局。如果后续要试 YOLOv11 或 YOLO 系列其他变体目录结构和 class 文件都不用动只看训练入口的配置方式。这也是当初做数据集时把 images 和 labels 分开、用 data.yaml 统一描述的原因迁移成本低所有的模型代码都认这套约定。4. 数据可视化脚本三种用法把 4 类标注翻出来查质量4.1 标注框可视化脚本框是否贴合夜间目标数据可视化脚本是这个数据集里最值得先跑的部分。它做的事情很简单读取一张夜间图像和对应的 txt 标注把归一化坐标还原成像素坐标然后用 OpenCV 把矩形框和类名直接画在图上。代码不长但很实用import argparse import os import cv2 parser argparse.ArgumentParser() parser.add_argument(--image, requiredTrue, help图片路径如 night4/images/val/000123.jpg) parser.add_argument(--data, defaultnight4, help数据集根目录用于读取 classes.txt) args parser.parse_args() with open(os.path.join(args.data, classes.txt)) as f: names [line.strip() for line in f if line.strip()] img cv2.imread(args.image) H, W img.shape[:2] # 把 images 路径替换成 labels 路径jpg 后缀换成 txt label_path args.image.replace(/images/, /labels/).rsplit(., 1)[0] .txt with open(label_path) as f: for line in f: parts line.split() if len(parts) ! 5: continue cid, cx, cy, w, h map(float, parts) x1 int((cx - w / 2) * W) y1 int((cy - h / 2) * H) x2 int((cx w / 2) * W) y2 int((cy h / 2) * H) color (0, 255, 0) # BGR 绿色框 cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, names[int(cid)], (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) out vis_ os.path.basename(args.image) cv2.imwrite(out, img) print(saved:, out)运行方式python visualize_boxes.py --image night4/images/val/000123.jpg --data night4这个脚本里最容易改错的地方是路径替换。images/val/000123.jpg换成labels/val/000123.txt目录层级少一层都不行。跑出来后主要看三点框是否紧贴目标边缘夜间暗处目标是否漏标有没有一个目标被分到两个类里。特别要留意车灯周围很多标注会把车灯当成一个小目标标进去这类框会让模型学到“亮斑 车辆”。4.2 类别分布统计脚本4 类不平衡在哪儿一眼看清可视化脚本的第二大用途是统计类别分布。训练前看一眼 4 类的实例数能提前判断模型会不会对某个类严重漏检。脚本直接遍历 labels 目录下的所有 txt把第一个字段的 id 计数import glob from collections import Counter root night4 split train # 改成 val 或 test 可以分别统计 stats Counter() image_files glob.glob(f{root}/images/{split}/*.jpg) for imgf in image_files: label imgf.replace(/images/, /labels/).rsplit(., 1)[0] .txt try: with open(label) as f: for line in f: if line.strip(): stats[int(line.split()[0])] 1 except FileNotFoundError: print(missing label:, label) print(f{split} images:, len(image_files)) for cid in range(4): print(fclass {cid}: {stats[cid]} instances)跑完后如果看到类似class 0: 3200class 1: 2800class 2: 450class 3: 380这样的结果说明 motorcycle 和 bus 样本严重不足。这种情况下模型大概率会优先学 car 和 person两轮车和公交车的 mAP 会明显偏低。处理方式不是简单删图而是考虑对稀缺类做样本重复采样或用模型训练时的 cls loss 权重去补偿。可视化脚本在这里的作用是让你在投入训练前就意识到问题而不是等三个小时后看一张难看的 PR 曲线。4.3 批量网格抽查拼一张图看 16 个夜间样本逐张看图的效率太低尤其夜间图像看上二三十张之后人眼对暗部细节会疲劳。更实用的做法是一次取 16 张 val 图缩放到统一尺寸拼成 4×4 的网格一张图同时看 16 个样本的标注情况。我用一个很短的 Python 片段做这件事import cv2 import glob files sorted(glob.glob(night4/images/val/*.jpg))[:16] cell 320 # 每格图边长 rows [] for i in range(0, 16, 4): row_images [] for f in files[i:i 4]: im cv2.imread(f) im cv2.resize(im, (cell, cell)) row_images.append(im) rows.append(cv2.hconcat(row_images)) grid cv2.vconcat(rows) cv2.imwrite(visual_grid.jpg, grid) print(saved: visual_grid.jpg共, len(files), 张)注意这里拼的是原图不是标注框叠加后的图。我的习惯是先用 4.1 的单张脚本抽两张叠加框的图再用这个网格看整体的明暗分布、模糊程度、视角多样性。网格图里如果发现大量连续帧几乎一模一样说明划分时没有按场景去重这比类别不均衡更隐蔽也更能拖低验证分数的可信度。4.4 可视化结果带出的数据清洗动作可视化脚本看完后通常会带出三类清洗动作。第一类是删除“不可学”的图整张接近全黑、目标只露出不足 5% 且被严重遮挡、重复帧。这些图不是难例是噪声留在训练集里只会让 loss 曲线反复跳动。第二类是修正标注错误比如明显把一辆面包车标成 bus、把骑手标成 person这类错误靠脚本叠框后一眼就能看出来。第三类是补漏标夜间暗处的行人特别容易被遗漏如果 val 里大量图像的目标框数量明显少于肉眼可见目标说明这份数据集的 recall 先天不足。清洗时要控制删图比例。一次清洗删除超过 15% 的图像我会谨慎因为夜间数据集本来就难采集删太多容易把场景多样性削掉模型泛化能力会跟着降。宁可保留一部分“难但干净”的图也不要为了追求完美而把数据集削成单一场景。5. 避坑排查夜间训练与数据文件相关的 5 个高频问题5.1 训练 loss 在降、mAP 却不涨夜间小目标占比太高现象训练 loss 一路下行到 0.03 附近但 val 上的 mAP50-95 停在 0.25 上不去train 和 val 的差距也不大。原因夜间图像里大量目标非常小。监控视角下 30 米外的行人可能只有 15×30 像素在 640 分辨率下小于特征图上一个网格的感受野模型几乎没有能力学出稳定特征。loss 是从训练集里学的训练集里这类小目标同样存在但模型学到的是“模糊猜测”所以 loss 降了mAP 不涨。解决先用可视化脚本统计目标框的像素面积分布如果大量框面积小于 300 平方像素把 imgsz 提到 768 或 896 是最直接的方案。显存不够时推理阶段用切图方式把大图切成 640 的块再合并结果也能明显改善小目标召回。5.2 改了 class 文件顺序预测结果整体错位现象训练结束后val 可视化里 car 全部被标成 personperson 全部被标成 bus但框的位置完全没变。原因class 文件顺序在训练前被改动过或者标注工具重新导出了一份不同顺序的 classes.txt。标签 txt 里存的是 class_id它和具体类名的对应关系完全靠顺序。改了顺序所有 id 的语义整体平移训练的人看不到任何报错但预测结果从头错到尾。解决不要在已有数据集上改 classes.txt 顺序。如果必须改先写一个映射脚本读取旧 id 和旧类名按新顺序映射成新 id逐行重写所有 txt。改完之后用可视化脚本抽十张图检查类名和框的对应关系这一步不能省。5.3 混淆矩阵行和不是 1一类目标被打了两类标签现象训练完成后看 confusion_matrix.png发现某些行的百分比加起来超过 100%person 和 motorcycle 之间互相污染严重。原因标注规范不统一。骑摩托车的人如果被同时标了 person 和 motorcycle同一个视觉目标就有了两个框且这两个框高度重叠。模型训练时一会儿把这片区域当 person一会儿当 motorcycle最终这两类的混淆吞噬掉大量样本。解决回到 2.1 节的标注规则明确“骑在车上的人只标 motorcycle”。然后写一个脚本检查同一张图里不同类别框的 IoU如果 IoU 超过 0.5 且类别不同输出到待修列表人工确认后删除冗余框。5.4 batch 太小导致 BN 崩溃loss 训练中突然变 NaN现象训练到第 30 个 epochloss 毫无预兆地变成 NaN重启训练后又大概率在类似位置复发。原因BatchNorm 在 batch 太小比如 2 或 4时均值和方差的估计噪声非常大。夜间图像本身亮度方差高加上小 batch 的统计波动跑几十轮后数值直接溢出。8G 显存跑 640 分辨率很多人会下意识把 batch 降到 4这恰恰是导火索。解决batch 至少保持 8显存不够就先降 imgsz而不是降 batch。要么保持 batch4 但开启梯度累积让有效 batch 回到 16。另一个备用方案是把model从yolov8s.pt换成yolov8n.pt模型更小显存占用更低batch 也能维持住。5.5 可视化脚本大批量跑 OOM先攒网格再写盘现象跑批量可视化时脚本一次性读入 1000 张图内存冲到 20G 以上程序被杀一张输出也没留下。原因把整个目录的文件一次性读进列表再统一画图图像解码后的原始矩阵非常占内存。一张 1920×1080 的彩色图约 6MB1000 张就是 6GB叠加 Python 自身的对象开销内存很容易爆。解决改用分批处理每批 16 张拼成一张网格图写完盘就释放内存。4.3 节的网格脚本本质上就是在做这件事每轮只持有 16 张图拼完立即imwrite下一轮覆盖同一组变量内存占用被压在常数级别。6. 进阶验证把可视化脚本变成回归测试盯住夜间模型每一版改进6.1 每次训练生成 20 张 val 快照人工对比上一版夜间检测模型迭代最大的风险不是精度不涨而是“这版修好了摩托车却把 bus 搞崩了”。我现在的习惯是每次训练完立刻从 val 里固定抽 20 张图调用可视化脚本叠加标注框保存到以实验名命名的目录里mkdir -p runs/snapshots/exp_037 for f in $(ls night4/images/val/*.jpg | head -20); do python visualize_boxes.py --image $f --data night4 mv vis_*.jpg runs/snapshots/exp_037/ done关键是这 20 张图每次固定不变相当于给模型做回归测试。对比上一版快照重点看两类变化之前能检出的目标现在是否还在车灯、反光区域有没有新增误检。这类退化 mAP 数字上看不出来但快照对比一眼就能发现。我一般把快照目录按日期归档两周后翻出来还能定位是哪个改动引入的倒退。6.2 用同一个 data.yaml 复跑 val输出混淆矩阵和 PR 曲线所有改进实验收尾时用同一份 data.yaml 和同一个 val 集复跑验证是保证结论可比的前提。命令很简单yolo detect val \ datanight4/data.yaml \ modelruns/detect/train_exp037/weights/best.pt跑完后在输出目录里找confusion_matrix.png和PR_curve.png。对比不同版本时盯住两个具体位置一是 person 和 motorcycle 之间的混淆强度二是 PR 曲线上 recall 到 0.8 附近时 precision 的掉速。这两处往往能提前暴露数据标注问题比只看 mAP 一个数字可靠得多。这套验证流程做完改 class 顺序、调增强参数、换模型结构时就有了一个固定的回退锚点。我现在无论换哪个 YOLO 变体都先跑一遍可视化脚本确认 class 映射再开训练最后把快照回归跑一遍。这套流程救过我太多次也希望帮到你。本文还有配套的精品资源点击获取