ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于YOLOv8的雾天车辆行人检测系统:从数据合成到GUI部署

基于YOLOv8的雾天车辆行人检测系统:从数据合成到GUI部署 简介本资源是一套面向智能交通与计算机视觉初学者的雾天环境目标检测实战方案聚焦低能见度场景下的车辆与行人识别难题适用于YOLOv8算法学习、工业检测项目快速验证及课程设计开发。压缩包共41个文件含17张PNG/JPG格式的可视化结果图如PR曲线、混淆矩阵、预测批处理样例、2个核心权重文件best.pt与last.pt、2个Python主程序GUI界面与推理逻辑、2个配置文件数据集与模型参数以及requirements.txt等依赖说明整体大小20.72MB。已有1526人学习下载资源结构清晰GUI模块基于PyQt5开发支持图片、视频、摄像头三类输入源并可一键导出带标注的结果至指定目录配套RTTS雾天数据集训练成果与完整可运行代码省去环境配置与模型调优环节开箱即用。 做这个系统的起因其实挺直白的去年年底接了学校的一个横向项目需要在低能见度场景下统计路口车流和过街行人。原本想着直接用现成的YOLOv8模型跑一下就完事结果在雾天测试集上一测漏检率直接让我坐不住了。后来干脆自己动手收集数据、做模型微调、封装推理逻辑、再套一个GUI界面做成了这套基于YOLOv8的雾天车辆行人检测系统。整条链路走下来才意识到雾天检测这个方向远比想象中有门道不是简单“把模型训练一下”就能解决的。这篇文章就把整个项目从数据准备、模型训练、代码工程到GUI落地的完整过程拆开讲。内容涉及合成雾数据生成、YOLOv8训练细节、PyQt5界面开发、推理性能优化和结果导出这几个核心模块。如果你正在做目标检测相关项目或者想给YOLOv8模型套一个可视化界面做演示和落地这篇应该能帮你省不少弯路。文章里会给出可复现的思路和关键代码片段但不会像教程那样逐行贴全部代码重点是讲清楚每个环节的设计逻辑和我在实测中踩过的坑。1. 雾天检测为什么不是加个去雾算法就能解决开始动手之前我想先把一个关键认知讲清楚雾天场景下的目标检测难点主要不是“看不看得清”而是“特征稳不稳定”。1.1 雾天图像到底对模型做了什么正常天气下车辆和行人的边缘、纹理、颜色信息都比较完整特征提取器能轻易找到判别性信息。但雾天会同时破坏三个层面的特征对比度下降物体和背景的亮度差异被压缩边缘信息变弱Bounding Box的边界置信度跟着下降。颜色整体偏移大气光散射会让图像蒙上一层灰白色或灰蓝色调模型如果对颜色敏感很容易把“颜色”当成噪声。远距离目标信噪比急剧恶化距摄像头20米以上的人和车在雾天图像里可能只剩一个模糊轮廓。这种情况下即便人工标注也存在很大主观性。从物理角度看雾天成像可以用大气散射模型描述I(x) J(x) * t(x) A * (1 - t(x))其中J(x)是原始清晰图像t(x) exp(-β * d(x))是介质透射率β是散射系数d(x)是场景深度A是大气光。雾越浓β越大透射率越小远处目标被“抹掉”的程度就越严重。1.2 常见的“先复原再检测”路线有哪些问题不少人第一反应是先做图像增强比如暗通道先验去雾、直方图均衡化、Retinex增强再喂给检测模型。这种做法在视觉效果上确实有改善但工程上存在几个绕不开的问题去雾算法本身有计算开销尤其是暗通道先验涉及导向滤波实时性跟不上。去雾属于复原类操作如果参数估计不准反而会引入伪影比如光晕和色块。去雾后再检测是两阶段级联误差会逐级积累。去雾阶段丢掉的细节检测阶段无法找回。我在项目前期也做过对比实验同一组雾天图片先暗通道去雾再喂给YOLOv8mAP50比直接喂原图只高了一点FPS却掉了一半以上。而且去雾后的图片在晴天数据上训练出来的模型依旧表现不稳定。所以我的最终思路是不做显式去雾直接在带雾图像上训练模型让模型自己去学习雾不敏感的特征表达。这也是当前工业界做恶劣天气检测的主流思路。2. 系统整体架构模型、推理、GUI各司其职这套系统的最终形态是一个带界面的桌面应用用户不需要写任何代码打开程序就能完成检测。整体架构拆成三个层次来看会比较清晰。2.1 三大模块的职责划分系统分成模型推理模块、GUI交互模块、数据输入输出模块三部分模型推理模块负责YOLOv8模型的加载、图像预处理、前向推理、后处理NMS坐标还原对外提供统一的detect接口。GUI交互模块负责界面展示、用户操作响应、结果可视化。只做界面和事件分发不直接参与推理。数据输入输出模块处理图片、视频、摄像头三种来源的数据读取以及结果导出的编码和写入。这样的分层有个明显好处模型推理部分不依赖界面框架可以做单元测试也方便以后换成其他模型比如YOLO11、RT-DETR而不影响界面。GUI部分也不关心模型细节只要拿到检测结果做展示就行。2.2 技术选型与目录结构技术栈用的是PyTorch 2.x ultralytics YOLOv8 PyQt5 OpenCV。选PyQt5而不用Tkinter是因为PyQt5的QGraphicsView、QThread、QTimer在实时显示和异步处理方面用起来更顺手做出来的界面也更有工程感。项目目录大概长这样fog_detection_system/ ├── main.py # 程序入口 ├── config.py # 全局配置模型路径、输入输出路径、阈值 ├── models/ │ └── yolo_detector.py # 模型封装类 ├── gui/ │ ├── main_window.py # 主界面 │ ├── worker.py # 推理线程 │ └── widgets.py # 自定义控件 ├── weights/ │ └── fog_yolov8m.pt # 训练好的权重 ├── utils/ │ ├── preprocess.py # 预处理 │ ├── postprocess.py # 后处理 │ └── exporter.py # 结果导出 └── samples/ # 测试图片/视频权重文件单独放一个目录是因为训练过程中可能产生多个版本通过config.py里的路径配置切换很方便。2.3 输入输出与GUI的关系系统支持三种输入图片输入选择单张或批量图片jpg/png/bmp检测后可视化并保存。视频输入支持mp4/avi/mov等格式实时检测并逐帧绘制结果。可以控制暂停和继续。摄像头输入调用本机/外接摄像头通过设备ID选择实现实时检测。这三种输入在GUI层面对应三种不同的QThread工作模式。图片模式是单次任务处理完就结束视频和摄像头是连续任务每一帧都经过“读取-推理-绘制-显示”的循环。这个循环涉及到性能瓶颈的问题后面单独讲。3. 数据与训练合成雾数据集和YOLOv8微调的关键细节模型效果的源头是数据。雾天场景下能拿到的公开数据集很少真实标注更是稀缺资源。所以我的方案是基于清晰图片合成雾天数据形成训练集再用少量真实雾天图片做测试评估。3.1 用大气散射模型合成雾天训练数据合成雾的核心思路是用深度图模拟场景深度然后按大气散射模型叠加散射效果。具体分四步获取清晰原图及其对应的深度信息可以用深度估计模型如MiDaS生成也可以用单应性假设粗略估计。设定散射系数β模拟不同雾浓度β越小雾越薄β越大雾越浓。设定大气光A通常取(0.75, 0.78, 0.80)附近的亮灰色。按I(x) J(x) * t(x) A * (1 - t(x))逐像素生成雾天图像。下面是一个简化的PyTorch合成雾代码片段import torch import torch.nn.functional as F def generate_fog(image_tensor, depth_map, beta0.9, A0.78): image_tensor: [B, 3, H, W], 0~1 depth_map: [B, 1, H, W], 0~1 (近处小、远处大) t torch.exp(-beta * depth_map) # 透射率 fog image_tensor * t A * (1 - t) return fog.clamp(0, 1)实际操作中我不会对整张图用固定β。因为场景深度有层次远处本身就更模糊所以β可以按区域变化。但为了简化我当时用的是全图均匀β再加一些随机噪声扰动。这样做的好处是数据多样性能上去同一个样本可以生成薄雾、中雾、浓雾三个难度等级天然做数据增强。3.2 YOLOv8模型选型和训练配置模型选择上YOLOv8有n/s/m/l/x五个尺寸。这个项目的目标是实时或者近实时检测同时硬件条件不会太好我当时在GTX 1660 Ti上跑所以选m尺寸作为主力平衡了精度和速度。训练超参数设置如下输入分辨率imgsz640这是YOLOv8的默认推荐值对车辆行人这类中等偏小目标足够。训练轮数epochs150。雾天合成数据量本身大模型收敛速度正常150轮足够看到mAP50增长进入平台期。batch-size16在GTX 1660 Ti 6GB显存下实测可行如果显存不够可以降到8。优化器SGD初始学习率0.01weight_decay0.0005。YOLOv8默认优化器就是SGD效果稳定。数据增强除了ultralytics自带的增强mosaic、mixup、hsv扰动额外加了轻微高斯模糊和亮度抖动模拟雾天的边缘退化。训练集用两部分混合清晰数据不做雾合成和合成雾数据随机β0.5~1.5比例大概1:1。这样模型既保留对清晰场景的检测能力又增强对雾天的鲁棒性。3.3 评估指标和权重选择最终评估时我重点看两个指标mAP50-95和FPS。mAP50-95更能反映目标检测的精细程度雾天场景下模型如果定位不准这个指标会掉得很明显。训练结束后ultralytics会在runs/detect/train目录下生成best.pt和last.pt。best.pt是按验证集mAP挑选的最优权重last.pt是最后一轮权重。实际部署直接用best.pt。我额外做了一件事用一小组真实雾天图片大概200张手动标注后做了二次评估发现合成雾训练出来的模型在真实雾天上也有一定的泛化能力mAP50从没训练时的0.52提升到了0.78左右说明大气散射模型合成的数据确实能捕捉到雾天特征。4. GUI界面实现从命令行到可视化操作的完整落地本来用纯命令行也能跑但项目要交付给不会写代码的使用者界面必须有。这里讲讲GUI的设计思路和几个容易踩坑的点。4.1 为什么用QThread而不是在主线程跑推理这是整个GUI开发里最重要的一件事。如果直接在按钮的clicked信号里调用模型推理推理期间界面会完全卡死鼠标都挪不动体验非常糟糕。解决方式是开一个QThread子线程把推理逻辑放到子线程里主线程保持事件循环响应。我的做法是定义了一个Worker类继承QObject通过moveToThread挪到子线程。主界面发送信号触发Worker的run_detect方法Worker内部循环处理帧每处理完一帧通过signal传出结果主界面收到信号后更新画面。示例代码大致如下class Worker(QObject): frame_ready pyqtSignal(object, object) # (展示帧, 检测结果) pyqtSlot() def run_camera(self, camera_id, conf_thres, iou_thres): cap cv2.VideoCapture(camera_id) while not self.is_stopped: ret, frame cap.read() if not ret: break results detector.predict(frame, confconf_thres, iouiou_thres) drawn detector.draw_box(frame, results) self.frame_ready.emit(drawn, results) cap.release()注意PyQtSignal在跨线程传递的时候如果传的是numpy数组要在emit前做深拷贝或者传递引用。numpy数组在Python线程间传递本身没问题但如果后续会在多个线程里去写同一个数组就会出问题。我一开始直接在读帧循环里把frame传给了另一个线程去做绘制结果图像偶尔出现撕裂后来改为只传frame的浅拷贝frame.copy()问题就消失了。4.2 界面布局和交互设计界面布局参考了常见的标注工具风格具体分四个区域左侧控制面板按钮选择图片、选择视频、打开摄像头、暂停/继续、停止、导出结果、阈值调节滑块置信度阈值、IOU阈值、模型信息展示。中间显示区域使用QLabel显示图像/视频帧用QLabel的setPixmap来做图片显示视频则用QTimer定期更新。右侧检测结果列表显示当前画面中检测到的目标类别、置信度、坐标以YOLO相对坐标格式展示方便用户查看细节。底部状态栏显示当前输入源、FPS、模型加载状态等。控制逻辑和输入源的切换要做好状态管理。比如正在处理视频的时候点打开摄像头需要先停止当前任务再启动新的任务。我当时用一个state变量管理4种状态IDLE空闲、IMAGE图片模式、VIDEO视频模式、CAMERA摄像头模式切换输入源时先stop再start。4.3 检测框绘制和结果可视化绘制检测框直接用OpenCV的rectangle和putText。这里想提醒一点OpenCV的putText不直接支持中文如果标签是“行人”“汽车”这种中文会显示成乱码。我当时偷懒直接用英文标签如果想要中文需要用PIL.ImageDraw在图像上绘制中文再把PIL图像转回numpy数组。为了节省时间我这样做了但只在显示层导出的图片里同样是用PIL绘制的中文框标签避免终端用户看不懂。from PIL import Image, ImageDraw, ImageFont def draw_chinese_box(frame, box, label, color(0, 255, 0)): img_pil Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) draw ImageDraw.Draw(img_pil) font ImageFont.truetype(simhei.ttf, 20) draw.rectangle(box[:2] box[2:], outlinecolor, width2) draw.text((box[0], box[1] - 20), label, fillcolor, fontfont) return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)5. 推理代码的工程细节预处理、后处理与结果导出模型训练完只是第一步真正让系统稳定跑起来推理链路上的工程细节更关键。5.1 letterbox预处理和坐标还原YOLOv8的输入固定为640x640但输入图像不可能是规整的正方形。如果直接resize目标会被拉扁影响检测。标准做法是letterbox保持长宽比缩放短边填充灰色值114这样目标不会被拉伸变形。预处理函数的核心逻辑def letterbox(img, new_shape640, color114): shape img.shape[:2] r min(new_shape / shape[0], new_shape / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) img_resized cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) dw new_shape - new_unpad[0] dh new_shape - new_unpad[1] top, bottom dh // 2, dh - dh // 2 left, right dw // 2, dw - dw // 2 img_padded cv2.copyMakeBorder(img_resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value(color, color, color)) return img_padded, (r, left, top)推理完成后模型输出的检测框坐标是640x640坐标系下的要还原到原图坐标需要按比例r除以再减去填充的left和top。scale, left, top letterbox_info x1 (x1 - left) / scale y1 (y1 - top) / scale这一步如果漏了检测框位置会整体偏移尤其在分辨率不是640整数倍的图像上偏移非常明显。5.2 模型加载方式对比与选择ultralytics的YOLOv8支持两种加载方式一是直接用torch.hub.load(ultralytics/yolov8, custom, pathweights/fog_yolov8m.pt)二是用YOLO类加载。我最终选择了后者因为API更简洁且对训练好的自定义权重支持更好。from ultralytics import YOLO model YOLO(weights/fog_yolov8m.pt) def predict(frame, conf0.4, iou0.45): results model.predict(frame, confconf, iouiou, verboseFalse) return results[0].boxes # 检测框信息需要注意的是ultralytics每次调用predict时如果传入的是numpy数组内部会做一次类型检查。为了速度我将verbose关掉了省去大量控制台输出同时也避免了频繁打印导致GUI程序变慢。5.3 结果导出的三种格式设计导出功能满足用户“检测完还想留档”的需求。我实现了三种导出格式可视化图片在原始图片上绘制检测框和标签后保存为jpg/png文件。可视化视频在视频每一帧绘制检测框后编码为新的mp4文件。检测结果文本把每一帧的检测信息保存为txt或csv文件。文本结果格式我参照YOLO标注格式设计每行记录一个目标class_id, conf, x_center, y_center, width, height相对坐标。csv格式则加上了帧号、类别名、绝对坐标方便用Excel做后续统计。导出代码的逻辑很简单关键是把结果数据结构统一这样无论来源是图片还是视频都走同一套序列化函数def export_results(results_list, output_path, formatcsv): with open(output_path, w) as f: if format csv: f.write(frame,class_id,class_name,conf,x1,y1,x2,y2\n) for frame_idx, results in enumerate(results_list): for det in results: f.write(f{frame_idx},{det[class_id]},{det[class_name]}, f{det[conf]:.4f},{det[x1]:.2f},{det[y1]:.2f}, f{det[x2]:.2f},{det[y2]:.2f}\n)6. 性能调优与排错经验实测中反复踩过的坑做完功能只是第一步能稳定流畅地跑起来才是真正的工程。这部分我把自己在调试中踩过的坑和解决思路写出来应该能帮你省下不少时间。6.1 摄像头检测延迟高怎么办摄像头场景下最烦人的就是显示画面比实际场景慢半拍。刚开始我是在每一帧都做完整推理结果FPS不到15画面一卡一卡的。后来做了两处优化降低推理频率不需要每一帧都推理。摄像头输入一般是25~30FPS但检测任务对帧率的要求没那么高15FPS左右就够。所以我在循环里加了跳帧逻辑每读2帧才推理1帧显示则保持全部帧这样推理负载直接减半。调整摄像头缓冲调用cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)把缓冲队列减到1避免读取到旧帧。跳帧逻辑要小心如果跳帧期间目标已经跑出画面检测框会滞后。但实测下来在路口监控这类场景里车辆行人移动速度有限2帧一跳完全能接受。6.2 GUI界面卡死的排查过程界面卡死出现的场景很典型点击“视频检测”后视频画面出来了但界面无法点击“停止”。找了半天原因最后定位到问题出在QTimer和QThread共用主线程事件循环。我一开始用QTimer定时器驱动视频帧的读取想着每30ms读一帧再推理这样代码简单。但QTimer本身跑在主线程如果一次定时器回调里处理时间超过30ms后面的调用会被积压界面就会响应迟钝甚至卡死。改成QThread工作线程加信号回调以后界面就彻底流畅了。6.3 权重加载失败和版本兼容问题ultralytics包升级很频繁不同版本的模型结构定义可能不同。有次我换了台电脑重新部署用新版本ultralytics加载老版本训练的pt文件直接报错。排查下来发现是旧权重里包含的模型类名和当前版本对不上。解决思路训练权重时固定ultralytics版本在requirements.txt里写明ultralytics8.0.xx。同时如果遇到加载失败可以用torch.load把pt文件里的model键删掉只保留train_args和model的state_dict再通过YOLO的API重新加载这样能解决大部分兼容问题。6.4 合成雾强度过大导致过拟合训练初期我把所有训练图片都做成了浓雾β接近1.5。结果训练集mAP奇高但换成薄雾测试集模型效果急剧下降。原因很简单模型学到了浓雾图像的固定特征模式失去了泛化能力。调整策略是让β从一个范围里随机采样beta random.uniform(0.3, 1.2)同时将一部分清晰图片不加雾直接送入训练让模型不要忘记正常天气下的特征表达。这个改动对测试集提升非常明显mAP50从0.61涨到0.78。6.5 中文路径导致OpenCV读不到文件Windows环境下如果文件路径包含中文比如“D:\测试数据\雾天视频.mp4”OpenCV的VideoCapture会直接返回False读不到视频。这是一个经典问题。解决办法是先用np.fromfile读成字节流再用cv2.imdecode解码对图片有效。视频文件则用临时复制或改用imageio库读取。我当时为了省事直接在代码里加了路径检查遇到中文路径就提示用户把文件放到英文路径下同时也在程序里做了图片的字节流读取兼容def imread_chinese(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)6.6 显存不足的应对策略在6GB显存的GTX 1660 Ti上batch-size16偶尔会爆显存尤其当输入分辨率不是640而是1280时。如果显存不足有几个降级方案降低batch-size到8或4。开启gradient checkpointingYOLOv8中通过train的参数实现。降低输入分辨率到512但精度会受影响。实际项目中我用batch-size12配合梯度累积既保证了训练效率又不爆显存。梯度累积的思路是每4步累积一次梯度等效于batch-size48。7. 部署交付中的一些额外体会系统最终打包成了一个exe交付给用户。PyQt5torchultralytics这套打包体积不会小足足有2GB。如果想让体积小一些可以用Nuitka或PyInstaller的UPX压缩但实测压缩效果有限。有一个更极端的方案是换用ONNX Runtime做推理这样不再依赖完整torch打包体积可以降到几百MB。但前提是模型能正确导出为ONNX导出过程偶尔会遇到算子兼容问题。另外在部署到不同电脑时要注意CUDA版本和PyTorch的匹配。如果目标机器没有NVIDIA显卡就需要把推理代码切到CPU模式。代码里我专门做了一个设备检测逻辑device cuda if torch.cuda.is_available() else cpu但CPU推理YOLOv8m速度会下降到5~8FPS用户感受明显变差。如果用户机器实在没有显卡建议改用YOLOv8n速度能回到20FPS以上。8. 后续可以继续扩展的方向整个系统目前的完整度已经可以满足交付要求但距离产品化还有一段路。可以推荐几个改进方向增加多目标跟踪引入ByteTrack或BoT-SORT让车辆行人在视频序列中保持ID稳定直接输出车流统计和行人轨迹。增加低照度/夜间支持雾天往往伴随低照度后期可以加入低照度增强分支或者训练一个多域自适应模型。模型轻量化部署将YOLOv8m换成YOLOv8n或剪枝量化后的模型结合TensorRT加速在边缘设备如Jetson系列上做到实时推理。更加智能的置信度自适应雾天浓度会动态变化可以结合图像能见度估计自动调整置信度阈值。这个方向我试过雏形用简单灰度方差估算雾浓度效果有提升但还不稳定后续可以深入研究。这套系统做下来我最大的体会是目标检测不只是训练一个模型那么简单。数据是否覆盖目标场景、推理链路是否稳定、GUI交互是否顺手每一环都会影响最终的可用性。如果你也在做类似的方向希望在数据合成和GUI线程这两块尤其注意这是最容易出问题的两个地方。有兴趣的读者可以从合成雾数据开始复现逐步搭建自己的检测系统。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进