☰
烟火识别算法实战:RTSP实时流与MP4回放告警叠框
2026/10/28 4:58:38 网站建设 项目流程

简介:面向视频监控、安防算法与计算机视觉学习者的一套烟火识别与烟雾检测工具包,聚焦图片、RTSP实时流和mp4视频中的火焰、烟雾目标检测,并支持输出带检测框的告警图片,适合做算法验证、场景演示与二次开发参考。压缩包共874个文件,约324.75MB,主体为843张jpg样本图,另含动态库、可执行程序、模型权重、批处理与shell脚本、演示mp4、png图示及pdf使用文档,兼顾样本、运行依赖与操作说明。已有242人学习下载。读者可借助现成工具快速完成图片、实时流与视频文件的检测测试,查看告警叠框效果,并参考文档与脚本了解模型调用、样本组织和工具运行方式,减少从零搭建环境的时间;对需要烟火分析、烟雾分析实践素材的读者,也便于开展效果对比与功能验证。

1. 烟火识别工具到底解决什么问题:从一张告警叠框图说起

值班室里最怕的不是没有告警,而是告警图发过来只有一团糊掉的亮斑,值班员没法判断是火光还是车灯。这套烟火识别算法与烟雾检测工具,把图片、RTSP 实时流和 MP4 文件统一接进同一条检测链路,烟雾识别结果直接叠框落到告警图里,烟火分析不依赖人工一直盯屏。它适合园区、仓储、林区边缘做离线验证,也适合已有摄像头的场景先跑回放、再决定要不要上前端。下面按输入源拆开讲,重点说参数怎么设、哪里容易翻车。

2. 烟火识别算法的输入输出链路:图片、RTSP、MP4三条通道怎么接

图片、RTSP 实时流和 MP4 文件看起来是三种输入,落到检测环节其实都绕不开同一件事:拿到一帧可解码的图像,送进烟火识别算法,再把结果画回原图。工具包如果给每条通道各写一套逻辑,后面调阈值、改叠框样式、加告警去重都要改三遍,维护成本会很高。我一般会先把输入源抽象成“帧迭代器”,图片只吐一帧,视频和 RTSP 持续吐帧,后面的推理、画框、落盘完全复用。这样做的另一个好处是回放验证和实时检测能用同一套参数,MP4 上跑出来的告警图才有参考价值。

2.1 输入源抽象:为什么图片、RTSP、MP4要统一成帧读取

图片检测是单帧离线任务,读一次就结束;MP4 是有限帧序列,跑完自动停;RTSP 是持续流,断流后还要考虑重连。三者的生命周期不同,但检测器只关心frame的宽高、通道数和像素格式。统一成生成器后,主流程只写一个for frame in open_source(...),图片和视频的差异被收进打开函数里。常见做法是用 OpenCV 的VideoCapture同时吃文件和 RTSP 地址,图片则用imread单独读一次再yield出去。下面这段是最小封装,实际工具包里可能已经内置类似入口,拿不到入口时可以用它兜底。

import cv2 def open_source(source): # source 可以是图片路径、mp4 路径或 rtsp 地址 if source.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")): frame = cv2.imread(source) if frame is None: raise RuntimeError(f"图片读取失败: {source}") yield frame return cap = cv2.VideoCapture(source) # RTSP 场景下适当增大缓冲,减少首帧解码失败的概率 cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) if not cap.isOpened(): raise RuntimeError(f"视频源打开失败: {source}") try: while True: ok, frame = cap.read() if not ok: break yield frame finally: cap.release()

这段代码的逻辑很直白:图片走一次性分支,读完就返回;视频和 RTSP 走循环分支,直到read()返回失败。CAP_PROP_BUFFERSIZE设成 2 是给 RTSP 留一点缓冲,但不同后端对这个参数的支持程度不一样,不能把它当成万能开关。图片路径判断用了常见后缀,实际包里如果还有webp或tif,按需补进去。finally里的release()不能省,长时间跑 RTSP 时漏掉释放,进程句柄会越积越多。

提示:RTSP 地址里不要把账号密码写进公开脚本,优先用环境变量或配置文件注入,换现场时只改配置不改代码。

2.2 从帧到告警叠框图:检测、阈值、画框、落盘的完整流程

拿到帧之后,下一步是送进模型。工具包对外暴露的推理入口可能叫detect、infer或predict,返回值常见的是[x1, y1, x2, y2, score, cls_id]这样的列表。烟火识别里火焰和烟雾通常分两类,有的模型还会把烟雾再细分浓烟、薄烟,具体看包内类别表。不管字段名怎么变,后面要做的事固定:按置信度过滤、按类别做非极大值抑制、把框画回原图、把带框图写到告警目录。下面这段是流程骨架,infer需要替换成工具包里的实际推理函数。

import cv2 import os import time def detect_and_draw(frame, infer, conf_thres=0.45, iou_thres=0.5): # infer(frame) 返回 [[x1, y1, x2, y2, score, cls_id], ...] raw_boxes = infer(frame) keep = [] for x1, y1, x2, y2, score, cls_id in raw_boxes: if score < conf_thres: continue keep.append([x1, y1, x2, y2, score, cls_id]) # 工具包若已内置 NMS,这里跳过;否则按 cls_id 分组做 NMS for x1, y1, x2, y2, score, cls_id in keep: label = "smoke" if int(cls_id) == 1 else "fire" color = (0, 165, 255) if label == "smoke" else (0, 0, 255) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) cv2.putText( frame, f"{label} {score:.2f}", (int(x1), max(int(y1) - 6, 12)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, ) return frame, keep def save_alarm(frame, out_dir, source_id, ts=None): os.makedirs(out_dir, exist_ok=True) ts = ts or time.strftime("%Y%m%d_%H%M%S") path = os.path.join(out_dir, f"{source_id}_{ts}.jpg") cv2.imwrite(path, frame) return path

conf_thres是置信度阈值,低于它的框不画也不落盘;iou_thres用在 NMS 里,控制重叠框合并程度。烟雾和火焰用不同颜色,值班员一眼能区分,但颜色不是关键,关键是类别标签要写对。int(cls_id) == 1只是示例映射,实际以包内类别顺序为准,映射反了会出现“烟被标成火”的尴尬。画框坐标必须转成整数,OpenCV 的rectangle对浮点坐标并不总是友好。save_alarm里按source_id + 时间戳命名,后面做告警去重和人工复核都靠这个命名找图。

注意:叠框坐标要映射回原图再画,模型输入如果做过 resize 或 letterbox,直接拿推理坐标画会偏。

2.3 最小可运行配置:路径、模型、阈值、输出目录

第一次跑不要一上来就接 RTSP,先用一张图确认模型能加载、类别映射对、叠框图能写出来,再换 MP4,最后才上实时流。配置项不用多,能把源、模型、阈值、抽帧间隔、输出目录和来源标识说清楚就够了。下面这份字典是我常用的起点,字段名按工具包实际配置改,值先按默认跑通再调。

CONFIG = { "source": "./samples/fire_01.jpg", # 可换成 ./samples/smoke.mp4 或 rtsp 地址 "model_path": "./weights/fire_smoke_model", # 按完整包内实际模型文件替换 "conf_thres": 0.45, "iou_thres": 0.50, "frame_interval": 5, # 视频流每 5 帧送检一次 "output_dir": "./alarms", "source_id": "cam_demo", }

source是入口,图片、MP4、RTSP 都走这里;model_path指向包内模型文件,不要自己猜文件名,按资源清单里的实际路径填。frame_interval对图片无效,对视频和 RTSP 才生效,设 5 表示每 5 帧取 1 帧送检,既能压推理压力,又不至于漏掉持续几秒的烟雾。conf_thres先给 0.45 是通用起点,烟雾通常比火焰更依赖场景,后面第 5 章会拆开调。output_dir最好单独挂一块盘或独立目录,告警图增长比日志快得多。source_id用来区分不同摄像头或不同回放文件,告警去重、批量归档都靠它。

配置跑通后,建议按“图片一张 → 短视频一段 → 实时流 5 分钟”的顺序加码。图片阶段看框位置和类别,短视频阶段看抽帧后有没有漏掉短时烟雾,实时流阶段看断流重连和告警落盘节奏。每一步都保留原始帧和叠框图,后面排查误报时才有黑匣子可看。工具包如果自带命令行入口,优先用自带参数,不要在外层再包一套重复逻辑,否则出问题很难定位是包内还是外层脚本。

3. RTSP实时流烟火检测:延迟、丢帧和告警风暴怎么压住

RTSP 实时流和 MP4 最大的差别不是协议,而是“帧会过期”。MP4 读慢一点没关系,帧还在文件里;RTSP 读慢一点,缓冲区里堆的是旧画面,等模型处理完再画框,告警图可能已经滞后十几秒。烟火识别要的是发现得早,不是把每一帧都算完。我一般会把 RTSP 通道拆成取流、抽帧、推理、告警四段,取流段只负责拿最新帧,推理段按固定间隔送检,告警段做冷却去重。这样即使模型单帧耗时偏高,也不会让告警图排长队。

3.1 RTSP取流参数:缓冲、超时、重连

OpenCV 打开 RTSP 时常见做法是指定CAP_FFMPEG后端,再设一个较小的缓冲。缓冲太大,画面延迟会堆起来;缓冲太小,网络一抖就断。实际取值没有统一答案,我一般从 1 到 3 之间试,配合重连逻辑观察 5 分钟内断几次。断流后不要立刻疯狂重连,给 2 到 3 秒间隔,连续失败到一定次数再抛错,让上层决定是换流还是告警“视频源异常”。下面这段是带重连的读取骨架。

import cv2 import time def rtsp_reader(url, reconnect_delay=3, max_fail=10): fail = 0 while True: cap = cv2.VideoCapture(url, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) if not cap.isOpened(): fail += 1 time.sleep(reconnect_delay) if fail >= max_fail: raise RuntimeError("RTSP 连续打开失败,检查地址、端口和网络") continue fail = 0 try: while True: ok, frame = cap.read() if not ok: break yield frame finally: cap.release() time.sleep(reconnect_delay)

reconnect_delay是断流后的等待秒数,设太小会把网络抖动放大成重连风暴;max_fail是连续打开失败上限,超过就抛错,避免脚本无声无息地空转。CAP_FFMPEG在常见 OpenCV 构建里可用,如果现场构建不带这个后端,换成默认后端再试,不要硬编码。BUFFERSIZE设 1 是尽量拿新帧,但有些后端会忽略这个设置,所以还要靠抽帧和队列上限兜底。yield出去的是原始帧,画框和落盘放到外层,取流函数只做一件事,排错时更容易定位。

3.2 抽帧与推理节流:不是每一帧都值得送进烟雾识别模型

烟雾识别模型单帧耗时通常在几十毫秒到几百毫秒之间,具体看输入尺寸和硬件。如果摄像头 25 帧每秒,每帧都送检,推理线程根本追不上取流线程,最后要么丢帧,要么延迟越积越大。常见做法是按固定间隔抽帧,比如每 5 帧取 1 帧,相当于 5 帧每秒送检;如果模型更慢,就调到 8 或 10。抽帧不是越稀越好,烟雾刚起时面积小,抽太稀可能漏掉前几秒。我的习惯是先按 5 跑一段回放,看告警图里烟雾出现的时刻比人工标注晚多少,再决定要不要加密。

def sample_frames(frames, interval=5): for idx, frame in enumerate(frames): if idx % interval != 0: continue yield idx, frame

interval是抽帧间隔,设 1 表示每帧都送,设 5 表示每 5 帧送 1 帧。这个函数只做计数过滤,不解决“推理慢于取流”的问题。如果推理耗时明显大于取流间隔,取流线程要只保留最新帧,旧帧直接丢掉,否则队列会越堆越长。常见实现是一个长度很小的队列,放新帧前先清空旧帧;或者取流线程维护一个latest_frame变量,推理线程每次取当前值。这样做的代价是可能跳过中间帧,但实时烟火检测本来就不需要每一帧都进模型。

3.3 告警叠框图落盘策略:命名、去重、保留周期

实时流跑起来后,另一个容易翻车的点是告警风暴。同一团烟在画面里持续十几秒,如果每帧都落盘,几分钟就能写出几千张图,磁盘和人工复核都会被拖垮。常见做法是按source_id + 类别做冷却,同一个来源同一种类别在冷却期内只落一张代表图。冷却时间按场景设,园区室外 30 秒到 1 分钟比较常见,林区边缘可以更长。文件名里带时间戳和类别,后面按事件归档时不用再打开图片猜。

import time class AlarmGate: def __init__(self, cooldown=30): self.cooldown = cooldown self.last = {} def allow(self, source_id, label): now = time.time() key = (source_id, label) if now - self.last.get(key, 0) < self.cooldown: return False self.last[key] = now return True

cooldown是同一来源同一类别的冷却秒数,设 30 表示 30 秒内只放行一次。source_id必须能区分摄像头或回放文件,否则两个摄像头会互相压制。label用fire和smoke分开,火焰和烟雾的告警节奏不一样,混在一起去重会漏掉另一类。落盘时建议同时保留原始帧和叠框图,叠框图给值班员看,原始帧给后面复盘用。保留周期按磁盘容量定,常见做法是告警图保留 7 到 30 天,原始帧只保留告警前后几秒,避免把整段视频都存下来。

4. 烟火检测落地排查:五类翻车现场与参数回退方案

这套工具在图片上跑通不代表现场能用,RTSP、MP4、叠框、长时间运行各有一批坑。下面五类是我在烟火检测落地里见得最多的翻车现场,每条按现象、原因、解决写,参数不要一次改太多,改一项就回放同一批素材看差异。

4.1 现象:RTSP地址能打开但读几帧就断,告警图时有时无

现象是脚本启动后能出几张图,过一会儿read()返回失败,重连后又恢复,告警图时间戳断断续续。原因通常不是模型,而是取流缓冲、解码后端和网络抖动叠加。缓冲设得太大,旧帧堆积后解码器状态异常;设得太小,网络一抖就断。还有的现场 RTSP 地址带特殊字符,配置文件解析时被截断,表现也是“能打开但读不久”。

解决分三步:先把CAP_PROP_BUFFERSIZE在 1 到 3 之间试,别直接上大缓冲;再把重连延迟设到 2 到 3 秒,连续失败上限设 10 次,超过就抛错让上层记录;最后把 RTSP 地址放进配置文件,用环境变量注入账号密码,避免特殊字符被 shell 吃掉。回放验证时不要只看“有没有图”,要看两张告警图的时间间隔是否接近冷却时间,间隔忽大忽小说明取流段还在抖。

4.2 现象:烟雾误报多,火焰漏报,告警图里只有一片灰

现象是白天车灯、反光、扬尘被标成烟雾,真火焰反而没框;或者烟雾框很大但置信度很低。原因常见有三类:火焰和烟雾共用一个置信度阈值,烟雾半透明、边界模糊,阈值低了误报多,阈值高了漏报;模型输入尺寸太小,远处小火苗在 resize 后只剩几个像素;抽帧间隔太大,短时火焰没被送进模型。

解决时先把火焰和烟雾阈值拆开,火焰通常可以略低,烟雾略高,具体看回放;再把输入尺寸和抽帧间隔一起看,远处小火点要把输入尺寸调大或加 ROI 裁剪,不能只降阈值。告警图里如果只有灰块没有清晰边界,先把原始帧留下来对比,确认是模型没学好还是画框坐标偏了。参数回退顺序建议是:先恢复默认阈值,再恢复默认抽帧,最后才动模型输入尺寸,一次只改一项。

4.3 现象:告警叠框图坐标错位或框跑到左上角

现象是框的位置和火焰、烟雾对不上,有的框缩在左上角,有的框比实际目标大一圈。原因多半是推理前做了 resize 或 letterbox,画框时直接用了模型输入坐标系,没有映射回原图。还有的脚本把x1, y1, x2, y2的顺序写反,或者把归一化坐标当成像素坐标用,表现也是框乱飞。

解决时先确认推理入口返回的是原图坐标还是模型输入坐标。如果是模型输入坐标,要按缩放比例和 padding 还原:先减 pad,再除以缩放比,最后 clip 到原图宽高。画框前打印一组坐标和原图宽高,肉眼对一下就能发现数量级不对。叠框图不要只存不查,批量跑 20 张图,按文件名排序快速翻一遍,框偏是很容易看出来的。

4.4 现象:MP4文件跑完没有输出告警图

现象是命令执行完没有报错,但输出目录是空的。原因可能是视频编码后端不支持,VideoCapture能打开但读不出帧;也可能是路径含空格或特殊字符,配置文件解析后路径被截断;还可能是阈值太高、抽帧间隔太大,整段视频没有一帧达到告警条件。写盘权限不足也会静默失败,尤其是输出目录挂到只读盘时。

解决时先用同一目录下的图片跑一张,确认模型和写盘正常;再打印视频总帧数和实际读取帧数,读出来是 0 就换后端或转码;最后把阈值临时降到 0.1、抽帧间隔设 1,看能不能出图。输出目录先用手动mkdir建好,确认当前用户有写权限。MP4 回放是验证烟火分析参数的好素材,不要因为一次没出图就怀疑模型,先把读取链路查清楚。

4.5 现象:长时间运行内存上涨,进程被系统杀掉

现象是跑几小时后内存持续上涨,告警图写入越来越慢,最后进程消失。原因常见的是VideoCapture没有在异常分支释放,告警图缓存越积越多,日志文件无限增长,或者推理框架的张量没有及时释放。RTSP 断流重连时如果每次都新建对象不释放旧对象,句柄和内存都会涨。

解决时把所有cap.release()放进finally,重连前先释放旧对象;告警图落盘后不要把所有图都留在内存里做拼接,写完就丢;日志按天轮转,别让一个文件涨到几个 G;推理侧如果工具包提供close()或reset(),在长循环里定期调用。上线前跑一次 2 小时连续回放,观察内存曲线,比看单张告警图更能发现问题。

5. 烟火分析参数怎么调:阈值、抽帧、ROI和告警去重的组合表

烟火分析的参数没有一套放之四海皆准的值,仓库、园区、林区、工地的干扰源完全不同。调参时不要同时动五个参数,先把输入源固定,再用同一批 MP4 回放做对照。火焰和烟雾的置信度分开设,抽帧间隔跟模型耗时挂钩,ROI 把明显不该看的区域挡掉,告警冷却压重复图。下面给的是起点配置,不是标准答案,实际值按回放结果微调。

5.1 置信度阈值与NMS:火焰和烟雾要分开调

火焰颜色饱和、边界相对清楚,置信度阈值可以略低;烟雾半透明、受光照和扬尘影响大,阈值通常要高一点。把两类共用一个conf_thres,常见结果是火焰漏报和烟雾误报同时出现。NMS 的iou_thres控制重叠框合并,烟雾框往往比火焰框大且重叠多,设得太低会把一片烟拆成多个框,设得太高又会把相邻两个火点合成一个。常见起点是火焰 0.45 到 0.50,烟雾 0.50 到 0.55,NMS 0.45 到 0.55。

调的时候先固定抽帧和 ROI,只动阈值,每改 0.05 跑同一段回放,记录告警图数量和误报类型。火焰阈值降得太低,车灯和反光会进来;烟雾阈值降得太低,扬尘和水汽会进来。阈值不是越低越灵敏,漏报和误报要一起看。工具包如果支持按类别输出分数,把分数分布打印出来,比只看有没有框更有用。

5.2 ROI与时段策略:把误报源挡在推理之前

ROI 是最省算力的降误报手段。画面里的马路、路灯、显示屏、水面反光区,如果不在监测范围内,直接在推理前裁掉或遮掉,比后面调阈值有效。ROI 用多边形点集配置,注意点顺序和原图坐标一致,不要用模型输入坐标。时段策略是另一个维度:夜间车灯多,火焰阈值可以略高;白天扬尘多,烟雾阈值可以略高;傍晚逆光时叠框图容易过曝,可以保留原始帧做对照。

ROI 不要画得太碎,碎多边形维护成本高,现场换机位就要重画。常见做法是画一个包含监测区域的大框,再把明显干扰区扣掉。时段策略也不建议按小时写死太多分支,先按白天、夜间两段跑回放,看误报是否集中在一个时段,再决定要不要加规则。

5.3 参数组合表:不同场景的起点配置

下面这张表是起点值,适合第一次跑通后做对照。火焰和烟雾阈值分开,抽帧间隔按模型耗时调,ROI 按现场画,冷却时间按事件持续时间设。表里的值不要直接复制到所有场景,先跑同一批 MP4 回放,看告警图数量和框位置再微调。

场景火焰 conf烟雾 conf抽帧间隔ROI告警冷却说明
室内仓储0.500.553货架区15 秒灯光干扰多,烟雾阈值偏高
园区室外0.450.505围墙内30 秒车灯和夕阳容易误报火焰
林区边缘0.350.408林带60 秒允许少量误报,优先早发现
工地作业面0.450.505作业面20 秒扬尘容易误报烟雾,ROI 要收窄

配置可以写成一份 YAML,工具包如果已有配置文件,把字段名映射过去即可。下面的字段名是常见命名,实际以包内说明为准,不要硬套。

source: type: rtsp url: "rtsp://user:pass@192.0.2.10:554/stream1" reconnect_delay: 3 model: conf_fire: 0.45 conf_smoke: 0.50 iou_thres: 0.50 frame_interval: 5 roi: enabled: true points: [[120, 80], [1180, 80], [1180, 680], [120, 680]] alarm: output_dir: "./alarms" cooldown_seconds: 30 keep_days: 7

conf_fire和conf_smoke分开后,调参时能单独看火焰漏报和烟雾误报。frame_interval和模型耗时挂钩,模型慢就调大,但不要大到烟雾已经扩散才出图。roi.points是原图像素坐标,按左上、右上、右下、左下顺序写,换分辨率后要重画。cooldown_seconds压重复告警,keep_days控制告警图保留周期,磁盘小就缩短。参数组合表里的值配合这份配置用,先跑 MP4 回放,再切 RTSP,不要反过来。

6. 从单机跑到批量验证:用MP4回放做回归,把告警图变成可复核证据

单张图跑通只能说明链路没断,真正要确认烟火识别算法有没有退化,得用同一批 MP4 做回放回归。每次改阈值、改抽帧、换模型,都跑同一组样本,把告警图按文件名归档,人工快速翻一遍,看框位置、类别、数量有没有明显变化。批量脚本不用复杂,能把输入目录里所有 MP4 跑完、按文件名建子目录、保留命令行参数就够了。

6.1 批量回放与告警图归档

#!/usr/bin/env bash set -euo pipefail # 用法:./batch_run.sh ./samples ./alarms/regression INPUT_DIR="${1:-./samples}" OUT_DIR="${2:-./alarms/regression}" mkdir -p "$OUT_DIR" for f in "$INPUT_DIR"/*.mp4; do [ -e "$f" ] || continue name="$(basename "$f" .mp4)" # cli 入口按完整包内实际脚本替换;--source/--out 参数名以包内说明为准 python run_detect.py --source "$f" --out "$OUT_DIR/$name" --conf 0.45 --interval 5 done

INPUT_DIR放同一批回放样本,OUT_DIR按视频名建子目录,后面对比不同参数时直接看目录差异。--conf和--interval是示例参数,工具包实际参数名可能不同,按包内说明映射。set -euo pipefail让脚本在单个视频失败时停下来,避免一批跑完才发现某个文件没输出。归档时保留原始文件名,不要用时间戳覆盖,否则无法判断是哪段素材出的告警。

6.2 用叠框图做人工复核的检查点

检查项通过标准不通过先看
告警图数量与人工标注事件数差距可解释阈值、抽帧间隔
框位置框贴合火焰或烟雾区域坐标映射、letterbox
烟雾类别烟和火没有混标cls_id 映射
重复告警同一事件冷却期内不重复cooldown 设置
落盘完整性每个 MP4 都有对应子目录写权限、路径空格

从那以后我每次改阈值前,都先用同一批 MP4 回放一遍,把告警图按文件名排好再人工过一遍;不这样做,参数越调越像玄学。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询