1. 这个项目到底要解决什么问题:三类行为的识别难点拆解
1.1 为什么行为检测比"人车识别"更难
我第一次接到这个需求时,第一反应是"这不就是目标检测嘛,框三类目标而已"。实际做下来才发现,行为检测跟传统意义上的物体检测完全是两码事。
拿人车识别举例:车有固定的外形、固定的尺寸比例,人虽然在姿态上有变化,但整体结构仍然是"头+躯干+四肢",类别之间的差异非常明显。但你让模型去识别"一个人正在吸烟"、"一个人正在喝水"、"一个人正在打电话",这三个目标有两个共同特点:第一,关键物体都很小——烟头可能只有几十个像素,手机和杯子在远距离监控里也不大;第二,行为本身的"画面特征"不在物体本身,而在物体和人体部位的相对关系上——烟不在手上而是在嘴附近,杯子举起来倾向头部,手机贴在耳边。这就意味着模型不能只学会"认出烟",还得学会"认出烟出现在特定位置、特定姿态下"。
1.2 三类行为各自的坑
先说吸烟。烟支本身是细长条,颜色通常是白色或黄色,在手部背景复杂的情况下特征非常弱。更难的是吸烟动作是一个连续过程:手持烟自然下垂、抬手、放进嘴里、放下。真正需要判定为"吸烟行为"的其实是"烟靠近嘴部"这个关键帧,但视频里大部分时间是手拿着烟在别的位置。如果模型把"手持烟"也判成吸烟,误报率会高到没法用。
喝水的问题在于杯子的多样性——纸杯、玻璃杯、保温杯、透明塑料杯,材质不一样,视觉特征差异极大。透明玻璃杯装水后几乎就和背景融为一体,模型很容易漏检。再叠加一个因素:喝水动作通常很快,从举起杯子到放下可能不到两三秒,如果摄像头帧率低或者推理速度慢,这个动作可能直接被跳过。
打电话相对好一些,因为手机本身形状比较固定,但问题在于遮挡——手机贴在耳朵上时,手部会遮挡屏幕的大部分区域,模型看到的其实是一只手捂在脸旁边。这时候"手机的特征"丢失,模型容易被误导成"手摸脸"或者"手抱头"等其他动作。
1.3 破局思路:检测"行为关键区域"而不是"完整行为"
想明白这些问题之后,我调整了策略:不去搞姿态估计算"手到嘴距离",也不去试图框出整个行为过程的完整状态,而是把目标定义成"行为关键区域"。具体来说:
- 吸烟:检测目标为"嘴部附近的烟支"(而不是任意位置的烟)
- 喝水:检测目标为"手部举起并倾斜的杯子"
- 打电话:检测目标为"贴在耳边的手机"
这样做有一个很实际的好处:标注难度和训练目标都变得清晰。YOLO系列模型天生擅长的是"定位+分类",你把"行为关键区域"定义清楚了,它就能学会。后续在部署时我还在后处理里加了一个"连续帧确认"逻辑,进一步过滤掉单帧误检,这个后面细讲。
2. 算法选型:为什么最终锁定YOLOv8
2.1 候选方案横向对比
做行为检测,市面上能用的开源方案其实不少,我当时的挑选范围包括传统视觉方案、Faster R-CNN、SSD、YOLOv5、YOLOv7、YOLOv8。把这些放在一起对比:
| 方案 | 推理速度 | 训练难度 | 生态成熟度 | 在这个场景里的实际表现 |
|---|---|---|---|---|
| 传统视觉(肤色检测+背景差分) | 快 | 低 | 低 | 光照一变化就崩,误检率极高 |
| Faster R-CNN | 慢 | 中 | 中 | 精度尚可,但实时性不达标 |
| SSD | 中 | 中 | 中 | 小目标检测能力偏弱,烟头很容易漏 |
| YOLOv5 | 快 | 低 | 高 | 能达到要求,但收敛过程略折腾 |
| YOLOv7 | 快 | 低 | 中 | 速度快,部署资料不如v8齐 |
| YOLOv8 | 快 | 低 | 高 | 精度、速度、部署体验综合最优 |
传统视觉第一个被淘汰,原因是室内外光照一变化,肤色阈值就失效了。Faster R-CNN的精度确实好,但单帧推理时间太长,摄像头实时检测基本没法用。SSD对小目标不友好,而我们的烟头、远处的手机恰恰都是小目标,直接排除。
剩下的几个YOLO系列版本里,我最后选了YOLOv8。
2.2 YOLOv8在这个场景中的四个关键优势
第一是anchor-free检测头。YOLOv5和更早版本需要预设一组锚框尺寸,如果预设的尺寸跟你的目标物大小不匹配,就得做聚类来调整。YOLOv8改成了直接预测中心点和宽高,省掉了锚框调参这一步。我当时标注完数据后,没做任何锚框聚类,直接训练,效果就很稳。
第二是C2f模块和梯度流设计。这个结构上的改进让深层特征提取更充分。我自己的实测感受是,在同样迭代轮数下,YOLOv8的收敛速度比v5明显更快,训练到后期loss曲线也更平滑。
第三是正负样本分配策略。YOLOv8在训练时会动态决定哪些anchor算正样本,不像老版本那样完全靠阈值一刀切。我标注的时候有一些边界框其实画得并不完美,比如烟头框稍微偏大了一点,YOLOv8的动态匹配机制会把它当作"可以学习的正样本"来处理,容错能力明显更好。
第四是生态省心。ultralytics这个库把训练、验证、导出、推理全封装好了,一条命令就能跑训练,模型能直接导出成ONNX和TensorRT格式。对于我这种"不想跟底层细节死磕"的人来说太关键了。做这种小目标行为检测,时间应该花在数据和调优上,不应该花在复现算法上。
2.3 模型规格的取舍:nano还是small
YOLOv8系列按参数量从大到小有x、l、m、s、n几个规格。我当时在nano和small之间纠结了很久。nano在CPU上跑得更快,但小目标检测精度会差一些;small精度好,但对硬件要求高。
最后的决定是:开发阶段用small做实验、跑精度,部署阶段根据目标设备的性能决定。如果目标设备是带GPU的,直接上small;如果是纯CPU,先用nano跑通,再考虑量化压缩。这个"开发用大模型、部署用小模型"的思路,我觉得在行为检测项目里是通用的——先确保功能做对了,再考虑速度。
3. 数据集从零到可训练的全流程
3.1 数据来源与采集覆盖维度
模型能不能用,数据是决定性因素。我整理数据时主要走了两条路:一是收集公开的抽烟和手机使用相关图片数据,二是在实际场景里自采照片。
自采部分我总结了一套覆盖维度清单,照着采集能显著减少后期"某类场景下识别不稳定"的问题:
- 光照维度:室内日光灯、室外晴天、傍晚弱光、逆光
- 角度维度:摄像头平视、俯视、斜俯视
- 距离维度:近处(约3米)、中等距离(约5到8米)
- 人的维度:不同体型、不同肤色、是否戴帽子/眼镜
- 道具维度:香烟(不同长短)、水杯(纸杯、透明杯、保温杯)、手机(不同大小、是否带壳)
这套维度看起来繁琐,但非常值得做。我第一版数据只顾着收集"动作标准"的照片,结果模型在逆光和戴帽子的情况下误报率明显上升,后来补了这些难例数据才压下来。
3.2 YOLO格式标注:每一行文本背后是什么
YOLO数据集的标注格式是这样的:每一张图片对应一个txt文本文件,文件名跟图片同名,文本里每一行代表一个目标物体。一行包含五个数值:
类别id 中心点x坐标 中心点y坐标 目标宽度 目标高度注意,坐标全部是归一化的,取值范围0到1,也就是用坐标值除以图片宽高。比如一张1280x720的图片里,一个目标中心点在(640, 360),宽为200,高为300,那么这一行就是:
0 0.5 0.5 0.1562 0.4167我第一版标注就犯了个低级错误:小目标框太大。标注烟头时下意识把整个手部一起框进去,导致模型学习到的特征里"手"的权重过大,后来推理时发现手靠近嘴但没有烟也会被误判成吸烟。正确做法是:框贴着目标物边界走,宁可稍微紧一点,也不要松。如果有些目标是"半遮挡"状态,比如手机被手挡住一半,就按可见部分来框。
3.3 数据增强策略:多不等于乱
YOLOv8的ultralytics框架内置了Mosaic增强、HSV色彩扰动、随机翻转等策略。Mosaic能把四张图拼成一张训练,变相增加了每张训练图里的目标数量,对学习"小目标"特别有帮助——因为我们场景里的烟头和手机在远处确实非常小。
但增强不能无脑全开。我踩过一个坑:喝水杯子里有透明玻璃杯,HSV色彩增强往"偏蓝偏亮"方向拉太多之后,模型把很多反光物体误判成了杯子。后来我把HSV增强强度调低,并且没有开垂直翻转——因为我们采集的数据几乎全是平视或俯视角度,垂直翻转会把"上方"和"下方"的概念搞乱,杯子原本在嘴下面,翻过去就跑到眼睛上面了,这反而不符合实际语义。
类别不平衡也必须处理。实话说,打电话的照片最好收集,吸烟其次,喝水的"关键区域"动作(杯子倾斜、靠近嘴)样本最少。我的解决办法是:对喝水类别做过采样——在训练配置里给这个类别更高的采样权重,同时在增强时对它应用更强的随机旋转,把有限的样本通过几何变化扩充出更多变体。
4. 训练实战:超参设置、指标分析与模型导出
4.1 环境准备与训练命令
环境部分其实没什么门槛,我用的配置大致是:Python 3.9,PyTorch 2.x,显卡只是普通的入门级8G显存。安装ultralytics一步搞定:
pip install ultralytics训练数据准备成一个标准目录结构:
datasets/ ├── data.yaml ├── train/ │ ├── images/ │ └── labels/ └── val/ ├── images/ └── labels/data.yaml里面写清楚类别名称和路径:
train: datasets/train/images val: datasets/val/images nc: 3 names: ['smoking', 'drinking', 'calling']然后一条命令启动训练:
yolo detect train data=data.yaml model=yolov8s.pt epochs=100 imgsz=640 batch=16 patience=204.2 关键超参:为什么这样设置
有几个参数我需要单独解释一下。
imgsz(输入图片尺寸)我选的是640而不是1280。很多人觉得大分辨率必然更好,确实大图能保留更多小目标细节,但显存占用是平方级增长的,推理速度也会显著下降。在监控场景下,摄像头分辨率通常是1080p,模型训练时用640学习到的特征,推理时对原始大图做缩放,小目标信息仍然有损失。一个更实用的做法是:训练用640,推理时对画面按区域切块(tile),把大图切成小块分别检测,这个方案后面详细说。
batch(批大小)设置成16,配合8G显存刚好合适。如果显存不足,优先降低batch而不是降低imgsz。我的经验是batch过小(比如2到4)会导致训练收敛不稳,因为梯度估计噪声太大。
patience=20表示验证集指标连续20个epoch不提升就提前停止训练。这个参数非常重要——YOLO训练到后期非常容易过拟合,如果没有early stopping,模型会从"泛化不错"慢慢退化成"只认识训练图",而你自己很难察觉。
4.3 结果指标怎么看:不要只盯着mAP50
训练结束,ultralytics会输出一组指标:mAP50、mAP50-95,以及每类的precision和recall。很多初学者只看mAP50,但在这个项目里,mAP50高但实际视频中频繁误检的情况非常常见。
我的经验是重点看两个东西:
一个是每类的recall(召回率)。recall低意味着该类别大量目标根本没检出来。我训练时"drinking"类的recall一度只有0.62,远低于另外两类的0.8以上。这不一定是模型的问题,很可能是喝水样本太少或者标注框不一致。后来把样本补上、标注统一后,recall明显回升。
另一个是混淆矩阵。YOLO训练完会在验证集上生成confusion_matrix.png。我第一次看到结果时发现,smoking类别的大量误检其实被分到了"background"类里面——也就是说模型把很多背景区域误判成了烟。这说明负样本(没有目标的普通图片)不够。往训练集里加入一批纯背景图,再标注成empty(也就是不标注任何目标),这个误判立刻少了很多。
4.4 模型导出:从PyTorch到ONNX
训练好的best.pt可以直接在Python里用ultralytics加载推理,但对实际部署来说,导出成ONNX更通用,加载速度也更快。导出命令非常简单:
yolo export model=best.pt format=onnx opset=12导出后的ONNX文件可以用onnxruntime加载:
import cv2 import numpy as np import onnxruntime as ort session = ort.InferenceSession("best.onnx") input_name = session.get_inputs()[0].name在CPU上,ONNX Runtime比直接跑PyTorch会快不少。如果你手里的设备是GPU,还可以继续导出成TensorRT格式,但那一步对环境和显卡驱动要求比较苛刻,属于锦上添花,不必一开始就折腾。
5. UI界面与实时推理:把模型变成能用的工具
5.1 界面框架选型:PyQt5还是Gradio
模型训完之后,真正的挑战来了:怎么让一个非技术用户顺畅地使用这个系统?这就轮到UI界面出场。
我当时对比了两种方案:Gradio和PyQt5。Gradio是Python里非常方便的开发库,几行代码就能起一个网页界面,摄像头画面也能直接传上去,看起来很有现代感。但它的定制能力有限,如果我想做"每一路摄像头独立配置告警区域""检测结果按小时统计报表"这类功能,Gradio会变得很别扭。
PyQt5做起来复杂得多,但适合"真正做产品"的场景。最后的选型是根据使用场景定的:
| 对比维度 | Gradio | PyQt5 |
|---|---|---|
| 开发速度 | 快,几小时能跑通 | 慢,需要设计界面和信号槽 |
| 定制自由度 | 中 | 高 |
| 摄像头多路接入 | 需要额外适配 | 天然支持,QThread多线程管理方便 |
| 打包分发 | 相对简单 | 需要PyInstaller处理 |
| 适合场景 | 快速演示、实验室验证 | 现场部署、长期运行的安防工具 |
如果只是给导师或同学演示一下模型效果,Gradio够了。但如果你想把这套系统放到机房或者值班室,让它连续跑几天不出乱子,PyQt5才是更稳的选择。这篇文章的场景是后者。
5.2 推理主循环与线程设计
用PyQt5做实时摄像头UI,一个最大的原则是:模型推理绝对不能放在界面主线程里。PyQt5的界面是事件循环驱动,如果在界面刷新过程中去跑模型推理,界面会直接卡死,摄像头画面一帧一帧地跳。
我的结构是这样的:一个线程专门读摄像头帧,一个线程跑模型检测,主线程只负责把检测结果画到界面上。摄像头读帧和推理之间用一个队列传递数据,如果推理速度跟不上摄像头帧率,就丢掉当前帧直接处理下一帧——这比让程序越积越慢要好得多。
核心代码骨架大概是这个思路:
class VideoThread(QThread): frame_ready = pyqtSignal(object) def run(self): cap = cv2.VideoCapture(0) while self.isRunning: ret, frame = cap.read() if ret: self.frame_ready.emit(frame)推理线程拿到帧后,调用ONNX模型推理,得到检测框和类别,把结果通过另一个信号传回主线程。主线程用QPainter在QLabel上绘制矩形框和标签文字。这个分离结构还有一个好处:如果后面要接多路摄像头,每个摄像头对应一对"读取线程+推理线程",互不干扰。
5.3 告警与统计逻辑:如何避免"假报警"
模型输出的是每一帧的检测结果,但如果直接把每个检测框都当作一次"行为告警",系统会非常吵——因为模型偶尔会在某个角度下把手机框错判成烟头。
我的处理方式是加入"连续帧确认"机制:同一个目标框在连续的N帧内,如果都检出同一类别且中心点偏移不大(IoU阈值判断是同一个目标),才判定为一次有效行为。比如抽烟动作,连续5帧都检出"嘴部附近有烟",这时候才在界面上弹告警。喝水和打电话同理。
此外,我还在界面里加了两个统计维度:一个是各类行为的总次数,另一个是按时间段生成的行为频次表。这对实际场景非常有用——比如管理者最关心的可能不是某人某刻喝了口水,而是某个区域连续三个小时里打电话行为的高峰时段。这种结构化记录能帮决策者发现真正的管理问题,而不是给一堆乱七八糟的单帧检测框。
6. 实测部署中的坑与解决记录
6.1 光照和反光的干扰
第一个坑来自光照。现场摄像头装在室内靠窗位置,下午两三点阳光直射进来,人的脸部在逆光下几乎是一片暗。模型对"吸烟"的检测率骤降——因为烟支在阴影里变成了灰黑色,跟背景融到一起了。
我用的解决办法分两步。第一步是在图像预处理环节对低亮度帧做自适应直方图均衡化(CLAHE),把暗部细节提亮。第二步是回到数据层面,专门补了一批逆光和暗光环境下的训练样本。双管齐下之后,暗光场景的漏检率明显降低。这个经历也说明了一个道理:行为检测系统的鲁棒性,一多半要靠"模拟真实场景的数据"来支撑,光调算法是不够的。
6.2 类间混淆的修正
第二个典型问题是"喝水"和"打电话"互相误判。从画面上看,当一个人把杯子举到嘴边时,手部位置在脸的下方;打电话时手贴在耳朵附近。但某些角度下,比如斜俯视摄像头,两个位置在二维画面里非常接近。模型的目标特征是"手部附近有一个物体",物体类别特征不明时,就会混淆。
我在后处理里加了位置先验修正:如果检出的是"drinking"类别,且目标框中心点位于图片上部(靠近耳朵区域),则降低它的置信度;如果检出的"calling"类别目标中心点位于图片下部,同理降低。这种基于先验知识的后处理,代码量不大,但对精度提升很有帮助。
6.3 小目标漏检与切块推理
第三个坑是最顽固的:远距离小目标漏检。摄像头装在车间或者走廊角落时,人的脸部可能在整幅1080p画面里只占很小的区域,烟头更是只有十几个像素。YOLOv8在640分辨率下训练,对这种极小目标天然不敏感。
尝试过直接提高推理分辨率到1280,效果有提升但速度几乎减半。最后采用的方案是"切块推理(tiling)":先在全图上做一次低分辨率检测,找到可能有人脸/手部出现的候选区域,再把这些区域切出来放大,单独过一遍模型检测小目标。这个两级策略在工程上很常见,性能开销相对可控,而且烟头和手机的检测率提升非常明显。
6.4 CPU推理性能的兜底策略
最后说说性能。这套系统如果是跑在带GPU的工控机上,完全不用操心帧率问题。但实际部署中很多现场只有普通办公电脑,CPU推理就成了瓶颈。
在纯CPU环境下,我最后的配置是:YOLOv8n + ONNX Runtime + 隔帧处理(每2帧处理1帧)+ 显示线程与推理线程分离。这样在普通电脑上能保持画面流畅且检测结果基本连续。如果检测响应速度还不够,可以考虑把画面缩放后再送模型推理,或者用更多线程跑ONNX Runtime(需要按实际情况测试,线程并不是越多越快)。
这个优化思路的核心是"接受不完美":行为检测任务对单帧的精确度容忍度较高,当前帧没检测到,下一帧再补上,配合前面说的连续帧确认机制,最终效果依然可用。这也是我在这个项目里学到的一个重要经验——实时检测系统不是做单帧竞赛,而是要在整个时间序列上做稳定判断。
最后分享一个个人体会。这套系统从训练到部署,真正花时间的地方其实不在模型本身,而在"认清场景中的难点"这一步。行为检测的关键是你得先想明白:你的摄像头装在什么位置、灯光怎么样、目标离得有多远、检测结果要用来做什么。这些信息决定了你要怎么定义检测目标、怎么采集数据、怎么做后处理。把这些想透了,YOLOv8这样一个成熟的框架加上一份用心整理的数据集,就已经足够撑起一个能落地使用的系统。后续如果想扩展,可以考虑加入行为时序语义,比如用短时间窗口内的检测序列来判断更复杂的连续动作,也可以把单摄像头换成多视角融合。先把现在这套跑稳,再谈更大也来得及。