简介:本资源面向计算机、人工智能、自动化等专业学生与开发者,提供一套基于YOLOv9的监控场景员工玩手机行为识别检测系统,可用于毕业设计、课程项目或企业安防场景的二次开发。压缩包共192个文件,约75.25MB,包含83个Python源码、30个YAML配置文件、3个pt权重文件,以及训练日志、指标曲线图、测试图片与标注数据等,覆盖从数据准备、模型训练到推理测试的完整流程。资源内附详细运行教程,指导完成Anaconda与PyCharm环境配置、依赖安装、数据集yaml修改、train_dual.py训练参数调整及detect_dual.py推理测试,并给出命令行训练示例与常见参数说明。目前已有160人学习关注。读者可获得可直接运行的YOLOv9玩手机检测工程、预训练模型、指标曲线与排错思路,便于快速复现结果并迁移到自有数据集。
1. 监控场景玩手机检测:从 YOLOv9 权重到可复现的 Python 识别系统
厂区监控室里,值班主管盯着一面 16 路电视墙,最头疼的不是有人打瞌睡,而是产线工人低头刷手机——等发现时人已经刷了十分钟。靠人盯屏不现实,靠通用目标检测模型直接跑也不行:手机在画面里往往只有几十像素,还经常被手、工服、桌面遮挡,模型要么漏检,要么把工牌、对讲机、螺丝刀误判成手机。这套「监控场景玩手机检测-基于 YOLOv9 的员工玩手机识别检测系统」要解决的正是这个具体问题:用 YOLOv9 训练一个专门识别「人 + 手机 + 玩手机姿态」的检测器,配上 Python 推理脚本、训练好的模型权重和指标曲线,让一套监控视频流能自动标出违规玩手机行为。它适合两类人:一类是想把 YOLOv9 真正落到工业安防场景的算法工程师,另一类是手里有监控数据、想跑通一套完整检测流程的 Python 开发者。下面按「数据怎么准备 → 模型怎么训 → 指标怎么看 → 推理怎么接 → 坑在哪」的顺序,把这条链路拆开讲清楚。
2. 玩手机检测的数据集构建与 YOLOv9 标注规范
2.1 为什么通用 COCO 模型在监控场景下会翻车
先说清楚一个反直觉结论:拿 COCO 预训练的 YOLOv9 直接推理监控画面,玩手机这一类几乎不可用。原因有三层。第一层是尺度问题,COCO 里手机的平均像素面积在 100×100 以上,而 1080P 监控画面里,一个工人手里的手机可能只有 30×50 像素,YOLOv9 的 P3 特征层虽然对小目标友好,但预训练权重里根本没有「小手机」这种分布。第二层是场景差异,监控是俯视或斜俯视视角,手机常处于「被手握住、屏幕朝向身体」的姿态,和 COCO 里「手持手机正对镜头」的样本分布完全不同。第三层是类别定义,通用模型只检测「cell phone」这个物体,而业务要的是「玩手机行为」,也就是人 + 手机 + 头部低垂姿态的组合,单靠一个 cell phone 框无法判定违规。
所以正确做法是重新定义检测类别。我一般会设三个类:person(人)、phone(手机)、playing(玩手机行为框,覆盖人和手机的整体区域)。前两类用于定位,第三类用于直接判定违规。这样后处理时只要playing类置信度超过阈值就报警,逻辑简单,误报也容易控制。如果数据量足够,也可以只标playing一类,做成单类检测器,推理更快,但可解释性差一些,排查误报时不好定位是手机没检到还是姿态判断错了。
2.2 从监控视频抽帧到 YOLO 格式标注的完整流程
数据来源通常是现场监控录像或公开的工地、车间视频。第一步是抽帧,不要每秒都抽,相邻帧几乎一样,浪费标注成本。我一般按 2~3 秒一帧抽,一个 10 分钟的视频抽 200~300 帧就够。用 OpenCV 写个脚本:
import cv2 import os # 抽帧脚本:每 interval 帧保存一张,避免冗余 video_path = "monitor.mp4" out_dir = "frames" os.makedirs(out_dir, exist_ok=True) interval = 60 # 假设 30fps,60 帧约 2 秒一帧 cap = cv2.VideoCapture(video_path) idx = 0 saved = 0 while True: ret, frame = cap.read() if not ret: break if idx % interval == 0: # 可选:缩放到 1280 宽,减小标注和训练压力 h, w = frame.shape[:2] if w > 1280: frame = cv2.resize(frame, (1280, int(h * 1280 / w))) cv2.imwrite(f"{out_dir}/frame_{saved:05d}.jpg", frame) saved += 1 idx += 1 cap.release() print(f"共保存 {saved} 帧")这段脚本的关键参数是interval,它决定抽帧密度。30fps 视频里 60 帧约等于 2 秒,动作变化能被覆盖又不至于重复。resize到 1280 宽是权衡:太小手机像素丢失,太大标注和训练都慢。抽完帧后用 LabelImg 或 X-AnyLabeling 标注,导出 YOLO 格式,每张图对应一个.txt,每行是class_id cx cy w h,坐标全部归一化到 0~1。
标注时有三条血泪经验。第一,phone框要贴着手机可见部分画,不要把手也算进去,否则模型学到的是「手」而不是「手机」。第二,playing框要覆盖人头到手机的整体区域,不要只框手机,否则和phone类混淆。第三,遮挡超过 70% 的手机不要标,标了反而引入噪声。标完检查一遍类别平衡,如果playing样本不到person的十分之一,后面训练要加过采样或 focal loss。
2.3 数据集划分与 data.yaml 配置
标注完成后按 8:1:1 划分训练、验证、测试集。目录结构建议这样组织:
dataset/ images/ train/ val/ test/ labels/ train/ val/ test/ data.yamldata.yaml是 YOLOv9 训练入口,内容如下:
path: ./dataset train: images/train val: images/val test: images/test nc: 3 names: ['person', 'phone', 'playing']nc是类别数,必须和标注里的class_id最大值一致,否则训练时报 index 越界。names顺序要和标注时类别顺序完全对应,错一个位置整个模型就学歪。常见错误是把path写成绝对路径后换机器跑不起来,建议用相对路径,训练时在项目根目录执行。
注意:如果监控画面是红外或夜间模式,白天和夜间样本要分别抽帧并混合进训练集,否则模型在夜间几乎全漏。夜间样本至少占 20%。
3. YOLOv9 训练配置:从预训练权重到指标曲线解读
3.1 YOLOv9 选型理由与模型结构关键点
YOLOv9 相比 YOLOv8 最大的变化是引入了 PGI(可编程梯度信息)和 GELAN 结构,简单说就是在浅层保留更完整的梯度信息,让小目标检测更稳。这对玩手机检测很关键,因为手机本身就是小目标。实际选型时,如果显存只有 8G,选yolov9-t或yolov9-s;显存 12G 以上可以上yolov9-m。不要一上来就yolov9-e,参数量大、训练慢,监控场景的数据量通常撑不起这么大的模型,过拟合风险高。
预训练权重用官方 COCO 版本做初始化,虽然 COCO 没有玩手机类,但底层边缘、纹理特征可迁移,比从头训收敛快很多。常见做法是冻结 backbone 前几轮,先训 head,再解冻全量微调。YOLOv9 官方代码里通过--freeze参数控制,一般设--freeze 10冻结前 10 层。
3.2 训练命令与超参数设置
假设你已经拉好 YOLOv9 代码环境,训练命令如下:
python train_dual.py \ --weights yolov9-s.pt \ --data dataset/data.yaml \ --img 640 \ --batch 16 \ --epochs 150 \ --device 0 \ --workers 8 \ --freeze 10 \ --patience 30 \ --name playing_phone逐个参数说明。--img 640是输入分辨率,监控小目标建议用 640 起步,如果手机还是漏检可以升到 960,但显存和速度会明显下降。--batch 16在 8G 显存上跑 yolov9-s 比较稳,爆显存就降到 8。--epochs 150配合--patience 30,意思是 30 轮验证指标不提升就早停,避免无效训练。--freeze 10冻结前 10 层,数据量小于 5000 张时强烈建议加。--workers 8是数据加载线程,和 CPU 核数相关,设太大反而拖慢。
训练过程中重点看三个输出:box_loss、cls_loss、mAP50。box_loss下降说明框位置在收敛,cls_loss下降说明类别判断在变好。如果box_loss降但cls_loss震荡,通常是类别不平衡,playing样本太少,需要加过采样。如果两个 loss 都不降,检查学习率,YOLOv9 默认 lr0=0.01,小数据集可以降到 0.001。
3.3 指标曲线怎么读:mAP50、mAP50-95 与 PR 曲线的实际含义
训练完在runs/train/playing_phone/下会生成results.png、PR_curve.png、confusion_matrix.png。指标曲线不是用来看「好不好看」的,是用来定位问题的。
mAP50是 IoU 阈值 0.5 时的平均精度,反映「框大致对不对」。玩手机检测里mAP50到 0.85 以上基本可用。mAP50-95更严格,要求框位置非常准,这个值通常比mAP50低 0.2~0.3,如果低太多说明框回归不准,可能是标注框抖动大。PR_curve看每个类的曲线,如果phone类曲线明显低于person,说明手机样本不够或太小。confusion_matrix最有价值,能直接看出phone被误判成playing的比例,如果这个比例高,说明两类定义重叠,需要重新界定标注规则。
我一般会重点看confusion_matrix的归一化版本,横轴真实类、纵轴预测类。理想情况是对角线深、其他浅。如果background列很深,说明误报多,需要提高推理置信度阈值或补充负样本。
提示:指标曲线里的
val/box_loss如果后期反弹,是过拟合信号,回退到反弹前的 epoch 权重,或者加数据增强(mosaic、mixup)。
4. Python 推理脚本:把训练好的模型接到监控视频流
4.1 推理环境依赖与模型加载
训练完得到best.pt,推理侧只需要 ultralytics 或 YOLOv9 官方推理接口加 OpenCV。依赖清单:
pip install torch torchvision opencv-python numpy如果用的是 YOLOv9 官方仓库,推理脚本基于detect.py改;如果用 ultralytics 封装版,直接YOLO('best.pt')即可。我一般用后者,接口稳定、跨平台好。加载模型时注意device参数,有 GPU 用cuda:0,没有就cpu,CPU 推理 640 分辨率大概 5~8 FPS,勉强能跑但实时性差。
4.2 单帧检测与玩手机行为判定逻辑
核心推理代码如下:
from ultralytics import YOLO import cv2 model = YOLO("best.pt") CONF = 0.45 # 置信度阈值 IOU = 0.5 # NMS IoU 阈值 PLAYING_ID = 2 # playing 类索引 cap = cv2.VideoCapture("monitor.mp4") while True: ret, frame = cap.read() if not ret: break results = model.predict(frame, conf=CONF, iou=IOU, verbose=False) for r in results: for box in r.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) x1, y1, x2, y2 = map(int, box.xyxy[0]) if cls_id == PLAYING_ID and conf > CONF: # 判定为玩手机,画红框并报警 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(frame, f"PLAYING {conf:.2f}", (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow("detect", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()逻辑说明:model.predict返回每帧的检测结果,遍历boxes拿到类别、置信度和坐标。只有cls_id == PLAYING_ID且置信度超过阈值才判定违规。CONF=0.45是经验值,太低误报多,太高漏报多,实际部署时根据现场误报率调。IOU=0.5控制 NMS 合并重叠框,玩手机场景里人和手机框可能重叠,IoU 设太高会保留重复框。
4.3 多路视频流与报警去抖
单路跑通后,实际监控是 8 路、16 路。多路推理有两个做法:一是多进程,每路一个进程独立跑;二是批处理,把多路帧拼成一个 batch 送模型。前者简单但吃内存,后者效率高但要改推理接口。我一般用多进程,每路一个Process,通过队列把报警事件汇总到一个主进程写日志。
报警去抖很重要。玩手机动作可能持续几秒,逐帧报警会刷屏。做法是维护一个状态字典,同一路同一区域连续 N 帧检测到playing才触发一次报警,N 一般设 15~30(约 0.5~1 秒)。报警后设冷却时间,比如 30 秒内同一区域不重复报。这样日志干净,值班人员也不会被轰炸。
注意:多路推理时 GPU 显存是瓶颈,16 路 640 分辨率 yolov9-s 大概需要 10G 以上显存。显存不够就降分辨率到 480 或换更小的模型。
5. 玩手机检测落地避坑:5 个真实踩坑记录
5.1 坑一:手机太小导致漏检率居高不下
现象:训练指标mAP50到 0.88,但现场推理时手机漏检严重,尤其是远距离工位。原因:训练集里手机像素分布和现场不一致,抽帧时缩放到 1280 宽,但现场摄像头是 1080P 且工人距离更远,手机实际像素更小。解决:一是训练时开启--img 960或更高分辨率,二是抽帧时不要缩放,保留原始分辨率,三是补充远距离样本。如果还不行,可以在推理前对画面做 1.5 倍放大再送模型,代价是速度下降。
5.2 坑二:工牌、对讲机被误判成手机
现象:模型把胸前工牌、手持对讲机识别成phone,进而触发playing误报。原因:这些物体在监控画面里都是矩形小目标,纹理和手机相似,标注时如果没标这些负样本,模型没见过就会乱判。解决:专门收集一批含工牌、对讲机、螺丝刀的负样本图,标注时只标person,不标phone,让模型学会区分。另外可以在data.yaml里加一个other类,把这些干扰物单独标出来,效果更稳。
5.3 坑三:夜间红外画面几乎全漏
现象:白天检测正常,夜间切红外后playing类几乎检不到。原因:训练集全是白天彩色样本,红外画面是灰度、对比度低、纹理丢失,模型分布外泛化失败。解决:夜间样本必须进训练集,至少占 20%。如果夜间样本难标,可以用白天样本做灰度化 + 对比度扰动增强,模拟红外效果,但效果不如真实红外样本。
5.4 坑四:置信度阈值设太高导致漏报
现象:为了压误报把CONF调到 0.7,结果大量真实玩手机行为漏报。原因:playing类本身置信度分布偏低,因为姿态多样、遮挡多,模型给的分普遍不如person高。解决:不要一刀切设阈值,按类设。person可以 0.5,playing设 0.35~0.4,配合去抖逻辑压误报。另外看PR_curve选阈值,找 F1 最高的点。
5.5 坑五:换摄像头后模型性能骤降
现象:同一套模型,A 车间跑得好,换到 B 车间误报漏报都变多。原因:摄像头安装角度、焦距、光照不同,画面分布变了。解决:每换一个场景,至少抽 100 帧做验证,如果指标掉超过 10%,需要补充该场景样本做微调。微调时学习率调小到 0.0001,训 20~30 轮即可,不要从头训。
6. 进阶技巧:用滑动窗口滤波稳定玩手机判定
最后一章讲一个我实际部署时最常用的技巧:滑动窗口滤波。逐帧判定玩手机有个玄学问题——模型偶尔会在某一帧把手机误判成playing,或者真实玩手机时中间有几帧漏检,导致报警断断续续。滑动窗口滤波就是用一个固定长度的队列存最近 N 帧的判定结果,队列里playing占比超过阈值才最终判定。
from collections import deque class PlayingFilter: def __init__(self, window=15, ratio=0.6): self.window = window # 窗口帧数 self.ratio = ratio # 触发比例 self.buf = deque(maxlen=window) def update(self, is_playing): self.buf.append(1 if is_playing else 0) if len(self.buf) < self.window: return False return sum(self.buf) / self.window >= self.ratio参数怎么设:window=15在 25FPS 下约 0.6 秒,能滤掉单帧抖动;ratio=0.6表示窗口内 60% 帧判定玩手机才触发,兼顾灵敏度和稳定性。如果现场误报多,把ratio提到 0.7;如果漏报多,降到 0.5。这个滤波器对每路视频单独维护一个实例,不要共用。
验证滤波效果的方法:拿一段已知违规时间的测试视频,跑推理并记录每次报警的时间戳,和真实违规时间段对比。理想情况是报警时间落在违规区间内,且不早于违规开始、不晚于违规结束太多。如果报警频繁出现在违规前后,说明window太大,滞后明显,降到 10 试试。
我自己的习惯是:任何检测系统上线前,先用滑动窗口滤波跑一遍历史视频,统计误报率和漏报率,两个指标都达标才接实时流。这套玩手机检测方案从数据标注到推理部署,最花时间的不是训模型,而是标注规则统一和负样本收集。模型本身用 YOLOv9 已经够强,真正决定落地效果的是数据质量和后处理逻辑。希望帮到你。
本文还有配套的精品资源,点击获取