ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

夜间行人检测:5000张数据+三种标签格式+YOLO11跨平台训练

夜间行人检测:5000张数据+三种标签格式+YOLO11跨平台训练 简介面向夜间监控场景的行人检测需求这份数据集整理了5000张真实夜间、低光环境下行人图像涵盖街景、道路、遮挡及严重遮挡等多样场景适用于公共场所监控、智慧交通等实际项目也可作为通用行人检测数据集的夜间场景补充。数据采用LabelImg标注质量较高同时提供VOC、COCO、YOLO三种格式标签可直接用于YOLO等主流算法训练。配套的YOLO11一键训练脚本支持GPU、CPU、Mac多平台运行并附有博主训练结果日志供参考适合监控安防方向算法工程师与研究人员快速上手。资源包内共1个PDF文件大小约6.07MB包含数据集详细介绍、标注截图及百度网盘获取方式实际图片数据需按文档指引下载。文档中还附有数据缩略图与LabelImg标注实例便于在获取完整数据前了解画质与标注样式。已有611人浏览学习作为夜间行人检测的基础数据与训练参考实用价值突出。1. 夜间行人检测卡住的从来不是模型而是数据夜间行人目标检测是安防、车载和边缘设备里最常被问到的需求之一白天跑得好好的模型一入夜就漏检、误检一起上。问题大多不在模型结构而在数据夜间图像亮度低、噪声大行人常常只有一小块模糊轮廓公开数据集里又很少有专门针对夜间的行人标注。这套方案的做法是直接把数据补齐——5000张夜间行人图每张都带VOC/COCO/YOLO三种格式的标签再配一个同时支持GPU(GPUs)/CPU/Mac三平台的YOLO11一键训练脚本让拿到手的人不用折腾格式转换和环境配置直接就能把训练跑起来。适合正在做夜间行人检测、被数据预处理和跨平台训练环境反复拖住的研究者和工程师。如果你已经在用YOLO11或其他检测模型手头缺的不是模型代码而是夜间场景的标注数据那么这套组合解决的是从数据到训练的最后一公里。接下来我会按数据检验、格式转换、脚本适配、踩坑排查这个顺序把每个环节的落地细节讲清楚。2. 夜间行人检测为什么难先做数据体检再谈训练2.1 夜间图像的三个典型特征低照度、暗部噪声、小目标占比高夜间行人和白天行人最大的区别不是“人变了”而是“图变了”。一张夜间监控图里行人区域的平均灰度值往往比白天低40%以上暗部噪声在增强对比度后会被同步放大同时行人轮廓的边缘锐度明显下降。这些特征直接决定了训练策略不能拿白天数据集硬凑也不能简单套用白天的增强参数。拿到5000张夜间行人图之后第一件事不是开训练而是做数据体检。我一般会先统计整批图像的亮度分布和边界框尺寸分布这两个指标直接决定增强策略和学习率上限。用Python跑一个快速统计脚本输出每张图的灰度均值和每个标注框的像素面积。import cv2 import os from glob import glob img_dir images/train boxes [] # 假设已有解析好的bbox列表: (x1, y1, x2, y2) brightness_list [] for img_path in glob(os.path.join(img_dir, *.jpg)): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) brightness_list.append(img.mean()) print(f平均亮度: {sum(brightness_list)/len(brightness_list):.2f}) print(f最暗图像亮度: {min(brightness_list):.2f}) print(f最亮图像亮度: {max(brightness_list):.2f}) # 统计bbox面积分布 areas [(x2-x1)*(y2-y1) for x1, y1, x2, y2 in boxes] sorted_areas sorted(areas) print(fbbox面积中位数: {sorted_areas[len(sorted_areas)//2]:.0f}px) print(f最小10%的bbox面积: {sorted_areas[len(sorted_areas)//10]:.0f}px)这段代码的逻辑很简单用OpenCV读灰度图算均值得到亮度分布再把标注框面积排序看小目标占比。参数说明里有两个关注点一个是亮度均值低于80的图如果超过三成就一定要在训练前加亮度增强另一个是bbox面积中位数如果低于32×32说明小目标居多训练时的imgsz建议设为640而不是1280——分辨率设得太高反而会让小目标在缩放采样中丢失。2.2 5000张图的规模够不够按场景分布而非只看总数很多人一看到5000张第一反应是“够吗”这个问题的答案取决于场景分布而不是总数。5000张如果全是同一个路口的同一视角训练效果还不如按场景分层的2000张。夜间行人检测的常见分布是城市道路、小区出入口、园区周界、地下停车场四个场景每个场景的数量要相对均衡。拿到数据集后我会按文件名前缀或目录结构把图像按场景分组确认每个场景至少有800到1000张。如果某个场景数量特别少考虑用另一批数据做fine-tuning而不是把全部数据混在一起训。评估指标也要按场景分开算综合mAP高但停车场场景mAP低的情况很常见这种时候单纯加数据不如检查停车场场景的标注质量。标注质量检查同样不能省。夜间图的标注难点在于低照度下行人边界不清晰标注框容易偏大或偏小。我常用一个简单的检查方法把标注框画回图上生成预览图肉眼抽查100张重点看遮挡严重的区域和远距离小目标区域。抽查时发现框偏大还是偏小要记录比例批量修正后用修正后的标签再训练这比模型训完再回头查数据省时间得多。提示夜间行人标注最容易出问题的不是“有行人没标”而是“把路灯阴影、灯箱广告里的人形轮廓标成了行人”。这一类误标注会让模型在误检指标上很难看清理时优先处理。2.3 标签结构核对VOC/COCO/YOLO三类格式的关键字段差异标题里提到5000张图都带三种格式标签这意味着数据组织上有多个目录。训练前要核对的是三类格式的字段完整性而不是直接信任文件存在。VOC格式是XML文件每张图对应一个XML。关键字段是width和height必须和实际图片尺寸一致bndbox的四点坐标必须是整数。COCO格式是单个JSON文件关键字段是images数组里的id与annotations里的image_id能对上categories里的id从1开始且连续。YOLO格式是TXT文件每行一个目标格式是class_id x_center y_center width height注意center和width、height都是归一化到0到1之间的浮点数。最常翻车的是VOC和YOLO之间的坐标换算。VOC给的是左上角(xmin, ymin)和右下角(xmax, ymax)的绝对像素值YOLO要的是中心点坐标和宽高的归一化值。换算公式是x_center (xmin xmax) / 2 / widthy_center (ymin ymax) / 2 / heightw (xmax - xmin) / widthh (ymax - ymin) / height。如果图片在预处理阶段被resize过这些归一化值必须基于resize后的尺寸重新算不然边界框会整体偏移。import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() yolo_lines [] for obj in root.findall(object): cls obj.find(name).text class_id class_map[cls] # 需要预先定义类别映射 box obj.find(bndbox) xmin int(float(box.find(xmin).text)) ymin int(float(box.find(ymin).text)) xmax int(float(box.find(xmax).text)) ymax int(float(box.find(ymax).text)) # 越界截断标注框超出图像边界时必须裁剪否则训练时loss计算会出NaN xmin max(0, min(xmin, img_width - 1)) xmax max(0, min(xmax, img_width - 1)) ymin max(0, min(ymin, img_height - 1)) ymax max(0, min(ymax, img_height - 1)) x_center ((xmin xmax) / 2) / img_width y_center ((ymin ymax) / 2) / img_height w (xmax - xmin) / img_width h (ymax - ymin) / img_height yolo_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return yolo_lines这段脚本是VOC转YOLO的核心逻辑其中越界截断是很多人容易漏掉的步骤夜间图像的标注在低照度下容易出现框的一端超出图像边缘这类标签不处理直接训练轻则mAP下降重则训练过程中loss变成NaN。class_map是你自己的类别映射表必须和训练配置里的names顺序完全一致YOLO训练时读取的是数字ID而不是类别名。3. 把VOC/COCO/YOLO三种格式对齐转换脚本与边界坑3.1 三格式互转的最短实现路径以VOC为主基准三种格式里VOC的XML结构最直观字段语义最接近人类阅读习惯我通常把它作为转换基准。做法是先把COCO和YOLO都转成VOC需要什么格式再从VOC生成这样只需要写两套转换逻辑比维护六套互转脚本省心得多。COCO转VOC时注意JSON里的segmentation字段在行人检测里基本用不到只需要读bbox字段它的格式是[x, y, width, height]和VOC的xmin/ymin/xmax/ymax差一个换算。YOLO转VOC时要做的是把归一化坐标乘以图像宽高还原成像素值再换算成左上角右下角格式。两类转换的共同点是都要查一下image_id对应的图片实际尺寸不能凭感觉猜。import json from PIL import Image def coco_to_voc(coco_json_path, image_dir, output_xml_dir): with open(coco_json_path, r) as f: coco json.load(f) # 构建 image_id - 文件名 的映射 id_to_file {img[id]: img[file_name] for img in coco[images]} # 构建 category_id - 类名 的映射 cat_id_to_name {cat[id]: cat[name] for cat in coco[categories]} # 按 image_id 分组 bbox 标注 anns_by_image {} for ann in coco[annotations]: img_id ann[image_id] if img_id not in anns_by_image: anns_by_image[img_id] [] anns_by_image[img_id].append(ann) for img_id, anns in anns_by_image.items(): file_name id_to_file[img_id] img_path os.path.join(image_dir, file_name) img Image.open(img_path) w, h img.size xml_root ET.Element(annotation) ET.SubElement(xml_root, filename).text file_name size_elem ET.SubElement(xml_root, size) ET.SubElement(size_elem, width).text str(w) ET.SubElement(size_elem, height).text str(h) for ann in anns: x, y, bw, bh ann[bbox] # COCO的bbox是(x, y, w, h) obj_elem ET.SubElement(xml_root, object) ET.SubElement(obj_elem, name).text cat_id_to_name[ann[category_id]] bndbox ET.SubElement(obj_elem, bndbox) ET.SubElement(bndbox, xmin).text str(int(x)) ET.SubElement(bndbox, ymin).text str(int(y)) ET.SubElement(bndbox, xmax).text str(int(x bw)) ET.SubElement(bndbox, ymax).text str(int(y bh)) xml_path os.path.join(output_xml_dir, os.path.splitext(file_name)[0] .xml) tree ET.ElementTree(xml_root) tree.write(xml_path, encodingutf-8, xml_declarationTrue)这段代码的逻辑是遍历COCO的JSON结构把annotations按image_id分组每张图生成一个XML文件。x bw和y bh是COCO转VOC唯一的数学变换其他都是字段映射。参数说明里有一个容易踩的细节PIL.Image.open拿到的是图像实际尺寸但某些数据集里coco[images]里的width和height字段可能与实际图片不一致要以实际图片为准否则标注偏移几个像素在夜间小目标上会被放大成明显错位。3.2 转换后必须做的三步校验文件数、坐标范围、类别映射转换脚本跑完之后不能直接开训练先做三步校验。第一步是文件数对比VOC的XML数量、COCO的images数量、YOLO的TXT数量必须一致且和图片文件数量一致。不一致的话多半是某张图没有标注文件或者是转换脚本里漏了空标注图的处理。第二步是坐标范围校验所有YOLO的归一化坐标必须在0到1之间VOC的像素坐标必须在图像尺寸范围内。这个可以用脚本批量检查发现越界就按上一节里的方法做截断。第三步是类别映射校验三套标签里的类名或类别ID必须统一比如VOC里叫pedestrianCOCO里叫personYOLO里class_id对应到的是同一个类这个不一致会让训练时的类别数量统计错乱而且报错信息往往不直观。def validate_yolo_labels(txt_dir, img_dir, class_count): import numpy as np errors [] for txt_path in sorted(glob(os.path.join(txt_dir, *.txt))): img_path os.path.join(img_dir, os.path.splitext(os.path.basename(txt_path))[0] .jpg) if not os.path.exists(img_path): errors.append(f缺失图片: {img_path}) continue with open(txt_path, r) as f: lines f.read().strip().split(\n) for line in lines: parts line.split() if len(parts) ! 5: errors.append(f字段数错误: {txt_path}: {line}) continue cls_id int(parts[0]) coords [float(x) for x in parts[1:]] if cls_id 0 or cls_id class_count: errors.append(f类别ID越界: {txt_path}: {cls_id}) if any(c 0 or c 1 for c in coords): errors.append(f坐标越界: {txt_path}: {line}) if errors: print(f发现 {len(errors)} 个错误前10个:) for e in errors[:10]: print(e) else: print(校验通过)这个校验脚本的核心逻辑是逐行检查YOLO标注的五个字段class_count是你训练配置里的类别总数。如果类别ID等于class_count或更大说明转换脚本的类别映射表有遗漏。坐标越界是一个很隐蔽的问题因为YOLO训练时不会直接报错只会让loss曲线异常波动最后mAP莫名其妙地低。3.3 数据集划分夜间场景不能随机划分数据集的train/val划分看起来是最简单的步骤实际上夜间行人数据在这里有一个大坑随机划分会把同一个场景的连续帧同时分进train和val导致val的mAP虚高。比如同一个路口连续拍了200帧随机划分后160帧在train里、40帧在val里val里测的是模型“见过”的场景的夜间行人指标当然好看但部署到新场景就露馅。我一般按场景划分而非按图片划分每个场景的图片数量乘以0.8取整前80%按时间顺序进train后20%进val。这样能保证val数据是模型没有见过的场景分布指标更接近实际部署效果。如果有超过15%的图片是连续帧且画面内容高度相似先按每隔N帧抽帧的方式做去重再按场景划分。import random from collections import defaultdict random.seed(42) # 按目录前缀识别场景例如 scene1_001.jpg, scene2_001.jpg def get_scene(filename): return filename.split(_)[0] img_files os.listdir(images) scene_to_files defaultdict(list) for f in img_files: scene_to_files[get_scene(f)].append(f) train_files, val_files [], [] for scene, files in scene_to_files.items(): files.sort() # 按时间顺序排序 split_idx int(len(files) * 0.8) train_files.extend(files[:split_idx]) val_files.extend(files[split_idx:]) # 写入train.txt和val.txt with open(train.txt, w) as f: f.write(\n.join(train_files)) with open(val.txt, w) as f: f.write(\n.join(val_files))这个划分逻辑的关键是files.sort()先按时间排序再按比例切分确保val是时间上靠后的连续片段。split_idx用0.8还是0.85取决于数据量5000张的规模用0.8比较稳妥val保留1000张左右的量级评估指标的置信度更高。如果你有多个摄像头拍摄的同一场景按摄像头ID再分一层别让val里混入同场景不同机位的图片。4. 用YOLO11一键训练脚本打通GPU/CPU/Mac三平台4.1 三平台训练的环境差异与脚本设计思路YOLO11支持的平台范围很广Windows/Linux的NVIDIA GPU、纯CPU环境、Apple Silicon的Mac都能跑但三个平台的底层依赖完全不同。GPU环境需要CUDA、cuDNN和PyTorch的CUDA版本配套CPU环境不需要CUDA但推理速度慢Mac环境走的是MPS后端PyTorch从1.12开始支持但并非所有算子都有MPS实现。一个脚本同时适配三平台核心思路是自动检测环境而不是让用户手动配置。脚本启动时先读取torch.cuda.is_available()判断GPU是否可用再判断torch.backends.mps.is_available()判断Mac的MPS是否可用最后默认落入CPU。这三层判断不需要用户干预训练参数里的device字段会自动设为cuda:0、mps或cpu。import torch def detect_device(): if torch.cuda.is_available(): device cuda:0 print(f使用GPU: {torch.cuda.get_device_name(0)}) elif hasattr(torch.backends, mps) and torch.backends.mps.is_available(): device mps print(使用Mac MPS加速) else: device cpu print(使用CPU训练速度会较慢) return device device detect_device()这段代码放在训练脚本的最前面detect_device()返回的device字符串直接传给YOLO11的model.train()方法。注意hasattr(torch.backends, mps)这个判断不能省因为旧版PyTorch根本没有mps属性直接调用is_available()会抛AttributeError。另外MPS只支持Apple Silicon芯片Intel Mac会落入CPU分支。4.2 一键训练脚本的核心参数讲解epochs、batch、imgsz、patience训练脚本里除了设备检测最重要的就是model.train()的参数设置。YOLO11的API很统一关键参数就那么几个但每个都要按夜间行人场景调整不能照搬白天目标检测的配置。epochs我设100起步夜间行人检测的收敛比白天慢因为低照度下的特征学习难度更高60轮以内往往还在剧烈波动。batch需要按显存大小调整GPU上一般8到16CPU和MPS上建议4到8因为这两个平台的显存带宽有限batch太大会直接显存溢出。imgsz是输入图像分辨率夜间小目标多用640如果标注框大面积在32×32以下建议试1280但要用GPU。from ultralytics import YOLO model YOLO(yolo11n.pt) # 或 yolo11s.pt按显存选择 results model.train( datanight_pedestrian.yaml, epochs100, batch8, imgsz640, devicedevice, patience15, save_dirruns/night_pedestrian, optimizerauto )patience15是早停轮数15轮内val mAP没有提升就停止训练。夜间行人数据集的val波动比白天大patience设太小容易早停设太大会浪费时间15是一个比较折中的值。optimizerauto让YOLO11自己按batch size选优化器省得手动调SGD还是AdamW的参数省掉很多玄学调参的环节。4.3 跨平台训练的预期速度与合理预期训练速度是很多人关心的实际问题三平台的差距很大需要提前设好预期。一张NVIDIA RTX 4090上跑yolo11n、5000张图、640分辨率、batch 16大约每分钟处理80到120张100轮大概在两三个小时内完成。Apple Silicon的MPS后端速度大约是GPU的1/3到1/2同样的配置可能要六个小时以上。CPU环境最慢只能当验证流程用完整训练一百轮可能要十几个小时到几十个小时。考虑到夜间行人检测的模型推理场景往往在边缘设备上我的习惯是先在GPU上用yolo11n跑通流程验证数据和标签没有问题再切换到目标部署平台跑完整训练。如果你没有独立GPUMac用户建议至少用M1 Pro以上的芯片CPU用户建议把imgsz降到480并把epochs降到50先确认整个pipeline没有报错再慢慢加量。注意MPS后端在PyTorch和YOLO11的某些版本上存在梯度同步问题具体表现是训练loss在几十轮后突然变成NaN。如果遇到这个情况先试试torch.backends.mps.sync_debug_on_error True打开调试或直接切换到CPU训练。这个坑在Mac上做训练的教程里经常被一笔带过实际踩到的人不少。5. 训练路上的五个常见坑与排查思路5.1 标注框越界导致loss变成NaN或mAP异常低现象训练能正常启动但loss在几轮后出现NaN或者训练结束后mAP异常低大部分类别AP接近0。原因YOLO格式的归一化坐标不在0到1之间通常是转换脚本没有做越界截断或者原始标注框本身超出了图像边界。解决用3.2节的校验脚本全量检查TXT标签把越界的行单独输出逐条看是整体偏移还是单边越界。整体偏移说明转换时图像尺寸用错了单边越界修正对应坐标即可。修正完重新训练不用改其他参数。5.2 夜间图像增强过度导致误检飙升现象训练时用了大量亮度增强和对比度增强训练mAP不错但实际部署时误检很多灯箱广告、路牌反光被识别为行人。原因夜间图像的亮度增强如果太激进会把暗部噪声放大成类似人形的纹理特征模型学到了这些噪声模式。解决把亮度增强的范围收窄比如原来的亮度(-0.5, 0.5)改成亮度(-0.2, 0.2)同时加一个限制亮度增强只作用于局部区域而不是整图。YOLO11的增强参数在model.train()里用hsv_h、hsv_s、hsv_v控制其中hsv_v是亮度扰动夜间场景建议设0.2左右而不是默认的0.4。5.3 Mac MPS报算子不支持错误现象在Mac上跑训练脚本报错是NotImplementedError或Operator not implemented for MPS位置通常在数据加载或损失计算阶段。原因PyTorch的MPS后端还在持续完善中YOLO11的某些算子如特定形态的RoIAlign在MPS上没有实现会回退到CPU但有少数算子直接抛异常。解决先升级PyTorch到最新版MPS的支持在近几个版本有明显改善。如果仍然报错在训练脚本里加环境变量PYTORCH_ENABLE_MPS_FALLBACK1让不支持的算子自动回退CPU。要注意的是这个环境变量要在import torch之前设置否则不生效。5.4 CPU训练时内存不足或swap频繁现象CPU训练启动后系统变得卡顿训练日志里每个epoch耗时越来越长甚至直接OOM被杀。原因CPU训练时的num_workers设置过高多个进程同时加载图像导致内存峰值很大。另一个原因是batch设置过大CPU内存被激活图占满。解决把num_workers降到2或4batch降到4或2同时把cacheTrue关掉。CPU训练时cacheTrue会把整批图像缓存到内存里显存不吃紧但内存会爆。改成cacheFalse后训练时间会增加但系统稳定性大幅提升。5.5 训练后推理效果差但val mAP却很高现象val mAP有0.8以上但拿一段没参与训练的视频去测检测结果很差漏检严重。原因数据划分时没有按场景分离val里混入了与train高度相似的连续帧导致val指标虚高。解决按3.3节的场景划分逻辑重新切分数据集确保val里的场景完全独立。重新训练后对比新val mAP如果比之前的虚高数据下降了0.1到0.2说明之前的数据划分确实有问题。这个做法比任何模型优化都更有效数据划分干净了指标才有参考价值。6. 进阶夜间增强策略与模型导出验证训练完成后最后一公里的工作是确认模型在夜间场景的真正实力。我的习惯是准备一段没有出现在train和val里的夜间视频用model.predict()逐帧跑一遍统计漏检帧和误检帧的比例。这样比单独看mAP更能反映实际效果。夜间推理时的增强策略和训练时不同。训练时用亮度扰动是为了模拟不同暗度推理时用model.predict(sourcevideo.mp4, imgsz640, conf0.25, iou0.45)就够不需要再做图像增强。conf0.25是置信度阈值夜间场景阈值设太低会增加误检设太高又容易漏检用0.25起步然后通过视频实测调整。模型导出有两个方向部署到边缘设备用model.export(formatonnx)跑推理脚本用model.export(formattorchscript)。导出后要做一件事——用同一张图测试导出前后模型的推理结果确保mAP没有明显的精度损失。ONNX模型可以用onnxruntime验证TorchScript模型用torch.jit.load后前向传播对比输出的检测框坐标和置信度是否一致。自动化验证这一步最容易被跳过但夜间场景模型的部署失败几乎都是导出精度损失和数据分布偏移导致的。把detect脚本固定下来输入一段夜间视频输出每帧的检测框和置信度再对照原始视频目测一轮这个流程跑通了整个方案才算真正落地。我自己的习惯是每次换数据集来源时都用同一段夜间视频做回归验证保证新旧模型的mAP对比建立在同一基础之上。夜间行人检测的方向会持续有数据积累模型和脚本只是工具数据质量才是真正决定效果上限的部分。希望这些经验能够帮你在做夜间行人检测时少走一些弯路。本文还有配套的精品资源点击获取
返回列表