首页 / 资讯中心 / 文章详情

烟火识别落地实战:RTSP流接入、误报抑制与阈值回归

烟火识别落地实战:RTSP流接入、误报抑制与阈值回归 ★ FEATURED ARTICLE
简介LNTON羚通烟火识别算法与烟雾检测工具包面向安防监控、园区消防、森林防火等场景的算法学习者和工程开发者提供图片、RTSP实时流及mp4视频中的烟火检测与烟雾识别能力并可将告警目标以叠框形式输出便于快速验证与集成。压缩包共874个文件以843张jpg样本图片为主另含dll运行库、exe工具程序、bat批处理脚本、mp4演示视频、weights模型权重及pdf使用文档等整体约324.75MB工具与素材配套较完整。已有243人学习下载适合作为烟火识别入门实践与效果测试的参考。包内附使用文档和可执行工具样本图片量充足便于开展模型效果比对、阈值调整、告警叠框检查和不同输入源的联调测试也能为后续二次开发与场景适配提供可复用的脚本、依赖与排错思路。1. 烟火识别工具落地前先想清楚误报率这件事某园区值班室里有块大屏接的是仓库顶上的球机。这套烟火识别与烟雾识别工具第一次跑通值班员看到的是满屏红框——冷却塔冒的白汽、叉车大灯扫过地面的反光、远处路灯的灯罩全被框成了烟雾和火焰。反过来真出问题时它又未必看得见因为画面里烟还细。所以先给个反直觉的结论烟火算法真正难的地方是两件事把误报压到人能接受的量级以及把图片、RTSP 实时流、mp4 文件这三路输入稳定地喂进模型。这套工具做的正是这件事输入一个源输出带检测框和时间戳的告警图。适合做安防二次开发、园区巡检、算法快速验证的从业者也适合想拿真实视频跑通一遍检测链路的新手。下面按接入、算法、告警、踩坑、调参的顺序拆。2. 三路输入统一接入图片、RTSP 实时流与 mp4 的取帧链路三种输入的区别不在「能不能读」而在「读的节奏不一样」。图片是一次性解码读完就结束mp4 是按时间轴匀速推进读快了没意义RTSP 是网络流快慢由对端编码器和链路决定还会断。如果三路各写一套 for 循环后面加跳帧、加告警、加重连就要改三遍改到最后自己都记不清哪份是最新的。我一般会先做一层帧迭代器把三种源归一成同一个read()语义上层检测逻辑完全不感知输入类型。2.1 把三种输入归一成同一个帧源判断顺序很关键先看是不是纯数字摄像头序号再看是不是 rtsp/rtsps 前缀最后才落到本地文件。顺序反了会把0当成文件路径去isfile白白多一次 IO。图片走单独的分支因为 OpenCV 的imread在 Windows 下碰到中文路径会安静地返回None不报错等到后面frame.shape才炸排查起来很费时间。import os import re import numpy as np import cv2 RTSP_RE re.compile(r^rtsps?://, re.I) IMG_EXT (.jpg, .jpeg, .png, .bmp, .webp) class ImageCapture: 把单张图片包装成和 VideoCapture 一致的 read() 接口只吐一帧 def __init__(self, path): self.path path self._done False self._frame self._imread_unicode(path) staticmethod def _imread_unicode(path): # cv2.imread 遇到中文/空格路径会返回 None用二进制读 imdecode 绕过 data np.fromfile(path, dtypenp.uint8) if data.size 0: return None return cv2.imdecode(data, cv2.IMREAD_COLOR) def read(self): if self._done or self._frame is None: return False, None self._done True return True, self._frame def release(self): self._frame None def build_capture(source, open_timeout_ms5000): if isinstance(source, str) and source.isdigit(): source int(source) # 0 - 0本机摄像头 if isinstance(source, str) and RTSP_RE.match(source): # 必须在创建 VideoCapture 之前设置晚了不生效 os.environ[OPENCV_FFMPEG_CAPTURE_OPTIONS] ( rtsp_transport;tcp|stimeout;%d % (open_timeout_ms * 1000) ) cap cv2.VideoCapture(source, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 只保留 1 帧防缓冲堆积 return cap if isinstance(source, str) and os.path.isfile(source): if os.path.splitext(source)[1].lower() in IMG_EXT: return ImageCapture(source) return cv2.VideoCapture(source) # mp4 / avi 等容器 raise ValueError(无法识别的输入源: %r % (source,))这段代码有三个地方值得单独说。一是OPENCV_FFMPEG_CAPTURE_OPTIONS里的stimeout单位是微秒5000毫秒要写成5000000写成5000等于没设超时断流时会一直堵在那里。二是CAP_PROP_BUFFERSIZE只在部分后端有效设了不一定生效但不设一定更糟属于成本极低的保险。三是ImageCapture故意只返回一帧这样上层的while True: ok, frame cap.read()不用为图片单独写分支一段代码通吃。2.2 RTSP 拉流的硬参数与断流重启实时流是整个工具里最容易翻车的部分。它不像 mp4 有明确的 EOF网络抖一下、相机重启一次read()就返回False如果外层没有重试逻辑进程会静默退出或者挂在那里不动。更隐蔽的是「延迟滚动」拉流线程的解码速度快于下游消费速度时缓冲越堆越多画面时间戳离真实时间越来越远最后看起来是「卡住」其实是排着队。参数设置位置建议值说明rtsp_transport环境变量tcpUDP 丢包会导致花屏和马赛克检测框乱跳stimeout环境变量50000005s单位微秒控制建连和读超时CAP_PROP_BUFFERSIZEVideoCapture1减小解码缓冲抑制延迟累积fail_threshold业务层2030 次连续失败到这个数才判定断流retry_base业务层3.0 秒指数退避避免对端没起来时疯狂重连import time def frames_with_retry(source, max_retry10, retry_base3.0, fail_threshold30): 带重连的帧生成器断流后指数退避重建不把对端打崩 retry 0 while retry max_retry: cap build_capture(source) if not cap.isOpened(): retry 1 time.sleep(min(retry_base * retry, 30)) continue fails 0 try: while True: ok, frame cap.read() if not ok or frame is None: fails 1 if fails fail_threshold: # 偶发丢帧不算断流连续失败才算 break time.sleep(0.05) continue fails 0 retry 0 yield frame finally: cap.release() # 不 release 会泄漏句柄一天下来句柄数爆掉 retry 1 time.sleep(min(retry_base * retry, 30))fail_threshold这个阈值不能设太小。现场有相机做过码率自适应一次关键帧丢了后面连着三四十帧读不出来是常事阈值设成 3你会看到进程一天重连几百次日志刷得比画面还热闹。反过来设成 300断流后要等半分钟才重连告警就漏了。20 到 30 是踩过几次之后觉得比较稳的区间。retry在成功读到一帧后必须清零否则偶发断流累积十次就把重试额度耗尽了。2.3 跳帧策略算力该花在哪一帧上25fps 的流全量跑检测一天 216 万帧GPU 和电费都扛不住而且没意义——烟雾从出现到扩散开是秒级的帧与帧之间的信息冗余极大。按时间间隔抽帧到 12fps 就足够抓到事件。但抽帧的实现方式有讲究用read()循环计数跳帧跳过的帧照样做了 BGR 转换CPU 白烧用grab()跳过、retrieve()取值跳过的帧不解码成 Mat能省下可观的 CPU。def sample_frames(cap, interval_sec1.0, default_fps25.0): 按秒抽帧grab 只推进解码retrieve 才真正取图像 fps cap.get(cv2.CAP_PROP_FPS) if not fps or fps ! fps or fps 1e-3: # 部分流返回 0 或 nan fps default_fps step max(1, int(round(fps * interval_sec))) idx 0 while True: ok cap.grab() if not ok: break if idx % step 0: ok, frame cap.retrieve() if ok and frame is not None: yield idx, frame idx 1fps那一行fps ! fps是判断nan的写法某些网络流的元数据不规范CAP_PROP_FPS会返回nan直接拿去算step会抛异常。interval_sec的取值和场景相关仓库、车间这类开阔场所 1.0 秒够用电池仓、充电区这类起烟快的地方建议 0.5 秒。抽得再密意义不大因为误报也跟着涨。3. 从像素判火到目标检测烟火识别算法的选型与叠框输出图片、流、文件三路接进来之后核心问题变成「这一帧里有没有火和烟」。这条路线上有几种完全不同的做法选错了后面怎么调参都别扭。我见过不少人一上来就用 RGB 阈值判火焰像素白天在实验室里效果拔群装到现场第一天就被夕阳和高光反射打回原形。3.1 三条算法路线的取舍火焰的视觉特征相对明确高亮、暖色、跳动。烟雾则麻烦得多——半透明、无固定边缘、形状随气流变化而且和雾、蒸汽、粉尘、扬尘在像素层面几乎同分布。这就是为什么「烟雾分析」比「火焰识别」难也是为什么单纯的颜色规则在烟雾上基本无效。路线典型做法优点主要问题适用场景颜色 运动阈值RGB/HSI 空间取火焰色域配帧差法判跳动无需训练、CPU 就能跑夕阳、车灯、焊接弧光误报极高室内、光照恒定的封闭空间背景建模 纹理高斯混合建模找运动区再算纹理和梯度能抓到缓慢扩散的烟相机一抖就全废树影、雨雪干扰大固定机位、背景干净的室内目标检测模型单阶段检测器火焰/烟雾两类一起出抗干扰强泛化好需要标注数据烟雾框抖动明显大多数现场场景检测 时序分类检测框裁图后送时序模型判真假误报压得最低链路长、延迟高、部署复杂误报代价高的场合这套工具走的是第三条路线的工程化封装同时对图片、流、文件三种输入统一输出叠框告警图。实际用下来单帧检测的漏报主要来自小目标——几十米外的初期火点可能就十几个像素输入尺寸给到 640 基本就没了。如果监控距离远要么裁 ROI 单独送检要么把输入尺寸提到 960 以上代价是帧率下降得靠跳帧策略找平衡。3.2 预处理与后处理参数conf、IoU、输入尺寸预处理最容易被忽略的是缩放方式。直接resize到 640×640 会把宽高比拉变形细长的烟柱被压扁特征就变了。正确做法是等比缩放加灰边填充也就是常说的 letterbox同时把缩放比例和偏移记下来后处理时好还原坐标。def letterbox(img, new_size640, color(114, 114, 114)): 等比缩放 灰边填充返回图像、缩放比、左上偏移用于坐标还原 h, w img.shape[:2] r min(new_size / h, new_size / w) nh, nw int(round(h * r)), int(round(w * r)) resized cv2.resize(img, (nw, nh), interpolationcv2.INTER_LINEAR) canvas np.full((new_size, new_size, 3), color, dtypenp.uint8) top, left (new_size - nh) // 2, (new_size - nw) // 2 canvas[top:top nh, left:left nw] resized return canvas, r, (left, top) def restore_box(box, r, pad, orig_shape): 把模型输出的 xyxy 还原回原图坐标并裁掉越界部分 left, top pad h, w orig_shape[:2] x1 max(0, min(w - 1, (box[0] - left) / r)) y1 max(0, min(h - 1, (box[1] - top) / r)) x2 max(0, min(w - 1, (box[2] - left) / r)) y2 max(0, min(h - 1, (box[3] - top) / r)) return [x1, y1, x2, y2]后处理这块两个参数的取舍决定误报和漏报的平衡点。置信度阈值conf调低小烟小火的召回上去了白汽、反光也跟着进来调高误报少了但薄烟和远景火点漏掉。IoU 阈值控制 NMS 的合并力度烟雾框本来就在抖动IoU 设得太低比如 0.3会把同一团烟拆成两三个框告警图看起来像有多处火情设到 0.5 以上相邻的两个真实目标又可能被合掉。参数建议区间调高的后果调低的后果conf_thres0.300.45漏报增加薄烟最先丢误报激增白汽反光全进iou_thresNMS0.450.55同团烟多框告警图误判为多点相邻火点被合并数不准输入尺寸640常规/960远距帧率下降显存吃紧小目标丢失最小框面积原图 0.02%0.1%远处小火点漏掉噪点被当目标最小框面积这个过滤条件我是强烈建议加的模型偶尔会在纹理复杂的地面吐出一个几像素的框置信度还不低肉眼一看就是噪声。按原图面积比例过一道能干掉相当一部分无意义的框。3.3 告警图的叠框、标注与落盘告警图是这个工具最直接的交付物值班员不看日志只看图所以画得好不好直接影响可用性。有两个坑反复出现一是cv2.putText不支持中文直接传中文标签会显示成一串问号二是所有框都画在缩放后的图上再放大保存结果是框线糊、字看不清。import os import time def draw_alerts(frame, dets, warn_textFIRE/SMOKE): 在原分辨率上叠框线宽和字号按短边自适应避免小图看不见、大图太细 vis frame.copy() h, w vis.shape[:2] thickness max(2, int(round(min(h, w) / 400))) font_scale max(0.6, min(h, w) / 1200.0) for d in dets: x1, y1, x2, y2 [int(v) for v in d[box]] # 火焰用红、烟雾用橙BGR 顺序别写反 color (0, 0, 255) if d[cls_name] fire else (0, 165, 255) cv2.rectangle(vis, (x1, y1), (x2, y2), color, thickness) label %s %.2f % (d[cls_name], d[score]) tw, th cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness)[0] cv2.rectangle(vis, (x1, y1 - th - 8), (x1 tw 6, y1), color, -1) cv2.putText(vis, label, (x1 3, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), thickness) stamp time.strftime(%Y-%m-%d %H:%M:%S) cv2.putText(vis, stamp, (10, h - 12), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 255, 0), thickness) return vis def save_image_unicode(path, img, quality90): 中文路径安全落盘imencode 到内存缓冲再二进制写文件 os.makedirs(os.path.dirname(path), exist_okTrue) ok, buf cv2.imencode(.jpg, img, [int(cv2.IMWRITE_JPEG_QUALITY), quality]) if not ok: raise IOError(编码失败: %s % path) buf.tofile(path)thickness和font_scale按短边自适应这一手是从血泪经验里来的1080p 的图上用 2 像素线宽缩略图预览时框几乎看不见反过来在 720p 上用 4 像素框粗得像马赛克。时间戳一定要烧进图里因为文件名可能被人工改名、被下载工具重命名图本身的时间戳是最后一道追溯依据。如果确实需要中文标签常见做法是用 PIL 加载一个中文字体画完再转回 ndarray代价是每帧多一次转换只建议在最终落盘那一次转不要每帧都转。4. 告警抑制连续帧确认、冷却窗口与告警图归档检测器单帧输出的结果是不能直接拿去告警的。烟雾框在相邻帧之间会来回跳一团烟有时被框成三块、有时又消失一帧直接按帧触发的结果是一小时刷出几百张图值班员看两张之后就不看了。告警抑制不是可选项是让这个工具从「能跑」变成「能用」的分界线。4.1 连续帧确认用滑动窗口压掉单帧误报「连续 N 帧都命中才告警」这个思路是对的但实现上不建议用严格的连续计数。严格连续的问题是只要中间夹一帧漏检计数就清零而烟雾漏检在实拍里非常常见。我一般用固定长度的滑动窗口窗口内命中次数超过阈值就认为成立。from collections import deque class ConfirmGate: 滑动窗口确认窗口内命中 hit 次才放行比严格连续更抗单帧抖动 def __init__(self, window8, hit3): if hit window: raise ValueError(hit 不能大于 window) self.window window self.hit hit self.buf deque(maxlenwindow) # maxlen 固定防止无限增长吃内存 def update(self, present, cls_namesmoke): self.buf.append(1 if present else 0) if len(self.buf) self.window: return False return sum(self.buf) self.hit def reset(self): self.buf.clear()参数要结合抽帧间隔来理解。按 1fps 抽帧、window8就是 8 秒的观察窗hit3表示 8 秒里有 3 帧看到烟才确认。如果是 0.5fps同样的数字对应 16 秒反应就太慢了。火焰建议比烟雾激进得多window4, hit2因为火灾的确认成本远低于误报成本烟雾则可以放宽到window10, hit4尤其在户外场景白汽和薄雾的持续时间往往比真烟更长窗口拉长反而有助于区分。同一路视频里如果有多个目标类别确认门要按类别分开建不能共用一个 buffer否则火焰命中的帧会「帮」烟雾攒票两个都变灵敏。4.2 冷却窗口别让一个火点刷出两百张图确认通过之后还有一层冷却。同一个持续燃烧的火点在确认放行之后如果每帧都触发一晚上能写出几千张几乎一样的图磁盘压力和后续人工筛选成本都很高。冷却逻辑就是同类别在一个时间窗内只允许输出一次。import time class Cooldown: 按类别分别冷却冷却期内只计数不落图 def __init__(self, seconds_mapNone): self.seconds_map seconds_map or {fire: 20, smoke: 90} self.last_ts {} self.suppressed {} def allow(self, cls_name, nowNone): now now or time.time() gap self.seconds_map.get(cls_name, 60) last self.last_ts.get(cls_name, 0) if now - last gap: self.last_ts[cls_name] now self.suppressed[cls_name] 0 return True self.suppressed[cls_name] self.suppressed.get(cls_name, 0) 1 return False冷却时长的设置要看告警的处置周期。火焰设 20 秒左右逻辑是「已经报过火警了值班员正在处置20 秒内再报一次没意义但也不能太久否则火势扩大没人知道」。烟雾设 90 秒因为它本来就慢10 秒内报三次完全是噪声。suppressed计数器别丢掉它记录的是被压掉的次数事后复盘时能看出某个点位是不是长期在误报边缘反复触发——这种点位比偶发误报更值得处理。冷却只用时间戳不用帧号。因为重连之后帧号会归零用帧号判断冷却会导致重连后立刻重复告警。4.3 告警图的命名、分桶与回查告警图是证据命名和目录结构决定了半年后还能不能找回来看。我见过把所有图平铺在一个目录里的做法跑到第三天目录里两万多张图Windows 资源管理器打开就卡死。推荐按「点位 / 日期 / 类别」三层分桶文件名里带时间戳和序号import os import time def make_alert_path(root, cam_id, cls_name, tsNone, seq0): 按 点位/日期/类别 分桶文件名含到毫秒的时间戳避免同秒覆盖 ts ts or time.time() t time.localtime(ts) day time.strftime(%Y%m%d, t) stamp time.strftime(%Y%m%d_%H%M%S, t) _%03d % int((ts % 1) * 1000) safe_cam .join(c for c in str(cam_id) if c.isalnum() or c in -_) or cam fname %s_%s_%04d.jpg % (safe_cam, stamp, seq) return os.path.join(root, safe_cam, day, cls_name, fname)safe_cam那行过滤不是多余的。点位 ID 如果是从配置文件里读的偶尔会带斜杠或冒号拼进路径要么报错要么写到意料之外的目录。毫秒后缀是为了防同秒覆盖一路流在 1fps 采样下一个类别同一秒内最多出一张但多路视频聚合到一个目录时就会撞。配套建议写一个轻量索引哪怕只是一个追加写的 JSONL每行记录路径、点位、类别、置信度、框坐标、触发时间和被压掉的次数。真出事后回查时按点位和时间段过滤 JSONL 比翻目录快一个数量级。5. 避坑与排查拉流卡死、误报泛滥、内存爬升、输出异常这一章基本都是踩出来的每条按现象、原因、解决三段写。有些问题第一次遇到会以为是模型不行其实跟模型一点关系没有。5.1 拉流侧延迟越滚越大与断流不退出现象工具跑十几分钟后画面里的时间戳比实际时间落后二十多秒再往后干脆不动了但进程还活着日志也不报错。原因这是典型的解码缓冲堆积。上游相机按 25fps 稳定推流下游检测只消费 1fps中间的解码缓冲越积越多read()拿到的永远是队列里最老的那一帧。表现出来是「延迟」实际是「排队」。解决三件事一起做。第一cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)让缓冲尽量只留一帧第二抽帧用grab()/retrieve()跳过的帧不做 BGR 转换消费速度跟得上第三如果下游有多线程处理给帧队列设maxsize队列满时丢最老的帧而不是阻塞。现象相机断电或交换机重启之后进程既不退出也不重连一直挂着。原因read()在某些后端下会阻塞很长一段时间或者返回False之后外层没有重试分支循环直接跳出但主线程还在等。解决设stimeout让阻塞有上限外层套重连循环并做指数退避日志里打上重连次数和累计失败帧数方便判断是偶发还是链路彻底断了。5.2 误报侧白汽、车灯、夕阳与反光现象冷却塔、蒸汽管道、冬季呼吸口附近烟雾告警一天几十条截图点开看全是白汽。原因白汽和烟在像素层面高度相似都是低对比度、边缘模糊的灰白色团块。检测器学到的是「模糊的浅色区域」两者在特征空间里挨得很近。解决三个方向。一是补难负样本重训把现场的白汽截图加进负样本集这是最有效的二是给这些固定点位配 ROI 掩膜水塔、排气口区域直接屏蔽别让模型看三是提高烟雾的窗口阈值window10, hit5白汽往往一阵一阵的真烟则是持续扩散窗口拉长对两者区分有帮助。现象夜间车灯扫过地面、工地电焊弧光、夕阳直射镜头被判成火焰。原因火焰的高亮和暖色特征和这些光源重叠。尤其是焊接弧光颜色分布几乎和火焰一致。解决加框内亮度上限过滤像素几乎饱和到 255 的框直接丢掉对已知有焊接作业的时段或区域临时降灵敏度或者干脆停用火焰通道。还有一个取巧的办法是加运动一致性判断真火焰的轮廓是持续抖动的车灯扫过是一次性的用相邻帧的框 IoU 序列能筛掉一部分。5.3 资源侧内存与显存缓慢爬升现象连续跑 8 小时后 RSS 从 400MB 涨到 3GB最后被系统 OOM 掉。原因重连时没有release()句柄和解码上下文泄漏告警图列表忘了设maxlen日志里把整帧图像对象挂在了记录上。这三个是最常见的。解决重连路径用try/finally保证release()所有deque显式设maxlen日志只打路径和元数据不打图像对象。上线前建议挂个简单的内存采样每 10 分钟记一次 RSS曲线抬头就能定位到是哪次改动引入的。现象GPU 显存随着运行时间线性上涨最后报 OOM。原因推理没包在torch.no_grad()里计算图被保留或者每次推理都新建张量而没有复用缓冲区。解决推理统一加with torch.no_grad():把预处理结果写进预分配的数组如果确实需要长期稳定加一个定时重启策略比如每 24 小时重启进程虽然不优雅但在现场是最省事的兜底。5.4 输出侧中文路径、编码器不可用与糊图现象告警图有时候保存成功、有时候静默失败日志里没有任何异常。原因cv2.imwrite遇到中文路径返回False而不抛异常如果代码没检查返回值就变成「静默失败」。解决统一走imencodetofile并检查imencode的返回值失败就抛出来。读图侧同理np.fromfileimdecode。现象导出带框视频时报Could not open codec或者文件生成出来是 0 字节。原因容器和编码器不匹配。mp4v在多数 OpenCV 发行版里可用avc1/H264依赖系统里的编码库不一定装了。解决先试mp4v失败再降级到XVID.avi同时确认写入尺寸和帧的实际尺寸完全一致宽高必须是偶数奇数尺寸会导致编码器直接拒绝。现象告警图点开看框的位置偏了十几像素或者框线糊成一团。原因框是在 letterbox 之后的图上画的或者坐标还原时忘了减 padding、忘了除缩放比。解决所有绘制都在原分辨率图上做缩放比和偏移在还原时一次性算清调试阶段可以先把还原后的框画出来和原图对比偏差肉眼可见。6. 用一段真实视频做阈值回归把参数钉死置信度阈值这个东西靠肉眼看几张图调出来的结果基本不可靠。同一段视频conf 从 0.30 调到 0.35可能只是某团白汽消失了也可能顺带把一处真烟也丢了肉眼盯十帧是看不出来的。我后来固定下来的做法是做一个最小回归集每次改参数都跑一遍用数字说话。回归集不需要大但要分层。正样本我一般选 1520 段覆盖白天明火、夜间明火、初期薄烟、浓烟四类每段 30 秒到 1 分钟难负样本选 2530 段专挑现场最容易误报的蒸汽排放、雾天清晨、车灯扫射、焊接作业、水面反光、夕阳直射。每段标注只需要一句话这段里有没有真火真烟、大概在第几秒出现不用逐帧框标。然后写一个批量跑格点的脚本固定模型和输入尺寸只扫 confimport json import subprocess def run_grid(video_list, conf_list, out_jsongrid_result.json): 对每个 conf 跑一遍回归集统计命中段数和误报段数 result [] for conf in conf_list: hit, miss, false_alarm, clean 0, 0, 0, 0 for item in video_list: cmd [ python, detect_cli.py, --source, item[path], --conf, str(conf), --save-dir, ./_tmp_grid, --json-out, ./_tmp_grid/events.json, ] subprocess.run(cmd, checkTrue, capture_outputTrue) with open(./_tmp_grid/events.json, r, encodingutf-8) as f: events json.load(f) fired len(events) 0 if item[has_target]: hit 1 if fired else 0 miss 0 if fired else 1 else: false_alarm 1 if fired else 0 clean 0 if fired else 1 result.append({ conf: conf, hit: hit, miss: miss, false_alarm: false_alarm, clean: clean, }) with open(out_json, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) return result if __name__ __main__: with open(regression_set.json, r, encodingutf-8) as f: videos json.load(f) rows run_grid(videos, [0.25, 0.30, 0.35, 0.40, 0.45, 0.50]) for r in rows: print(r)脚本本身没什么技巧价值在于把「感觉」换成一张表。跑完通常长这样conf正样本命中正样本漏报难负样本误报干净通过0.2519/20114/28140.3019/2019/28190.3518/2024/28240.4016/2041/28270.4513/2070/28280.5011/2090/2828看完这张表选点就很清楚了0.35 是命中率还守得住、误报已经明显下降的拐点再往上调到 0.40误报几乎清零但正样本掉了 4 段多掉的那 2 段里如果有初期薄烟这个代价在现场是不能接受的。最后通常落在 0.320.38 之间配合前面说的连续帧确认能在保住召回的同时把误报再压一档。参数选定之后一定要做快照别让它散落在代码里。我一般写一个config.yaml把 conf、iou、输入尺寸、抽帧间隔、确认窗口、冷却时长、ROI 掩膜路径全部列进去随模型文件和回归结果一起版本化# config.yaml —— 一次调参一次快照改之前先复制一份带日期后缀 model: path: models/fire_smoke.onnx input_size: 640 detect: conf: 0.35 iou: 0.50 min_box_ratio: 0.0005 classes: [fire, smoke] sample: interval_sec: 1.0 confirm: fire: {window: 4, hit: 2} smoke: {window: 10, hit: 4} cooldown_sec: fire: 20 smoke: 90 roi: mask_dir: masks/ enable: true这套流程跑熟之后改任何参数都有据可依先改 yaml再跑格点对照表看是赢了还是输了输了就回滚。之前吃过一次亏为了压白汽把 conf 从 0.34 直接提到 0.55现场确实不报了两周后一处纸箱阴燃从头到尾没触发事后翻录像才看见。从那以后我每次动阈值都强制先把回归集跑一遍哪怕只改 0.01也把结果表附在配置文件的注释里。希望这套流程帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站