楼宇AI烟雾火焰检测落地:三段式管线与误报抑制 楼宇安防这块最怕的不是设备不够多而是报警响了但没人知道是不是真的。我做过几个楼宇视频分析的项目踩过的坑基本都集中在同一件事上高精度AI火灾烟雾检测算法在实验室里跑出漂亮的mAP一上线就被蒸汽、粉尘、反光地面搞到运维人员直接关掉告警推送。楼宇环境的特点是空间大、光源杂、干扰源密集、误报成本高——一次误报可能意味着消防联动被触发、整栋楼疏散、物业被投诉。这篇文章就把我这些年在楼宇场景里落烟雾火焰检测的完整链路拆开讲从需求定义、方案选型、数据标注、训练调参到视频流工程、现场部署和误报排查。适合正在做智慧楼宇、园区安防、视频监控智能化的开发者和方案负责人看也适合刚接触目标检测、想搞清楚落地和demo差在哪的同学。1. 需求拆解楼宇场景到底需要一个什么样的检测算法1.1 传统感烟探测器和视频检测的分工边界先说清楚定位问题这是很多方案在汇报阶段就翻车的原因。点型感烟探测器依靠烟雾颗粒进入迷宫腔体触发价格便宜、可靠性经过了长期验证是消防报警系统的法定组成部分。但它的物理特性决定了两个先天短板一是等烟到高大空间比如中庭、展厅、物流仓库烟雾要上升到十几米高的顶棚才能被感知中间可能已经烧了几分钟二是看不见火感烟探测器对明火阶段的快速蔓延响应不如感温、感光探测器。视频检测的价值恰恰在这两个缝隙里。相机装在离地三五米的位置视野覆盖的是人眼高度到中高区的空间烟雾在起火初期几十秒内就会进入画面。我在一个层高十二米的展厅项目里做过对比测试模拟烟源点燃后画面在约18秒时就能看到淡淡的烟羽而顶棚感烟探测器在接近两分钟后才动作。这个时间差对于人员疏散和初期处置是实打实的价值。但边界必须说清楚视频检测是辅助手段和复核手段不替代法定的火灾自动报警系统。方案汇报时如果暗示可以少装烟感基本会被消防审查直接打回来。正确的表述是视频检测提供早期预警和现场可视化确认缩短从报警到确认火情的决策时间同时给消防控制室提供画面证据。这个定位定下来后面的指标设计才有依据。1.2 高精度在工程上的真实定义标题里的高精度是个营销词落到工程上必须翻译成可验收的数字。我通常会跟甲方一起把指标拆成四类缺一类就会在验收时扯皮指标类型具体定义楼宇场景参考值检出能力召回率 Recall有火/烟时至少命中一次的比例明火 ≥ 0.95烟雾 ≥ 0.85误报水平单路相机日均误报次数 FP/day/camera≤ 0.3重要区域 ≤ 0.1响应延迟从画面出现可见烟/火到系统输出告警的时间≤ 20 秒稳定性连续运行 30 天无进程崩溃、无内存泄漏可用率 ≥ 99.5%注意这里的召回率是事件级而不是帧级。一帧检出率90%听起来不错但如果每次烟雾只在画面里停留几秒、模型又恰好漏了那几帧事件级召回可能只有60%。反过来事件级召回只要在烟雾存续期间命中一次就算成功这就要求算法在时间维度上给足机会——这直接引出了后面要讲的抽帧策略和时序确认机制。误报指标是最容易被低估的。假设一栋楼有40路相机单路日均0.3次误报全楼一天就是12次告警。消防控制室的值班人员被无效告警轰炸一周就会形成看到告警先怀疑是误报的心理惯性这才是智能分析系统真正的死亡方式。所以我在项目里宁愿把召回率降两三个点也要把日均误报压到0.3以下。1.3 场景清单与干扰源盘点先列负样本再谈算法动手写代码之前我坚持做一件事拿着相机点位图把每个点位的干扰源列出来。这一步花两小时能省后面两周的调参时间。典型的干扰源清单大致是这样地下车库汽车尾气尤其冬天冷启动、轮胎扬尘、车灯直射镜头产生的光晕、地面反光。大堂与中庭大面积玻璃幕墙的日照变化、地面大理石镜面反射、装饰水景的水雾。食堂与后厨周边蒸锅蒸汽、油烟、明火烹饪这个要区分烹饪明火不是火灾但视觉上极像。走廊与电梯厅人员走动带起的光影变化、门开合造成的曝光跳变。施工/装修区域切割粉尘、焊接火花、临时照明。室外连廊与屋顶云、雾霾、雨丝、飞虫贴近镜头。我一般会把这些整理成一张点位-干扰源-处理策略的对照表比如食堂点位强制开启蒸汽抑制分支车库点位加大低对比度小目标的采样权重。这份表格后面会直接变成难负样本采集清单和现场调参依据是整条链路里性价比最高的一份文档。2. 方案选型为什么我最终都走向检测分类时序三段式2.1 只做单帧目标检测的天花板在哪里烟雾这个目标和行人、车辆有个本质区别它没有固定形状。烟雾是半透明的、边界弥散的、形态随时间连续变化的同一团烟在零点几秒内可以从一小撮扩散成半屏白雾。这就导致两个问题。第一是标注本身不可靠。两个人标同一张图的烟雾框IoU 能差到0.3以下因为哪里算烟、哪里算背景根本没有客观边界。模型学的是一个模糊的目标mAP 上不去是正常的——楼宇场景里烟雾检测 mAP0.5 能做到 0.75~0.85 已经算是相当能用的水平如果有人跟你说他的烟雾模型 mAP 0.95大概率是数据集太干净或者训练集测试集有泄漏。第二是单帧信息不足以区分相似物。蒸汽、粉尘、白色墙面在被强光照射时单帧纹理特征和稀薄烟雾高度相似。我试过只靠一个检测头硬扛结果就是召回和误报像跷跷板阈值调高漏报调低误报怎么都找不到平衡点。真正让误报降下来的是把这块区域像不像烟拆成形状像不像 运动方式像不像 持续时间够不够三个独立判断。2.2 三段式管线的设计逻辑我现在的标准架构是三段第一段轻量检测头负责找候选。用 YOLO 类单阶段检测器在每一帧上扫一遍把可能是烟或火的区域框出来置信度阈值压得比较低比如 0.25目标是宁可多框不要漏框。这一段追求的是召回和速度不追求精度。第二段分类头负责细分辨。把候选框裁出来送进一个轻量分类网络MobileNet 级别的就够做烟/火/蒸汽/粉尘/反光/正常背景的多分类。裁图分辨率可以给到 224 或 256比检测头看到的细节更多判别力明显更强。这一步是整个管线里降误报的主力。第三段时序模块负责确认与抑制。维护一个以空间网格 类别为 key 的状态机统计最近 N 秒内的命中情况。只有当同一区域在时间窗口内累计命中达到阈值才真正对外输出告警。这个设计的核心思路是把精度从单帧模型里搬出来交给多阶段协同。好处有三个检测头可以放心做轻量化边缘设备算力压力小分类头可以增量迭代改分类模型不用重训检测模型时序模块是纯逻辑代码调参不需要 GPU现场工程师就能改。坏处是链路变长、端到端延迟增加但楼宇场景对延迟的容忍度是秒级这点代价完全值得。# pipeline.yaml 三段式管线配置示例 detector: model: smoke_fire_det_v3.onnx input_size: 640 conf_thres: 0.25 # 低阈值保召回 iou_thres: 0.45 max_det: 30 classifier: model: smoke_cls_v2.onnx input_size: 224 classes: [smoke, fire, steam, dust, reflection, background] keep_thres: 0.60 # smoke/fire 概率低于此值直接丢弃候选 temporal: window_sec: 12 min_hits: 4 # 窗口内至少命中4次 grid_size: 16 # 画面切成16x16网格做区域聚合 cooldown_sec: 180 # 同区域告警后冷却时间上面这份配置是我在一个园区项目里实际用的min_hits: 4配合 12 秒窗口意味着模型需要在约 1/3 的采样帧上给出阳性判断稀薄烟雾这种时隐时现的情况可能会被压掉。所以我的做法是给高置信度单帧开一个旁路只要分类头给出的 smoke 概率超过 0.9即使只命中一次也直接告警不等待时序确认。这个旁路在早期预警上很关键。2.3 硬件选型与算力账怎么算边缘侧我一般分三档档位典型硬件可承载路数5fps 抽帧适用场景轻量带 NPU 的边缘小盒子算力 4~8 TOPS4~8 路单栋小楼、便利店、小型仓库主力入门级推理卡T4/A2 级别16G 显存16~32 路写字楼、园区、医院中心服务器级 GPU64 路以上大型综合体、多楼栋汇聚算力账要自己算一遍别信厂商的单卡百路。以主力档为例每路 1080p 视频按 5fps 抽帧20 路就是 100 帧/秒。YOLO 级别的轻量模型在 640 输入、FP16 精度下单帧推理约 6~8ms含前处理和后处理100 帧/秒需要 0.6~0.8 秒的算力也就是单卡 60%~80% 的利用率。这已经偏高了还要留给分类头、解码和突发流量。所以我一般按 5fps/路、单卡不超过 24 路来规划留 30% 余量。如果按 1fps 抽帧呢算力降到 1/5但早期发现能力会明显下降——烟雾扩散最快的阶段就在前几十秒1fps 意味着你可能错过最佳判断窗口。我做过对比1fps 下烟雾事件的检出延迟平均增加 8~15 秒这个损失对楼宇场景来说不太划算。折中方案是动态抽帧画面平静时 1fps一旦检测头给出低置信度候选立刻把该路提升到 8~10fps 持续 30 秒。这套机制能把平均算力压到接近 1fps 的水平同时保留事件期的细节。3. 数据与标注决定精度的七成工作量3.1 数据从哪来别指望公开数据集公开的烟雾火焰数据集普遍存在三个问题场景单一大多是野外、森林、工业堆场、画质太好没有监控视频的压缩噪声和低照度问题、正样本过于典型浓烟滚滚、明火熊熊。拿这种数据训出来的模型扔到写字楼大堂里遇到稀薄的白烟基本抓瞎。我的做法是三级数据来源现场真实数据占比 40%~50%从项目现场相机的历史录像里扒。别小看这一步物业那边通常存着 30~90 天的录像里面往往包含装修粉尘、蒸汽、真实的小范围冒烟事件是宝贵的难负样本来源。跟甲方签个数据使用协议脱敏后只用画面特征不涉及具体信息。可控实验数据占比 30%在空场地用发烟装置专业烟雾发生器、安全环保的烟饼在不同光照、不同距离、不同背景板条件下拍摄。这一批数据的价值在于标签干净、参数可控可以精确控制烟量、距离、背景用来校准模型的尺度不变性。合成数据占比 20%把烟雾素材以 alpha 混合方式贴到真实背景上配合透视变换、光照匹配、噪声注入。合成数据不能当正样本主力但可以有效扩充负样本多样性——把烟雾贴到厨房、车库、走廊背景上能大幅增加困难负样本的数量。有一点必须提醒合成数据和真实数据之间有明显域差异如果合成数据占比过高模型会学到贴图纹理这种伪特征实测表现反而下降。我的经验是合成数据不超过 25%而且必须保证验证集全部来自真实数据。3.2 烟雾标注的规范比工具重要标注烟雾最容易出问题的是框多大合适。框太紧只框住最浓的部分模型学不到边缘的稀薄区框太松把大片背景框进去模型会学到错误的上下文关联。我们内部定的规范是这样的可见即标人眼在正常观看距离下能辨认出的烟羽轮廓都要纳入框内包括边缘稀薄部分。判断标准是如果这是火这块区域需不需要疏散。一块一框不合并同一场景有两股独立烟源即使靠得近也分开标避免模型学到大框大烟的错误先验。被遮挡超过 60% 不标比如柱子后面的烟标了反而引入噪声。动态帧采样一段 10 秒的烟雾视频按每 5 帧取 1 帧标注保证时间上的连续性。不要只标最清晰的那几帧模糊帧、半遮挡帧才是真实场景的常态。火焰和烟雾分开标允许重叠明火阶段往往既有火焰又有烟雾两个类别独立标注不要用一个框同时标两类。另外强烈建议加一个smoke_thin/smoke_dense的子类别或者至少在标注属性里记录浓度等级。后期训练时可以做分级加权把稀薄烟雾的损失权重提上去——早期预警的价值全在薄烟阶段等浓烟滚滚了那已经是中后期了。3.3 难负样本采样误报的真正解药我见过太多团队把 80% 的精力放在加正样本上结果模型召回上去了误报也上去了。实际上误报是由负样本决定的不是正样本。难负样本的采集策略我总结成三找找相似纹理蒸汽、粉尘、白墙、雾霾、白云、反光地面。这类样本要占到负样本总量的 60% 以上。找相似运动飘动的窗帘、晃动的树叶如果相机对外、喷洒的水雾、汽车尾气。找相似光照逆光、镜头眩光、夜间红外补光下的过曝区域、曝光突变瞬间。采集量的经验值每个干扰类别至少 800~1500 张覆盖不同时段白天/夜晚/黄昏、不同天气、不同相机型号。车库尾气这类高频干扰要单独建子集做到 3000 张以上。还有一个技巧是负样本挖掘模型训练完第一版之后拿它去跑现场的长时间录像比如一周把所有置信度在 0.3~0.7 之间的误检帧截下来人工筛一遍把真的误报加进负样本集再训第二版。这一轮下来误报通常能降 40%~60%比调阈值有效得多。这个流程我在每个项目里都跑基本是标准动作。3.4 数据增强模拟监控视频的脏监控视频有几个天然缺陷训练时要主动模拟低码率压缩导致的块效应、镜头脏污、低照度下的噪点、广角畸变、雨天的画面模糊。import cv2 import numpy as np import random def monitor_style_augment(img, boxes): 模拟真实监控画质的增强boxes 同步做几何变换 h, w img.shape[:2] # 1. 随机 JPEG 压缩模拟低码率 if random.random() 0.5: q random.randint(35, 75) _, enc cv2.imencode(.jpg, img, [cv2.IMWRITE_JPEG_QUALITY, q]) img cv2.imdecode(enc, cv2.IMREAD_COLOR) # 2. 高斯噪声模拟低照度噪点 if random.random() 0.4: sigma random.uniform(3, 12) noise np.random.normal(0, sigma, img.shape).astype(np.float32) img np.clip(img.astype(np.float32) noise, 0, 255).astype(np.uint8) # 3. 光照扰动模拟日照变化和补光灯 if random.random() 0.5: factor random.uniform(0.65, 1.35) img np.clip(img.astype(np.float32) * factor, 0, 255).astype(np.uint8) # 4. 局部模糊模拟镜头脏污与失焦 if random.random() 0.25: k random.choice([3, 5]) img cv2.GaussianBlur(img, (k, k), 0) # 5. 对比度抖动 if random.random() 0.3: alpha random.uniform(0.8, 1.2) img np.clip(img.astype(np.float32) * alpha, 0, 255).astype(np.uint8) return img, boxes这段增强我放在训练管线的 Mosaic 之前执行效果比单纯调 HSV 好很多。注意别和几何变换混在一起写否则框坐标容易对不上调试起来很痛苦。另外压缩增强不要设得太狠质量低于 30否则会把稀薄烟雾直接压没了模型反而学不到东西。4. 模型训练与调参把召回和误报同时压下去的实操4.1 基线选择与训练配置检测头我一般从 YOLO 系列的 nano/small 尺寸起步理由很实在楼宇场景的目标尺度偏大烟雾在画面里往往占几百像素不需要极强的小目标能力边缘部署要求低延迟社区生态成熟改结构、导出 ONNX、量化都有现成路径。曾经试过用 DETR 系模型精度在某些场景确实好一点但推理速度和部署复杂度在边缘侧吃亏最后还是回到单阶段检测器。关键训练配置如下以 640 输入、单卡为例参数取值说明输入分辨率640×640楼宇场景可试 768烟雾细节更多但算力涨 44%batch size32可用梯度累积凑显存不够就 16 累积 2 步初始学习率0.01SGD/ 0.001AdamW预训练权重加载时降 10 倍训练轮数150~250烟雾数据集收敛慢别用默认的 100暖启动3 epoch避免一开始就把预训练特征打散多尺度训练0.5~1.5 倍随机缩放应对不同安装高度和焦距训练轮数这里要说一句烟雾这类边界模糊的目标损失下降比常规目标检测慢得多前 60 轮 mAP 可能一直在低位徘徊很多人这时候就以为模型不行了。我的经验是至少跑到 150 轮配合余弦退火学习率往往在 120 轮之后还有一波明显提升。4.2 正负样本不平衡与损失加权楼宇场景的真实分布极度不均衡一栋楼一天 24 小时99.99% 的帧是背景。即使你按 1:3 的比例采样正负帧模型仍然会偏向预测背景。处理手段有三层采样层训练时保证每个 batch 里至少有 25% 是含正样本的帧含稀薄烟雾的帧单独设权重保证它在 batch 中的占比不低于 8%。损失层检测头用 CIoU 分类 BCE对smoke_thin类别设 1.5 倍损失权重分类头用 Focal Lossγ 取 1.5~2.0。后处理层不同类别用不同置信度阈值火焰 0.45、浓烟 0.40、薄烟 0.30。薄烟阈值压低靠后面的时序模块兜住误报。这三层配合下来薄烟的召回能从 0.6 左右提到 0.8 以上而误报增量被时序模块吃掉了大部分。这个思路的本质是把误报的控制权从单帧阈值转移到时间维度是整篇文章里我最想强调的一点。4.3 阈值怎么标定别拍脑袋阈值标定必须有数据支撑。我的做法是准备一个标定集从现场录像里截取连续 7 天的片段包含至少 10 次真实烟/火事件和全部已知干扰源然后跑一遍推理画出 PR 曲线按业务需求选点。具体操作步骤把标定集按事件切分成片段每段标注是否真实事件和事件起止帧。跑推理记录每个候选框的类别、置信度、时间戳、位置。对每个候选阈值0.2 到 0.8步长 0.05计算事件级召回和日均误报次数。画一张召回-误报散点图找拐点。我在一个项目里的实测数据大概是这样的分类头阈值事件级召回日均误报单路0.450.932.10.550.900.90.600.870.350.700.790.08最终选的是 0.60配合时序确认后日均误报降到 0.12事件级召回保持在 0.86。这个取舍是跟甲方一起定的——他们更怕误报愿意接受一点点召回损失。所以阈值从来不是技术问题是业务问题一定要把选择权交给使用方并且把这张表摆到桌面上讲清楚。4.4 蒸馏与量化把模型塞进边缘设备训练完的模型往往偏大直接部署到边缘盒子跑不动。压缩走两步第一步知识蒸馏。用训练好的大模型比如 medium 尺寸当教师指导小模型nano 尺寸学习。蒸馏时不只是学 logits还要学中间特征层我一般选检测头前一层做特征对齐。实测下来蒸馏后的小模型比直接训的小模型 mAP 高 3~5 个点尤其在薄烟类别上提升明显。第二步量化。ONNX 导出后转 TensorRT优先用 FP16精度损失极小速度快一倍左右如果还要再快上 INT8但必须做校准集校准集要用真实的现场画面至少 500 张覆盖不同光照。INT8 在烟雾这种低对比度目标上容易掉点我通常只在算力实在不够的轻量档位用并且会重新跑一遍标定集验证掉点幅度超过 3 个点就退回 FP16。# ONNX 导出与 TensorRT 引擎构建示意 yolo export modelruns/detect/train/weights/best.pt formatonnx opset12 simplifyTrue imgsz640 trtexec --onnxbest.onnx \ --saveEnginebest_fp16.engine \ --fp16 \ --workspace4096 \ --minShapesimages:1x3x640x640 \ --optShapesimages:8x3x640x640 \ --maxShapesimages:16x3x640x640动态 batch 很关键现场路数会变固定 batch 会导致低负载时算力浪费、高负载时排队。optShapes设成实际平均并发数性能最好。5. 视频流工程实现从解码到告警的完整链路5.1 解码与抽帧最容易拖垮系统的环节算法再好解码跟不上就是白搭。1080p、H.264 的 RTSP 流单路用 CPU 软解大约占 1~2 个核20 路就能把一台服务器吃满。我的标准做法是硬解 零拷贝用 GPU 的 NVDEC 做解码帧数据留在显存里直接送推理避免 CPU-GPU 之间来回拷贝。抽帧策略用前面说的动态方案class AdaptiveSampler: 动态抽帧平静期低频候选期高频 def __init__(self, base_fps1, burst_fps8, burst_sec30): self.base_interval 1.0 / base_fps self.burst_interval 1.0 / burst_fps self.burst_sec burst_sec self.burst_until 0.0 self.last_ts 0.0 def should_process(self, ts, has_candidateFalse): if has_candidate: self.burst_until ts self.burst_sec interval self.burst_interval if ts self.burst_until else self.base_interval if ts - self.last_ts interval: self.last_ts ts return True return False这套逻辑的实际效果稳态算力接近 1fps但事件期的有效采样率能到 8fps。要注意has_candidate的判断本身也要耗算力所以候选判定用一个更轻的模型或者直接复用上一帧的检测结果不要为此再跑一遍完整推理。5.2 时序确认与告警抑制的实现细节这是整条链路里最便宜也最有效的一环纯逻辑代码不需要 GPU但降误报效果比调模型还明显。import time from collections import defaultdict, deque class TemporalConfirmer: def __init__(self, grid16, window_sec12, min_hits4, high_conf0.90, cooldown_sec180): self.grid grid self.window window_sec self.min_hits min_hits self.high_conf high_conf self.cooldown cooldown_sec self.history defaultdict(deque) # key - deque[(ts, conf)] self.last_alarm {} # key - ts def _cell(self, box, w, h): x1, y1, x2, y2 box cx, cy (x1 x2) / 2 / w, (y1 y2) / 2 / h return int(cx * self.grid), int(cy * self.grid) def update(self, ts, detections, img_w, img_h): alarms [] for cls_name, conf, box in detections: key (self._cell(box, img_w, img_h), cls_name) q self.history[key] q.append((ts, conf)) # 清理窗口外的记录 while q and ts - q[0][0] self.window: q.popleft() # 冷却期内不重复告警 if ts - self.last_alarm.get(key, -1e9) self.cooldown: continue # 高置信旁路 if conf self.high_conf: alarms.append((key, conf, high_conf)) self.last_alarm[key] ts continue if len(q) self.min_hits: alarms.append((key, conf, temporal)) self.last_alarm[key] ts return alarms几个细节值得单独说网格粒度16×16 意味着每个格子约占画面 6%对于烟雾这种会扩散的目标比较合适。如果网格太细同一团烟会在多个格子间跳动累计不上太粗则会把不同区域混在一起。可以用 12×12 到 20×20 之间调。冷却时间180 秒是我在多数项目里的默认值。同一位置告警后即使烟还在也不该反复推送给值班员。但冷却期内心跳式的状态更新还是要保留用于前端显示持续检测中。跨网格聚合烟雾扩散会跨格子所以要额外加一个宽松的聚合逻辑——相邻 3×3 网格内如果总命中数达到阈值也算确认。这一点在初期的稀薄烟雾上很重要。5.3 告警输出与平台对接告警输出我坚持带三样东西时间戳、截图、10 秒短视频片段。只推一个检测到烟雾的文字告警值班员无法判断等于没推。截图要带检测框和置信度短视频从前 5 秒到后 5 秒让他能看见烟是怎么变化的。推送格式建议用结构化 JSON通过消息队列或 HTTP 回调给上层平台{ event_id: evt_20240512_103245_0031, camera_id: cam_bf1_012, camera_name: 某楼栋大堂东侧, timestamp: 2024-05-12T10:32:4508:00, class: smoke, confidence: 0.83, bbox: [312, 208, 468, 355], trigger_mode: temporal, hits_in_window: 5, snapshot_url: /snap/20240512/evt_..._03.jpg, clip_url: /clip/20240512/evt_..._03.mp4, level: warning }level字段建议做成可配置的分级高置信单帧旁路触发为critical时序确认触发为warning仅候选未确认为info只入库不推送。分级之后值班员可以只订阅critical和warning减少打扰。6. 现场部署与环境适配算法之外的功夫6.1 相机选型与安装角度这一块经常被算法工程师忽略但它对最终效果的影响不比模型小。焦距不要一味追求广角。广角能覆盖更大范围但烟雾在画面里的像素占比变小检测难度上升。我的经验值是在监控距离 30 米处烟雾的可辨识像素宽度至少要达到 40 像素。按 1080p 横向 1920 像素估算如果画面横跨 60 米那 1 米宽烟雾只有 32 像素偏小。这种点位我会建议换成 6mm 或 8mm 镜头视角收窄到 30~40 米覆盖。安装高度与俯角烟雾是向上飘的相机俯角太大会只看到地面看不到烟羽。大堂点位建议俯角不超过 30 度车库可以适当大一些。另外要避开正对窗户和强光源逆光会让烟雾变成一片剪影特征全部丢失。补光夜间用红外补光时烟雾在红外下的成像和可见光差异很大如果训练集里红外样本不足夜间性能会断崖式下降。解决办法是在数据采集阶段就专门拍夜间红外样本至少占正样本的 30%。6.2 分点位参数标定一个模型多套配置同一套模型在不同点位上表现差异很大因为干扰源不同。我的做法是给每个点位单独配一份参数档案包括分类头阈值、时序窗口、最小命中数、是否开启蒸汽抑制、是否开启小目标增强。现场标定的流程是装好之后连续采集 3 天覆盖工作日和周末跑一遍推理看误报分布然后针对性调整。调整原则是只动配置文件不动模型——模型是全局的配置是局部的这样后续模型升级不会推翻现场调好的参数。这里有个实战心得把点位参数做成热加载现场工程师改完 yaml 不需要重启服务。我们做过一次统计某项目上线首月调了 47 次参数如果每次都要重启服务可用性根本保证不了。6.3 与消防系统的联动边界联动这块要非常保守。我的建议是视频检测系统的输出只进入独立的安防平台或消防控制室的辅助看板不直接触发声光警报和消防联动设备。真要联动必须经过人工确认或者设置双重条件视频确认 感烟探测器同时动作。原因很现实任何视觉算法都有误报哪怕日均 0.1 次一年也会误触发几次全楼疏散这个社会成本和运营成本没人能承担。把视频检测放在发现—确认的链条里而不是触发—执行的链条里是负责任的定位。7. 常见问题与排查实录7.1 误报排查先看画面再看模型误报上来别急着改代码先把误报截图一张张看。我整理了一张速查表现象可能原因处理方向集中在固定几个点位点位存在持续干扰源加点位白名单区域或提高该点位阈值集中于特定时段日照角度变化、补光灯开启、人员集中按时间段切换参数配置阴雨天误报暴增雨雾、镜头水珠、画面整体低对比度增加雨天负样本启用图像去雾预处理夜间误报多红外成像特征偏移、噪点补夜间红外训练样本提高分类头阈值误报框集中在画面边缘广角畸变拉伸纹理异常边缘区域降权或裁剪误报呈点状小框小目标误检飞虫、灯光点设置最小框面积阈值最小框面积这个特别实用。烟雾在楼宇场景里很少小于画面面积的 0.3%把所有小于这个面积的候选直接过滤掉能消掉一大类飞虫、灯光点、反光的误报代价几乎为零。7.2 漏报排查问题往往不在阈值漏报比误报难查因为没告警的时候你不知道模型看到了什么。我的排查顺序是回放录像看模型输出日志。如果日志里连低置信度候选都没有说明检测头就没找到问题在召回如果检测头找到了但被分类头或时序模块杀掉了问题在后段。检测头没找到通常是三个原因目标太小、对比度太低、训练集缺这类样本。对应处理是提高输入分辨率、开启图像增强如 CLAHE 对比度受限自适应均衡、补数据。被分类头杀掉说明分类头把烟误判成蒸汽/粉尘。这时候把裁图打印出来看往往是因为裁图分辨率太低或框不准。可以提高裁图尺寸、扩大裁剪时的外扩比例我一般外扩 20%。被时序模块杀掉多半是min_hits太高或窗口太短。稀薄烟雾可能断断续续出现这时候要降min_hits或者提高高置信旁路的灵敏度。我踩过最深的一个坑某项目一直有零星漏报查了两周才发现是抽帧逻辑的问题——动态抽帧在平静期是 1fps而烟雾出现的瞬间恰好落在了两次采样之间加上烟雾刚开始很淡下一次采样时已经飘出视野边缘。后来把基线抽帧提到 2fps配合运动触发画面像素变化幅度超过阈值就提频问题才解决。7.3 性能与稳定性问题几个高频的稳定性问题显存缓慢增长多半是解码器或推理框架的对象没释放。做法是给每个视频流单独一个处理进程/线程定期比如每 6 小时重启单个流避免全局崩溃。推理延迟抖动动态 batch 下不常见但如果是固定 batch 且路数超过 batch size会在排队处堆积。改成动态 batch 或者加一个最大等待时间如 20ms凑批。网络抖动导致流中断必须有重连机制指数退避重试并且断流超过 60 秒要标记该路为离线别让上层以为在正常运行。这一点在验收时经常被检查。告警风暴某个点位参数配错时可能短时间刷出几百条告警。加一个全局限流比如单点位每分钟最多 1 条、全局每分钟最多 10 条。7.4 几条踩坑换来的经验第一条先跑通闭环再谈精度。我见过团队花三个月优化模型结果现场连视频流都接不进来。正确的顺序是先搭一条能跑通的链路哪怕模型是预训练的、准确率只有 60%把解码、抽帧、推理、告警、推送全跑通然后再逐个环节优化。闭环跑通的那一刻你才会知道真正的瓶颈在哪。第二条给运维留一个反馈按钮。在告警看板上加两个按钮确认火情 / 误报。误报点击后自动把该片段入库每周导出来做一次增量训练。这个机制跑起来之后模型的现场适应性提升非常快比闭门造车强十倍。第三条别把阈值调得太完美。在标定集上调到最优的阈值到现场大概率不是最优的因为场景会变新开了餐厅、楼下开始装修。我一般会把阈值往保守方向偏一点宁可多留一点误报余量也不要让模型处于临界状态否则一点点环境变化就会导致漏报。第四条算力一定要留 30% 余量。现场永远会有意外临时接入的相机、突发的告警风暴、后台的模型热更新。我见过算力跑到 95% 的系统一次模型更新直接卡死十分钟告警全部丢失。第五条把不确定显式表达出来。与其让模型在 0.5 的置信度上硬做二分类不如增加一个疑似等级。低置信度、时序未确认的候选以info级别入库只在需要时展示。这样既不打扰值班员又保留了证据链事后复盘时能看清模型当时的判断路径。最后分享一个我一直在用的习惯每次项目上线后把前两周的所有告警按点位、时段、类别做一个分布统计画成热力图。哪几个点位告警密集、哪个时段集中爆发一眼就能看出问题在哪。这份统计我一般会直接交给物业让他们也参与到点位优化里来——毕竟他们比任何人都清楚那个位置每天早上七点到底在发生什么。