简介:这份动态火焰识别源程序面向计算机视觉入门者、毕业设计学生及安全监控方向的研究人员,解决视频流中火焰区域实时检测与识别的问题。程序基于MATLAB实现,通过预处理去噪与对比度增强、帧差分运动检测、HSV色彩空间火焰颜色特征提取,并结合Canny边缘检测、LBP纹理分析及SVM等分类模型完成火焰判别与标记,可作为图像处理与机器学习综合实践的参考范例。资源包共7个文件,包含4个m脚本文件、2个avi视频素材和1个fig界面文件,压缩包约59.52MB,脚本与视频素材配合便于直接运行调试与效果验证。目前已有942人学习下载,适合希望理解火焰识别完整流程、掌握帧差分与颜色纹理特征提取方法、并在此基础上改进算法或完成毕业设计的读者参考借鉴。
1. 动态火焰识别源程序:从监控视频里把火焰“抠”出来的那套代码
去年帮一个做园区安防的朋友看项目,他们最头疼的不是摄像头不够,而是烟雾报警器装了一堆,真着火的时候要么不响,要么厨房一炒菜就狂叫。后来他们换了个思路,用普通监控摄像头跑动态火焰识别,把火焰的视觉特征从视频流里实时抠出来。这套源程序就是干这个的——输入是摄像头或视频文件,输出是画面里有没有火焰、火焰在哪个位置。它适合两类人:一类是想快速跑通一个火焰检测 Demo 的开发者,另一类是想在它基础上做二次开发,比如接入自己的摄像头、改检测灵敏度、加报警推送。整份代码不依赖特殊硬件,普通 USB 摄像头加一台带显卡的机器就能跑,这也是我当初愿意拆它的原因。
2. 先跑起来:环境、依赖和第一帧检测结果
2.1 运行环境与依赖安装
拿到源码包之后,别急着改代码,先把环境跑通。这套动态火焰识别源程序的核心依赖是 OpenCV 做视频解码和图像预处理,NumPy 做矩阵运算,如果带深度学习模型的话还会用到 PyTorch 或 TensorFlow。我一般会先建一个干净的虚拟环境,避免和系统里已有的包打架。
# 创建虚拟环境,Python 版本建议 3.8 以上 python -m venv flame_env source flame_env/bin/activate # Windows 下用 flame_env\Scripts\activate # 安装核心依赖,版本号按源码包里的 requirements.txt 来 pip install opencv-python numpy pip install torch torchvision # 如果源码用了 CNN 分类器这里有个细节:OpenCV 装opencv-python就够了,不要装opencv-contrib-python,除非源码里明确用到了 SIFT 这类专利算法。装完之后用一行命令验证:
import cv2 print(cv2.__version__) # 能打印出版本号就说明解码器就绪如果这一步报ImportError,大概率是虚拟环境没激活,或者 pip 装到了系统 Python 里。我踩过这个坑,后来养成了每次先which python确认路径的习惯。
2.2 视频输入与第一帧读取
动态火焰识别的第一步是把视频流拆成帧。源码里通常会有两种输入方式:一种是直接读摄像头,一种是读本地视频文件。下面这段代码是我从源码里抽出来的最小可运行片段,用来验证视频通道是否正常:
import cv2 # 参数 0 表示默认摄像头,也可以换成 "test_fire.mp4" cap = cv2.VideoCapture(0) # 检查是否成功打开 if not cap.isOpened(): raise RuntimeError("视频源打开失败,检查摄像头索引或文件路径") while True: ret, frame = cap.read() if not ret: break # 视频读完或摄像头断开 # 把帧缩放到固定宽度,减少后续计算量 frame = cv2.resize(frame, (640, 480)) cv2.imshow("input", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码的逻辑很直白:VideoCapture负责打开视频源,read()逐帧读取,resize统一尺寸。参数0是摄像头索引,如果你有多个摄像头,可以试1、2。waitKey(1)里的数字是等待毫秒数,设成 1 是为了让画面尽量流畅,按q退出。很多新手会忘记cap.release(),导致摄像头被占用,下次再跑就报错,这个坑我见过太多次了。
2.3 火焰颜色空间转换与阈值初筛
火焰在 RGB 空间里受光照影响很大,但在 HSV 空间里,火焰的色调通常集中在红色到黄色区间,饱和度偏高,亮度也偏高。源码里一般会先做一次颜色阈值筛选,把疑似火焰的区域圈出来,再交给后续模块判断。
import cv2 import numpy as np def preprocess_frame(frame): # 转成 HSV,H 范围 0-179,S 和 V 范围 0-255 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 火焰的红色到黄色区间,经验值可调 lower = np.array([0, 100, 100]) upper = np.array([35, 255, 255]) # 生成二值掩码,白色区域就是疑似火焰 mask = cv2.inRange(hsv, lower, upper) # 开运算去掉噪点,闭运算填补空洞 kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return masklower和upper这两个数组是颜色阈值的上下界,H 通道 0 到 35 覆盖了红到黄,S 和 V 的下限设成 100 是为了排除灰暗区域。形态学操作里的kernel大小是 5×5,如果画面噪点多可以加大到 7×7,但太大会把小火苗也吃掉。这个函数返回的mask是一张黑白图,白色像素就是候选火焰区域。实际项目里,这一步的误检率不低,比如夕阳、红色衣服、车尾灯都可能被框进来,所以后面必须加动态特征判断。
3. 核心模块拆解:从颜色掩码到动态特征判定
3.1 运动检测:帧差法与背景减除
火焰是动态的,静态的红色物体不是火焰。源码里判断动态特征最常用的方法是帧差法或者背景减除。帧差法就是拿当前帧和上一帧做差,变化大的区域保留;背景减除则是维护一个背景模型,把前景抠出来。两种方法各有适用场景,我一般会先看源码用的是哪种。
# 帧差法示例 def motion_detect(prev_gray, curr_gray, threshold=25): # 计算两帧灰度差的绝对值 diff = cv2.absdiff(prev_gray, curr_gray) # 二值化,差异大于阈值的置为 255 _, motion_mask = cv2.threshold(diff, threshold, 255, cv2.THRESH_BINARY) # 膨胀一下,把断裂的区域连起来 kernel = np.ones((3, 3), np.uint8) motion_mask = cv2.dilate(motion_mask, kernel, iterations=2) return motion_maskthreshold=25是灰度差阈值,设太小会把光照变化也当成运动,设太大又漏掉缓慢燃烧的火焰。iterations=2是膨胀次数,目的是把零散的运动像素连成块。帧差法的优点是计算快,缺点是摄像头一动就全屏都是运动区域,所以它更适合固定机位的场景。如果源码用的是背景减除,通常会调cv2.createBackgroundSubtractorMOG2(),那个对光照渐变更鲁棒,但参数更多,调起来更费时间。
3.2 火焰区域筛选:面积、长宽比与圆形度
颜色掩码和运动掩码做交集之后,会得到一堆候选区域。源码里一般会用findContours把连通区域找出来,然后按面积、长宽比、圆形度过滤。火焰的形状通常是不规则的,长宽比不会太极端,面积也不会太小。
def filter_contours(mask, min_area=500): contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) valid_regions = [] for cnt in contours: area = cv2.contourArea(cnt) if area < min_area: continue # 太小的区域直接丢掉 x, y, w, h = cv2.boundingRect(cnt) aspect_ratio = w / float(h) # 长宽比在 0.3 到 3.0 之间才保留 if 0.3 < aspect_ratio < 3.0: valid_regions.append((x, y, w, h)) return valid_regionsmin_area=500是面积下限,按 640×480 的画面算,500 像素大概是一个拳头大小。aspect_ratio限制在 0.3 到 3.0 之间,是为了排除细长的反光条和太扁的红色广告牌。返回的valid_regions是矩形框列表,每个框代表一个疑似火焰区域。这一步之后,源码通常还会算一下圆形度或者凸包缺陷,进一步区分火焰和普通红色物体。
3.3 动态特征判定:闪烁频率与质心漂移
火焰和红色灯泡最大的区别在于,火焰的轮廓一直在抖动,质心也在小范围内漂移。源码里会维护一个短时间窗口,记录每个候选区域的面积变化和质心位置,如果变化幅度超过阈值,才最终判定为火焰。
from collections import deque class FlameTracker: def __init__(self, history=10): # 保存最近 history 帧的面积和质心 self.area_history = deque(maxlen=history) self.centroid_history = deque(maxlen=history) def update(self, area, cx, cy): self.area_history.append(area) self.centroid_history.append((cx, cy)) def is_dynamic(self): if len(self.area_history) < 5: return False # 样本太少,先不判断 # 面积变化率 area_std = np.std(self.area_history) / (np.mean(self.area_history) + 1e-6) # 质心漂移距离 xs = [p[0] for p in self.centroid_history] ys = [p[1] for p in self.centroid_history] drift = np.std(xs) + np.std(ys) # 面积波动大于 0.15 且质心漂移大于 2 像素,认为是动态火焰 return area_std > 0.15 and drift > 2.0history=10表示看最近 10 帧,大约 0.3 秒。area_std是面积标准差除以均值,反映面积波动比例,火焰一般会超过 0.15。drift是质心坐标标准差的简单相加,超过 2 像素就算有明显漂移。这两个条件同时满足才判定为动态火焰,能过滤掉大部分静止的红色干扰物。这个类在实际源码里可能会更复杂,比如加上卡尔曼滤波预测轨迹,但核心思路就是看“抖不抖”。
4. 避坑与排查:火焰识别跑不通时先看这几处
4.1 摄像头打开失败或画面全黑
现象是cap.isOpened()返回False,或者画面一片黑。原因通常是摄像头索引不对,或者被其他程序占用了。解决方法是先用系统自带的相机应用确认摄像头能正常工作,然后换索引试0、1、2。如果是 Linux 环境,还要检查当前用户有没有video组的权限,没有的话用sudo usermod -aG video $USER加一下,重新登录生效。
4.2 火焰区域框不出来或框得太多
现象是画面里明明有火,但一个框都没有,或者满屏都是框。原因一般是 HSV 阈值不合适。解决方法是把preprocess_frame里的lower和upper打印出来,用cv2.imshow单独看 mask 图。如果 mask 全黑,说明阈值太窄,把 S 和 V 的下限降到 50 试试;如果 mask 全白,说明阈值太宽,把 H 的上限从 35 降到 25。我一般会写一个带滑动条的小工具,实时调阈值,比盲猜快得多。
4.3 帧率太低导致画面卡顿
现象是视频一顿一顿的,waitKey响应很慢。原因是每帧都在做全图形态学操作和轮廓查找,计算量太大。解决方法是先把帧缩放到 320×240 再做检测,或者把形态学操作的 kernel 从 5×5 降到 3×3。如果用了深度学习模型,检查是不是在 CPU 上跑,换成model.to('cuda')能快好几倍。另外,findContours之前可以先做一次cv2.resize降采样,轮廓坐标再按比例放大回去。
4.4 误报频繁:夕阳、车灯、红衣服都被当成火
现象是检测框频繁出现在非火焰区域。原因是颜色阈值和动态特征不够鲁棒。解决方法是加一个“火焰像素占比”判断,只有候选区域里满足火焰颜色的像素比例超过 60% 才保留。另外可以把质心漂移的阈值从 2 像素提高到 5 像素,牺牲一点灵敏度换准确率。如果场景里有固定光源干扰,可以在背景减除里把那个区域屏蔽掉,用cv2.rectangle画个掩码盖住。
4.5 视频文件读完报错或提前退出
现象是cap.read()返回False,循环直接结束。原因是视频文件路径不对,或者编解码器不支持。解决方法是先用cv2.VideoCapture("test.mp4")单独测试,确认isOpened()为True。如果文件是 H.265 编码,OpenCV 默认可能解不了,需要装opencv-python的完整版或者用 FFmpeg 先转成 H.264。另外,循环里要判断ret,不要假设每一帧都能读到,网络摄像头偶尔会丢帧。
5. 二次开发:把检测结果接进自己的报警链路
5.1 改造检测输出为结构化事件
源码默认可能只是在画面上画框,但实际项目里你需要的是结构化数据。我一般会加一个回调函数,把每一帧的检测结果整理成 JSON,方便推给消息队列或者写数据库。
import json import time def build_event(frame_id, regions, confidence): # 把检测结果整理成标准事件格式 event = { "frame_id": frame_id, "timestamp": time.time(), "flame_detected": len(regions) > 0, "regions": [ {"x": x, "y": y, "w": w, "h": h} for (x, y, w, h) in regions ], "confidence": confidence } return json.dumps(event, ensure_ascii=False)frame_id是帧序号,timestamp用 Unix 时间戳,regions是矩形框列表,confidence可以简单用面积占比代替。这个 JSON 可以直接丢给 Kafka 或者 Redis,下游的报警服务订阅之后就能发短信、推 App。注意ensure_ascii=False是为了让中文路径或备注正常显示,不然会变成乱码。
5.2 接入报警推送与去重逻辑
火焰检测最怕的是连续几十帧都报同一个火,导致报警轰炸。我一般会加一个状态机,只有从“无火”变成“有火”的那一刻才触发报警,持续有火的时候只记录不推送。
class AlarmState: def __init__(self, cooldown=30): self.last_alarm_time = 0 self.cooldown = cooldown # 冷却秒数 def should_alarm(self, flame_detected): if not flame_detected: return False now = time.time() if now - self.last_alarm_time > self.cooldown: self.last_alarm_time = now return True return Falsecooldown=30表示同一个火警 30 秒内只推一次。这个逻辑简单但有效,能避免报警风暴。如果场景里火焰会熄灭又复燃,可以把冷却时间调短到 10 秒。实际部署时,我还会把报警事件和截图一起存下来,方便事后回溯,截图用cv2.imwrite保存当前帧就行。
5.3 模型替换与阈值调参的验证方法
如果源码用的是传统视觉方法,想换成深度学习模型,最稳妥的做法是保留原来的预处理和跟踪逻辑,只把分类器换掉。比如训练一个小的 CNN 输入 64×64 的候选区域,输出“火/非火”概率。验证的时候不要只看准确率,要看误报率和漏报率。我一般会准备两段视频:一段全是干扰物(夕阳、车灯、红衣服),一段是真实火焰,分别跑一遍,统计误报次数和漏报次数。调参的时候先保证漏报率低于 5%,再尽量压低误报率。如果两个指标冲突,优先保漏报率,因为漏报火灾的代价比误报大得多。
从那以后我每次拿到新的视觉检测源码,都强制先跑一遍干扰视频,确认误报在可接受范围内再上真实场景。这套动态火焰识别源程序的骨架很清晰,颜色筛选加运动检测加动态特征,三层过滤下来,普通场景够用了。希望帮到你。
本文还有配套的精品资源,点击获取