简介:基于MATLAB实现的烟雾检测代码包,面向图像处理入门者及安全监控、火灾预警等场景开发人员,帮助快速掌握从图像预处理、特征提取到区域分割与结果评估的完整算法流程。压缩包内仅含一个源程序文件,体积仅约1KB,结构紧凑,便于直接运行与修改调试。已有3245人学习参考,验证了其实用性;代码可读性强,适合快速上手与二次开发。文件围绕烟雾的颜色分布、边缘突变、纹理模式等关键特征设计检测逻辑,并涉及颜色空间转换、边缘检测算子、纹理特征分析、分类器判别等技术要点,适合结合典型烟雾图像开展实验与算法对比。通过阅读和运行该程序,读者可直观理解烟雾检测的主流思路,并以此为基础扩展实时视频流检测功能,进一步搭建完整的预警系统。 第一次接触图像中的烟雾检测时,我以为这只是一个“看图识别有没有烟”的小任务,真正动手做才发现,它比火焰检测难出好几个量级。火焰有相对稳定的形状和颜色,烟雾却是半透明、无固定轮廓、会随气流四处扩散的东西,同一段烟在不同光照下拍出来可能完全是两种模样。这篇博文是我在实际项目里做烟雾检测的完整记录,从传统图像算法到深度学习方案,再到部署时的误报排查,都会讲到。不管你是刚入门想做安防监控,还是已经在用OpenCV做图像预处理、想引入目标检测提升效果,这篇文章应该都能给你一些可落地的参考。
1. 烟雾检测为什么这么难:一个看上去简单但实际情况复杂的任务
1.1 烟雾和火焰的本质区别
很多人会把“烟火检测”当成一个任务,实际上两者难度完全不同。火焰因为温度高、颜色集中在橙红色区域,在图像里有清晰的边缘,用深度学习检测时,标注一个火焰框非常自然——它就是一个“物体”。但烟雾完全是另一回事。
烟雾是细颗粒悬浮物,它在图像里是半透明的,边缘不是一条线,而是一段渐变的过渡区域。一个室内小火源产生的烟,几秒内就能充满半个房间,你要怎么把“一房间的烟”框进一个矩形框里?边界在哪里?浓度稀薄到多少算烟?这些问题在标注阶段就会让人抓狂。
我之前做过一个对比实验:在同样的视频流里检测火焰和烟雾,火焰模型的 mAP 能到 0.85 以上,烟雾模型辛苦调到 0.72,还伴随着大量误报。这不是模型能力的问题,而是目标任务本身的定义就模糊得多。
1.2 烟雾检测难在哪儿:形状不固定、颜色不统一、背景干扰多
具体拆解下来,烟雾检测的难点集中在四个维度:
- 形状动态变化:烟雾受气流影响,每秒都在改变形状。上一帧是一个竖直烟柱,下一帧可能被吹成一片薄雾。这意味着模型很难学到稳定的形状先验。
- 颜色跨度极大:白烟、灰烟、黑烟、黄烟(某些化工场景)都算烟雾。白色烟雾在亮色背景下对比度很低,黑色烟雾在暗色环境中几乎和背景融为一体。颜色这个问题几乎无法用固定阈值解决。
- 背景干扰因素密集:雾、蒸汽、灰尘、炊烟、汽车尾气、工厂正常排气,在图像上和真正的火灾烟雾高度相似。强日光下的水汽蒸腾,也会被视觉算法判定为“白色飘动物”。
- 光照条件不稳定:白天强光、傍晚逆光、夜间红外补光,烟雾在不同光照下的灰度直方图差异巨大。同一个场景,白天有效的参数到了晚上完全失效。
再加上安防场景几乎都要求实时性,10秒才出一帧结果的算法没有工程价值,这进一步压缩了模型选择的空间。所以烟雾检测首先是一个“定义模糊 + 场景复杂 + 实时约束”三重压力下的问题,想要一个通用方案基本不现实,只有在具体场景里做针对性优化,才有落地可能。
2. 传统图像算法路线:从运动检测到特征建模的取舍
在深度学习普及之前,大家做烟雾检测主要靠传统图像算法组合,核心思路是:先找出图像中“变化”的区域,再对变化区域做特征筛选,看像不像烟。这套思路现在依然有参考价值,尤其在算力受限、只有CPU可用的场景。
2.1 运动检测基础:帧差法与背景建模
运动检测是传统方案的第一步,目的是把“动的区域”从静态背景里抠出来。最基础的是帧差法,直接比较相邻两帧对应像素的灰度差:
import cv2 cap = cv2.VideoCapture("smoke.mp4") ret, prev = cap.read() prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY) while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) diff = cv2.absdiff(prev_gray, gray) _, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY) cv2.imshow("diff", thresh) prev_gray = gray if cv2.waitKey(1) & 0xFF == ord('q'): break帧差法实现简单,但有两个致命问题:一是烟雾运动速度慢,相邻两帧之间的像素变化不大,很容易漏检;二是坏境里任何运动(树叶摇动、行人走过、云影移动)都会触发大量前景区域。
更实用的是背景建模,OpenCV 自带的 MOG2 或 KNN 都能做。MOG2 会对每个像素维护一个混合高斯分布,适应缓慢的光照变化,提取出来的运动前景比帧差法稳定不少。我在早期测试中,用 MOG2 提取前景后再做形态学开闭运算去除噪点,效果能比帧差法好一倍。但它的弱点也很明显:如果摄像头本身有抖动,或者场景里有反复运动的周期性物体(比如旋转的风扇),背景模型会不断被污染,最后输出一堆噪声。
2.2 颜色和纹理特征:如何把“疑似烟雾区域”筛出来
拿到运动前景后,并不是所有运动的区域都是烟,接下来要用颜色和纹理特征做二次筛选。烟雾在图像中最直观的特征是:会让背景变得“灰蒙蒙的”,而且这个区域内部的纹理往往比较平滑,细节被模糊掉。
我早期常用的筛选规则大致有这几条:
- 颜色空间约束:把前景区域转到 HSV 或 YCbCr 空间,烟雾区域的饱和度通常偏低,亮度处于中间段(不是极亮也不是极暗)。比如在 YCbCr 空间里,烟雾区域往往 $Cb$ 和 $Cr$ 的值非常接近,说明它“不偏红也不偏蓝”。
- 暗通道先验:这个思路来自图像去雾算法。烟雾和雾类似,会让局部区域的暗通道亮度明显高于正常背景。计算每个小块的暗通道值,如果偏高,就认为有烟雾覆盖的可能。
- 纹理平滑度:用拉普拉斯算子的方差来衡量区域清晰度。正常景物边缘多、梯度大,拉普拉斯方差高;烟雾区域因为散射作用,边缘被弱化,拉普拉斯方差明显偏低。这个特征在很多场景下比颜色特征更稳定。
- 小波域高频能量:烟雾区域的高频分量少,小波变换后的高频子带能量低。这个特征计算量稍大,但抗光照变化能力更强。
把运动前景、颜色约束、纹理特征组合起来,可以组成一个多级判断管道。核心逻辑是:先找到运动区域,再对每个连通域提取特征,设定阈值,超过阈值就判为烟雾。
2.3 传统方法的局限:为什么我在复杂场景下最终放弃了纯手工特征
这套传统管道在固定摄像头、背景简单、光照稳定的小场景里确实能跑,我甚至用它在烟尘试验场里取得了不错的效果。但一旦换到真实监控环境,问题就暴露了。
首先是阈值不可迁移。我在 A 场景调的饱和度阈值和暗通道阈值,拿到 B 场景完全不适用——B 场景背景有大量白色外墙,整个画面的暗通道天然偏高,误报率直接起飞。其次是运动检测在拥挤场景里基本失效,人一多、车一多,前景区域连成一片,特征筛选无从谈起。
最让我崩溃的是周末的树叶场景:一棵树的影子随风晃动,在运动检测里看起来就是一大片闪烁区域,颜色不偏、饱和度也低、纹理也平滑,最终被判成烟雾,一下午报了 40 多次警。调试到后面,我感觉自己不是在“写规则”,而是在给每一个背景植物写定制补丁。那一刻我就明白,传统特征工程在烟雾检测这种高度依赖语义的任务上,天花板太低了。于是我把重心转向了深度学习。
3. 深度学习方案:基于YOLO的烟雾检测落地
换成深度学习的直接原因是:模型可以把“什么是烟”的语义判断交给大量数据来学习,而不是靠我人去总结规则。在实测对比中,一个训练得当的 YOLO 模型在复杂背景下的准确率,轻松超过了我用两周时间调出来的传统管道。
3.1 为什么选择目标检测而不是分类或分割
同样是深度学习,选哪种任务形式也值得想清楚。我当时的备选有三个:图像分类、目标检测、语义分割。
- 图像分类只能回答“画面里有没有烟”,但如果一个画面里有三个烟源,分类模型既不能给出位置,也没法区分是几个烟源在冒烟。对安防联动来说,只知道“有烟”却不知道“哪里在冒烟”,价值很有限。
- 语义分割虽然能给出像素级区域,但烟雾本身边界模糊,标注员连边界都难以画准,标注一致性会非常差。我自己尝试验证过,同一段烟雾,两个标注员画出的边界 IoU 只有 0.6 左右,这样的标签会严重干扰分割模型的训练。
- 目标检测的矩形框标注最简单,也最能容忍烟雾边界的不确定性——框住“烟源附近的核心浓烟区”即可,不需要精确到像素。同时检测框自带位置信息,可以联动摄像头云台,还能输出多目标数量。
所以最终选了目标检测路线。实际项目中我同时检测火焰和烟雾两个类别,用同一个检测模型输出两类框,效果和后续告警联动都很方便。
3.2 数据集整理难点与Labeling经验
模型再好,没有数据也是白搭。烟雾检测的数据集是我投入时间最多、也最容易被低估的部分。
公开数据集方面,现有的火灾类数据集大多以火焰为主,烟雾样本占比少且场景单一。我收集了几个开源数据集后发现以下问题:白天户外场景占绝大多数,夜间红外帧少;工厂室内场景几乎为零;工业烟囱排放与火灾烟雾没有区分标签。所以光靠公开数据远远不够,必须自己补充。
补充数据最实用的来源有三个:一是从监控视频里截帧,找一个安全的可控测试点,用烟雾弹或烟饼制造烟雾进行拍摄;二是从公开的视频网站抓取森林火灾、工厂烟雾等真实片段,再按帧抽取;三是用图像增强手段扩展现有样本。
数据集整理阶段最大的坑是标注规范。烟雾透明区域到底算不算目标?我的处理原则是:只标注肉眼能明显识别的浓烟区域,淡到几乎看不见的不标。这样做的好处是标注一致性好,模型学到的是“明确烟雾”的视觉特征,而不是在透明区域上反复摇摆。另一个标注问题是不完整目标——有些烟雾从画面边缘蔓延进来,只有一个部分的框,这类“截断目标”我选择保留,因为真实检测中同样会遇到边缘起烟的情况。
数据增强上,除了常规的翻转、缩放、颜色抖动,我强烈建议加两种专门针对烟雾的增强:一种是模拟不同光照的亮度扰动,让模型适应白天和夜间差异;另一种是随机在背景区域粘贴烟雾图片块(类似CutMix),增加“小目标烟雾”的样本数量。我做了几十轮实验,发现对小烟雾目标来说,这种粘贴增强比单纯调亮度更有用。
3.3 模型选型与推理优化(YOLOv5 vs YOLOv8 vs 定制轻量模型)
模型选型上,我依次试过YOLOv5s、YOLOv8n、YOLOv8s以及更轻量的NanoDet,下面是实测对比(同数据集、同输入尺寸640x640):
| 模型 | 参数量 | 输入尺寸 | 推理耗时(Jetson Nano) | mAP50 | 备注 |
|---|---|---|---|---|---|
| YOLOv5s | 7.2M | 640 | 85ms | 0.74 | 稳定,部署资料多 |
| YOLOv8n | 3.2M | 640 | 55ms | 0.71 | 轻量,适合边缘设备 |
| YOLOv8s | 11.1M | 640 | 120ms | 0.78 | 精度最高,实时性吃紧 |
| NanoDet-Plus | 4.1M | 640 | 48ms | 0.69 | 最快,但小目标偏弱 |
综合评判之后,我选择了YOLOv8n作为主力模型:精度只比v5s低3个点,但速度几乎快一倍,而且在烟雾这种“目标虽大但纹理弱”的场景里,参数量差异带来的精度差距并不大。真正让精度提升的不是模型结构,而是数据质量。
部署优化这块,如果只跑推理不管效率,模型很难投入实际使用。我在NVIDIA边缘设备上会做 TensorRT 加速,用 FP16 精度,YOLOv8n 推理耗时能从55ms降到25ms左右,满足单路摄像头实时处理。如果设备更弱,只能CPU跑,可以尝试 OpenVINO 转换,配合 INT8 量化,效果也很可观。有一点要注意:INT8 量化对烟雾这种低纹理目标敏感,量化后 mAP 可能掉 5 个点以上,需要实测评估,不能为了省算力盲目量化。
4. 部署时最容易被忽视的问题:误报率、阈值和场景泛化
很多人在实验室里模型 mAP 挺高,一部署到现场就翻车,问题几乎都出在误报率上。烟雾检测的误报和一般目标检测误报不一样:消防物联网场景里,用户对“假警报”的容忍度极低——误报报多了,后面真火警来,也没人信了。
4.1 误报处理:云雾、蒸汽、灰尘和烟的区别
我在项目里遇到最多的误报,不是随机噪声,而是三类“看起来很像烟”的东西:
- 云和雾:整体缓慢漂移、半透明、颜色灰白,尤其阴天低空云层,几乎就是“放大的烟雾”。但云雾的运动尺度极大,覆盖画面往往超过三分之一,而且短时间内整体位移很小,不像火灾烟雾从局部源点扩散。
- 蒸汽:厨房、工厂锅炉房里的蒸汽,上升速度快,也半透明,非常难区分。我发现一个相对有用的特征:蒸汽在与空气接触后迅速消散,纹理变化快;烟雾则相对稳定,从上往下呈现出一个相对持续的扩散过程。
- 灰尘/扬尘:工地扬尘、车辆驶过扬起的尘土,在逆光下看起来和黄色烟雾几乎一样。但扬尘通常伴随明显的运动轨迹(车辆经过),且会很快落定。
针对这些误报,纯单帧图像很难区分,必须引入时序信息。我用了一种简单的运动一致性后处理:连续10帧中,检测框需要至少8帧都出现,且中心点位移不超过某个阈值(根据图像尺寸比例设定),才触发一次有效告警。这个策略简单粗暴,但能把云、蒸汽、瞬时光线变化引起的误检压掉一大半。真正的火灾烟雾是持续扩散的,不会只闪一两帧。
4.2 动态阈值与置信度策略
训练好的模型会输出一个置信度分数,部署时通常取一个固定阈值,比如0.5。但这个固定阈值在真实场景里不好用。
我遇到的实际问题是:白天光线充足时,模型对烟雾的置信度普遍较高,0.5 阈值很好用;但到了夜间红外模式下,同一片烟雾置信度会掉到0.3~0.4,如果坚持0.5阈值就会直接漏报。如果反过来把阈值降到0.3,白天误报又会增加很多。
解决方案是分时段设置阈值。在系统中读取当前时间,白天用0.45,夜晚用0.3,并且对夜间低置信度的告警强制进入“延时确认”流程:不在第一次检测到时立刻报警,而是连续确认5帧后再发出。这样既保住了夜间召回,又控制住了误报。
另外一个实用技巧是做区域级ROI设定。比如客户只关心车间东侧区域是否有烟,就可以通过配置界面把检测区域画出来,模型只对ROI内的检测框做统计。这个做法不是提升模型精度,而是从工程上把无关区域的误报直接排除,属于投入最小、收益最大的一项配置。
4.3 实际测试评估:我如何用混淆矩阵做评测
学术上习惯用 mAP 评价模型,但工程部署我更关心的是混淆矩阵:实际跑了一天监控视频,模型报了多少次真的烟雾、多少次误报、多少次漏报。
我在评测时会准备三段不同场景的连续视频:一段正常工厂作业(含蒸汽和叉车),一段晚间低照度场景,一段真实的烟雾火灾测试视频。跑完推理后,把模型输出的每个检测框按时间线保存为截图,然后人工逐张标注是 TP(真阳性)、FP(假阳性)还是 FN(假阴性)。最终统计:
- 误警率 = FP / (TP + FP):这个指标在安防场景里比 mAP 更重要,它直接决定了客户会不会关掉报警功能。
- 漏报率 = FN / (TP + FN):漏报意味着真火灾没发现,这是绝对底线。实际评估中,我发现很多模型漏报的不是“完全看不见的烟”,而是“烟还很淡、面积很小”的早期烟雾。针对这一点,后续优化重点应该放在采集更多早期浓烟小块的样本上。
多帧确认之后,误警率可以明显下降,因为大部分误报目标不会连续出现在8帧以上。真正导致误警率居高不下的,往往是场景里有一个持续存在的“假烟雾源”,比如固定排气口长时间喷出的蒸汽。这种问题靠模型和后处理都很难完全解决,我的做法是允许用户为每个摄像头单独配置“忽略区域”,把这种固定误报源直接框掉。
5. 我的实际项目总结与经验建议
5.1 一个完整的烟雾检测系统架构参考
到目前为止,一套可以稳定运行的烟雾检测系统大概长这样:
- 图像采集层:RTSP/RTMP 拉取摄像头视频流,或者用 SDK 直连网络相机。多路摄像头场景建议每路一个独立线程做拉流和推帧,避免一路卡顿拖垮全部。
- 预处理层:统一缩放输入到640x640,做归一化;低照度场景酌情做亮度和对比度增强。注意预处理一定要和训练时保持一致,否则模型效果会打折扣。
- 推理层:ONNX Runtime、TensorRT 或 OpenVINO 加载检测模型,输出检测框和置信度。
- 后处理层:NMS;ROI过滤;时序确认(连续N帧有效才判定);分时段阈值管理。
- 告警联动层:判定为烟雾后触发告警,保存当前帧截图,记录时间戳、置信度、摄像头编号,推送通知到监管平台。
在部署时,我把模型推理封装成独立服务,对外暴露一个简单接口:输入一帧图像,返回是否存在烟雾、检测框坐标、置信度。这样做的好处是更换模型或升级算法时,上层告警逻辑完全不用动。系统上线后,我还在持续记录误报警例,每两周挑一批典型误报补充进训练集做微调,模型的现场表现会越来越好。
5.2 给新手的建议与踩坑提示
如果你也准备做图像中的烟雾检测,根据我踩过的坑,有几条建议可以提前给到你:
- 先跑通再优化,不要一上来就追求完美。第一版直接用 YOLOv8n + 公开数据集 + 默认超参数,先跑通整个链路,看看漏报误报长什么样,再针对性地提升。
- 标注质量比模型结构重要。烟雾检测模型最怕的是标签不一致。同样的半透明烟,有人标了有人没标,模型学到的特征就会飘。不要追求样本数量级很大,5000张高质量标注图的效果,可能比20000张草率标注的图更好。
- 不要用测试集调参。训练阶段用验证集挑模型,最后才用没见过的测试视频做最终评估,否则你在测试集上看到的“高分”全是假象。我在项目中期犯过这个错误,调出来的模型在自己的测试片上几乎满分,一换新场景立刻露馅。
- 生产环境的误报会摧毁用户信任。宁可让系统延迟10秒报警,也要把瞬时误报压住。用户宁可看到晚一点的准确告警,也不希望一天收10条假警报。
- 最后分享一个小技巧:验证模型效果不必非等实时视频流,先拿历史监控视频离线抽帧跑一遍推理,观察检测框稳定性和置信度分布,就能提前发现大部分问题。等离线测试结果满意了,再上实时链路,能省下大量现场调试时间。
烟雾检测这个方向,真正的难点从来不是跑通模型,而是在复杂现实场景里让系统可靠、稳定、不扰民。希望这篇文章能帮你少走一些弯路,把你的检测系统稳稳地推进到生产环境里。
本文还有配套的精品资源,点击获取