
简介面向监控场景的员工玩手机检测系统本下载包提供基于YOLOv9的完整Python实现适用于深度学习目标检测课程设计、毕业设计及安防场景快速验证与二次开发。压缩包共192个文件整包大小约75.25MB主要包括83个Python源码脚本、30个YAML配置文件、33张测试样例图片、预训练模型pt文件、指标曲线csv以及非常详细的运行教程和标注好的数据集配置模板。代码在train_dual.py和detect_dual.py中分别覆盖训练与测试流程用户既可直接加载best.pt进行推理也能按文档指引从环境配置、数据集准备到参数调整完成全流程复现。此外reparameterization.ipynb辅助理解YOLOv9重参数化便于进一步学习改进。资源内代码均经过运行验证已有161人学习下载适合计算机、人工智能、自动化等专业学生或企业开发者使用。1. 监控场景下的玩手机检测为什么它需要一个专门的系统某园区安防负责人跟我抱怨过一件事监控室里八块屏幕最累的不是盯门口进出而是回放录像时逐帧找“谁在低头玩手机”。玩手机不是一个固定动作低头、举起手机、屏幕反光、手部遮挡任何单一视觉特征都不足以作为判断依据组合起来才叫“行为”。基于YOLOv9的员工玩手机识别检测系统就是在监控视角下替人眼完成第一轮筛选把视频帧送进检测网络输出“正在手持并使用手机”的检测框和置信度再由人去复核、处置。它解决的是全天候值守的疲劳问题把24小时录像变成几条高优先级的告警片段。这篇文章按我实际搭过一遍的思路展开从为什么选YOLOv9、数据怎么做、训练参数怎么调到指标曲线怎么读、部署踩了哪些坑全部按可复现的流程写适合正在做智慧园区、考勤行为分析或安全生产监督手上有监控码流但缺一个跑得通模型的工程师。2. 为什么监控场景选YOLOv9先从玩手机检测的特征难点说起2.1 玩手机检测的真正难点小目标、遮挡和“眼神相似”干扰固定摄像头的监控画面不像自动驾驶那样背景高速变化画面相对稳定但这反而暴露出三个更麻烦的问题。第一个是目标小。普通枪机在10到15米的距离上一个人头只有三四十像素高。玩手机这个动作的关键信息集中在手机屏幕、手部姿势和面部朝向这三处而屏幕区域往往只有十几个像素。大多数检测网络经过多层下采样后这些小目标的特征已经被池化操作冲得很淡到深层特征图里几乎就是一片噪点模型根本“看不清”。第二个是遮挡。工位隔板、键盘、水杯、同事走过都会把手部或手机挡住一半。检测框经常只框到半只手臂这时网络很容易把它误判成正常的办公动作。第三个是相似类别干扰。喝奶茶、拿文件、按计算器、低头看笔记本在二维画面里和玩手机非常像。模型如果只学到“手出现在面前”就会把举着水杯喝水的人也框成玩手机。这些难点合在一起决定了你不能直接拿一个通用预训练模型往监控画面上一挂就完事。通用模型是在自然图像上训练的玩手机类别的样本占比很低即便能检出手机也难以区分“拿着手机”和“正在使用手机”。所以这类系统必须走一条专门的路针对监控视角采集数据做类别语义的统一再用一个对小目标友好的网络结构微调。2.2 YOLOv9的PGI机制把浅层梯度信息从深层瓶颈里救出来YOLOv9在网络结构上的核心改进是PGI可编程梯度信息和GELAN。这两个名词听起来玄但放到监控场景里其实对应着很具体的痛点。常规检测网络在反向传播时梯度要逐层往回传。浅层网络负责边缘、纹理、角点这些基础特征深层网络负责语义信息。问题是网络越深梯度的信息经过的中间层越多到浅层时已经衰减得很厉害这就是所谓的信息瓶颈。监控画面里玩手机的手部细节、屏幕亮斑恰恰是浅层特征如果浅层拿不到有效梯度网络就学不到这些细节最终表现为小目标漏检。PGI做的事情是通过一个辅助可逆分支生成额外的梯度信息直接补偿给主干网络相当于给浅层开了一条“梯度货梯”绕过深层的拥挤通道。GELAN则是把ELAN和CSPNet的思路融合了一下在同样参数量下让特征聚合更高效。对做部署的人来说它带来的直接好处是权重文件不大推理速度不差但小目标召回率比同量级的旧结构高出一截。这点在玩手机检测上非常关键因为我们需要模型看清的是一个屏幕亮块和一只手的相对位置而不是整张画面里有什么物体。2.3 和YOLOv5/YOLOv8对比选YOLOv9的真话与不换模型的条件很多团队对这个任务的第一反应是“直接用YOLOv8”毕竟生态成熟、代码上手快。这种选择没有错但要注意边界条件。YOLOv5在光线稳定、场景单一的办公室数据上表现完全够用如果现有系统已经用YOLOv8跑通了且漏检和误报都还在可接受范围那就没必要为了换而换。我见过A同学的项目原先用YOLOv8在白天灯光充足的办公区跑效果尚可后来监控点位挪到走廊和库房夜间红外画面里特征严重退化漏检马上就来了。换成YOLOv9并重新微调后夜间小目标的召回率提升非常明显代价是迁移学习的工作量多个两三天。从结构差异上说YOLOv8用的是Anchor-Free加分布式回归边界框的预测更细腻在通用目标检测上很稳YOLOv9则把优化重点放在特征金字塔底层的梯度保持上。玩手机检测恰恰是“目标小、遮挡多、特征集中在浅层”的任务PGI带来的梯度补偿比回归头的细腻度更直接。还有一个务实的理由YOLOv9的C3/C2f模块改动幅度不大从YOLOv8迁移过来的数据处理代码和部署代码基本能复用团队学习成本可控。所以我的建议是如果是从零搭一套监控行为检测直接选YOLOv9如果有现成的YOLOv8流水线且误报率还能忍就先优化数据不要急着换骨架。3. 把玩手机检测跑起来数据、训练与推理全流程3.1 数据集怎么做采集、清洗与标注规则玩手机检测没有哪个公开数据集能直接覆盖你自己的监控点位视角所以自采数据是必经步骤。常见做法是找目标摄像头录一到两周的原始码流按时段和天气抽帧重点覆盖早中晚光线差异、逆光、夜间红外、远距离人员走动这些情况。抽帧之后要清洗。同一个员工在同一个工位上连续出现300帧这类重复帧对训练没有帮助反而会让模型过拟合到背景上。我的做法是先用OpenCV按场景切换检测抽帧画面的平均像素差超过阈值才保留这样能把一小时的录像压缩到几百张有效帧。单类别检测任务建议保留2000到3000张标注帧每帧里平均有2到5个目标。标注规则必须严格统一否则模型学到的类别语义会漂移。我一般定三条硬规则第一只有“手持且正在使用手机”才算正样本手机放在桌上没人碰不算第二打电话时手机贴在耳边也算使用中但要在标签里统一归为同一类不要再拆分第三检测框不是只框手机屏幕而是框住人和手机的整体最小外接区域。第三点很多人会做错。如果只框手机屏幕模型学到的是“亮屏矩形”而不是“人在玩手机”遇到手机放在桌上亮屏导航就会误报。把人和手机一起框进去网络才能同时利用手部姿势、屏幕位置和面部朝向的组合信息。标注工具用LabelImg或X-AnyLabeling都行导出YOLO格式的txt文件。注意YOLO格式的坐标是归一化的中心点和宽高标注完要抽20%数据做二次复核两个人背靠背标注不一致的地方逐条讨论。数据质量不够后面训练和调阈值全都是白费功夫。3.2 环境准备选一套不会中途劝退你的CUDA/PyTorch组合训练玩手机检测模型不需要追最新版本稳定组合比新特性重要。我常用的环境是这样搭的# 基于 conda 隔离环境避免把系统 Python 环境搞乱 conda create -n yolov9 python3.9 -y conda activate yolov9 # 安装 PyTorch注意先跑 nvidia-smi 看驱动支持的最高CUDA版本 # 以 CUDA 11.8 为例安装时带上对应 index-url 即可 pip install torch2.0.1 torchvision0.15.1 # 训练和推理需要的其他依赖 pip install opencv-python pyyaml tqdm tensorboard seaborn pandas onnx这里有几个参数细节要说明。Python选择3.9是因为和PyTorch 2.0.1的预编译包兼容性最好3.10以上偶尔会遇到opencv或onnx的wheel不匹配问题。CUDA版本不要盲目装最新先执行nvidia-smi看驱动上限驱动是535的机器装CUDA 12.1也没问题但驱动版本不够时只会黑屏报错。另外不需要装Ultralytics的YOLOv8包YOLOv9工程是独立的train.py脚本装ultralytics反而会把torch版本或者依赖改乱休息环境里不装它。3.3 训练命令与参数表让YOLOv9学会“玩手机”这一条新类别训练前先准备好数据描述文件YOLOv9直接支持YAML配置# data/phone_use.yaml path: /data/phone_use_dataset # 数据集根目录 train: images/train # 训练集图片目录 val: images/val # 验证集图片目录 nc: 1 # 类别数玩手机 names: 0: phone_use # 类别名称然后执行训练命令python train.py \ --data data/phone_use.yaml \ --cfg models/yolov9-c.yaml \ --weights yolov9-c.pt \ --batch-size 16 \ --imgsz 640 \ --epochs 100 \ --workers 4 \ --device 0 \ --project runs/train \ --name phone_use_v1几个关键参数在实际项目里是这样定的参数我的取值说明--imgsz640精度和显存的平衡点低于640时屏幕亮块太小特征丢失严重--batch-size16对应约12GB显存显存只有8GB就改成8配合梯度累积--epochs100单类别行为检测够用配合早停通常60轮左右收敛--workers4数据加载线程数过大有时反而因为IO争抢导致训练不稳定--weightsyolov9-c.pt预训练权重从公共发布渠道下载不要用--resume误加载旧迭代训练过程中观察终端输出的box_loss和cls_loss前20个epoch下降明显后面进入平台期。如果cls_loss很快降到很低但box_loss还在震荡通常是标注框质量不统一回去检查标注而不是加训练轮数。训练完成后执行验证命令确认验证集上的指标文件和最佳权重都被正确导出python val.py \ --data data/phone_use.yaml \ --weights runs/train/phone_use_v1/weights/best.pt \ --task val训练脚本会在runs/train/phone_use_v1/weights目录下生成best.pt和last.pt同时输出results.csv这是后面所有指标曲线的数据来源。3.4 把权重接到监控视频流一段可运行的推理Pipeline训练完的权重要接上视频流才能变成检测系统。推理代码不复杂但有几个坑要先绕开。权重文件是完整checkpoint不能直接load_state_dict要先取里面的model字段再转float。下面是核心推理片段import cv2 import torch import numpy as np # 加载训练好的权重 ckpt torch.load(runs/train/phone_use_v1/weights/best.pt, map_locationcpu) model ckpt[model].float().eval() # 取模型字段并转float model.cuda() # 缩放预处理保持宽高比统一到640×640避免画面被压扁导致手机形状失真 def prepare_input(frame): h, w frame.shape[:2] scale 640 / max(h, w) nh, nw int(h * scale), int(w * scale) resized cv2.resize(frame, (nw, nh)) canvas np.full((640, 640, 3), 114, dtypenp.uint8) canvas[:nh, :nw] resized blob canvas[:, :, ::-1].transpose(2, 0, 1)[None] / 255.0 return torch.from_numpy(blob).float().cuda() cap cv2.VideoCapture(rtsp://your_camera_stream) while cap.isOpened(): ret, frame cap.read() if not ret: break blob prepare_input(frame) with torch.no_grad(): preds model(blob) # preds是未解码的原始预测经过non_max_suppression后得到 # 形如 [x1, y1, x2, y2, score, class] 的检测框数组 dets non_max_suppression(preds, conf_thres0.55, iou_thres0.45)[0] for det in dets: x1, y1, x2, y2, score det[:5].cpu().numpy() # 坐标还原到原始帧尺寸这一步容易漏漏了画框就会错位 x1, x2 int(x1 / scale_w), int(x2 / scale_w) y1, y2 int(y1 / scale_h), int(y2 / scale_h) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.imshow(phone_use_detect, frame) if cv2.waitKey(1) 0xFF ord(q): break这里最关键的是坐标还原。模型输入是640×640画布预测框坐标在输入坐标系里要除以缩放系数再还原到原始帧否则画框位置整体偏移看起来像模型“没对准人”。另一个坑是NMS参数conf_thres默认我会先从0.55起步等第4章看完指标曲线再决定往高还是往低调。4. 指标曲线怎么读判断玩手机模型能不能上线的四个出口4.1 训练输出里那些曲线文件每一行都是什么训练结束后打开runs/train/phone_use_v1目录里面除了weight文件夹还有results.csv和results.png。results.png是YOLOv9训练过程自动绘制的曲线图集合包括train/box_loss、val/box_loss、metrics/precision、metrics/recall、metrics/mAP_0.5和mAP_0.5:0.95。很多人只看训练loss降得够不够低这是不对的。训练loss低只能说明模型记住了训练集验证集上的precision和recall才是能不能上线的关键。每次训练过程会有一项“最佳权重”的选择逻辑脚本比较的是验证集mAP_0.5不是loss。best.pt是验证集上mAP最高的那个epoch存档last.pt是最后一轮的存档。做部署时默认用best.pt但有一个例外如果best.pt对应的epoch太早比如第20轮而后面30轮val_loss还在继续降建议把后面那轮权重导出来人工验证一轮脚本的自动选定标准在单类别任务上偶尔会漏掉更好的候选。4.2 P-R曲线和F1曲线玩手机场景里召回率比精确率更值钱读取results.csv里每个epoch的precision和recall画出P-R曲线能看到一个典型的矛盾精确率高的时候召回率低召回率高的阈值下误检又变多。在玩手机检测场景我的做法是优先保召回率。原因是漏检的代价大于误检漏掉一个玩手机行为意味着管理流程里少一次干预误检一次最多是值班员多看一眼回放、点掉一条告警。告警本身是需要人工确认的所以模型的初筛召回率比精确率更重要。F1-Confidence曲线帮助更大。横轴是置信度阈值纵轴是F1分数曲线最高点对应的阈值就是这个数据集上的“理论最优阈值”。但要注意这个最优是统计意义上的实际监控场景的误报预算不同阈值不能直接套用。误报预算宽松的工厂可以选F1最高点误报预算紧张、告警会直接推送给主管手机的环境就必须往高阈值的右侧移动。4.3 混淆矩阵把“拿着手机没玩”和“正在玩手机”分开才是真本事混淆矩阵是容易被忽略的一张图。玩手机检测只有两个类别加一个background矩阵里真正要看的不是对角线的数字而是“背景被错分为玩手机”和“玩手机被错分为背景”这两格的分布。我的经验是如果背景误检集中在“手拿其他物品”的样本上说明训练数据里缺这类负样本要去补饮料瓶、文件夹、计算器的图片如果误检集中在“手机亮屏但人没看”的样本上说明标注规则没有约束“使用中”这个语义模型只学到了亮屏特征。这两种情况的处理方向完全不一样一个是加数据一个是改标注规则。混淆矩阵能帮你定位到底是哪一种而不是盲目堆数据。4.4 上线前真正要看的不是mAP而是每小时的误报次数mAP是模型横向对比的学术指标监控系统上线前要看的指标是“误报次数/小时”和“漏检率/小时”。这两个指标没法从results.csv里直接算得用一段真实监控视频回放做模拟。我的做法是挑一个工作日上午的4小时录像里面包含正常办公、打电话、喝水、看文件等各种行为把模型跑一遍统计检测框总数、经过人工复核后的误报数算出每小时的误报次数。之前有个项目mAP_0.5到了0.94看起来非常好但跑了真实录像一小时误报70多次原因就是背景里有大面积玻璃反光屏幕亮斑和玻璃反光在特征上太接近。这个测试必须在真实点位做实验室验证集代表不了现场光线和背景。系统能不能上线就看这个误报次数能不能被值班员消化。5. 监控部署避坑五个真实踩过的坑与排查方法5.1 漏检低头角度大、手机完全被遮挡时模型像“瞎了”现象在同一批摄像头录像回放时模型对正脸玩手机检测得很好但员工侧身低头、手机被隔板挡住一半时几乎全部漏检。原因数据分布偏了。采集数据时人和摄像头基本是正面相对模型学到的是正脸加手部的位置组合。监控实际场景里摄像头在头顶斜下方大量出现俯视角度和侧身角度特征分布出现偏移。解决回看训练集统计标注框的宽高比和角度覆盖。我补了约500张侧身、背身、俯视角度的样本只训了额外50轮侧身漏检就显著下降。如果不想重新标注这么多先用现有模型跑一批未标注的监控帧挑出置信度低于0.3的漏检样本做人工标注这种半自动补充效率很高。5.2 误检把喝水、按计算器、看文件都框成了玩手机现象模型在正常办公场景频繁出框显示“phone_use”但回放录像发现对方只是在喝水或低头看纸质文件。原因模型学到的是“手部抬到面部高度”这个位置特征没有学到和手机屏幕出现强相关。常见情况是数据里与“手在面部附近”的负样本不足模型只能靠位置猜。解决一方面补负样本收集手拿水杯、拿文件、扶眼镜的帧作为背景类直接放到训练集里背景类样本数量要达到正样本的一半左右另一方面调高置信度阈值观察降低误报的效果。如果阈值到0.75还压不住误检基本就是特征学偏了靠阈值解决不了必须回头补数据。5.3 训练显存不够batch_size降到4还在报OOM现象在8GB显卡上batch_size调到4仍然报CUDA out of memory训练直接中断。原因定时是imgsz和workers的设置问题但更常见的是梯度累积参数和显存碎片。玩手机检测的输入图640×640即便batch_size4加上模型中间层的特征图缓存8GB也吃紧。解决两条路。第一条是把imgsz降到480训练速度和显存压力都改善但小目标召回率会掉我通常不推荐。第二条更推荐保持imgsz640batch_size4同时在训练脚本里开启梯度累积等效于batch_size为16时的梯度更新步长。另外把workers调成2减少子进程占用的CPU内存占用也可以缓解一些意外OOM。5.4 部署画面模糊人只有30像素高模型完全检不到现象摄像头的焦距覆盖了整条走廊画面里人很小检测框时有时无看起来像模型不稳定。原因不是模型问题是输入分辨率不够。监控画面里的人体如果只有三四十像素高手部和屏幕的区域只有个位数像素640的输入分辨率救不回来。解决在摄像头端或者视频解码端先做ROI裁切把画面里人员密集的通道区域裁出来再送进模型。我常做的做法是用OpenCV的selectROI框出主通道和工位区域裁切后的人体高度至少有100像素再进模型。如果裁完仍然很小需要在相机侧调整焦距或安装高度。部署时画质达标模型精度才有意义。5.5 夜间红外画面下颜色特征失效导致大量漏检现象夜间开启红外补光后画面变成灰度白天的模型在夜间几乎检测不到手机屏幕亮光。原因训练集里没有覆盖红外灰度画面模型学到的颜色特征比如屏幕的蓝色或白色色偏在灰度画面里完全失效。解决从夜间录像里抽帧补齐训练集把灰度画面加入训练。如果点位多、夜间时长占比高可以额外训练一个夜间专用模型白天用彩色权重、夜间切灰度权重按时间表切换。另一个小技巧是训练时做颜色抖动增广把饱和度随机降到0让模型提前见过灰度形态提高跨光线泛化能力。6. 进阶技巧从单帧检测到迟滞告警把误报率再压一半单独一帧的检测结果不应该直接变成告警。监控画面里员工起身拿个文件模型可能偶发一帧误检摄像头码流瞬间卡顿也会产生异常帧。如果每个框都推送给管理者告警就疲劳了。我通常会再加一层迟滞判断连续多帧命中且命中次数达到阈值才触发一次告警。# 迟滞告警伪代码核心是计数器而不是单帧布尔值 hit_count 0 ALARM_FRAMES 3 # 连续3帧命中才告警 SKIP_FRAMES 30 # 告警后30帧内不重复告警 for frame in video_stream(): dets infer(frame) hit_count hit_count 1 if len(dets) 0 else 0 if hit_count ALARM_FRAMES: send_alarm(frame, dets) hit_count 0 skip_until frame_index SKIP_FRAMES elif frame_index skip_until: pass # 静默期避免同一个人连续触发多条告警这段逻辑里ALARM_FRAMES控制灵敏度取3意味着大约0.1秒内的连续命中才确认人正常快速走过不会被误触发。SKIP_FRAMES做防重同一个员工站在那里玩10分钟手机只发一条告警而不是几百条。实际使用中我把置信度分成两档处理0.55到0.75的框进入待确认队列只有0.75以上且连续3帧命中才直接告警这样把误报率又压低了一半。更进一步可以把告警结构化输出成JSON交给现有的安防平台对接{ event_type: phone_use, camera_id: CAM-0102, timestamp: 2025-06-01 14:23:11, conf: 0.87, bbox: [412, 288, 560, 430] }这样模型系统就不再是一个孤立的Python脚本而是一个上游检测模块。管理者在平台里看到的是结构化事件流点开即回放复核效率大幅提升。这套系统做下来我最大的教训是阈值不要拍脑袋调。最早我把conf_thres调到0.25觉得“检得越多越好”结果上线第一天误报几百条摄像头前的绿植反光、路过的不锈钢保温杯全在报警。后来才明白阈值是从F1曲线和业务误报预算里反推出来的不是感觉。先定误报预算再回头选阈值这个顺序不能反。希望帮到你。本文还有配套的精品资源点击获取