简介:面向夜间监控与低光行人检测需求,这套资源包含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张只够让模型记住训练集,泛化能力完全不够。所以后续脚本里默认开了pretrained=True,这是这套方案能“少图出活”的关键前提。
选择夜间行人图的时候,别只看数量,要看场景分布是否覆盖:城市主干道的人行横道、无路灯的小巷、地下通道、雨雪天气、逆光(行人背对车灯)、远距离小目标(图像中行人高度小于32像素)。如果5000张里全是“路灯下的正面行人”,那模型学完也只能处理路灯下的正面行人。这是数据采集阶段最容易被忽略、后面训练最难补的缺口。
另外,5000张看起来不少,但夜间场景退化严重,有效信息密度比白天低,实际训练时建议打开数据增强。YOLO11默认的Mosaic、MixUp、HSV扰动对夜间图尤其有用——HSV扰动可以模拟不同色温的路灯,Mosaic能把多个夜间场景拼在一张图里,相当于免费扩充了小目标样本数量。夜间行人的标注质量直接影响训练上限,如果标注框偏大或偏小超过15%,后期几乎只能靠重新标注修正。
2.2 VOC、COCO、YOLO三种格式到底差在哪
同一批标注,为什么要弄三种格式?因为不同的工具链吃不同的格式:老牌语义分割和检测复现常以VOC为基础,目标检测论文和评测广泛用COCO的JSON,而YOLO系脚本默认要的是每张图对应一个TXT文本。很多工程师拿到标注数据后第一个翻车点不是标注本身,而是格式不匹配导致训练脚本报错。
三种格式的核心差异在于坐标表示方式:
| 格式 | 容器 | 坐标形式 | 边界框表达 |
|---|---|---|---|
| VOC | XML(每图一个) | 绝对像素(整数) | xmin, ymin, xmax, ymax(左上右下) |
| COCO | JSON(全数据集一个) | 绝对像素(浮点/整) | x, y, width, height(左上角+宽高) |
| YOLO | TXT(每图一个) | 归一化浮点(0-1) | x_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要的是中心点归一化坐标。转换时绕一步:先用(xmin+xmax)/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", encoding="utf-8") as f: f.write("\n".join(lines)) # 遍历XML目录,为每张图生成同名TXT xml_dir = "labels_voc" txt_dir = "labels_yolo" os.makedirs(txt_dir, exist_ok=True) 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", encoding="utf-8") as f: json.dump(coco, f, ensure_ascii=False, indent=2) 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版本的torch,CPU平台装CPU版即可,Mac上要求MPS支持的torch(Apple 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", default="yolo11s.pt", help="预训练权重,可选yolo11n/s/m/l/x") parser.add_argument("--data", default="data.yaml", help="数据集配置") parser.add_argument("--epochs", type=int, default=100, help="训练轮数") parser.add_argument("--imgsz", type=int, default=640, help="输入分辨率") parser.add_argument("--batch", type=int, default=None, help="默认按设备自动选择") parser.add_argument("--device", default=None, 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 # workers:Mac上过大容易报“进程启动失败”,CPU上过小拉不满CPU if device == "mps": workers = 0 # MPS后端与DataLoader多进程兼容性差,直接关掉最稳 elif device.startswith("cuda"): workers = 8 else: workers = 4 model = YOLO(args.model) model.train( data=args.data, epochs=args.epochs, imgsz=args.imgsz, batch=batch, device=device, workers=workers, patience=20, # 验证集mAP连续20轮不涨就早停 pretrained=True, # 迁移学习是5000张小数据集的命根子 cache="ram" if device.startswith("cuda") else False, # GPU机器缓存图片提速 project="runs/night_detect", name="yolo11s_night" ) # 训练完成后自动导出ONNX,方便后续部署验证 best_model = YOLO("runs/night_detect/yolo11s_night/weights/best.pt") best_model.export(format="onnx", imgsz=args.imgsz, half=True 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上如果显存只有6G,16会爆显存,要降到8甚至4;Mac的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_decay=0.0005;如果是batch=2导致的极端波动(batch越小梯度噪声越大),把batch加到8以上,哪怕用梯度累积也要保证有效batch>8。
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),把增量数据的影响控制在半小时内看完。做检测的都知道,模型能不能落地,往往不取决于你一开始有多少数据,而取决于你有没有一个快速吃进新数据的闭环。把这条闭环跑通,夜间行人检测才算真正落地。希望我的这些踩坑记录和训练习惯能帮到你。
本文还有配套的精品资源,点击获取