
简介这份动态火焰识别源程序面向计算机视觉入门者、毕业设计学生及安全监控方向的研究人员提供一套基于MATLAB的完整火焰检测实现方案可在视频流中实时定位并标记火焰区域用于火灾预警与相关课题验证。压缩包共7个文件约59.52MB以m脚本文件为主承担视频帧预处理、帧差分、颜色与纹理特征提取及分类识别等核心逻辑另含avi视频素材用于测试与效果演示以及fig图形文件保存界面或结果视图便于直接运行与二次调试。目前已有942人学习下载说明其在同类课题中具备一定参考价值。读者可借此理解HSV色彩空间转换、Canny边缘检测、LBP纹理分析及SVM分类等方法的工程落地方式并对照完整代码结构梳理从读帧到输出的处理链路为课程设计、毕业设计或算法改进提供可复用的实践范例。1. 动态火焰识别源程序从视频流里揪出早期火点的那套代码监控画面里飘过一片晚霞传统烟雾传感器毫无反应但摄像头画面里那片橙红色区域正在以特定频率抖动——这种场景就是动态火焰识别要解决的问题。这套源程序的核心思路很直接不依赖温度、颗粒物这些接触式传感器而是从可见光视频流里提取火焰的颜色分布、闪烁频率、面积增长趋势三类特征用帧间差分和色彩空间阈值做初筛再用分类器做二次确认。它适合做安防监控二次开发、嵌入式视觉方案预研、消防物联网原型验证的从业者。代码包通常包含特征提取模块、检测主流程、配置文件和一个可替换的模型推理接口拿到手改几个参数就能跑通自己的视频源。下面按“先跑通再调优”的顺序拆一遍。2. 火焰特征提取的底层逻辑RGB-HSV 双空间阈值怎么定2.1 为什么不能只用 RGB 阈值火焰在 RGB 空间的表现受光照影响极大。白天阳光直射下的火焰像素可能是 (255, 200, 150)而夜间火焰可能是 (220, 80, 30)单纯用 RGB 这个条件去卡白天会把大量暖色物体误判成火焰。常见做法是转到 HSV 空间用 H色调和 S饱和度做第一层过滤再回到 RGB 空间用亮度差做第二层确认。HSV 空间里火焰的 H 分量通常集中在 0°~35° 和 330°~360° 两个区间S 分量普遍高于 0.4V 分量则根据环境光动态调整。我一般会先用一段已知包含火焰的视频跑一遍统计把 H 和 S 的直方图拉出来看峰值位置再定阈值。源程序里通常有一个config.yaml或params.json里面hue_lower、hue_upper、sat_min、val_min这几个参数就是干这个的。不要直接抄默认值不同摄像头白平衡差异能到 15% 以上。2.2 帧间差分与闪烁频率的代码实现火焰区别于静态橙色物体的关键特征是闪烁。火焰边缘在 5~15 Hz 范围内有周期性抖动这个频率范围是大量实验统计出来的低于 3 Hz 会被风吹动干扰高于 20 Hz 摄像头采样率跟不上。源程序里一般用滑动窗口做帧间差分计算连续 N 帧中火焰候选区域的像素变化率。import cv2 import numpy as np from collections import deque class FlameFeatureExtractor: def __init__(self, window_size15, fps25): self.window deque(maxlenwindow_size) self.fps fps # 闪烁频率有效范围低于3Hz视为缓慢移动高于20Hz视为噪声 self.freq_low 3.0 self.freq_high 20.0 def extract(self, frame_bgr): hsv cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV) # 双区间Hue掩码覆盖红橙两段 mask1 cv2.inRange(hsv, (0, 100, 120), (35, 255, 255)) mask2 cv2.inRange(hsv, (330//2, 100, 120), (360//2, 255, 255)) mask cv2.bitwise_or(mask1, mask2) # 形态学去噪开运算去掉孤立噪点 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) self.window.append(mask) if len(self.window) self.window.maxlen: return None # 计算窗口内像素变化率作为闪烁强度 stack np.stack(self.window, axis0).astype(np.float32) flicker np.mean(np.abs(np.diff(stack, axis0)), axis0) flicker_score np.mean(flicker) # 频率估计对候选区域做FFT取主频 region stack[:, mask 0] if region.shape[1] 10: return None signal np.mean(region, axis1) fft_vals np.abs(np.fft.rfft(signal - np.mean(signal))) freqs np.fft.rfftfreq(len(signal), d1.0/self.fps) peak_freq freqs[np.argmax(fft_vals[1:]) 1] if len(fft_vals) 1 else 0 is_flame_freq self.freq_low peak_freq self.freq_high return { mask: mask, flicker_score: float(flicker_score), peak_freq: float(peak_freq), freq_valid: bool(is_flame_freq) }这段代码的逻辑分三层第一层用 HSV 双区间掩码把红橙色区域抠出来inRange的上下界就是前面说的需要按场景调的参数第二层用形态学开运算去掉零散噪点kernel大小 5×5 是经验值分辨率高于 1080p 可以适当加大第三层把连续 15 帧的掩码堆叠算像素级差分均值作为闪烁强度同时对候选区域做 FFT 取主频。window_size和fps必须匹配25 fps 下 15 帧窗口覆盖 0.6 秒刚好能捕捉 3~20 Hz 的完整周期。如果摄像头是 30 fps窗口要相应调到 18 帧左右否则频率分辨率不够主频会漂。2.3 面积增长率的计算与阈值火焰从起火点向外蔓延时候选区域面积会持续增长。源程序里一般用最近 30 帧的面积序列做线性拟合斜率超过某个阈值就判定为“增长中”。这个阈值不能设死因为摄像头距离不同火焰在画面里占的像素数差异巨大。常见做法是用画面总像素数的百分比来归一化比如每秒增长超过画面面积的 0.05% 就算有效增长。代码里通常有一个area_history列表配合np.polyfit算斜率。提示面积增长率对镜头抖动非常敏感如果摄像头装在振动环境里先做电子防抖或者用背景减除把全局运动去掉否则整片画面都在“增长”。3. 检测主流程搭建从视频源到报警输出的完整链路3.1 视频源接入与帧预处理源程序支持的视频源一般有三种本地视频文件、USB 摄像头、RTSP 流。配置文件里用source_type区分source_path填对应地址。RTSP 流有个坑OpenCV 的VideoCapture默认用 TCP 还是 UDP 取决于后端网络不稳时 UDP 会丢帧导致帧间差分算出来全是噪声。我一般强制走 TCP在 URL 后面加?tcp或者设置环境变量OPENCV_FFMPEG_CAPTURE_OPTIONSrtsp_transport;tcp。帧预处理包括缩放和去噪两步。缩放是为了统一处理分辨率源程序默认把长边缩到 640这个尺寸下火焰特征还保留得不错再小就会丢细节。去噪用高斯模糊ksize取 5 或 7太大把火焰边缘的闪烁也抹掉了反而降低检测率。import cv2 import os os.environ[OPENCV_FFMPEG_CAPTURE_OPTIONS] rtsp_transport;tcp def open_source(cfg): if cfg[source_type] file: cap cv2.VideoCapture(cfg[source_path]) elif cfg[source_type] usb: cap cv2.VideoCapture(int(cfg[source_path])) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) elif cfg[source_type] rtsp: cap cv2.VideoCapture(cfg[source_path]) else: raise ValueError(unsupported source type) if not cap.isOpened(): raise RuntimeError(cannot open video source) return cap def preprocess(frame, target_long_side640): h, w frame.shape[:2] scale target_long_side / max(h, w) if scale 1.0: frame cv2.resize(frame, (int(w*scale), int(h*scale))) frame cv2.GaussianBlur(frame, (5, 5), 0) return frameopen_source里对 USB 摄像头显式设了 1280×720因为默认分辨率往往是 640×480火焰像素太少会影响频率估计。RTSP 那行环境变量必须在VideoCapture之前设置放在 import 之后就行。preprocess的缩放比例用长边算保证不同宽高比的视频缩完后长边一致后续面积阈值才能用同一套参数。3.2 多帧确认与误报抑制单帧检测结果抖动很大源程序一般用“连续 M 帧中至少 N 帧命中”的策略做确认。比如 M10、N7意思是 10 帧窗口里至少 7 帧检测到火焰特征才触发报警。这个策略能把闪电、车灯扫过这类瞬态干扰滤掉。参数在配置里叫confirm_window和confirm_hits调大 N 降低误报但增加漏报调小反之。我一般先用 N/M0.7 跑一段看误报和漏报哪个更不可接受再微调。还有一个抑制逻辑是区域一致性连续命中的帧里火焰候选区域的质心不能跳太远。如果第 1 帧质心在画面左上、第 5 帧跑到右下那大概率是两处独立干扰而不是同一团火焰。源程序里用质心欧氏距离做判断阈值取画面对角线的 10%。3.3 报警输出与日志记录检测到确认火焰后源程序一般做三件事在画面上画框并叠加FIRE标签、写一条带时间戳和置信度的日志、触发一个回调函数可以是发 HTTP 请求、写数据库、点亮 GPIO。回调接口通常留成一个 Python 函数指针在配置里指定模块路径和函数名。日志格式我习惯用 JSON Lines每行一条方便后续用jq或 pandas 分析。import json import time class AlarmHandler: def __init__(self, log_pathfire_alarm.jsonl, callbackNone): self.log_path log_path self.callback callback def trigger(self, frame_id, bbox, confidence, extraNone): record { ts: time.time(), frame_id: frame_id, bbox: [int(v) for v in bbox], confidence: round(float(confidence), 4), extra: extra or {} } with open(self.log_path, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n) if self.callback: self.callback(record) return recordbbox用[x, y, w, h]格式和 OpenCV 的rectangle参数一致省得转换。confidence保留四位小数够用又不至于日志膨胀太快。callback如果抛异常源程序里一般会 catch 住并写一条 error 日志不让报警链路把主检测循环拖死——这个细节很多开源实现没做跑久了回调一挂整个进程就卡住。4. 避坑与排查动态火焰识别源程序落地时的五个血泪教训4.1 白天误报率飙升晚上正常现象白天检测器频繁触发晚上同一场景几乎不报。原因HSV 的 V 分量阈值设太低白天阳光照射的墙面、地面 V 值很高H 和 S 也落在火焰区间被误判。解决把val_min从默认的 120 提到 180 以上同时加一条规则——候选区域的平均 V 值如果超过画面整体平均 V 值的 1.5 倍才保留。这个比值判断能有效区分“自身发光”和“被照亮”。4.2 RTSP 流跑几分钟后卡死现象程序启动正常跑 3~5 分钟后cap.read()返回 False画面冻结。原因RTSP 流有会话超时OpenCV 的 FFmpeg 后端在某些版本下不会自动重连。解决在主循环里加超时计数连续 30 帧读失败就cap.release()然后重新open_source。重连间隔用指数退避第一次 1 秒、第二次 2 秒、最多到 16 秒避免网络没恢复时疯狂重试把 CPU 打满。4.3 火焰频率估计忽高忽低现象同一团火焰peak_freq在 2 Hz 和 18 Hz 之间跳。原因FFT 的频率分辨率等于fps / window_size25 fps 配 15 帧窗口分辨率是 1.67 Hz加上火焰信号本身不是单频峰值位置容易漂。解决把窗口拉长到 30 帧分辨率降到 0.83 Hz同时用抛物线插值对 FFT 峰值做亚 bin 修正。代价是响应延迟增加约 0.6 秒看场景能不能接受。4.4 面积增长率对镜头变焦误判现象摄像头自动对焦或变焦时画面里所有物体都在“变大”面积增长率爆表触发误报。原因面积增长率没有区分全局缩放和局部增长。解决加一个全局运动补偿——用光流或者简单的帧间仿射变换估计背景缩放系数把候选区域面积除以这个系数再算增长率。源程序里如果没带这个模块可以先用cv2.estimateAffinePartial2D对背景特征点做粗配准。4.5 多火焰场景下只报一个框现象画面里两处独立火焰检测器只输出一个合并后的大框。原因形态学操作和连通域分析时用了过大的 kernel把两个火焰区域连在一起了。解决把MORPH_OPEN的 kernel 从 5×5 降到 3×3连通域分析后用面积阈值过滤掉小于 50 像素的碎片再对每个连通域单独跑频率和面积增长判断。如果两个火焰确实靠得很近那合并成一个框在报警语义上也能接受但日志里要记录连通域数量。5. 进阶调优用轻量分类器替代纯阈值规则纯阈值规则在受控场景够用但换一个摄像头、换一种燃料比如酒精火 vs 木材火颜色分布差异能到 20% 以上又得重新调参。更稳的做法是在阈值初筛之后接一个轻量分类器输入是候选区域的 HSV 直方图 闪烁频谱 面积增长斜率输出是火焰/非火焰二分类。模型不用大三层全连接或者一个 1D CNN 就够参数量控制在 10k 以内嵌入式 CPU 上单帧推理能压到 5 ms 以下。训练数据自己采正样本用打火机、酒精灯、蜡烛在不同光照下录负样本用晚霞、车尾灯、红色衣物、暖色灯光。每段视频切 2 秒片段提取上述三类特征拼成向量。我一般正负样本各 2000 条起步用sklearn的MLPClassifier先跑个基线准确率能到 95% 以上再考虑换 PyTorch 做端到端微调。import numpy as np from sklearn.neural_network import MLPClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report import joblib def build_features(hsv_hist, flicker_spectrum, area_slope): # hsv_hist: 32维, flicker_spectrum: 16维, area_slope: 1维 return np.concatenate([hsv_hist, flicker_spectrum, [area_slope]]) # X: (N, 49), y: (N,) 0/1 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) clf MLPClassifier(hidden_layer_sizes(32, 16), max_iter500, early_stoppingTrue) clf.fit(X_train, y_train) print(classification_report(y_test, clf.predict(X_test))) joblib.dump(clf, flame_clf.joblib)特征拼接顺序必须和推理时一致hsv_hist用cv2.calcHist对 H 通道算 32 bin 直方图并归一化flicker_spectrum取 FFT 前 16 个 bin 的幅值area_slope就是前面算的面积增长率。hidden_layer_sizes(32, 16)这个结构在 2000 条数据上不容易过拟合再大就得加 Dropout 或 L2。训练完用joblib存推理时加载一次常驻内存别每帧都 load。验证方法很简单拿一段没参与训练的 10 分钟视频统计误报次数和漏报次数。误报看每小时的 false positive 数漏报看火焰出现到首次报警的延迟。我一般要求误报低于 2 次/小时、延迟低于 3 秒达不到就回去调特征窗口或者补负样本。从那以后我每次换摄像头或者换场景都强制走一遍这个验证流程不再凭感觉调阈值。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。