恶劣天气下行人车辆检测:1000张YOLO标注数据如何带来实战提升
简介面向自动驾驶、智能交通监控等场景下的目标检测算法研究与模型训练聚焦雨雪、雾霾、沙尘等恶劣天气中道路行人、车辆等目标的识别难题。图像与YOLO格式标注文件一一对应共7个类别涵盖car、bus、person、truck等常见交通参与者并已按训练集与测试集划分便于直接开展模型训练与效果评估。包内共2000个文件其中972张jpg图像、1027个txt标注文件另含1个py可视化脚本运行即可快速查看标注效果无需额外整理数据。资源压缩包大小约133.76MB已有157人学习下载。数据场景均取自真实恶劣天气样本覆盖降雪、浓雾、沙尘暴等多种复杂条件可用于验证模型在低可见度下的鲁棒性也适合作为YOLOv5等算法改进实验的基准数据。配套脚本与清晰的目录结构能帮读者省去预处理时间直接聚焦模型设计与调优。1. 恶劣天气下的行人车辆检测1000张YOLO标注数据到底能解决什么同一个检测模型在晴天测试集上mAP能跑到90%上下一换到雨雾场景直接跌到60%这种情况在工程里并不少见。问题不在模型结构而在它从没见过训练集之外的天气分布。公开数据集大多在良好天气下采集模型记住的是晴天的亮度、对比度和纹理一遇到雨雪就失准。这套约1000张、已标注、YOLO格式的恶劣天气道路行人车辆数据解决的正是这个分布偏移问题雨、雾、雪、夜间等真实路况中的目标都已框好类别和坐标全部归一化可以直接进YOLO系列训练管线。对正在做智慧交通、辅助驾驶或路侧感知的从业者来说这是低成本补足恶劣天气短板的一条捷径。这篇文章会把数据从验证、清洗、训练到避坑的完整链路讲透拿到手就能用。2. 恶劣天气为什么让检测模型失准成像退化与标注表达的双重约束2.1 雨雾雪对图像质量的影响路径雨雾对图像的影响并不是简单的“图变模糊了”。物体反射的光在穿过雨滴、雾气和雪花时一部分光子被散射掉到达镜头的能量减少传感器信噪比随之下降。表现出来的就是对比度低、颜色发灰原本清晰的边缘变成一段段不稳定的梯度。检测算法依赖的边缘和纹理特征在这种信噪比下开始变得不可靠。雾天更麻烦的地方在于退化程度和距离相关。近处的行人还能看清轮廓远处的人几乎融进背景里整个图像的退化是空间不均匀的。这个特性导致全局对比度增强这类预处理手段效果非常有限——你把近处拉得太亮远处照样是一团灰。雨天则是另一套干扰雨线在图像里形成斜向的亮条纹在卷积核的感受野内产生强烈的梯度扰动很容易被当成误检边缘镜头上残留的水滴还会造成局部模糊和光晕。雪天的高反射区域让白色地表、车身和行人混在一起亮度最高的地方往往不是目标。夜间又是一套完全不同的逻辑。为了补偿进光量传感器增益被拉高暗部噪声变大深色行人衣物和柏油路背景几乎无法区分。如果叠加雨夜路面反光图像里会大面积出现高亮带状区域这些区域和行人轮廓在亮度分布上非常接近。所以整套恶劣天气检测的核心难点就是模型在一个天气域上学到的东西到另一个天气域完全不成立。这不是玄学是训练分布和推理分布不匹配。合成数据能模拟部分统计特征但渲染的雨线、光晕和真实摄像头拍的仍然有肉眼可见的差异所以真实恶劣天气样本始终是稀缺资源。2.2 YOLO格式标注的表达边界拿到这套YOLO标注先要清楚txt每一行在表达什么类别ID、归一化中心x、归一化中心y、归一化宽、归一化高数值都在0到1之间。这种格式的好处是训练时不管输入分辨率是640还是1280坐标都能直接换算不需要为每张图单独维护宽高信息这也是它成为目标检测事实标准的原因。但越是简单格式隐含缺失的信息越多。首先它没有天气标签。1000张图里哪些是雨、哪些是雾、哪些是夜间txt里完全看不出来。要做按天气切分验证集或按天气评估只能靠文件名前缀辅助区分或者人工过一遍单独维护一个天气分组文件。这一步不能省直接随机切分训练集和验证集的话同一个场景的不同帧会同时出现在两边验证集mAP虚高到没有参考意义。其次它没有遮挡和截断标记。一个行人被树干挡了一半标注员通常还是画一个完整外接矩形框里其实只有一半是目标。模型学到的是“树干附近有人”这种关联在真实环境下就会出现误检。还有一点没有目标ID所以这1000张只能训练检测器不能直接训练跟踪器。它的定位应该是真实恶劣天气域的锚点数据先在通用预训练权重上做微调再结合增强和无标注数据进行二次挖掘而不是指望单靠它硬训出一个万能的模型。3. 拿到1000张标注图的第一件事清洗、可视化与分布统计3.1 解析YOLO标注并检查图片-标注对很多人拿到标注数据的第一反应是直接开训这是翻车概率最高的一步。不同来源的数据拼在一起txt里出现空行、行尾多余空格、坐标越界、类ID越界都是常见问题。我一般会先跑一个检查脚本把整批标注解析一遍确认格式合法、图片和标注一一对应。import os from pathlib import Path def check_yolo_annotations(images_dir: str, labels_dir: str, num_classes: int): 检查 images/labels 是否一一对应并逐行校验 YOLO 格式。 images_dir: 图片目录 labels_dir: 标注目录 num_classes: 类别数用于校验 class ID 是否越界 img_paths {p.stem: p for p in Path(images_dir).glob(*.jpg)} errors [] for label_file in Path(labels_dir).glob(*.txt): stem label_file.stem if stem not in img_paths: errors.append(f无对应图片: {label_file}) continue with open(label_file, r) as f: for line_no, line in enumerate(f, 1): line line.strip() if not line: errors.append(f{label_file}:{line_no} 空行) continue parts line.split() if len(parts) ! 5: errors.append(f{label_file}:{line_no} 字段数{len(parts)}, 应为5) continue cls, cx, cy, w, h parts cx, cy, w, h map(float, (cx, cy, w, h)) if not (0 cx 1 and 0 cy 1 and 0 w 1 and 0 h 1): errors.append(f{label_file}:{line_no} 坐标越界) if int(cls) num_classes: errors.append(f{label_file}:{line_no} 类别ID {cls} 越界) print(f检查完成共发现 {len(errors)} 个问题) for err in errors[:50]: print(err) return errors if __name__ __main__: check_yolo_annotations(bad_weather/images, bad_weather/labels, num_classes4)这段脚本做三件事检查图片和标注是否同名配对避免训练时找不到对应文件校验每行是否恰好5个字段空行和行尾多余空格会被拦下校验坐标是否都在0到1区间同时校验类别ID是否越界。坐标越界常见于标注工具导出异常比如某个框的宽或高被算成0一旦进入训练loss曲线会变得非常奇怪。参数说明num_classes按你的names文件实际类别数传一般道路场景有行人、自行车、摩托车、汽车等四到六类。glob(*.jpg)假设图片是jpg格式如果拿到的是png或webp记得改后缀匹配。检查脚本输出的前50条错误基本能定位问题来源如果错误集中在某个标注批次直接找对应来源重新导出。3.2 可视化标注在模糊图上框得准不准txt解析没问题不代表框画得准。恶劣天气图本身就模糊标注员在低对比度环境下很容易把行人的边界多框一截或少框一截。尤其是雾天远处的小目标标出来的框经常和真实轮廓差很远。我要把这批图全部可视化检查一遍通常用网格拼图的方式快速过。import cv2 import numpy as np import math from pathlib import Path def visualize_grid(image_dir: str, label_dir: str, class_names: list, cols: int 5, output: str check_grid.jpg): 把多张图的标注框画出来, 拼成网格图, 方便快速人工抽检. files sorted(Path(label_dir).glob(*.txt))[:25] cell_w, cell_h 640, 480 rows math.ceil(len(files) / cols) canvas np.zeros((cell_h * rows, cell_w * cols, 3), dtypenp.uint8) for idx, label_file in enumerate(files): img_path Path(image_dir) / (label_file.stem .jpg) img cv2.imread(str(img_path)) if img is None: continue img cv2.resize(img, (cell_w, cell_h)) ih, iw img.shape[:2] with open(label_file, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, cx, cy, bw, bh parts cls_id, cx, cy, bw, bh int(cls_id), float(cx), float(cy), float(bw), float(bh) x1 int((cx - bw / 2) * iw); y1 int((cy - bh / 2) * ih) x2 int((cx bw / 2) * iw); y2 int((cy bh / 2) * ih) color (0, 255, 0) if cls_id 0 else (0, 0, 255) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) r, c divmod(idx, cols) canvas[r*cell_h:(r1)*cell_h, c*cell_w:(c1)*cell_w] img cv2.imwrite(output, canvas) print(f已生成 {output})代码中按类别ID分颜色0号类别用绿色其余用红色。如果你的类别更多最好做一个颜色列表类别ID直接作为索引取色。这段脚本的files默认取前25张实际项目里我会改成按天气类型分层抽样——雨、雾、雪、夜间各抽十几张拼成多张网格图而不是只看最开头的连续文件。可视化检查主要看三点框是否紧贴目标边界、是否有漏标的目标、是否有框明显偏离目标。雾天场景里漏标率通常比错标率更高因为标注员自己也看不清远处目标。抽检后发现大量漏标的不要直接改标注先把这批图挑出来单独处理避免和其他正常样本混在一起反复检查。3.3 类别与目标尺寸分布统计训练前做一次类别和目标尺寸的统计能提前预警很多问题。我用Python直接统计每一类的目标数量和归一化框面积分布结果几秒钟就能出来。import numpy as np from pathlib import Path def stat_distribution(label_dir: str, num_classes: int): counts np.zeros(num_classes) areas {i: [] for i in range(num_classes)} for label_file in Path(label_dir).glob(*.txt): with open(label_file, r) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls, cx, cy, w, h int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) counts[cls] 1 areas[cls].append(w * h) for cls in range(num_classes): if areas[cls]: arr np.array(areas[cls]) print(f类{cls}: 目标数 {int(counts[cls])}, f框面积中位数{np.median(arr):.4f}, f小目标占比{(arr0.01).mean()*100:.1f}%) stat_distribution(bad_weather/labels, 4)这里w*h是归一化面积小于0.01的目标在640×640输入下对应大概64×64像素以下属于检测最容易丢的尺寸范围。如果在某个类别里这个占比超过50%基本可以确定这个类别用小模型配上常规推理会漏得很惨需要单独增加多尺度训练或分块推理策略。这个统计还有一个用途验证标注质量。如果某个类别只有几十个目标而另一个类有几千个说明收集路线本身有偏或者标注时有人漏标了整个类别。两种情况处理方式不一样前者是数据收集计划问题后者是标注质量问题。4. 让模型适应恶劣天气从数据增强到训练参数调优4.1 数据增强原则Mosaic什么时候该关数据量只有1000张左右时数据增强是比换模型结构更值得投入的方向。我的原则是增强不是为了让模型看到更多不一样的图而是为了让模型对真实场景中的光照、色温、尺度变化不敏感。常用的组合是Mosaic、Copy-Paste、随机缩放、随机HSV扰动。每一项都有边界条件。Mosaic拼接四张图能极大丰富上下文和小目标比例但它会把不同天气的图拼在一起产生“同一张图里既晴又雨”这种不存在的伪上下文。训练前80%的epoch开着没问题后20%必须关掉让网络在最后阶段回到真实分布上收敛。随机HSV的扰动范围也要克制亮度和饱和度扰动过大会把白天图变成类似曝光的奇怪图让网络学到不该存在的颜色分布。还有一个容易踩的坑不要在增强环节里用“全局对比度降低”来模拟雾天。真实雾的退化是距离相关的空间不均匀退化全局对比度变换模拟不出来这种分布模型在合成退化图上练出来的能力搬到真实雾图上基本没有迁移效果。要补雾天数据老老实实收集真实雾图或者用专门的雾渲染工具按深度信息做逐像素退化不要指望一个对比度参数走天下。4.2 训练参数参考小数据量怎么定基于1000张真图和预训练权重做微调训练参数建议按下面这个表起步再根据实际曲线调整。参数推荐值说明输入分辨率640×640与预训练尺寸一致不做额外分辨率提升batch size16显存不够就降8小于8要小心BN统计不稳定epochs150配合早停使用数据量小容易过拟合优化器SGDlr0.01小数据上用AdamW前期收敛快但容易过拟合学习率调度cosine decay后期学习率缓慢下降对微调友好weight decay5e-4过大欠拟合过小过拟合Mosaic前80% epoch开最后20%关闭让模型回到真实分布随机HSVh0.015, s0.5, v0.4模拟一天内色温和亮度变化早停 patience20验证集连续20个epoch不升就停这些参数里最需要解释的是学习率。预训练权重已经具备通用的特征提取能力用0.01的SGD微调全部层收敛稳定不容易破坏已有的底层特征。如果换AdamW初始学习率我会降到0.001否则前期拟合太快直接把预训练特征冲掉了。batch size小于8时BN层的统计量会因为单batch内样本太少而不稳定特别是数据本身包含多种天气一个batch里可能全是同一种天气BN统计就偏了。遇到这种情况要么加batch size要么在数据加载时做天气层面的混洗保证每个batch尽量覆盖多种天气。4.3 按场景切分验证集避免mAP虚高随机切分是这条链路上最容易翻车的操作。同一段道路、同一台摄像机在不同时刻拍的图随机分到训练集和验证集里模型等于提前见过了验证集的背景mAP看起来很高换个路段直接崩。正确做法是按场景分组切保证验证集里的每一组场景都不在训练集出现。# 按场景前缀划分训练/验证集, 避免同场景数据泄漏 import random from pathlib import Path random.seed(12) img_files sorted(Path(bad_weather/images).glob(*.jpg)) # 文件名前段作为场景ID, 例如 rain_roadA_001.jpg - rain_roadA groups {} for p in img_files: parts p.stem.split(_) group _.join(parts[:-1]) groups.setdefault(group, []).append(p) all_groups sorted(groups.keys()) random.shuffle(all_groups) val_groups all_groups[: int(0.2 * len(all_groups))] with open(train.txt, w) as ft, open(val.txt, w) as fv: for g, imgs in groups.items(): for img in imgs: line str(img) \n if g in val_groups: fv.write(line) else: ft.write(line)这段脚本的核心是groups的构造方式把文件名里最后一个下划线后面的序号去掉前面部分当成场景ID同一个场景的所有帧只进一个集合。如果你的文件名没有这么规整的结构就用目录名或者拍摄批次信息做分组。注意脚本里random.seed(12)要保留保证每次运行切分结果一致否则你改了训练参数之后没法确定指标变化是模型改的还是数据切分变了。train.txt和val.txt只是列表文件的一个示例形式实际框架可能用其他格式比如直接的图片路径列表或索引文件关键逻辑是“按组切分”不是“按图片切分”。5. 恶劣天气检测的5个常见坑与排查方法5.1 坑1雨雾天标注框把雨滴和雾气一起框进去了现象模型在推理时对雨丝和雾气团区域产生高置信度误检框住的对象像一团模糊的亮斑而不是行人和车辆。原因低可见度下标注员看不清目标边界画框时把目标边缘外的雾幔、雨滴区域一起包括了进去。模型学到的是“这个区域有雨有雾就是目标”的错误关联。解决先把所有标注可视化过一遍重点看雨滴密集和雾气浓度高的图。发现框明显超出目标边界的手动收缩边界框。推理阶段对输入图先做过雨雾缓解预处理降低背景杂波的干扰。不要指望模型自己从错误标注里学到正确的边界。5.2 坑2txt行尾空格和空行导致训练中断现象训练跑到某个epoch直接报ValueError检查数据格式全部符合预期但某一张图就是加载不了。原因标注工具导出时某些txt末尾多了一个空行或者一行只有空格。解析时候按行读取空行被当成一个无效目标坐标解析直接崩溃。解决用本文3.1节的检查脚本跑一遍全量标注把空行和行尾多余空格全部揪出来。清洗规则就三条忽略空行、strip掉行首行尾空白、按空白字符split切割字段。这类问题大概率是数据来源不同造成的检查脚本跑完基本能清零。5.3 坑3随机切分产生数据泄漏mAP虚高现象训练集和验证集mAP都超过0.85但拿同一模型去另一个路段或者另一台摄像机拍摄的数据上测试mAP直接掉到0.6以下。原因随机切分时同一场景不同帧同时进了训练集和验证集模型已经记住验证集图像的背景纹理验证集失去了评估价值。解决按场景分组切分保证验证集里的场景和训练集没有任何重叠。按天气类型单独评估更加严格比如雨、雾、雪、夜间各自出一份mAP这样能看到模型在哪种天气下真正弱而不是平均分掩盖问题。5.4 坑4行人类别mAP被车辆类别掩盖现象整体mAP看起来过得去打开每类指标才发现车辆0.85、行人只有0.55平均值完全掩盖了短板。原因行人目标在数据里占比少尺寸又偏小类别不平衡时模型倾向于学习容易的车辆类行人类别没有获得足够的梯度。解决损失函数里给行人类别加权重或者对行人样本做过采样。训练时开启多尺度训练推理时考虑分块预测让小目标有更多特征表达的机会。评估标准要定在行人类别的AP上而不是整体mAP——应用场景里漏检一个行人的代价远远大于误检一辆车。5.5 坑5夜间数据不足导致光照变化下误检激增现象白天和雨雾天的检测都正常一到夜间照片就把路灯、车灯误判成行人或者把行人漏掉。原因夜间的样本量占比太少而且夜间图像的动态范围极大高亮的灯光和低亮的目标同时出现模型没有见过足够的极端亮度差异。解决先把数据里夜间样本单独抽出来数一下占比如果太少直接目标性获取夜间数据比任何增强都有效。训练层面可以对输入做明暗随机化推理阶段尝试局部直方图均衡再送入模型。如果数据里根本没有夜间图不要硬扛先定数据补采计划模型调参解决不了数据缺失。6. 从1000张到更多用伪标签和难例挖掘扩展数据规模6.1 伪标签转换与人工抽检闭环1000张数据微调出来的模型通常已经在特定天气域上有了可用精度。下一步不要急着部署先让它干活去收集更多数据。常见做法是跑伪标签在未标注的真实天气视频或图片上推理把高置信度的预测框直接转成YOLO格式再人工抽检后汇入训练集。这个流程能把真实恶劣天气的样本量滚动扩大而且每一步都在当前模型能力范围内。import cv2 from pathlib import Path def export_pseudo_labels(model, image_dir: str, out_dir: str, conf_thres: float 0.7): 在未标注的恶劣天气图片上推理, 把高置信度框导出为YOLO txt。 只保留置信度conf_thres的预测, 人工抽检后再汇入训练集。 model 为训练好的推理模型, 按实际框架接入即可。 Path(out_dir).mkdir(parentsTrue, exist_okTrue) for img_path in Path(image_dir).glob(*.jpg): img cv2.imread(str(img_path)) h, w img.shape[:2] results model.predict(img, confconf_thres) boxes results[0].boxes # 以实际训练框架的推理接口为准 label_path Path(out_dir) / (img_path.stem .txt) with open(label_path, w) as f: for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cls_id int(box.cls[0]) conf float(box.conf[0]) if conf conf_thres: continue cx (x1 x2) / 2 / w cy (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h f.write(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n)代码里model.predict只是占位调用不同训练框架的推理接口差别很大关键逻辑是拿到预测框后用xyxy转成归一化cxcywh再落地。conf_thres0.7是起点整个参数决定伪标签的纯度设太高可用的框太少设太低混入的错误标注就多。我一般先跑一遍看输出置信度的分布——如果绝大多数框都集中在0.9以上可以适当降到0.6多捞点样本如果低置信度的框占了一大片说明这批图和已标注数据分布差异太大不要急着用先补标注再谈。伪标签进来之后不是直接合并进训练集要先做人工抽检。抽检重点不是看框画得准不准而是看有没有漏检——比如一张雾天图里模型只框了一个近处行人远处还有三个小目标这种低置信度漏检恰恰是最值得收集的难例。把漏检的图挑出来人工补框后单独进一个难例池网模型下次训练时重点加权比单纯堆数据有效得多。这套伪标签加难例挖掘的闭环我一般每个迭代周期都跑一遍先推理未标注数据统计置信度分布抽检漏检案例再重训模型。数据从1000张慢慢滚到几千张的过程中每轮都在朝模型最弱的场景倾斜而不是平均分配样本量。先把这套流程跑通再去考虑更复杂的模型结构这样的投入产出比高得多。希望帮到你。本文还有配套的精品资源点击获取