上个月接了一个公共场合人员行为分析的需求,对方定义得很明确:在摄像头画面里,自动识别出抽烟、打电话、玩手机、饮酒四类行为,发现问题实时告警。需求文档只有一句话,真正动手才发现,"识别行为"和"检测物体"完全是两码事——打电话的人可能手里没拿手机,抽烟的动作一晃就过去,玩手机和翻资料在侧后方视角下几乎长得一模一样。这篇就把我从数据准备到模型训练、从误报排查到部署优化的完整过程整理出来。核心就是两套东西:一套深度学习CNN做人员行为识别,对画面中的人体区域做行为分类;一套YOLO做目标检测,专门定位手机、烟支这类关键物件。两条链路汇合之后,再用状态机把连续帧结果揉成一个稳定的告警事件。这套方案从标注做到调优,踩了不少坑,下面的内容全部来自实际项目沉淀,希望能给正在做同类项目的同学省点时间。
1. 需求拆解:为什么"看人"和"看行为"要分开建模
1.1 行为识别不是单一任务,而是"检测+分类"的组合
很多人第一次听到"行为识别",第一反应是找个现成的动作识别模型,把视频片段扔进去,直接输出"抽烟""打电话"这样的标签。这个思路在实验室里跑通很容易,拿到监控场景就崩。原因很朴素:公共场合的监控画面最少有几十个目标,你必须先知道"谁在哪",才有可能判断"它在干什么"。整图分类只能告诉你画面里大概率发生了抽烟行为,却没法告诉你抽烟的人是谁、在哪个位置,更没法去重和跟踪。
这就决定了项目必须走"两阶段"路线:先用目标检测把人从画面里抠出来,再对每个人体区域做行为分类。目标检测负责回答"有什么、在哪",行为分类负责回答"在做什么"。两个任务解耦之后还有一个额外的好处:可解释性。线上出误报的时候,你能快速定位是检测漏了人,还是分类把动作判错了,而不是对着一个黑盒模型干瞪眼。
1.2 四类行为的视觉特征与难点拆解
做数据之前,我先把四类行为的视觉特征彻底拆了一遍。所谓的"行为",本质上是"人体姿态 + 手持物体 + 物体与身体部位的空间关系"三件事的组合。我用下表把每类行为的特征和难点列清楚:
| 行为 | 关键物体 | 视觉特征 | 最容易翻车的点 |
|---|---|---|---|
| 抽烟 | 烟支、烟头 | 手持烟支靠近嘴部,可能伴随烟雾 | 烟支目标太小,烟雾不稳定不可靠 |
| 打电话 | 手机 | 手机贴近耳侧,单手持机 | 与玩手机共用同一个物体,仅靠物体无法区分 |
| 玩手机 | 手机 | 低头,双手或单手持机,屏幕朝向面部 | 侧后方视角看起来和翻资料几乎一致 |
| 饮酒 | 杯、瓶 | 容器举起至嘴边,有倾斜动作 | 和喝水无法从画面语义上严格区分 |
这张表是后面所有工作的出发点。你会发现四类行为里有三类都依赖"手持物体"这个强先验,只有打电话和玩手机是同一物体、两种姿态。所以模型选型时,目标检测通道必须能稳定检出手机、烟支、杯瓶这些小目标,行为分类通道才能拿到有效的输入。
1.3 项目交付边界:出什么结果、不出什么结果
动手之前,我还和需求方把交付边界谈清楚了。系统最终输出的是:每个人的位置框、行为标签、置信度、触发告警的事件截图和录像片段。不做人脸身份识别,不做跨摄像头人员轨迹追踪,也不判断饮酒行为里喝的是酒还是水——后面这条边界尤其重要,因为视觉算法本质上只能看到"容器入口"这个动作,液体的性质从像素里推断不出来。
把边界定清楚不是保守,而是工程现实。需求方需要的是一套"能帮助管理人员缩小巡查范围"的工具,而不是一个什么都管的全能裁判。边界越清晰,后续的指标定义、误报标准和验收方式就越明确,团队内部也少很多扯皮。
2. 数据准备功夫:标注规范决定模型上限
2.1 三级标注体系:人体框、物体框、行为标签
行为识别的数据标注和普通分类不一样,不能只给一张图打个"抽烟"的标签就完事。我在项目里用的是一套三级标注体系:
- 第一级:人体框,把画面里参与行为的人框出来;
- 第二级:物体框,标注手机、烟支、杯瓶这三类"行为关键物";
- 第三级:行为标签,给每个人体框打上抽烟、打电话、玩手机、饮酒、正常行为五分类之一。
为什么要三级都标?因为行为分类模型如果不学"人体和物体的空间关系",很容易走捷径。举个例子,一个人坐在工位上低头看桌面,手里拿着手机充电线,模型可能因为画面里出现手机就判成"玩手机",但实际上它只是整理桌面。有了物体框和人体框的空间约束,模型才能学到"手机贴近耳朵才是打电话""烟支靠近嘴边才是抽烟"这类真正有区分力的特征。
标注工具方面,市面上开源工具挑一款顺手的就行,关键是导出格式要统一。我个人习惯把标注结果整理成"人体框 + 物体框 + 行为标签"的表格结构,方便后续做数据筛选和分布统计。
2.2 正负样本比例与难例清单
四类行为的正样本数量,我建议每类至少准备3000到5000张,并且要覆盖不同摄像头角度、不同距离、不同光照。这个量听起来不小,但公共场合监控素材其实很好收集——只要有原始视频,用自动检测脚本先粗筛一遍,再人工精标,效率会高很多。
比正样本更重要的是负样本。行为识别模型翻车,一大半原因不是正样本太少,而是负样本里缺少"长得像正样本但不是"的难例。我整理了一份项目专用的难例清单:
- 揉鼻子、摸下巴、咬笔头——容易和抽烟混淆;
- 整理衣领、戴耳机——容易和打电话混淆;
- 低头看资料、拿笔记录、戳屏幕——容易和玩手机混淆;
- 喝水、端着杯子走路、举杯碰杯——容易和饮酒混淆。
这些难例每类最好准备2000张以上,单独放进训练集,否则模型上线后第一天就会被现场画面教做人。负样本的收集策略也很简单:先跑一版初版模型,把线上误报截图全部回流到标注池,这就是最真实的难例来源。
2.3 数据增强:监控场景特有的扰动与多样性
监控画面的数据增强,不能只做随机翻转和颜色抖动。我实际用下来最有效的几招是:亮度对比度扰动(模拟逆光和夜间红外)、运动模糊(模拟人走动时的拖影)、随机裁切和缩放(模拟不同镜头焦距)、叠加雨雾纹理(模拟室外场景)。
还有一个容易被忽略的增强是"人体区域外扩"。行为分类的输入是人体框裁剪图,如果裁剪得太紧,手部动作和手持物体会被切掉一半;太松,又会混入大量背景噪声。训练时我会对裁剪框做随机外扩,外扩系数在1.1到1.4之间浮动,让模型对"框得不够准"这件事有一定容忍度。这个细节直接影响线上效果,因为目标检测框不可能永远给得那么精确。
3. 模型选型与联动方案:CNN负责"在做什么",YOLO负责"有什么"
3.1 为什么不选端到端视频识别模型
市面上给人印象很深的动作识别模型,大多是端到端的视频理解模型,输入连续十几帧,输出一个行为类别。我在这个项目里刻意没有选这类方案,理由有三个。
第一,四类行为的判别不依赖强运动信息。打电话和玩手机的区别在"手机靠近耳朵还是靠近胸口",抽烟和摸下巴的区别在"手里有没有烟支",这些是静态的姿态和物体关系,不是瞬间的速度和轨迹。为它们引入时序建模,收益很低。
第二,监控场景对算力成本极其敏感。一路摄像头跑一个视频模型,GPU占用率会长期拉满,部署多路时成本直线上升。而"单帧检测 + 单帧分类 + 状态机平滑"这套组合,在保证效果的同时,计算量小一个数量级。
第三,工程排错方便。端到端模型出了问题,你很难知道是哪里错了;而两阶段模型的每一个环节都可以单独可视化、单独评测、单独优化。
3.2 CNN行为分类网络:ResNet50搭配输入设计的取舍
行为分类通道我选的是ResNet50作为骨干网络,输入尺寸224×224,输入内容是带外扩的人体框裁剪图。为什么不是更大的输入或更强的网络?因为行为分类吃的是"人体区域",本身分辨率就有限,盲目加大输入尺寸只会让模型去拟合背景细节,而不是行为特征。实测下来,224输入配合ResNet50,在四类行为上的分类精度已经足够满足现场需求,而推理速度快,CPU上单张裁剪图大约十几毫秒。
训练时我给类别加了权重,每类的权重和样本量成反比,同时使用了标签平滑,平滑系数取0.05。这个组合让模型在类别不平衡的情况下收敛更稳,输出概率也不会出现"过于自信"的极端值。分类阈值我最终定在0.6,低于这个值一律视为正常行为——宁可漏报,也不要拿一堆误报去骚扰管理人员。
3.3 YOLO目标检测的类别定义与适配小目标
目标检测通道用的是YOLO系列架构,类别定义只有四种:人、手机、烟支、杯瓶。没有把"抽烟""喝酒"这些行为直接放进检测类别,因为行为是姿态层面的概念,用检测框来表达反而别扭。
YOLO在这个项目里最大的挑战是烟支这类小目标。一根烟在1080p画面里的人体区域里,往往只有十几个像素宽。针对这个问题,我做了三个处理:一是输入分辨率用640,保证小目标至少能占据足够的像素;二是训练时重点回放"人体区域附近的小物体"样本,让模型学会在拥挤区域找小目标;三是推理时不直接对整图做细粒度检测,而是在人体框内部再跑一次小尺度的目标检测,专门找手机、烟支、杯瓶。
3.4 双模型联动规则:检测框如何参与行为判定
双模型不是简单地把分类结果和检测结果拼在一起,而是要设计一套联动规则。我用的是"物体先验 + 空间约束 + 分类置信度"三合一的判定逻辑:
- 抽烟:人体框内检出烟支 + 行为分类结果为抽烟 + 烟支中心点与嘴部区域的距离小于人体框对角线的一定比例,三个条件同时满足才告警;
- 打电话:人体框内检出手机 + 手机中心点与耳部区域的距离在阈值内;
- 玩手机:人体框内检出手机 + 手机中心点位于躯干前侧 + 行为分类结果为玩手机;
- 饮酒:人体框内检出杯瓶 + 杯瓶中心点与嘴部距离在阈值内。
嘴部区域和耳部区域不需要额外的关键点模型,我这里直接用人体框的几何先验估算:嘴部大约在人体框横向中间、纵向40%到55%的位置;耳部在人体框横向两侧边缘、纵向30%到45%的位置。这个估算很粗糙,但对联动规则来说已经够用,而且实现成本几乎为零。如果想要更稳定,后续可以再接一个轻量级关键点模型,把嘴、耳、手的位置精确化,效果会更好。
4. 训练与评估:参数、损失函数、指标口径
4.1 数据划分:按场景分层,避免同人同场景串数据
行为识别数据划分有个特别容易踩的坑:同一段视频里截出来的连续帧,内容高度相似,如果随机划分,训练集和验证集里会出现"同一个人的同一个动作",验证指标虚高得离谱。我的做法是按场景和人物ID做分层划分,保证同一个人的画面不会同时出现在训练集和验证集里。划分比例是70%训练、15%验证、15%测试,其中测试集单独锁死,只在最终评估时使用一次。
4.2 CNN分类器训练参数与损失设计
行为分类器我用的是标准的训练流程,关键参数如下:
| 参数 | 取值 | 说明 |
|---|---|---|
| 骨干网络 | ResNet50 | 预训练权重初始化 |
| 输入尺寸 | 224×224 | 人体框外扩后裁剪 |
| 优化器 | SGD | momentum 0.9,weight decay 5e-4 |
| 初始学习率 | 0.01 | 2个epoch warmup后生效 |
| 学习率调度 | 余弦退火 | 总训练50到60轮 |
| 批量大小 | 32 | 单卡即可 |
| 损失函数 | 交叉熵 | 按类别逆频率加权,标签平滑0.05 |
| 数据增强 | 翻转/颜色抖动/运动模糊/随机外扩 | 每轮随机组合 |
训练时有两个小技巧值得提一下。一是混合增强,把难例样本和普通正样本按一定比例混合送入训练,能显著降低模型对难例的过拟合。二是早停策略,不以验证集loss为准,而是以"四类行为各自的召回率最小值"为准,保证每一类行为都不掉队。
4.3 YOLO检测器的微调策略
检测器我没有从零训练,而是用公开的预训练权重做微调。微调分两段:前10个epoch冻结骨干网络,只训练检测头,让模型先学会把新类别映射到已有的特征空间;之后解冻骨干,用更小的学习率做全模型微调。学习率初始值取0.0001,同样用余弦退火。
训练时长大约120个epoch,数据增强开了马赛克增强和复制粘贴增强,这两项对"人群中检测小物体"的泛化能力帮助很大。后处理参数我固定在置信度0.25、NMS IoU 0.45。特别注意:烟支的置信度阈值不要和普通物体用同一个值,烟支目标小、特征弱,阈值太高会大量漏检,我单独把它降到了0.15,靠后续的空间约束规则来压误报。
4.4 评估指标:每小时误报率比准确率更重要
看行为识别模型的效果,不能只看准确率。准确率在类别不平衡时很容易骗人——98%的准确率可能只是因为正常行为样本太多了。我在项目里用的是三套口径:
- 按类别的精确率和召回率,四类行为分别统计,任何一个类别召回率低于85%都不算达标;
- 误报率按"路/小时"计算,也就是单路摄像头每小时产生多少条错误告警,这个指标直接决定管理人员会不会对系统失去信任;
- 告警延迟,从行为发生到触发告警的平均时间,目标是5秒以内。
三个指标里我最看重每小时误报率。技术上它可以做到很低,因为状态机可以帮忙过滤抖动,但状态机调得过严又会牺牲召回率和延迟。这三者之间的平衡,是行为识别系统能否真正被用起来的关键。
5. 推理部署:从单帧结果到稳定告警的状态机
5.1 抽帧策略与计算资源分配
公共场合行为识别不需要每帧都跑模型。实际部署时,我按每秒10帧的节奏抽帧分析,这个频率对"烟支靠近嘴边""手机举到耳边"这类持续数秒的行为已经足够,漏检的概率很低,计算开销却少了整整一个数量级。多余的算力留给追帧和事件录像存储,比盲目提高推理频率划算得多。
5.2 推理管线代码骨架
整个推理管线可以用下面这段伪代码概括:
def analyze_frame(frame): # 全图目标检测:先找所有人 dets = yolo_detect(frame) persons = [d for d in dets if d.cls == "person"] # 人体框裁剪,外扩后统一尺寸送分类 crops = [crop(frame, d.box, expand=1.3) for d in persons] labels = cnn_classify(batch_encode(crops)) # 在每个人体框内做小目标检测 for person, label in zip(persons, labels): objs = yolo_detect_in_box(frame, person.box, classes=["phone", "cigarette", "cup"]) state = tracker.update(person.id, label, objs) if state.triggered: save_event(frame, person, state)这段代码看起来简单,里面的细节却不少。外扩系数1.3是为了让手部动作和手持物体完整出现在分类输入里;分类是批量做的,一个画面里几十个人也只算一次推理;小目标检测是限制在人体框范围内做的,避免整图搜索导致算力浪费和误检。
5.3 状态机参数:确认、保持、冷却、去重
单帧模型输出不能直接作为告警依据,因为光线变化、目标抖动、遮挡都会让单帧结果跳变。我的做法是给每个跟踪目标维护一个四态状态机,参数如下:
| 参数 | 取值 | 作用 |
|---|---|---|
| 确认帧数 | 3帧 | 连续3帧判定为同一行为才进入告警态 |
| 保持帧数 | 15帧 | 告警触发后持续15帧不重复告警 |
| 冷却时间 | 5秒 | 同一目标同一行为告警后冷却5秒 |
| 事件去重窗口 | 60秒 | 同一目标同一行为的告警事件60秒内合并 |
这套参数的经验依据是:10帧每秒的抽帧率下,连续3帧大约相当于0.3秒,一个正常的"举杯入口"动作至少持续0.5秒以上,确认帧数设太低会放大噪声,设太高会漏掉快速动作。保持帧数配合冷却时间,既避免了单次行为产生刷屏告警,又不会漏掉目标反复在不同区域做出同一行为的情况。
5.4 性能实测与优化手段
我在一台消费级显卡上实测,1080p画面下整条管线的吞吐是每秒15帧左右,其中全图目标检测大约耗时30毫秒,人体区域分类批量推理大约20毫秒,小目标检测n次大约10到30毫秒。单路摄像头每秒10帧的抽帧需求远低于这个上限,所以理论上单卡可以跑多路摄像头,实际部署时还要留出解码、录像、事件写入的余量。
如果路由数再多,有两个优化方向:一是把人脸之外的区域降分辨率,只对包含人体的区域做高质量推理;二是把模型做量化加速。分类模型量化到整数精度后,精度损失大约一个点,推理速度能再翻一倍,对监控场景来说这个交换非常合算。
6. 误报排查实录:四类行为最容易翻车的边界
6.1 抽烟:烟支小、烟雾弱,先把目标找出来
最早一版方案里,我试图用"烟雾检测"来判断抽烟,上线第一天就翻车。室外风一吹,树叶阴影、汽车尾气、甚至水蒸气都会触发烟雾误报;室内无烟环境下,真抽烟的又因为没烟雾而漏报。烟雾根本靠不住。
后来我把判定逻辑彻底改掉:先靠YOLO在人体框内找烟支,烟支检出后再用空间约束判断"烟支是否靠近嘴部区域",分类器只作为辅助信号。改了之后误报率从每小时两位数降到了个位数。但烟支小目标依然会漏,我的应对是训练时专门做了"手持烟支"的密集负样本——把大量"手指捏着笔、捏着烟盒、捏着硬币"的画面喂给检测器,逼它学会区分真正的烟支轮廓。
6.2 打电话与玩手机:同一个物体,两种姿态
电话和手机在YOLO眼里是同一个类别,区分它们全靠空间关系和姿态。最典型的一个误报场景是:一个人手拿手机低头看,头稍微歪一点,手机靠近脸颊,分类器就会把它从"玩手机"判成"打电话"。
我处理这个问题的方式是引入手机与耳部区域的距离判定。规则很简单:手机中心点落在耳部区域附近判定为打电话,落在躯干前侧判定为玩手机,两个区域之间算模糊地带,交给分类器的置信度裁决。模糊地带的处理逻辑是:分类置信度高于0.75才放行,否则一律判为玩手机。因为从管理视角看,把打电话误判成玩手机的影响,远小于把玩手机误判成打电话。
6.3 饮酒与喝水的语义边界交给业务方
饮酒检测在视觉层面有一个本质矛盾:画面里看不到"酒",只有"容器入口"这个动作。有人端着透明水杯喝水,和端着白酒杯仰头,像素特征几乎一致。强行区分只会让模型学会过拟合训练集里的杯子形状,毫无泛化意义。
我的处理是:系统只输出"疑似容器入口行为",把是否属于饮酒的最终判定交给业务方。上线时需求方也认可了这个边界——管理者真正需要的是"有人在这里喝东西"这样一条现场线索,至于喝的是不是酒,现场人员比算法更清楚。这种把语义边界明确划出去的思路,在算法项目里非常重要,它能避免你陷入一个根本不可能解决的问题。
6.4 远距离、遮挡与逆光:监控画质的现实问题
监控画面里最无奈的情况是目标太小。一个人站在20米外,整个人体框在1080p图像里可能不到50像素宽,别说烟支,连手部动作都看不清。遇到这种问题,调模型没用,得从部署方案上解决。
我的建议是:对需要重点管控的区域配置独立的特写摄像头,或者让球机在检出可疑行为时自动变焦跟踪。另一种退而求其次的做法是用区域掩码,把干扰区域排除在分析范围之外,同时用跟踪算法在目标短暂被遮挡时保持状态,避免一个人走进柱子后面再出来就被当成新人重新计算告警。
7. 合规边界与持续迭代:把系统安全交出去
7.1 人脸匿名化与数据留存策略
公共场合的行为分析系统,人脸信息是无意中采集到的高敏感数据,工程上必须做脱敏处理。我在这套系统里加了一层轻量级人脸检测,在录像落盘和人脸区域裁剪之前,对人脸部分做模糊化处理。告警事件只保存带行为标签的片段,不保存完整原始视频流。
数据留存周期也做了限制,默认只保留30天,到期自动清理。访问告警片段需要单独的权限记录,系统里会留完整的操作日志。这些措施不是做样子,而是让这套系统能在一个明确、可控的边界里运行,避免"能检测行为"被误解成"可以无限采集和保存人员信息"。
7.2 分析区域用掩码限定,别让算法管不该管的地方
上线前配置现场区域时,我在系统里加入了多边形掩码,把卫生间门口、更衣室周边、员工临时休息区这些位置直接排除在分析范围外。掩码之外的目标不检测、不分类、不触发告警,边界本身也不在画面中显示。
这个小功能从技术上讲只是几行代码的事,但对系统能否被现场接受影响非常大。区域配置要能可视化、要能按摄像头独立设置、还有支持运营人员随时调整。行为分析系统不是越灵敏越好,而是该管的管住、不该管的绝对不碰。
7.3 数据回流闭环与模型迭代节奏
系统上线只是开始。我搭建了一个回流闭环:现场告警和误报截图都会进入一个审核界面,管理员每天花十分钟标记"对/错",标记过的数据自动进入下一轮训练集。每周做一次增量训练,每月做一次完整评估和模型发布,版本都保留回滚能力。
这套循环对行为识别尤其重要,因为现场的物体和姿态分布永远比训练集丰富。我见过太多项目上线后就不管了,三个月后准确率一路下跌,最后被业务方弃用。实际上,只要把回流闭环跑起来,每隔一段时间把新难例喂回模型,系统的稳定性就能一直维持,甚至越用越准。
最后再分享一个体会:行为识别系统的难点从来不在模型本身,而在数据口径的清晰、边界规则的设计和持续迭代的纪律。把这三件事做好,CNN加YOLO这套组合完全是够用的,不必一上来就追逐复杂的视频模型。项目收尾时我把整套管线沉淀成了可复用的组件,后面再接同类需求,标注规范、联动规则和状态机参数可以直接复用,省下来的时间比当初调模型的时间还多。