烟火识别算法落地实战:图片、RTSP与mp4统一接入及告警叠框
简介LNTON羚通烟火识别算法与烟雾检测工具面向安防监控、消防预警及AI视觉开发者解决图片、RTSP实时流与mp4视频中的烟火检测和烟雾识别需求输出带告警叠框的结果图适合具备一定计算机视觉基础、需要快速落地烟火分析功能的工程人员。资源包共874个文件约324.75MB以843张jpg样本图片为主辅以dll动态库、exe可执行程序、bat批处理脚本、mp4测试视频、weights模型权重及pdf使用文档覆盖样本生成、模型推理与结果输出等环节。目前已有241人学习下载。工具内附使用文档与可执行程序亲测可用读者可借助样本图片与批处理脚本完成数据准备通过模型权重与动态库直接运行烟火检测并利用mp4与RTSP流验证实时识别效果快速搭建从样本到告警叠框的完整流程。1. 烟火识别算法落地从一张告警叠框图说起凌晨两点仓库外围的摄像头拍到一缕白烟值班室的屏幕上却什么都没弹出来。第二天调录像才发现那缕烟持续了四十多秒足够烧起来。这件事之后我开始认真折腾烟火识别算法也就是标题里说的 LNTON羚通烟火识别算法、烟雾检测工具这套东西。它要解决的核心问题很具体把图片、RTSP实时流、mp4文件这三种输入统一接进来跑烟火检测和烟雾识别最后输出一张带告警框的图片。对做安防、园区、仓储、森林防火的工程师来说这套流程能不能稳定跑起来比模型精度多一个点重要得多。这篇笔记就按我实际搭过的路径把选型、拉流、推理、叠框、排错一层层拆开新手能照着复现熟手能直接看到参数边界和踩坑点。2. 三种输入源怎么统一图片、RTSP实时流与mp4文件的接入设计2.1 为什么要把三种输入抽象成同一个帧迭代器图片、RTSP流、mp4文件看起来是三种完全不同的东西但落到检测环节它们最终都变成「一帧一帧的 BGR 图像」。如果每接一种输入就写一套推理逻辑代码会迅速失控改一个叠框样式要动三处。我一般会先定义一个统一的帧源接口把「打开、读帧、释放」三件事抽出来图片源读一次就结束视频文件和 RTSP 流则循环读帧。这样后面的检测、叠框、告警保存只认帧不认来源。这个抽象带来的直接好处是调参只调一处换输入源不动检测代码。代价是要处理好「读帧失败」的语义——图片读完是正常结束RTSP 读失败可能是断流mp4 读失败可能是文件损坏三者要区分对待否则实时流一断就整个进程退出。import cv2 class FrameSource: 统一帧源图片、mp4、RTSP 都走这里 def __init__(self, src, is_streamFalse): self.src src self.is_stream is_stream # 图片用 imread视频/流用 VideoCapture if isinstance(src, str) and src.lower().endswith((.jpg, .png, .jpeg)): self.mode image self.frame cv2.imread(src) else: self.mode video self.cap cv2.VideoCapture(src) # RTSP 走 TCP减少丢包导致的花屏 if is_stream: self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) def read(self): if self.mode image: f, self.frame self.frame, None return f is not None, f ok, frame self.cap.read() return ok, frame def release(self): if self.mode video: self.cap.release()逻辑说明FrameSource用文件后缀判断是不是图片是图片就一次性读入内存否则走VideoCapture。CAP_PROP_BUFFERSIZE设成 1 是为了让 RTSP 尽量取最新帧而不是排队取旧帧——实时告警场景里处理一帧旧画面比丢一帧更糟。参数上is_streamTrue时建议同时把解码方式设成 TCP避免 UDP 丢包造成的画面撕裂这个在 OpenCV 里通过环境变量或后端参数控制不同版本写法略有差异常见做法是设OPENCV_FFMPEG_CAPTURE_OPTIONS。2.2 RTSP 拉流地址的拼法与 mp4 的读取差异RTSP 地址各家摄像头不一样海康常见的格式是rtsp://用户名:密码IP:554/Streaming/Channels/101大华是rtsp://用户名:密码IP:554/cam/realmonitor?channel1subtype0。主码流清晰但码率高做烟火检测其实子码流就够能显著降低解码压力。mp4 文件则简单得多直接传路径即可但要注意有些 mp4 是 HEVC 编码OpenCV 默认后端不一定能解遇到打不开先确认编码格式。# 先用 ffprobe 确认流/文件能不能通再进代码省得在 Python 里瞎猜 ffprobe -v error -show_entries streamcodec_name,width,height -of defaultnw1 rtsp://user:pass192.168.1.64:554/Streaming/Channels/102 ffprobe -v error -show_entries streamcodec_name -of defaultnw1 test.mp4逻辑说明ffprobe能在不写代码的前提下告诉你编码是 h264 还是 hevc、分辨率多少。如果这里就报错问题在地址或网络不在检测代码。参数上-v error只打印错误输出干净-show_entries streamcodec_name只取编码名。这一步是我排查「代码读不到流」时的第一站能省掉大量无效调试。2.3 抽帧策略实时流不能每帧都推理RTSP 流一般 25 帧每秒如果每帧都跑检测普通工控机根本扛不住而且烟火是缓变目标相邻帧结果几乎一样。我一般按时间抽帧比如每 0.5 秒处理一帧既保证响应速度又把算力降下来。mp4 文件可以按帧间隔抽图片则只处理一次。抽帧逻辑要放在帧源和检测之间别写进检测函数里。import time class FrameSampler: 按时间间隔抽帧实时流和文件都适用 def __init__(self, interval_sec0.5): self.interval interval_sec self.last_ts 0.0 def should_process(self): now time.time() if now - self.last_ts self.interval: self.last_ts now return True return False逻辑说明should_process用单调递增的时间戳判断是否到了处理间隔。参数interval_sec是核心实时告警建议 0.3 到 0.5 秒太小算力浪费太大可能漏掉快速起烟。mp4 离线分析可以设成 0.2 秒甚至按帧因为不要求实时。注意这里用time.time()而不是帧计数因为 RTSP 实际帧率会波动按帧计数会导致处理节奏漂移。3. 烟火检测推理与告警叠框从模型输出到一张能看的告警图3.1 检测结果的数据结构要先定死不管用什么检测模型输出最好统一成[x1, y1, x2, y2, score, class_id]这种结构。烟火检测通常分两类烟smoke和火fireclass_id 用 0 和 1 区分。把结果结构定死之后叠框、保存、上报都只依赖这个结构换模型不用改下游。我见过太多项目因为模型输出格式变来变去最后叠框代码里全是 if-else。def postprocess(raw_output, conf_thres0.4, iou_thres0.45): 把模型原始输出整理成统一结构含 NMS boxes [] # raw_output 假设为 [N, 6]: x1,y1,x2,y2,score,class_id for det in raw_output: x1, y1, x2, y2, score, cls det if score conf_thres: continue boxes.append([x1, y1, x2, y2, score, int(cls)]) # 按类别分别做 NMS避免烟和火互相抑制 keep [] for c in set(b[5] for b in boxes): cls_boxes [b for b in boxes if b[5] c] cls_boxes.sort(keylambda x: x[4], reverseTrue) while cls_boxes: best cls_boxes.pop(0) keep.append(best) cls_boxes [b for b in cls_boxes if iou(best, b) iou_thres] return keep def iou(a, b): xx1, yy1 max(a[0], b[0]), max(a[1], b[1]) xx2, yy2 min(a[2], b[2]), min(a[3], b[3]) w, h max(0, xx2 - xx1), max(0, yy2 - yy1) inter w * h area_a (a[2]-a[0]) * (a[3]-a[1]) area_b (b[2]-b[0]) * (b[3]-b[1]) return inter / (area_a area_b - inter 1e-6)逻辑说明conf_thres是置信度阈值烟火检测里建议 0.35 到 0.5太低误报多太高漏报。iou_thres控制重叠框合并0.45 是通用值。关键点是按类别分别做 NMS——烟和火经常同时出现且框重叠如果混在一起做 NMS火框可能把烟框压掉导致只报火不报烟。这个细节很多现成代码没处理是血泪经验。3.2 告警叠框的样式与保存逻辑告警图片要让人一眼看懂框的颜色、标签、置信度都得有。我一般火用红色框烟用橙色框标签写「fire 0.87」这种。叠框之后按时间戳命名保存同时可以留一份原图方便事后核对。保存路径要按日期分目录不然跑几天目录里几万张图找都找不到。import os import cv2 from datetime import datetime COLORS {0: (0, 140, 255), 1: (0, 0, 255)} # 0烟橙 1火红 LABELS {0: smoke, 1: fire} def draw_and_save(frame, detections, out_diralarm): os.makedirs(out_dir, exist_okTrue) vis frame.copy() for x1, y1, x2, y2, score, cls in detections: color COLORS.get(cls, (0, 255, 0)) cv2.rectangle(vis, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) text f{LABELS.get(cls, cls)} {score:.2f} cv2.putText(vis, text, (int(x1), int(y1) - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) if detections: ts datetime.now().strftime(%Y%m%d_%H%M%S_%f) path os.path.join(out_dir, f{ts}.jpg) cv2.imwrite(path, vis) return path return None逻辑说明COLORS和LABELS把类别映射成颜色和文字改样式只改这两个字典。draw_and_save只在有检测结果时保存避免存一堆空图。参数上cv2.rectangle的线宽 2 在 1080p 上够清晰4K 建议加到 3 到 4。putText的 y 坐标减 6 是为了让文字不压框线。文件名带微秒防止同一秒多张告警互相覆盖。3.3 把三种输入串成一条完整流水线前面几块拼起来就是主循环打开帧源按间隔抽帧推理后处理叠框保存。RTSP 流要加断线重连mp4 读完自然退出图片处理一次就结束。主循环里还要控制日志别每帧都打印不然日志文件比告警图还大。def run(src, is_streamFalse, interval0.5): source FrameSource(src, is_streamis_stream) sampler FrameSampler(interval) while True: ok, frame source.read() if not ok: if is_stream: # 实时流断了等两秒重连 time.sleep(2) source FrameSource(src, is_streamTrue) continue break if not sampler.should_process(): continue dets infer(frame) # 你的推理函数 dets postprocess(dets) draw_and_save(frame, dets) source.release()逻辑说明run是整条流水线的入口。is_streamTrue时读帧失败会重连这是 RTSP 场景必须有的网络抖动、摄像头重启都会导致断流。interval控制抽帧节奏。infer是占位替换成实际模型调用即可。注意重连时重新构造FrameSource而不是复用旧的cap旧对象在断流后往往已经不可用。4. 烟火识别落地避坑五条踩出来的排查记录4.1 现象RTSP 能连上但画面花屏、检测框乱跳原因默认走 UDP 传输网络稍有丢包就花屏解码出来的帧本身是坏的检测自然乱。解决强制 RTSP over TCP。OpenCV 里通过设置OPENCV_FFMPEG_CAPTURE_OPTIONS为rtsp_transport;tcp实现不同版本写法有差异设完用ffprobe确认能稳定读流再进代码。这个坑我踩过不止一次花屏问题九成出在传输层。4.2 现象mp4 文件用 OpenCV 打不开返回空帧原因文件是 HEVCH.265编码OpenCV 默认后端不带对应解码器。解决先用ffprobe看codec_name如果是 hevc要么换用带 HEVC 支持的构建要么先用 ffmpeg 转成 h264 再处理。转码命令ffmpeg -i in.mp4 -c:v libx264 -c:a copy out.mp4离线分析场景完全可接受。4.3 现象实时流跑几分钟后内存持续上涨原因VideoCapture的缓冲区在堆积或者每帧都创建了新的大对象没释放。解决设CAP_PROP_BUFFERSIZE为 1抽帧后及时让旧帧被回收叠框用frame.copy()而不是在原图上反复画。另外日志别缓存用行缓冲或定期 flush。4.4 现象烟和火同时出现时只报了一个原因NMS 没按类别分开做火的高分框把烟的框抑制掉了。解决按 class_id 分组分别做 NMS就是 3.1 里那段代码的做法。这个坑很隐蔽因为单类测试时完全正常只有两类同时出现才暴露。4.5 现象告警图片保存了几万张磁盘爆了原因没有去重和限流同一处烟持续十几分钟每 0.5 秒存一张。解决加告警冷却时间同一区域同一类别在 N 秒内只存一张或者按小时聚合只保留每个事件的首帧和末帧。冷却时间我一般设 30 到 60 秒既不错过事件又不至于刷爆磁盘。5. 进阶用告警冷却与区域屏蔽把误报压下来跑通流水线只是第一步真正决定这套烟火识别工具能不能上线的是误报率。我踩过最大的坑是傍晚的晚霞、车灯、蒸汽管道全被当成烟火一天几百条告警值班员直接不看了。后来我加了两样东西误报降了一个数量级。第一是告警冷却。同一类别、同一大致区域在冷却窗口内只报一次。实现上给每个类别维护一个最近告警时间戳检测到目标时先判断是否在冷却期内。区域判断可以用框中心点落在哪个网格来粗略划分不必做复杂的跟踪。class AlarmCooldown: 按类别网格做告警冷却压重复告警 def __init__(self, cooldown_sec60, grid4): self.cooldown cooldown_sec self.grid grid self.last {} # key: (cls, gx, gy) - ts def allow(self, cls, cx, cy, w, h): gx, gy int(cx / w * self.grid), int(cy / h * self.grid) key (cls, gx, gy) now time.time() if now - self.last.get(key, 0) self.cooldown: self.last[key] now return True return False逻辑说明cooldown_sec是冷却窗口60 秒适合大多数场景grid把画面切成 4×4同一格内同类目标才算重复。allow返回 True 才保存告警图。参数上网格太细会导致同一处烟因为框轻微移动就被当成新事件太粗又会把不同位置的告警合并4×4 到 6×6 是比较稳的范围。第二是区域屏蔽。摄像头画面里总有固定干扰源比如某个反光的窗户、某段蒸汽管。做法是维护一个屏蔽区域列表检测框中心落在屏蔽区内就直接丢弃。屏蔽区可以硬编码也可以做成配置文件现场调试时改配置不用改代码。参数建议值作用调大后果调小后果conf_thres0.35~0.5置信度阈值漏报增多误报增多iou_thres0.45NMS 重叠阈值框合并过度重复框增多interval_sec0.3~0.5抽帧间隔响应变慢算力吃紧cooldown_sec30~60告警冷却漏掉连续事件告警刷屏grid4~6冷却网格数重复告警事件被合并验证方法很直接拿一段已知有烟火的 mp4 离线跑统计告警数量和实际事件数算误报率和漏报率再拿一段纯干扰视频晚霞、车灯跑看会不会误报。两个指标都达标再上实时流。我现在的习惯是任何烟火识别方案上线前先用这两段视频各跑一遍参数表照着上面调基本能避开大部分翻车。希望帮到你。本文还有配套的精品资源点击获取