夜间行人检测:5000张图三种格式标签与YOLO11跨平台训练
简介面向夜间监控与低光行人检测需求这套资源包含5000张真实场景夜间行人高质量图片涉及夜间街景行人、道路行人、遮挡行人及严重遮挡行人等丰富场景并采用LabelImg逐张标注标注质量可靠统一提供VOC(xml)、COCO(json)、YOLO(txt)三种格式可直接接入YOLO等主流检测算法训练。同时附赠YOLO11一键训练脚本覆盖GPU、CPU、Mac(M芯片)三平台有效降低环境配置门槛并提供博主训练结果日志作对照参考。实际交付为单个PDF文档约6.07MB内含数据集缩略图、标注截图、训练示例及完整数据集的获取方式适用于公共场所监控夜间行人检测、智慧交通等落地场景也可作为通用行人检测数据集的夜间场景补充。目前已有611人学习适合目标检测初学者、算法工程师及监控项目开发者从数据准备到模型训练快速上手。1. 夜间行人目标检测5000张图、三种格式标签与三平台训练这件事夜间行人检测是目标检测落地时最容易被“白天模型”坑的场景路灯下的低照度、远处的小目标、车辆大灯造成的过曝随便一条都能让白天训练出来的权重在夜间应用时精度断崖。原因不是模型不行而是训练数据里根本没有足够的夜间样本。这篇笔记要讲的这套方案本质上是一个“数据训练闭环”5000张夜间行人图像配套VOC/COCO/YOLO三种格式的标签加上一个能自动识别GPU/CPU/Mac平台并拉起YOLO11训练的脚本。适合手里有夜间场景需求、正在头痛数据标注和格式转换、又不想在环境配置上耗两天的工程师。2. 夜间行人数据集的构成与三种标注格式的取舍2.1 5000张夜间行人图为什么是“够用”的起点夜间行人检测的首要问题不是模型是数据分布。白天数据集里的行人通常是清晰、全身、光照均匀的占比大夜间恰恰相反低照度导致纹理细节丢失远处的行人可能只有十几个像素再加上雨雾、树影、灯光的干扰行人会以半身、遮挡、背光等形态出现。如果直接拿公开日间数据集比如COCO的person类来训练模型学到的是“有清晰轮廓的两足目标”到夜间部署时检出率会低到让人不敢上线。5000张是什么概念按训练集4000、验证集500、测试集500的划分8:1:1对一个单类别行人检测来说配合YOLO11预训练权重做迁移学习基本能覆盖夜间主要场景。但如果是从零训练不加载预训练权重5000张只够让模型记住训练集泛化能力完全不够。所以后续脚本里默认开了pretrainedTrue这是这套方案能“少图出活”的关键前提。选择夜间行人图的时候别只看数量要看场景分布是否覆盖城市主干道的人行横道、无路灯的小巷、地下通道、雨雪天气、逆光行人背对车灯、远距离小目标图像中行人高度小于32像素。如果5000张里全是“路灯下的正面行人”那模型学完也只能处理路灯下的正面行人。这是数据采集阶段最容易被忽略、后面训练最难补的缺口。另外5000张看起来不少但夜间场景退化严重有效信息密度比白天低实际训练时建议打开数据增强。YOLO11默认的Mosaic、MixUp、HSV扰动对夜间图尤其有用——HSV扰动可以模拟不同色温的路灯Mosaic能把多个夜间场景拼在一张图里相当于免费扩充了小目标样本数量。夜间行人的标注质量直接影响训练上限如果标注框偏大或偏小超过15%后期几乎只能靠重新标注修正。2.2 VOC、COCO、YOLO三种格式到底差在哪同一批标注为什么要弄三种格式因为不同的工具链吃不同的格式老牌语义分割和检测复现常以VOC为基础目标检测论文和评测广泛用COCO的JSON而YOLO系脚本默认要的是每张图对应一个TXT文本。很多工程师拿到标注数据后第一个翻车点不是标注本身而是格式不匹配导致训练脚本报错。三种格式的核心差异在于坐标表示方式格式容器坐标形式边界框表达VOCXML每图一个绝对像素整数xmin, ymin, xmax, ymax左上右下COCOJSON全数据集一个绝对像素浮点/整x, y, width, height左上角宽高YOLOTXT每图一个归一化浮点0-1x_center, y_center, w, h中心点宽高VOC格式的XML是树状的除了bndbox还有object name、difficult、truncated等标签COCO的JSON按images、annotations、categories三个数组组织annotation里除了bbox还有area、iscrowd、segmentation等字段YOLO格式最简单每行一个目标依次是class_id、x_center、y_center、w、h。这里有一个很容易踩的细节VOC的坐标是绝对的像素值COCO的bbox的x、y也是绝对像素但YOLO必须除以图像宽高做归一化。如果直接拿VOC的xmin、ymin、xmax、ymax除以图像尺寸得到的是左上右下归一化坐标这不是YOLO要的格式——YOLO要的是中心点归一化坐标。转换时绕一步先用(xminxmax)/2得到中心点x再除以图像宽度。另一个隐蔽差异是类别ID的约定。VOC里每个类别有字符串名字比如“person”COCO和YOLO都用整数ID。转换时如果不维护一张类别名到ID的映射表比如person: 0、cyclist: 1到训练阶段会出现“class_id 0 是什么”的困惑尤其当你把COCO预训练权重拿来做迁移学习时类别ID和预训练类别ID对不上模型头部的输出维度就会出错。2.3 标注工具的选择与数据划分常见做法是先用LabelImg打VOC格式的标因为它操作直观、可以导出XML然后再写脚本转成COCO和YOLO。LabelImg打标完成后会生成XML目录图像目录和XML目录一一对应文件名相同这个约定后面转换脚本要用。打标的时候建议给“难以辨认的行人”也标注上但可以在VOC的difficult字段标记转换时可以决定是否保留这类样本——训练时去掉过高难度的样本反而有助于稳定收敛。数据划分不要在转换之后再做而应该在原始图像阶段就划分好。先shuffle再按8:1:1切分train/val/test然后把三种格式的标签都按这个划分生成。如果先转换再划分容易在脚本里漏掉某一个格式的划分逻辑后面训练时发现val集和测试集有重叠模型性能虚高。划分时注意设置随机种子确保每次复现结果一致。图片名不要用中文和空格YOLO训练脚本在读取路径时对中文路径的兼容性一直不太好这是很多人训练到一半才发现数据集加载为0的原因。划分完以后检查一下三张子集里的场景分布如果全部夜间图里雨雾天占30%别让测试集里一张雨雾天都没有否则报告出来的mAP会给人错误的乐观感。3. VOC转YOLO与COCO转换脚本与四个边界坑3.1 从VOC标注到YOLO的TXT文件一张图一个文件的脚本假设目录结构是images/图片、labels_voc/XML、labels_yolo/待生成TXT。写一个Python脚本完成转换。import os import xml.etree.ElementTree as ET # 类别名 - ID 映射顺序要和训练用的data.yaml一致 CLASS_MAP {person: 0, cyclist: 1} def voc_to_yolo(xml_path, img_w, img_h, txt_path): 单个XML转YOLO TXT。 img_w和img_h必须来自该XML对应图片的真实尺寸不能用过路值。 tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): name obj.findtext(name) if name not in CLASS_MAP: continue # 不在类别表里的目标直接跳过避免训练报错 xmlbox obj.find(bndbox) xmin float(xmlbox.findtext(xmin)) ymin float(xmlbox.findtext(ymin)) xmax float(xmlbox.findtext(xmax)) ymax float(xmlbox.findtext(ymax)) # 边界裁切标注可能略微超出图像范围 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: continue # 裁切后无效框直接丢弃 # YOLO格式归一化的中心点 宽高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{CLASS_MAP[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) # 遍历XML目录为每张图生成同名TXT xml_dir labels_voc txt_dir labels_yolo os.makedirs(txt_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_file) # 从同名图片读取宽高不要信任XML里的size节点常见于截图/缩放后不一致 img_path os.path.join(images, xml_file.replace(.xml, .jpg)) from PIL import Image with Image.open(img_path) as im: img_w, img_h im.size txt_path os.path.join(txt_dir, xml_file.replace(.xml, .txt)) voc_to_yolo(xml_path, img_w, img_h, txt_path)这段脚本的处理逻辑分三层第一层解析XML的object节点过滤掉不在CLASS_MAP里的类别第二层对坐标做边界裁切因为标注时手滑点出图像范围是常见事第三层才是真正的坐标换算。参数说明img_w和img_h用PIL从同名图片读取而不是读XML里的size是因为有时图片被批处理过但XML没同步更新用XML里存的旧尺寸算归一化坐标会产生系统性偏移。CLASS_MAP的顺序就是训练时类别ID的顺序这个顺序必须和后面data.yaml里的names列表完全一致否则训练出来的模型类别会错位。3.2 VOC转COCO的JSON注意三个字段必须对齐COCO格式比YOLO复杂但迁移学习时经常需要。脚本里最核心的是生成images、annotations、categories三个列表并且id要全局唯一。import json import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_coco(xml_dir, img_dir, out_json, categories): 把整个VOC标注目录转成一个COCO JSON。 categories: [{id: 0, name: person, supercategory: person}] images [] annotations [] ann_id 1 for img_id, xml_file in enumerate(sorted(os.listdir(xml_dir))): if not xml_file.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_file) img_name xml_file.replace(.xml, .jpg) img_path os.path.join(img_dir, img_name) with Image.open(img_path) as img: width, height img.size images.append({ id: img_id, file_name: img_name, width: width, height: height }) tree ET.parse(xml_path) root tree.getroot() for obj in root.iter(object): name obj.findtext(name) cat_id None for c in categories: if c[name] name: cat_id c[id] break if cat_id is None: continue xmlbox obj.find(bndbox) xmin float(xmlbox.findtext(xmin)) ymin float(xmlbox.findtext(ymin)) xmax float(xmlbox.findtext(xmax)) ymax float(xmlbox.findtext(ymax)) xmin max(0, min(xmin, width - 1)) xmax max(0, min(xmax, width - 1)) ymin max(0, min(ymin, height - 1)) ymax max(0, min(ymax, height - 1)) if xmax xmin or ymax ymin: continue w xmax - xmin h ymax - ymin annotations.append({ id: ann_id, image_id: img_id, category_id: cat_id, bbox: [xmin, ymin, w, h], # COCO: 左上角 宽高 area: w * h, iscrowd: 0, segmentation: [] # 检测任务可留空不需要多边形 }) ann_id 1 coco { images: images, annotations: annotations, categories: categories } with open(out_json, w, encodingutf-8) as f: json.dump(coco, f, ensure_asciiFalse, indent2) categories [ {id: 0, name: person, supercategory: person}, {id: 1, name: cyclist, supercategory: person} ] voc_to_coco(labels_voc, images, annotations_coco.json, categories)这个脚本有两点和常规写法不一样一是COCO的annotation id是全局自增的不能每个图从1重新开始否则一些训练代码在验证阶段会拿错annotation二是bbox的坐标原点是左上角宽度和高度是像素差不是右下角坐标相减之后再归一化。很多从VOC迁移过来的人在这里会习惯性除以图像宽高结果COCO数据里全是0到1的小数训练时边界框回归直接发散。注意COCO格式的segmentation字段在纯检测任务里可以留空列表但如果后面要做实例分割微调这个字段必须补成多边形坐标否则训练会报TypeError。3.3 格式转换的四个边界坑坐标、ID、空文件与路径第一个坑是坐标越界。标注时框体偶尔会超出图像边界VOC格式里能存负数或超过宽高的值YOLO训练框架读取到x_center大于1.0或小于0.0时通常会忽略该目标但有些老版本会直接报错。解决办法就是转换时裁切到[0, img_w-1]范围内并且在裁切后判断框是否还有正面积没有就直接丢。这个边界裁切逻辑在两个脚本里都写了别嫌麻烦删掉。第二个坑是类别ID不一致。YOLO预训练权重是在COCO 80类上训出来的如果你的数据yaml里把person定义为class 0、cyclist定义为class 1那迁移学习时模型头部的输出通道会被重置这没有问题——但如果你加载的是别人定义好的已经微调过的权重里面person可能是第0类也可能是第12类COCO原始定义里person是第0类但有些项目会重新排列。统一做法是自定义数据集一律从class 0开始连续编号别去对齐COCO原始序号。第三个坑是空标注文件。夜间场景里难免有几张图一个目标都标不出来或者目标小到没法标。转换脚本必须允许生成空的TXT/JSON不要在遍历时因为某张图没有有效框而报错退出。YOLO训练框架对空TXT文件是能正常处理的它会把它当作纯背景图参与训练这在夜间这种低目标密度场景里甚至是好事。COCO格式里则表现为某张image_id没有任何annotation这同样合法。第四个坑是路径与文件名隐式依赖。三个格式转换脚本都会用“文件名相同但扩展名不同”的约定来定位文件和图片如果原始素材里出现了同名但不同格式的图片比如a.jpg和a.png同时存在转换后会产生两个同名TXT相互覆盖。遇到这种素材最省事的方案是先重命名去重而不是在脚本里做特判。4. 用YOLO11一键训练脚本在GPU/CPU/Mac三平台跑起来4.1 环境准备一个requirements文件齐活YOLO11的官方实现是基于Ultralytics框架的训练入口简化到一条命令但跨平台环境的差异集中在torch有没有正确识别计算设备。GPU平台要装CUDA版本的torchCPU平台装CPU版即可Mac上要求MPS支持的torchApple Silicon或AMD显卡机型。一条pip命令搞定pip install ultralytics torch torchvision这里的坑在于如果机器上有NVIDIA显卡pip默认装的torch是CPU版或CUDA版本不匹配训练时脚本会静默回退到CPU速度慢到让人误以为脚本卡死。检查方法是进Python跑一句import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU)Mac用户则要确认torch版本是2.x以上且torch.backends.mps.is_available()返回True。老版本torch的MPS支持不完整可能会在训练中报“not implemented”错误。CPU平台没什么好验的但建议确认一下内存足够——YOLO11s在640分辨率下CPU训练8线程时单EPOCH在5000张图上可能要跑十分钟以上心态先放平。4.2 data.yaml的组织方式这是训练脚本的前置条件YOLO11训练的第一步不是跑脚本而是写data.yaml路径写错是新手最常见的问题。一个能用的data.yaml长这样# data.yaml path: /Users/yourname/night_pedestrian # 数据集根的绝对路径 train: images/train # 相对于path的训练图片目录 val: images/val # 验证图片目录 test: images/test # 测试图片目录可省略 nc: 2 # 类别数和CLASS_MAP的键数量一致 names: [person, cyclist] # 顺序必须和标签里的class_id对应这里有个隐藏要求YOLO训练框架在train/val目录下会找对应的labels目录具体规则是images/train对应的标注目录为labels/train如果images目录下没有同名子目录就会报“found no labels”的提示。见到这个提示先别急着改脚本检查labels/train里到底有没有txt文件以及文件名是否和图片文件名完全一致不含扩展名。另外path写绝对路径最省事写相对路径容易因为工作目录不对而找不到数据。4.3 一键训练脚本自动选择设备与参数一键训练的“一键”主要体现在设备自动选择和参数默认值上。脚本逻辑是依次探测CUDA、MPS、CPU选到第一个可用的计算设备然后根据设备类型给不同的batch和workers默认值最后拉起train。# train_yolo11.py import argparse import torch from ultralytics import YOLO def detect_device(): 按优先级返回计算设备字符串cuda - mps - cpu if torch.cuda.is_available(): return cuda:0 if hasattr(torch.backends, mps) and torch.backends.mps.is_available(): return mps return cpu def main(): parser argparse.ArgumentParser() parser.add_argument(--model, defaultyolo11s.pt, help预训练权重可选yolo11n/s/m/l/x) parser.add_argument(--data, defaultdata.yaml, help数据集配置) parser.add_argument(--epochs, typeint, default100, help训练轮数) parser.add_argument(--imgsz, typeint, default640, help输入分辨率) parser.add_argument(--batch, typeint, defaultNone, help默认按设备自动选择) parser.add_argument(--device, defaultNone, help手动指定设备覆盖自动检测) args parser.parse_args() device args.device if args.device else detect_device() print(f[Info] 自动选择计算设备: {device}) # 不同平台的默认超参数CPU和Mac用更小的batch避免内存爆炸 if args.batch: batch args.batch elif device.startswith(cuda): batch 16 else: batch 8 # workersMac上过大容易报“进程启动失败”CPU上过小拉不满CPU if device mps: workers 0 # MPS后端与DataLoader多进程兼容性差直接关掉最稳 elif device.startswith(cuda): workers 8 else: workers 4 model YOLO(args.model) model.train( dataargs.data, epochsargs.epochs, imgszargs.imgsz, batchbatch, devicedevice, workersworkers, patience20, # 验证集mAP连续20轮不涨就早停 pretrainedTrue, # 迁移学习是5000张小数据集的命根子 cacheram if device.startswith(cuda) else False, # GPU机器缓存图片提速 projectruns/night_detect, nameyolo11s_night ) # 训练完成后自动导出ONNX方便后续部署验证 best_model YOLO(runs/night_detect/yolo11s_night/weights/best.pt) best_model.export(formatonnx, imgszargs.imgsz, halfTrue if device.startswith(cuda) else False) if __name__ __main__: main()# 一键运行 python train_yolo11.py --model yolo11s.pt --data data.yaml --epochs 150 --imgsz 640脚本的关键逻辑在设计上是分层的detect_device()做设备探测batch和workers的默认值按设备类型分别给出训练参数把patience早停打开结束自动导出ONNX。模型大小的选择上夜间小目标多优先yolo11s起步显存够大再上yolo11m不建议一上来就用yolo11l——5000张数据不足以发挥大模型的优势还可能过拟合。几个参数建议根据实际情况调整batch在GPU上如果显存只有6G16会爆显存要降到8甚至4Mac的workers设成0看起来是“关闭多进程”会让训练变慢但至少不会反复崩溃epochs用早停的话设150-300都可以实际训练很可能在60轮左右就触发patience停掉。imgsz不建议低于640夜间小目标本来就小分辨率再低就更难检测了。4.4 训练日志怎么看loss和mAP才是进度的真相训练过程中终端会滚动输出每一轮的metrics重点关注三个数box_loss边界框回归损失、cls_loss分类损失、mAP50-95整体检测精度。box_loss过高说明回归没收敛cls_loss高说明模型把行人和其他夜间物体混淆mAP50-95稳步上升就说明模型在学东西。如果看到mAP50到了0.5但mAP50-95只有0.2左右说明框的位置精度不够多数情况是分辨率太低或者难例太多。还有一个容易被忽略的点训练日志里的“All labels empty”提示通常意味着label路径没配对而不是数据为空。遇到这个先看labels/train目录里有没有txt文件再看data.yaml里path是否绝对路径最后确认图片扩展名大小写一致。5. 避坑记录夜间行人训练中我反复遇到的五个翻车点5.1 现象Loss为NaN训练直接中断原因学习率过大或batch过小导致梯度爆炸尤其在数据集只有几千张且加载了预训练权重时某些批次的样本里全是难例梯度范数突然拉高。解决把lr0从默认值调低到0.001甚至0.0005或在train参数里加weight_decay0.0005如果是batch2导致的极端波动batch越小梯度噪声越大把batch加到8以上哪怕用梯度累积也要保证有效batch8。5.2 现象mAP在验证集很高但实际跑一张白天街景全漏检原因训练集里的夜间行人分布太集中比如大量样本都来自同一段路的监控截图背景纹理被模型当成了关键特征。这属于典型的过拟合而非模型能力问题。解决回到数据层面按场景检查验证集和测试集里是否有重复或近似重复的图片监控视频连续帧特别容易重复如果重复率超过10%要先做去重再训练其次是加强数据增强把Mosaic强度打开并加入随机遮挡。5.3 现象Mac上训练启动时报DataLoader worker进程崩溃原因Ultralytics在Mac MPS后端上DataLoader多进程和MPS的显存管理有兼容性问题worker数大于0时容易在每轮开始时崩溃。解决在脚本里显式把workers设为0并加一句启动参数“--noplots”减少绘图开销。这是三平台里最“玄学”的一处人员在Mac上耗时耗力调了半天最后发现只是workers的问题。附带建议是Mac上不要用yolo11m以上模型mps的推理效率在中等模型上比CUDA差得远训练时间和发热都不划算。5.4 现象验证集mAP不低但模型把“路灯杆”误检成行人原因夜间场景里行人教材少而路灯杆的形状、高度和夜间行人有相似性如果训练数据里负面样本没有行人的纯夜景图太少模型没有见过足够多的“像行人的不是行人”的样本。解决在数据划分时额外留出不进训练集的负样本图用YOLO11的val预测看哪些背景被误检把这些误检图作为“背景类”样本加入训练集不在TXT里写任何标注模型会把这图片当作纯背景学习。这是夜间检测提升精度最有效的手段5000张有标注图配2000张无标注夜景图往往比盲目再加5000张有标注图效果更好。5.5 现象转换脚本没有报错但训练日志里“All labels empty”原因标签TXT文件存在但内容为空或YOLO训练框架找不到labels目录。常见原因是转换脚本生成TXT时把坐标归一化成了0到1的值但训练脚本读取时按图像尺寸反解出了问题——通常是图片扩展名大小写不一致.JPG vs .jpg导致框架找不到对应图片。解决先检查labels/train下TXT数量是否等于images/train下图片数量再任选一个TXT文件打开看内容是否非空坐标值是否都在0-1之间最后确认图片扩展名统一。补充一个排查工具ultralytics框架自带数据集检查命令可以在训练前快速发现格式问题yolo check data.yaml这条命令会输出数据集是否有效、类别数、图片数和标注数比直接跑训练省时间。6. 进阶验证从mAP数字到真实夜间场景的最后一个闭环训练收敛后别急着宣称“模型能用了”。我要做的第一件事永远是写一个预测脚本在未参与训练的视频帧或照片上做一次实测分别看白天行人、夜间远距离行人和夜间遮挡行人三类样本的检测结果。mAP是统计指标但实际部署关心的是某一类具体的漏检。调confidence阈值是见效最快的手段夜间场景中误检少、漏检多把conf从默认0.25降到0.1能召回不少远距离小行人代价是路灯杆误检增加反之如果误检率太高调到0.4。调阈值不重训模型是部署现场最常用的“后悔药”。再往深一步验证时要关注的是“框抖动”。夜间光线暗模型对同一行人在连续几帧里的置信度波动大检测框会忽大忽小。解决办法是给预测输出加一个轻量的时序平滑对连续帧的检测框做IoU匹配匹配到的框用EMA更新坐标和置信度。这个技巧不用改模型纯推理端就能做效果立竿见影。最后的习惯是定期把跑不动或者漏检的样本收集回来重新标注加进训练集做一次增量训练。5000张图是起点不是终点夜间场景的特点是“每个新场景都在挑战模型没见过的情况”。我自己的做法是每收集到50张左右的难例就触发一次短训练30个epoch、8batch把增量数据的影响控制在半小时内看完。做检测的都知道模型能不能落地往往不取决于你一开始有多少数据而取决于你有没有一个快速吃进新数据的闭环。把这条闭环跑通夜间行人检测才算真正落地。希望我的这些踩坑记录和训练习惯能帮到你。本文还有配套的精品资源点击获取