☰
抽烟识别工程实践:小目标检测与时序判定落地
2026/10/27 11:23:53 网站建设 项目流程

1. 这个任务的难点在哪:从一根香烟的像素尺寸说起

基于深度学习的抽烟识别,本质上是一个小目标检测 + 细粒度动作理解 + 长尾事件判定三合一的复合任务。很多人第一次听到这个题目,第一反应是"不就是检测个烟吗,YOLO 跑一跑不就完了",我最早也是这么想的,结果第一版模型在测试集上 mAP 看着挺漂亮,拉到真实画面里几乎没法用——它会把圆珠笔、吸管、奶茶吸管、耳机线、棒棒糖的棍子、甚至是手指缝隙里的阴影全都框成香烟。这个项目的坑,八成不在模型结构上,而在问题定义和数据上。

先说清楚它适合谁看:如果你手上有一路或几十路固定机位摄像头,想在特定区域做抽烟行为的自动提示;或者你在做工地、厂区、林区的安全巡检类项目,需要把"有人点烟"这类稀疏事件从海量视频里筛出来,那么这套思路可以直接抄。如果你只想在手机相册里找出一张抽烟的照片,那完全没必要上这套架构,一个普通的图像分类模型微调就够了。下面我按真实做过一轮的流程,把每个环节的选择理由和踩过的坑都摊开讲。

1.1 三个必须拆开看的技术层级

抽烟这个行为在视觉上其实可以拆成三层,混在一起做必然失败。第一层是香烟本体检测,也就是在画面里找到一个细长的小物体;第二层是手口交互判定,判断手指是否夹持、嘴部是否含住;第三层是时序行为确认,判断这个动作是否持续了足够长的时间。这三层的技术手段完全不一样,硬塞进一个检测模型里,模型会学得很拧巴。

我见过不少方案试图用一个大模型端到端输出"smoking"这一个类别,训练出来的模型对"人站在那儿手抬到脸边"极其敏感,只要手靠近嘴就报警。原因是标注的时候把"抽烟"这个行为整体框成一个大框,模型学到的最强特征其实是"手在脸附近"这个姿态先验,而不是香烟本身。这种模型在打电话、擦汗、喝水、吃东西的场景下全部误报。

正确的拆法是:香烟本体用目标检测,输入高分辨率 ROI;手口交互用人体或手部关键点做几何计算,这一步几乎不需要训练,用现成的姿态模型就行;时序用跟踪 ID 加时间窗口统计。三层各自独立调试,出问题能快速定位是哪一层挂的。这个拆分思路我认为是这个项目里最重要的架构决策,后面所有章节都是围绕它展开的。

1.2 用一组数字说明为什么"单帧检测"走不通

我们做个简单的像素账。假设一路 1080P(1920×1080)的监控画面,一个人站在离相机 5 米左右的位置,人体高度大约占画面 400 像素。头部高度约 60 像素,嘴部宽度约 20 像素,一根正常香烟露在嘴外的部分,长度大概 15 到 25 像素,直径 3 到 5 像素。

现在你要把画面送进检测网络。如果按常规缩放到 640×640 输入,缩放比是 640/1920 ≈ 0.333,那么香烟在输入图里的长度就变成 5 到 8 像素,直径 1 到 1.7 像素。这个尺度基本等于噪声,任何检测器的特征图上都提取不到有效信息。这就是为什么很多人训出来的模型"看着能检,实际检不到"——他在数据集里用手机近景拍的样本上确实能检出,但真实监控距离下香烟已经是亚像素级别了。

解决办法有三个方向,我在项目里是三个都用了。第一是提高输入分辨率,直接 1280 输入,代价是算力翻倍;第二是切片推理,把 1080P 画面按 640×640 的窗口带重叠地切开,每块单独推理再合并结果,等效于把有效分辨率提高了一个档次,代价是推理次数增加;第三是两级 ROI 裁剪,先跑一个轻量的人体/头部检测,把嘴部和手部区域裁出来,放大到 256×256 再送进香烟检测器。第三种的性价比最高,因为大部分画面区域根本没有人和手,全画面高分辨率推理是极大的浪费。

我实测下来的结论是:全画面 1280 输入的召回大概能到 0.6 左右,加上两级 ROI 裁剪后能到 0.85 以上,而总推理耗时反而比全画面 1280 更低,因为第二级只在极小的 ROI 上跑。这个账很值得算,后面第 6 章会给出具体的算力预算表。

2. 数据集怎么攒:抽烟识别项目真正的成本中心

我做过几个视觉项目,越做越确信一件事:模型结构的选择在半年后基本会过时,但一份标注规范清晰、场景覆盖到位的数据集,三年后还能接着用。抽烟识别尤其如此,因为它的正样本天然稀疏且场景差异巨大。室内顶装摄像头俯拍、厂区侧装摄像头平视、巡检机器人移动视角、夜间红外补光,这几种画面里香烟的成像特性完全不同,指望一个模型通吃所有场景是不现实的。

2.1 采集:场景覆盖比数量重要

我一开始的思路是"多拍点",结果攒了两万多张图,一测发现全是一个办公室、一个角度、一批人,模型换个会议室就崩。后来调整策略,按"场景 × 机位高度 × 光照 × 距离"做一个正交表去补采,最终只用了大约 8000 张正样本图加 15000 张负样本图,泛化能力反而好得多。

具体的采集维度我是这么列的:距离分三档,3 米内、3 到 6 米、6 米以上,因为香烟像素尺寸差别巨大;光照分白天自然光、室内顶灯、逆光(人背对窗户,脸部接近剪影)、夜间补光四种;机位分顶装俯拍、平视侧拍、斜 45 度三种;人群要覆盖不同肤色、是否戴口罩、是否戴帽子、是否穿工装。这四维排列组合下来大概 100 多种组合,每种组合至少 30 到 50 个样本才勉强够用。

有一点必须强调:负样本的采集比正样本更费心思。正样本你只要找人在规定区域点烟就能拍到,负样本你得刻意去触发模型可能出错的场景。我专门安排人拍了一组"手拿圆珠笔在嘴边比划""喝奶茶用吸管""擦汗""打电话""吃冰棍"的视频,这类画面在真实环境里的出现频率远高于抽烟,它们才是误报的主要来源。数据集里如果没有这些硬负样本,模型上线第一天就会被误报淹没。

注意:采集涉及真实人物的画面,务必提前做好告知与授权,优先在自有场地、志愿者配合的前提下采集,涉及公共区域画面时做脱敏处理,只保留用于动作判别的局部区域。这条不是形式主义,后面第 8 章会细说。

2.2 标注规范:香烟框的歧义处理

香烟框的标注看起来简单,实际上歧义极多,如果不同标注员的标准不一致,模型学到的就是噪声。我总结了几条规则,在项目里强制执行,标注一致性明显提升。

第一,只标可见部分,不猜被遮挡的部分。手指夹着香烟时,被手指挡住的那一截不要脑补补全,只画露出来的那段。这样标注框会比较小,但语义是确定的。如果让标注员去猜,同一个人今天和明天的标准都不一样。

第二,含在嘴里的香烟,只标从嘴唇向外延伸的部分。嘴里的部分不可见,不要标。这带来的副作用是:嘴含香烟时框特别小(可能就 3×8 像素),所以训练时要有针对性地处理,这个在第 3 章会讲。

第三,电子烟和传统香烟标同一类,但在标注文件里加一个子类属性。工程上先合并成"香烟"一类保证召回,等模型稳定了再拆细。我一开始就拆成两类,结果两类都学不好,因为类间视觉差异其实很小,且各自样本都不够多。

第四,明确"疑似"的处理。手上有细长物体但看不清是不是烟的,一律不标正样本,放进"忽略区域"(ignore region),在计算 loss 时屏蔽掉。这个技巧救了我不少标注成本,很多模糊的边界样本不需要纠结,直接忽略即可。

标注格式我用的是常见的 YOLO txt 格式,一行一个框:

# class_id cx cy w h (均归一化到 0-1) 0 0.4823 0.6312 0.0104 0.0281 0 0.4901 0.6288 0.0097 0.0265

类别 0 就是香烟。对于 ROI 裁剪后的子图,坐标要重新归一化到子图坐标系,这点在写数据加载器时容易搞错,我的做法是标注阶段就同时保存原图坐标和裁剪图坐标两套,训练时按需取用。

2.3 硬负样本与长尾处理

抽烟识别是典型的极度不平衡问题。在一段真实的监控视频里,抽烟的帧占比可能不到千分之一,其余全是负样本。如果直接按真实分布采样,模型会倾向于全部预测为负类,准确率能到 99.9%,但毫无用处。

我的采样策略是:训练时按 1:3 的正负比做在线采样(每个 batch 里正样本占四分之一),同时把硬负样本(笔、吸管、打电话、擦汗)单独建一个子集,保证它们在每个 epoch 里至少出现一次。验证集和测试集则严格保持真实分布,用来看真实指标。训练分布和评测分布不一致是故意的,这一点很多新手会搞混,以为验证集也得按 1:3 采样,那样测出来的指标完全不能反映线上表现。

长尾方面还有一个细节:抽烟姿势本身也有长尾,最常见的是食指中指夹烟、抬手到嘴,比较少见的是叼在嘴里不抬手、低头点烟、蹲着抽烟、戴着口罩把烟塞进缝隙里抽。后几种我最初完全没覆盖,结果上线后这几类场景全是漏报。补采的时候我特意让志愿者把这几种姿势都演一遍,每个姿势至少 200 帧,成本不高但收益明确。

3. 模型架构:检测、关键点、时序三段式

前面说了要拆三层,这一章讲具体的落地结构。我给的整体流水线是:人体检测(粗定位人)→ 关键点提取(拿到鼻子、双耳、手腕、手肘坐标)→ 基于关键点生成嘴部 ROI 和手部 ROI → 在 ROI 上跑香烟检测 → 跟踪器给每帧结果打上 ID → 状态机做时序判定 → 输出事件。每一步的选型理由都值得展开说。

3.1 检测主干与输入分辨率的选择逻辑

香烟检测主干我选的是 YOLO 系列的轻量版本,v5n 或 v8n 这一档,不是因为它最先进,恰恰因为它在小目标场景下性价比最高且生态成熟。理由有三条:一是这一档的模型在 640 输入下推理开销小,ROI 裁剪后可以放心地用高分辨率跑;二是它的锚点配置对小目标比较友好,配合 P2 层特征(可选)能检到 8 像素左右的目标;三是训练脚本、数据增强、导出流程都很成熟,不需要自己造轮子。

关于输入分辨率,我在 ROI 阶段用的是 512×512,而不是 640。原因是 ROI 裁剪出来的区域本身就集中在人脸和手部,目标占比大了,512 足够。实测 512 和 640 的召回差距不到 2 个百分点,但耗时省了大约 35%。如果 ROI 裁剪的尺度没控好(比如把整个上半身都裁进来了),那就得用更大输入,所以在裁剪逻辑里我加了一个约束:ROI 的短边对应的原始像素不超过 400 像素,超过就说明裁剪太松,要么收紧要么提高输入尺寸。

这里有个容易忽略的细节:ROI 裁剪必须带上下文。如果你严格按嘴部坐标裁一个正方形,模型看到的是"一个嘴巴 + 一根细线",失去了手的上下文,容易把唇边阴影误判。我实际的裁剪是嘴部中心为参考,向上扩 1.5 倍头高、向下扩 1 倍头高,同时保持宽高比 1:1,这样手抬到嘴边的动作基本都在画面里。这个"带上下文的 ROI"是我调了两周才想明白的,一开始纯粹按几何中心裁,误报率高得离谱。

模型选型上我还对比过一个思路:直接做"香烟 + 手"的联合检测,把类和手都标出来,用一个模型同时输出。这个方案我没采用,原因是标注成本翻倍,而且手的姿态变化太多,模型容量压力大。后来用现成的姿态模型拿关键点,零标注成本就解决了手部定位问题,这个取舍我认为非常划算。

3.2 关键点辅助:把"手到嘴的距离"变成可计算量

这一步是整个方案里我最满意的部分,因为它几乎不增加成本,却把准确率抬高了一大截。用一个人体姿态模型(轻量档就够),拿到 17 个关键点里的鼻子、左右耳、左右肩、左右腕、左右肘。注意大多数姿态模型不直接给"嘴"的关键点,我的做法是用鼻子关键点做近似,因为鼻子和嘴在头部的位置关系稳定,且鼻子点的置信度通常最高。

有了这些坐标,可以算出三个非常有用的几何量。第一是腕-鼻距离,归一化方式是除以肩宽(用双肩距离做尺度归一,这样不同距离的人可比)。手腕离鼻子很近,说明手抬到了脸部附近。第二是肘-腕-鼻的夹角,这个角度在"手抬到嘴边"时通常小于 60 度,而"手自然下垂"时接近 180 度。第三是手部的运动方向,通过前后几帧的手腕坐标算出速度矢量,朝鼻子方向运动说明在"抬手"。

这三个量组合起来,能构成一个相当强的行为先验:手在脸附近 + 夹角小 + 正在朝嘴部移动。满足这三个条件的帧,才进入香烟检测的 ROI 分支;不满足的帧,连检测都不用跑,直接跳过。这样做的收益是双份的,既降了算力(大部分帧不需要跑第二级检测),又降了误报(模型看不到那些根本没抬手的画面)。

需要提醒的是,姿态模型在遮挡、侧身、逆光的情况下会掉点,置信度低于阈值时这几个几何量就不可信了。我的处理是:关键点置信度低于 0.3 的帧,不做"跳过"判定,而是仍然送进检测分支,宁可多算一点也不能漏。这个保守策略是有代价的(算力会上去),但比漏报强。

3.3 时序判定:轻量级方案与重量级方案的取舍

时序建模有两条路。一条是轻量级的状态机方案:不做真正的时序模型,只用跟踪器把每帧的检测框串成轨迹,然后在时间窗口里统计命中情况,比如"最近 3 秒内至少有 4 帧检测到香烟且手在嘴附近"。另一条是重量级的视频模型方案:把连续 16 帧或 32 帧作为一个 clip 送进轻量 3D 卷积或时序移位网络,直接输出动作类别。

我两条都试了,最终选的是状态机方案,理由很实际。视频模型在实验室的公开数据集上指标确实更好看,但落地时有三个问题:一是它需要固定帧率的连续帧,而实际监控流会因为网络抖动丢帧,得做缓冲对齐,工程复杂度上去了;二是它把"判断正确"和"解释清楚"绑在一起,出了问题没法跟使用方解释是哪一项触发的;三是它对算力的占用是数倍于单帧检测的,多路并发的时候非常吃紧。

状态机方案的缺点是需要手工设阈值,不是端到端学出来的。但换个角度想,这反而是优点:每个阈值都能单独调,现场调试的时候可以只调一个参数而不重新训练模型。我在实际部署里,把阈值做成了可配置项,不同的摄像头可以配不同的灵敏度,这个灵活性在真实项目里价值极高。

如果你手上的场景特别复杂,比如需要在密集人群里区分"谁在抽烟",那视频模型的价值会体现出来,因为它能利用人与人之间的相对运动。但绝大多数固定机位的场景,状态机足够了。

4. 训练与调参:从基线到可用的过程

这一章讲实操。我按真实的训练顺序来写:先跑通基线,再处理不平衡,最后联合调后处理参数。很多人卡在第一步就跑了,其实最难的是第三步——模型指标和现场表现之间的那道鸿沟。

4.1 训练环境与基线配置

环境没什么特别的,单卡 24G 显存足够跑这一档的模型。数据组织成标准的检测数据集目录结构,写一个 yaml 描述文件:

# smoke_det.yaml path: /data/smoke_dataset train: images/train val: images/val test: images/test nc: 1 names: ["cigarette"]

基线训练命令用官方脚本的形式,我加上几个改过的关键参数:

yolo detect train \ model=yolov8n.pt \ data=smoke_det.yaml \ imgsz=640 \ epochs=150 \ batch=32 \ optimizer=SGD \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=3 \ mosaic=1.0 \ scale=0.5 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4 \ fliplr=0.5 \ close_mosaic=15

几个参数的用意解释一下。mosaic数据增强对小目标帮助很大,它能把四张图拼成一张,等效于缩小目标、增加小目标密度,但要注意最后 15 个 epoch 关掉(close_mosaic=15),否则模型学到的目标尺度和真实推理时不一致。scale=0.5是随机缩放增强,我特意保留了较大的缩放范围,因为监控场景里人的远近差异确实很大。hsv_v=0.4把亮度扰动调高,是为了覆盖逆光和夜间补光的场景。

预训练权重我用的是通用检测数据集上训好的权重做初始化,这一步不能省。香烟这种小目标,从零训收敛很慢,用预训练权重能省掉一半的训练时间,而且最终指标更好。有几次我尝试从头训,训到 100 epoch 还没收敛到理想状态。

训练过程的观察重点不是 loss 曲线,而是验证集上小目标那一档的召回。如果整体 mAP 在涨但小目标召回不动,说明模型在学大目标(比如人身上的其他元素),这时候要去检查数据标注里是不是有大框混进来了。

4.2 验证集怎么划才不骗自己

这是我在这个项目里踩得最深的一个坑。第一版我按随机抽帧的方式划验证集,指标漂亮得不像话,mAP@0.5 能到 0.92,结果换一路摄像头就崩到 0.4。原因很简单:同一个人在同一场景里连续抽的帧,相邻帧之间几乎一模一样,随机划分导致训练集和验证集里存在大量近重复样本,模型实际上是"背答案"。

正确的划法是按摄像头、按人、按场景划分,不是按帧划分。我把 20 路摄像头分成三组,训练用 12 路、验证用 4 路、测试用 4 路,保证验证和测试的摄像头在训练时完全没见过。这样测出来的指标才接近真实上线表现。代价是数字会难看很多,我第一次按这个方式重划,mAP 从 0.92 掉到 0.68,但那个 0.68 才是真话。

划分方式验证集 mAP@0.5上新摄像头后的实际表现是否可信
随机抽帧0.920.40 左右严重高估
按时间段划分0.850.55 左右偏高
按摄像头+按人划分0.680.64 左右基本可信

还有一个小技巧:验证集里一定要混入足够比例的空场景帧(画面里根本没人)。如果验证集全都是有人物的画面,模型在真实场景里面对空画面时可能产生莫名其妙的误报,这种情况在夜间噪声大的画面里尤其明显。

4.3 阈值、NMS 与后处理的联合调优

模型训完,不要急着定阈值。检测阈值、NMS 的 IoU 阈值、ROI 尺寸、时序窗口长度,这四个参数是互相耦合的,得一起调。

先说置信度阈值。我不用 0.5 这种拍脑袋的值,而是画出验证集上香烟类的 PR 曲线,然后按工程目标反推。假设使用方要求"每路每天误报不超过 2 次",我可以算出每天每路的总检测帧数,再结合时序判定的过滤倍数,倒推出允许的单帧误报率,最后在 PR 曲线上找对应的工作点。这个计算过程在第 5 章展开。

NMS 的 IoU 阈值我调到 0.4,比默认的 0.45 略低。原因是香烟框细长,同一个目标可能产生多个重叠但形态略有差异的框,如果 IoU 阈值太高,会留下多个重复框,导致后面的规则引擎重复计数。调到 0.4 之后,重复框基本能压掉,同时不会误删挨得很近的两个不同目标(比如两个人同时抽烟)。

还有一个小参数容易被忽略:最小框尺寸过滤。ROI 图 512 输入下,我设的最小有效框是 3×3 像素,小于这个的直接丢弃。这个过滤很粗暴但有效,因为真香烟在 ROI 里通常不会小于这个尺寸,而噪声框往往就在这个量级。加上这一条,误报能再降一到两成。

5. 规则引擎:把误报率压下去的最后一道闸

我个人认为,抽烟识别项目能不能用,决定性因素不在模型,而在这一章。模型给你一个单帧判断,规则引擎决定这个判断要不要变成一条告警。没有规则引擎的纯模型方案,在现场基本活不过一周。

5.1 用一组算例说明为什么必须上时序

我们来算笔账。假设每路摄像头抽帧到 8 fps 送入流水线,一天运行 24 小时:

每路每天的送入帧数 = 8 × 3600 × 24 = 691,200 帧

如果单帧模型在某个阈值下的误报率(FPR)是 1×10⁻³(这个水平已经算不错了),那么每天的误报帧数大约是 691 次。即使你的召回是 0.9,这 691 次误报也会把使用方逼疯——平均每两分钟一条告警,没人会看。

要压到"每路每天不超过 2 次误报",帧级误报率需要降到 2.9×10⁻⁶ 这个量级。单帧检测模型基本做不到这个水平,除非你把召回压到很低。而时序规则恰好擅长这件事:它把"单帧命中"升级为"连续多帧命中",用时间换取误报率的指数衰减。

我的规则是:滑动窗口 3 秒(约 24 帧),窗口内满足以下全部条件才触发一次事件——检出香烟的帧数 ≥ 5 帧、检出帧的时间跨度 ≥ 1.5 秒、其中有 ≥ 60% 的检出帧同时满足"手腕-鼻子归一化距离 < 0.8 肩宽"。这个组合条件在实验室的回放数据上,把误报从每天几百次压到了每天 1 到 2 次,同时召回只掉了不到 5 个百分点,因为真实的抽烟行为通常持续 30 秒以上,三五帧的抖动过滤掉完全不影响检出。

5.2 状态机的代码实现

实际代码比上面描述的更细一些,我贴个精简版,保留核心逻辑:

# 精简版事件判定状态机(伪代码,仅示意逻辑) import time from collections import defaultdict, deque WINDOW_SEC = 3.0 MIN_HITS = 5 MIN_SPAN = 1.5 HAND_RATIO = 0.8 class SmokeEventFSM: def __init__(self): # 每个跟踪 ID 一条时间线,元素为 (timestamp, is_hit, hand_near) self.tracks = defaultdict(deque) self.last_alarm = {} def update(self, track_id, timestamp, det_hit, hand_near): dq = self.tracks[track_id] dq.append((timestamp, det_hit, hand_near)) # 滑出窗口外的旧数据 while dq and timestamp - dq[0][0] > WINDOW_SEC: dq.popleft() if len(dq) < MIN_HITS: return False hits = [x for x in dq if x[1]] if len(hits) < MIN_HITS: return False span = hits[-1][0] - hits[0][0] if span < MIN_SPAN: return False near_ratio = sum(1 for x in hits if x[2]) / len(hits) if near_ratio < 0.6: return False # 同一目标 30 秒内只报一次,避免刷屏 if timestamp - self.last_alarm.get(track_id, -1e9) < 30: return False self.last_alarm[track_id] = timestamp return True

这段代码里有两个设计点值得说。一是以跟踪 ID 为键,而不是以画面位置为键。同一个 ID 说明是同一个人的连续行为,不同 ID 的检测结果不能混在一起投票,否则拥挤场景里三个人各抽一下,会被合并成一个事件。二是告警去重,同一个目标 30 秒内只报一次。真实的抽烟行为持续几分钟,如果每 3 秒报一次,使用方会认为是系统故障。去重的冷却时间设多少要跟使用方约定,我一般设 30 秒到 2 分钟。

还有一个细节:跟踪器本身会丢 ID。人走动、被遮挡、转身的时候,ID 会切换,导致一个完整的抽烟事件被拆成两段,每段都不满足最小帧数,从而漏报。我的处理是做一个简单的轨迹拼接:新 ID 出现时,如果它的位置和历史轨迹的末端距离在阈值内,且时间差小于 2 秒,就认为是同一个目标,做 ID 继承。这个小逻辑能把漏报率再降几个百分点,代码量很小但收益不小。

6. 部署:算力预算与端侧落地

模型训完只是开始,能不能在目标硬件上跑够路数,才是项目能不能交付的关键。我在这一章给出实际的算力核算方法,你把这个表格套到自己项目的路数和帧率需求上,就能估出需要什么档位的硬件。

6.1 帧率与算力预算怎么算

假设硬件是某款国产边缘推理模组,NPU 算力标称 6 TOPS,CPU 是 8 核。实际能跑到多少,不能看标称值,要用实测的单帧耗时来算。我在这个项目里的实测数据是:

子任务输入尺寸单次耗时(NPU 实测)是否每帧必跑
人体/头部检测640约 6 ms是
姿态关键点256×192约 3 ms是
香烟检测(ROI)512约 8 ms否,约 20% 帧触发
跟踪 + 规则-约 1 ms是
解码 + 缩放-约 4 ms是

单帧的加权平均耗时 = 6 + 3 + 8×0.2 + 1 + 4 ≈ 15.6 ms,也就是说单路满帧率能跑到 60 fps 以上(理论上),但这个数字是理想值,多路并发时 CPU 调度、内存带宽都会拖后腿,实际打个七折比较稳,按 45 fps 算。

再看需求端。假设 16 路 1080P 摄像头,每路抽帧到 8 fps,总帧率需求 = 16 × 8 = 128 fps。按单路 45 fps 的能力,理论上 3 个模组就够了,但实际部署我会配 4 到 5 个,留 30% 到 40% 的余量,因为:夜间的红外画面噪声大,检测耗时会有波动;ROI 触发比例在某些场景下会远高于 20%,比如会议室里大家频繁抬手喝水,可能到 50%;还需要留出 CPU 做视频解码,1080P 解码本身就很吃 CPU。

如果是纯 CPU 方案(没有 NPU),单帧耗时会涨到 80 到 150 ms 这个量级,单路勉强能跑 6 到 10 fps,多路基本没戏。所以这个项目想上多路,边缘加速卡基本是必需的,不要指望用普通服务器 CPU 硬扛。

提示:抽帧率不是越高越好。抽烟是慢动作,8 fps 已经足够,甚至 5 fps 也能用。把抽帧率从 8 降到 5,算力需求直接降 37.5%,而召回损失很小。这个参数是我在项目里最先砍的,效果立竿见影。

6.2 量化与加速:这里最容易翻车

模型部署基本都要走量化,把 FP32 转成 INT8,速度能提升两到三倍。但量化对这个任务有特殊风险:香烟是小目标、低对比度,量化带来的精度损失在小目标上会被放大。

我的做法是分级量化。人体检测和姿态模型做完整 INT8 量化,它们的目标大、容错高;香烟检测模型先做 INT8 量化测一轮,如果小目标召回掉超过 5 个百分点,就退回 FP16 或者对首层和检测头保持 FP16 的混合精度。实测下来,香烟检测器用 FP16 的耗时比 INT8 高了大概 60%,但召回稳住了,这个代价我认为值得。

量化校准集的构造也有讲究。不能随便从训练集抽 100 张了事,要覆盖所有光照和场景。我用了 500 张,按场景分层抽样,夜间红外、逆光、室内顶灯各占一定比例。有一次我图省事只用了白天的图做校准,结果夜间场景的召回掉了将近 20 个百分点,排查了两天才定位到是校准集的问题。

还有一个工程细节:ROI 裁剪和对齐的方式,训练和推理必须完全一致。我在训练时用的是 OpenCV 的双线性插值缩放,部署时如果换成了硬件加速的缩放模块,插值方式不同,边缘像素会有细微差异,小目标检测对这种差异非常敏感。我的建议是部署前先做一致性验证:拿同一批图,分别用训练侧和推理侧的前处理跑一遍,比对输入张量的最大绝对误差,超过 1e-2 就要警惕。

7. 常见问题速查与踩坑记录

这一章是我把项目里遇到过的、以及帮别人排查过的问题整理成的速查表。抽烟识别的坑高度集中,看一遍能省很多时间。

7.1 典型误报与漏报的来源对照

先看误报。按我统计的频次排序,从高到低是这样的:

误报来源表现特征处理手段
笔、吸管、牙签等细长物手在嘴边,细长物体朝向嘴补硬负样本;规则引擎要求持续时间
手指缝隙阴影逆光下手指间出现深色细条提高 ROI 分辨率;加亮度均衡预处理
耳机线、挂绳细长且颜色深,靠近脸部补负样本;限制检测框的宽高比范围
口罩边缘、衣领深色边缘被误检补负样本;ROI 裁剪时排除边缘区域
远处的栏杆、电线背景里的细长结构用人体检测框做空间约束,框外的丢弃
夜间噪点随机亮点被误检夜间画面加降噪预处理;提高置信度阈值

再看漏报,这个更隐蔽,因为用户往往只反馈"没报",不告诉你为什么。

漏报来源表现特征处理手段
香烟被手完全遮住只露出一点点或完全不可见靠时序和手口交互补;接受一定漏报
距离过远香烟像素不足 3 个提高 ROI 输入尺寸;限制有效检测距离
逆光剪影人只剩轮廓补逆光样本;必要时加 HDR 预处理
口罩遮挡烟塞在口罩缝隙补此类特殊样本,这类场景占比低但确实存在
低头点烟姿态模型掉点,ROI 裁偏关键点置信度低时扩大 ROI 范围
只抽一口就熄持续时间短于规则窗口适当缩短窗口,但要权衡误报

7.2 现场调参的几条心得

第一条,不要在现场改模型,先改规则。上线后遇到误报多,第一反应不应该是回去重新训模型,而是调时序窗口和几何约束。我遇到过某路摄像头因为逆光严重,误报特别多,最后是靠给这一路单独配了一套更严格的阈值解决的,模型一行没改,半天搞定。不同摄像头配不同参数这件事,在工程上是完全可接受的,不要把"一套参数通吃"当成执念。

第二条,建立误报样本回流机制。每次使用方反馈误报,把那一段视频截下来,进负样本库,攒够 500 条就重新训一轮。这个机制跑起来之后,模型会自己越用越准。我在项目里做了个简单的回流工具,使用方点一下"这是一次误报",视频片段就自动上传归档,标注都不用重新做(因为已经知道是负样本了)。这个工具的开发成本大概两天,但后期省下的沟通成本是几十倍。

第三条,验证要按"事件"验,不要按"帧"验。使用方关心的是"今天漏了几个抽烟的人",不是"帧级召回是多少"。所以我做验收的时候用的是事件级指标:随机挑 100 个真实的抽烟事件,看检出多少个;再随机挑 24 小时的连续录像,数误报了几次。这两个数字才是能拿去汇报的。帧级指标只在我自己调模型的时候看。

第四条,别忽视摄像头的安装位置。这句话听起来像废话,但我真的遇到过因为摄像头装得太高、俯角太大,导致所有人都是低头姿势,姿态模型的鼻子关键点全被头顶挡住了,整个手口交互判定失效。最后是把摄像头下移了一米多解决。如果你的项目还在设计阶段,建议提前跟施工方确认机位高度在 2.5 到 3 米、俯角不超过 30 度,这个约束能省掉后面大量麻烦。

8. 合规与隐私:这个项目必须提前想清楚的事

做行为识别类的项目,技术上跑通只是第一步,能不能合规地上线,往往决定项目生死。这部分我踩过坑,也见过同行翻车,说几条我认为必须提前定下来的原则。这些不是应付检查的条文,是真正会影响项目推进节奏的东西。

8.1 数据最小化:能少收就少收

第一条原则是只处理判断所必需的数据。抽烟识别真正需要的信息是"手部区域""嘴部区域""香烟形态",完整的 1080P 全景画面里绝大部分内容对这个判断毫无贡献,却承载了最多的隐私风险。所以我在架构上做了两个选择:一是优先在端侧完成推理,只把"事件时间戳 + 目标 ID + 置信度 + ROI 小图"传回中心,原视频不上传;二是 ROI 小图只裁剪手部和嘴部区域,不保留人脸完整画面。

如果业务方确实需要事后回看,那就只保留事件前后各 10 秒的短片段,并且设置自动过期删除,比如 7 天后自动清理。存储策略要在项目启动时就跟使用方书面约定,不要等出了争议再补。我见过有项目把全部视频存了三个月,结果存储成本高得离谱,还给自己增加了不必要的责任。

第二条是告知。在部署区域设置醒目的提示标识,说明该区域有行为识别系统在运行、用于什么目的、数据保留多久。这件事在技术上零成本,但在实际推进中能避免大量误解和投诉。有些场地方会要求提供数据处理说明文档,提前准备好一份,写清楚采集范围、处理方式、存储周期、访问权限,比临时被问到时手忙脚乱强得多。

8.2 使用边界:把系统定位成"提示"而不是"证据"

这一条我特别想说。抽烟识别系统的准确率,哪怕优化到很好,也永远不可能 100%。它一定会漏,也一定会误报。如果你的系统被定位成"处罚依据",那么每一次误报都会变成一次真实的纠纷,这对系统精度的要求会高到不切实际,项目永远无法交付。

所以我在项目里坚持一个定位:系统输出的是"提示",最终的判断必须有人参与。系统负责从海量视频里把"可能有人在抽烟"的片段筛出来,把 24 小时的录像压缩成几条待确认的记录,把人工巡查的效率提高几十倍,这才是它真正的价值所在。人工复核这一步不是技术不成熟的表现,而是这类系统合理的工程形态。

这个定位带来的技术影响也很直接:因为它只是提示,所以我可以把召回放在更高的优先级,允许一定误报,用人工复核来兜底;同时它不需要追求毫秒级的实时性,可以把抽帧率降下来,硬件成本大幅降低。想清楚这一定位之后,整个项目的技术选型都会变得更清晰。

还有一个实际问题是密集场景的处理。如果画面里同时有几十个人,系统的告警可能会集中爆发,这时候要靠规则引擎里的去重和限流,把告警量控制在人工能处理的范围内。我一般会设置"每路每分钟最多 N 条告警",超出的合并成一条汇总,避免把使用方淹没。


最后分享一个我个人在项目里体会最深的小技巧。这个项目我前后做了几轮迭代,最有效的改进不是换了什么模型,而是把第一版就做出来的"两级 ROI"思路坚持做到了极致——后来我甚至把 ROI 从矩形改成了沿人体朝向旋转的平行四边形,按肩部朝向估计人体姿态角,这样侧身的人也能裁得正,召回又涨了三个点。这个改动代码不到五十行,效果比重新训三次模型都好。

所以如果你也要做这类小目标行为识别,我的建议是:把大部分时间花在"怎么让模型看到更清晰的目标"上,而不是"怎么让模型更聪明"。分辨率、ROI、上下文裁剪这些看起来很土的手段,往往比换个新模型管用得多。踩过坑之后我越来越相信这一点。

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

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

立即咨询