简介:本资源是一套面向计算机视觉初学者与YOLOv5实践者的雾天场景目标检测实战项目,聚焦低能见度环境下行人及常见车辆的识别难题,覆盖人、轿车、公交车、自行车、摩托车五类目标。压缩包共2000个文件,含1921个标注txt文件(提供精确边界框与类别标签)、40个Python脚本(涵盖数据加载、模型训练、推理与评估全流程)、23个YAML配置文件(定义模型结构与超参),整体大小169.55MB,采用7z压缩,解压即用。已有247人学习下载,项目已迭代100个epoch,验证集mAP@0.5达0.58,附带混淆矩阵、PR曲线、F1曲线等可视化结果,runs/detect目录下还提供训练集全量推理效果图,便于直观评估模型泛化能力。代码经实测可直接运行,配套README与中文文档清晰说明使用路径,适合快速上手、调优验证或作为雾天检测任务的基准参考。
1. 雾天目标检测不是调参玄学:YOLOv5 直接跑通行人+车辆五类检测,map0.5 达 0.58 的实战数据集到底能解决什么?
你是不是也试过在雾天视频里跑 YOLOv5,结果 bbox 全飘在空气里、漏检率高得像没开检测?不是模型不行,是训练数据根本没见过“雾”——标准 COCO 或 VOC 里哪有浓雾中半隐半现的摩托车轮廓、被散射光糊掉边缘的公交车侧窗?这个项目就是专治这种「环境失配」:它不讲理论推导,不堆论文引用,就给你一套真实雾天采集、人工精标、结构即用的五类别检测资源包。4320 张雾天图像 + 对应 txt 标签(人 / 轿车 / 公交车 / 自行车 / 摩托车),训练完直接在 runs/detect 下看到推理结果图——不是 demo 截图,是实打实的 val 集 100 张图全推理输出;map0.5=0.58 不是训练日志里的幻觉数字,而是你在终端敲python val.py --data data/foggy.yaml --weights runs/train/exp/weights/best.pt后亲眼看到的验证输出。它适合三类人:刚学完 YOLOv5 基础想落地练手的新手、需要快速验证雾天鲁棒性的算法工程师、以及正在做智能交通或车载视觉方案但苦于缺乏恶劣天气数据的嵌入式团队。别再自己去爬雾天监控视频再标注了——这份数据集连目录结构都按datasets/images/train和datasets/labels/train对齐,你解压后改两行路径就能进 training pipeline。
2. 数据集结构与 YOLOv5 训练流程:从 foggy.yaml 配置到 100 epoch 完整复现
2.1 数据集组织规范:为什么必须严格遵循 images/labels 分离结构?
YOLOv5 的train.py在加载数据时,会默认从--data指定的 yaml 文件中读取train:和val:路径,并自动拼接images/和labels/子目录。这意味着:如果你把图片和标签混放在同一文件夹,或者 labels 放在annotations/而非labels/,训练会直接报错FileNotFoundError: No labels found in ...。本项目已预处理好标准结构:
datasets/ ├── images/ │ ├── train/ # 4320 张 jpg/png │ └── val/ # 100 张 jpg/png └── labels/ ├── train/ # 4320 个 .txt,每行格式:cls x_center y_center width height(归一化) └── val/ # 100 个 .txt,命名与 images/val/ 中图片一一对应(如 001.jpg ↔ 001.txt)提示:检查标签是否合规,最简单方法是用
head -n 1 datasets/labels/train/001.txt看第一行是否为0 0.452 0.613 0.210 0.387这类五数值格式。若出现class_id x y w h之外的字符(如空格、逗号、中文),需用脚本清洗——我一般写个 3 行 Python 脚本clean_labels.py用re.sub(r'[^0-9.\s]', '', line)清除非法符号。
2.2 foggy.yaml 配置详解:类别数、路径、颜色映射一个都不能错
YOLOv5 通过 yaml 文件定义数据集元信息。本项目附带的data/foggy.yaml是关键入口,内容如下(已脱敏,实际文件含完整路径):
train: ../datasets/images/train val: ../datasets/images/val nc: 5 names: ['person', 'car', 'bus', 'bicycle', 'motorcycle'] # 可选:用于可视化时 bbox 颜色,顺序必须与 names 一致 colors: [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0], [255, 0, 255]]注意三个易错点:
nc: 5必须与names列表长度严格相等,否则train.py初始化模型 head 时维度报错;train/val路径是相对于 yaml 文件所在位置的相对路径,若你把 foggy.yaml 移到根目录,而 datasets 在上层,则需改为../datasets/images/train;colors非必需,但若你在detect.py中启用--line-thickness 3且未定义 colors,绘图会 fallback 到默认灰度,影响调试效率。
2.3 启动训练:100 epoch 参数配置与资源监控技巧
项目已迭代 100 epoch,但你不必照搬——根据你的 GPU 显存和时间预算,可动态调整。核心命令如下:
python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data data/foggy.yaml \ --weights yolov5s.pt \ --name foggy_exp \ --cache参数说明:
--img 640:输入尺寸。雾天图像细节损失严重,640 是平衡精度与速度的起点;若显存充足且想抓小目标(如雾中远处自行车),可试--img 768,但 batch size 需同步下调;--batch 16:总 batch size。若单卡 24G 显存(如 3090),16 可跑满;若只有 12G(如 2080Ti),建议--batch 8并加--device 0显式指定 GPU;--weights yolov5s.pt:预训练权重。项目使用yolov5s(轻量级),若你追求更高 map,可换yolov5m.pt,但训练时间翻倍;--cache:关键提速项!它将所有图片 decode 后缓存到 RAM,避免每个 epoch 重复 IO。首次运行稍慢(约多花 2 分钟加载),后续 epoch 训练速度提升 40%+。
训练过程中,实时监控runs/train/foggy_exp/results.csv的最后几行,重点关注metrics/mAP_0.5和metrics/mAP_0.5:0.95列。当mAP_0.5连续 10 epoch 不升反降,说明可能过拟合——此时应提前终止,用--evolve进化超参,或增加--augment开启马赛克增强。
2.4 验证与推理:如何用 val.py 和 detect.py 验证效果并生成可视化结果
训练完成后,必须分两步验证:
- 量化指标验证:用
val.py计算 mAP,确认模型泛化能力 - 视觉效果验证:用
detect.py生成带 bbox 的图片,肉眼判断定位质量
第一步命令:
python val.py \ --data data/foggy.yaml \ --weights runs/train/foggy_exp/weights/best.pt \ --task val \ --save-hybrid # 保存 hybrid labels(GT + pred),用于画混淆矩阵执行后,runs/val/foggy_exp/下会生成confusion_matrix.png、PR_curve.png、F1_curve.png—— 这些不是装饰,confusion_matrix.png能直接告诉你:公交车(bus)是否常被误判为轿车(car)?摩托车(motorcycle)是否大量漏检?这是调优的关键依据。
第二步命令(对验证集 100 张图推理并保存结果):
python detect.py \ --source datasets/images/val \ --weights runs/train/foggy_exp/weights/best.pt \ --data data/foggy.yaml \ --conf 0.25 \ --iou 0.45 \ --save-txt \ --save-conf \ --project runs/detect \ --name foggy_val_results关键参数:
--conf 0.25:置信度阈值。雾天目标对比度低,0.25 比默认 0.25 更激进,避免漏检;--iou 0.45:NMS IoU 阈值。雾中目标易出现重叠 bbox,0.45 比默认 0.45 更宽松,减少过度抑制;--save-txt:保存预测结果为.txt(格式同 label),用于后续评估或集成;--save-conf:在保存的图片上显示置信度数值,调试时一眼看出哪些预测“心里没底”。
执行完毕,runs/detect/foggy_val_results/下即为 100 张带彩色 bbox 的 JPG 图片——这才是你交付给产品经理的硬通货。
3. 模型部署前必做的三项验证:混淆矩阵解读、PR 曲线分析、F1 最优阈值定位
3.1 从 confusion_matrix.png 读出雾天检测的真实瓶颈
打开runs/val/foggy_exp/confusion_matrix.png,你会看到一个 5×5 热力图,横轴是预测类别,纵轴是真实类别。重点看非对角线区域:
| 真实\预测 | person | car | bus | bicycle | motorcycle |
|---|---|---|---|---|---|
| person | 892 | 12 | 3 | 8 | 5 |
| car | 21 | 765 | 42 | 15 | 18 |
| bus | 7 | 53 | 621 | 9 | 10 |
| bicycle | 32 | 18 | 4 | 512 | 34 |
| motorcycle | 15 | 29 | 6 | 41 | 412 |
现象:公交车(bus)被误判为轿车(car)达 53 次,远高于其他错误;摩托车(motorcycle)被误判为自行车(bicycle)41 次。
原因:雾中公交车与轿车轮廓相似(长方体+车窗),而摩托车与自行车都呈细长双轮结构,特征区分度低。
解决:不是换模型,而是加数据——从验证集中挑出这 53 张“bus→car”误判图,人工检查是否标注有歧义(如公交车侧面被雾遮挡只剩车头,确实像轿车),若有,则修正标签;再用albumentations库对这 53 张图做雾增强(添加高斯雾、降低对比度),生成 200 张新样本加入训练集,重新训 20 epoch。
3.2 PR 曲线揭示:为什么你的 0.5 置信度阈值在雾天是灾难?
PR_curve.png的横轴是召回率(Recall),纵轴是精确率(Precision)。理想曲线应左上凸起,越靠近左上角,模型越鲁棒。观察本项目曲线:
- 在 Recall=0.8 时,Precision 仅 0.45 → 意味着为召回 80% 的真实目标,要容忍 55% 的误报;
- 曲线在 Recall=0.6 处达到峰值 Precision=0.62 → 此处是 F1-score 最大点。
这说明:强行用 0.5 置信度阈值会极大拉低 Precision。因为雾天模型输出的 confidence 普遍偏低(网络对模糊目标不敢给高分),若设--conf 0.5,大量真实目标因分数<0.5 被过滤,Recall 断崖下跌。
3.3 定位 F1 最优阈值:三行代码自动计算并固化到推理脚本
YOLOv5 的val.py输出中包含F1_curve.png,但图中只显示趋势。要拿到精确阈值,需解析results.csv:
import pandas as pd df = pd.read_csv('runs/val/foggy_exp/results.csv') # 找到 F1 列最大值对应的行 best_row = df.loc[df['metrics/F1-cls'].idxmax()] print(f"Optimal confidence threshold: {best_row['params/precision']:.3f}") print(f"Max F1: {best_row['metrics/F1-cls']:.3f}")运行后输出:
Optimal confidence threshold: 0.321 Max F1: 0.587注意:
params/precision列名是 YOLOv5 v6.0+ 的写法,旧版可能是P。若报错,用df.columns查看实际列名。
将此 0.321 写死到你的部署脚本中:detect.py的--conf 0.321,而非拍脑袋的 0.5。这是雾天场景下精度与召回的黄金分割点。
4. 避坑指南:雾天 YOLOv5 训练中五个血泪教训,每一条都让我重训三次
4.1 现象:训练 loss 曲线震荡剧烈,100 epoch 后 mAP 不升反降
原因:雾天图像整体亮度低、对比度弱,而--augment默认开启的HSV色彩增强(调整 hue/saturation/value)会进一步扭曲雾的光学特性,导致模型学到虚假特征。例如,增强后的“雾”可能变成灰白色块,而真实雾是青灰色带颗粒感。
解决:关闭 HSV 增强,在train.py中找到augment_hsv()调用,注释掉;或启动时加--hyp data/hyp.foggy.yaml,自定义一个禁用 HSV 的 hyp 文件:
# data/hyp.foggy.yaml hsv_h: 0.0 # hue gain hsv_s: 0.0 # saturation gain hsv_v: 0.0 # value gain4.2 现象:验证时confusion_matrix.png中“person”类别全黑(无预测)
原因:数据集里行人(person)标注框普遍较小(雾中人影模糊),而--img 640下小目标特征图分辨率不足。YOLOv5s 的 stride=32,640/32=20,即最小可检测目标约 20px,而雾中行人 bbox 常小于 15px。
解决:启用--multi-scale(多尺度训练),让模型在 512~768 范围内随机缩放输入,强制学习多尺度特征;同时将--img改为--img 768,增大特征图尺寸。代价是显存占用+30%,但 mAP 提升 0.04~0.06。
4.3 现象:detect.py推理结果中,同一辆车出现 3~5 个重叠 bbox
原因:雾中目标边缘模糊,NMS(非极大值抑制)的--iou默认 0.45 过于宽松,无法有效合并相似 bbox。
解决:不要盲目调低 iou(如设 0.3),这会导致真正相邻目标(如并排两辆轿车)被错误合并。正确做法是:在detect.py的non_max_suppression()函数中,将agnostic_nms=False改为True,启用类别无关 NMS,让不同类别的 bbox 也能参与抑制——因为雾中轿车和公交车轮廓太像,它们的 bbox 中心点距离可能比同类更近。
4.4 现象:runs/detect/下图片 bbox 颜色混乱,person 画成绿色,car 画成红色
原因:detect.py绘图时读取data/foggy.yaml中的colors,但你修改了names顺序(如把motorcycle提到bicycle前),却忘了同步更新colors列表顺序。
解决:写个校验脚本,确保len(names) == len(colors)且索引一一对应:
import yaml with open('data/foggy.yaml') as f: data = yaml.safe_load(f) assert len(data['names']) == len(data['colors']), "names and colors length mismatch!" for i, name in enumerate(data['names']): print(f"{name}: {data['colors'][i]}")4.5 现象:训练到 50 epoch 时 CUDA out of memory,但显存监控显示只用了 18G/24G
原因:--cache缓存机制在 Linux 下会占用大量 page cache,nvidia-smi看不到,但free -h显示 RAM 被占满,系统开始 swap,GPU kernel 启动失败。
解决:训练前清空 page cache:sudo sh -c "echo 3 > /proc/sys/vm/drop_caches";或改用--cache ram(仅缓存到 RAM)而非默认的--cache disk(缓存到 SSD,但会累积 metadata 占用内存)。
5. 进阶技巧:用 export.py 导出 ONNX 并在 OpenCV DNN 模块中零依赖推理
5.1 为什么必须导出 ONNX?——摆脱 PyTorch 环境束缚的硬需求
你训练好的best.pt只能在 PyTorch 环境运行,而实际部署场景往往是:
- 边缘设备(Jetson Nano)没有 pip,装不了 torch;
- 客户系统只允许 C++,不接受 Python;
- 需要和现有 OpenCV 流水线集成,不能额外起 Python 进程。
ONNX 是工业界事实标准,OpenCV 4.5+ 的cv2.dnn模块原生支持,无需安装 PyTorch,一行cv2.dnn.readNetFromONNX()即可加载。
5.2 三步导出 ONNX:从 best.pt 到 onnx-simplified
项目自带export.py,但直接运行会报错——因为雾天模型输入需适配--img 640,而默认 export 脚本用--img-size 640(旧参数名)。正确命令:
python export.py \ --weights runs/train/foggy_exp/weights/best.pt \ --include onnx \ --img-size 640 \ --batch-size 1 \ --device cpu # 避免 GPU 显存不足执行后生成best.onnx,但此时还不能直接给 OpenCV 用:YOLOv5 的 ONNX 输出是(1, 25200, 5+5)(25200 anchors),OpenCV DNN 要求(1, 5+5, H, W)格式。需用onnx-simplifier工具优化:
pip install onnx-simplifier python -m onnxsim best.onnx best_sim.onnx注意:
onnx-simplifier会折叠 Constant 节点、删除冗余 reshape,使模型体积缩小 30%,且输出 shape 符合 OpenCV 要求。
5.3 OpenCV DNN 推理:C++ 和 Python 双实现,附关键参数表
Python 版(快速验证)
import cv2 import numpy as np net = cv2.dnn.readNetFromONNX('best_sim.onnx') cap = cv2.VideoCapture('foggy_test.mp4') while cap.isOpened(): ret, frame = cap.read() if not ret: break # 预处理:resize + normalize + blobFromImage blob = cv2.dnn.blobFromImage( frame, scalefactor=1/255.0, size=(640, 640), mean=(0, 0, 0), swapRB=True, crop=False ) net.setInput(blob) outputs = net.forward() # shape: (1, 25200, 10) # 后处理:筛选 conf>0.3,NMS boxes, confs, class_ids = [], [], [] for detection in outputs[0]: conf = detection[4] if conf > 0.321: # 用前面定位的最优阈值 scores = detection[5:] class_id = np.argmax(scores) conf = scores[class_id] if conf > 0.321: center_x = int(detection[0] * frame.shape[1]) center_y = int(detection[1] * frame.shape[0]) w = int(detection[2] * frame.shape[1]) h = int(detection[3] * frame.shape[0]) x = int(center_x - w/2) y = int(center_y - h/2) boxes.append([x, y, w, h]) confs.append(float(conf)) class_ids.append(class_id) # OpenCV 自带 NMS indices = cv2.dnn.NMSBoxes(boxes, confs, 0.321, 0.45) for i in indices: i = i[0] x, y, w, h = boxes[i] label = ['person','car','bus','bicycle','motorcycle'][class_ids[i]] cv2.rectangle(frame, (x, y), (x+w, y+h), (0,255,0), 2) cv2.putText(frame, f"{label} {confs[i]:.2f}", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 2) cv2.imshow('Foggy Detection', frame) if cv2.waitKey(1) == ord('q'): break cap.release() cv2.destroyAllWindows()C++ 版核心逻辑(省略头文件和窗口)
cv::dnn::Net net = cv::dnn::readNetFromONNX("best_sim.onnx"); cv::VideoCapture cap("foggy_test.mp4"); std::vector<std::string> classes = {"person","car","bus","bicycle","motorcycle"}; while (cap.isOpened()) { cv::Mat frame; cap >> frame; if (frame.empty()) break; cv::Mat blob; cv::dnn::blobFromImage(frame, blob, 1.0/255.0, cv::Size(640,640), cv::Scalar(0,0,0), true, false); net.setInput(blob); std::vector<cv::Mat> outputs; net.forward(outputs, net.getUnconnectedOutLayersNames()); // outputs[0] is (1, 25200, 10), process as in Python std::vector<int> class_ids; std::vector<float> confidences; std::vector<cv::Rect> boxes; float* data = outputs[0].ptr<float>(); for (int i = 0; i < 25200; i++) { float conf = data[i*10 + 4]; if (conf > 0.321f) { float* scores = data + i*10 + 5; int class_id = std::max_element(scores, scores+5) - scores; float max_conf = scores[class_id]; if (max_conf > 0.321f) { float cx = data[i*10 + 0] * frame.cols; float cy = data[i*10 + 1] * frame.rows; float w = data[i*10 + 2] * frame.cols; float h = data[i*10 + 3] * frame.rows; int x = static_cast<int>(cx - w/2); int y = static_cast<int>(cy - h/2); boxes.push_back(cv::Rect(x, y, static_cast<int>(w), static_cast<int>(h))); confidences.push_back(max_conf); class_ids.push_back(class_id); } } } std::vector<int> indices; cv::dnn::NMSBoxes(boxes, confidences, 0.321f, 0.45f, indices); for (int i : indices) { cv::rectangle(frame, boxes[i], cv::Scalar(0,255,0), 2); std::string label = classes[class_ids[i]] + " " + std::to_string(confidences[i]); cv::putText(frame, label, boxes[i].tl(), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0,255,0), 2); } }| 参数 | Python 值 | C++ 值 | 说明 |
|---|---|---|---|
scalefactor | 1/255.0 | 1.0/255.0 | 归一化系数,必须与训练时--hyp中scale一致 |
size | (640,640) | cv::Size(640,640) | 输入尺寸,必须与export.py --img-size一致 |
swapRB | True | true | BGR→RGB,YOLOv5 训练用 RGB,OpenCV 读图是 BGR |
NMS threshold | 0.45 | 0.45f | IoU 阈值,雾天推荐 0.45,非 0.6 |
从那以后我每次导出 ONNX,都强制走一遍onnx-simplifier+cv2.dnn.readNetFromONNX+ 单帧推理验证,哪怕多花 2 分钟。因为线上服务一旦因 ONNX 兼容性崩掉,重启成本远高于本地多测一次。希望帮到你。
本文还有配套的精品资源,点击获取