简介:一套基于YOLOv9的监控场景员工玩手机识别检测系统,面向计算机相关专业学生、教师及企业开发者,适用于毕业设计、课程项目或安防场景中的行为分析,解决监控画面内实时检测人员是否玩手机的问题。压缩包共192个文件,大小约75.25MB,包含83个Python源码、30个YAML配置、预训练PT权重、训练指标CSV、测试图片及标注文件等,代码结构完整,便于按模块学习和二次开发。目前已有161人学习下载,资源经过运行验证。除模型权重和检测脚本外,还提供详细运行教程、数据集准备说明、训练与推理的参数配置,以及损失和精度曲线,可帮助使用者系统掌握YOLOv9在实际数据上的训练流程、结果评估和部署要点。内含最优权重可直接对图片或视频完成推理,输出可视化检测结果,是完成课程设计、毕业设计或小型安全监测项目的实用参考。
1. 监控场景玩手机检测:为什么YOLOv9是当前落地性价比最高的选择
员工上班时间玩手机,靠人盯人查岗既不现实也招人反感。一套能自动从监控画面里识别“低头看手机”行为的系统,才是车间、办公室、营业厅真正需要的工具。基于yolov9的员工玩手机识别检测系统,用Python做训练和推理闭环,模型加载权重就能跑,监控视频流进来、告警框出去,全程不需要额外硬件加速卡(有GPU更好)。这套方案适合两类人:一类是安防集成商,在客户原有监控系统上叠加算法;另一类是刚入门目标检测的开发者,想拿一个贴近真实业务的数据集把yolov9的训练、评估、导出流程完整走一遍。下面的内容按源码包的实际使用路径,把环境搭建、数据标注、训练调参、避坑、部署验证一次讲透。
2. 搭建检测环境:依赖安装与模型权重加载
2.1 Python环境与YOLOv9依赖的安装顺序
先说结论:YOLOv9的训练和推理环境,Python版本卡在3.8到3.10之间最稳。很多翻车现场不是模型的问题,而是Python版本太高导致torch二进制包装不上,或者装了也用不了CUDA。我一般会为这个项目单独建一个虚拟环境,避免把系统Python弄乱。Windows和Linux下操作都一样:
conda create -n yolov9 python=3.9 -y conda activate yolov9 pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.1 --extra-index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txt逻辑说明:torch是YOLOv9的推理引擎,所有卷积、特征提取、NMS都在它上面跑。torchvision负责图像变换和一部分数据增强。先装torch再装requirements,是因为requirements里有些包(比如opencv-python、pyyaml、matplotlib)需要依赖已有的Python和torch基础环境。CUDA 11.8的torch版本对GTX 10系、20系、30系和RTX 40系都兼容,是最不容易出错的版本。
参数说明:python=3.9不要改成3.11或更高,torch对Python版本的官方支持有滞后。如果机器上没有conda,用python -m venv yolov9_env同样可以。requirements.txt里主要是opencv-python、numpy、pyyaml、matplotlib、tqdm、requests,版本冲突时先升级numpy到1.24以上,多半能解决。python安装这一步卡住的话,后面所有代码都跑不起来,所以虚拟环境是优先选择。
装完之后先验证CUDA是否真的可用,这一步能省下后面一整天的排查时间:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU mode")逻辑说明:第一行打印torch版本,第二行是关键——如果返回False,说明torch装成了CPU版本或者驱动没对上。第三行在确认可用之后打印显卡型号,用来核对是不是你机器上那块卡。
如果cuda.is_available()返回False,最常见的处理方式是卸载torch重装对应CUDA版本。驱动已装好的情况下,pip安装的torch会自动匹配驱动支持的CUDA,不需要单独装CUDA Toolkit。
注意:如果机器显存小于6GB,训练时batch-size要踩小,取4或8,否则直接OOM(显存溢出)报错。CPU-only的机器也能跑通本文所有流程,只是训练时间会慢一个数量级。
2.2 模型权重加载与目录结构
拿到源码包后,先看清目录结构。常见做法是train.py在根目录,models和data在子目录,权重文件以.pt结尾。解压后我习惯按这个顺序捋一遍:
ls -la du -sh .逻辑说明:ls查看根目录下有哪些文件和文件夹。du -sh查看整个工程占用的磁盘空间,如果权重文件有好几百MB,说明是完整训练好的模型,不是随机初始化参数。
确认目录之后,下一步是验证权重文件能正常加载。YOLOv9的模型定义在models目录里,但大多数时候不需要改它。直接用推理方式验证:
import torch # 加载官方yolov9模型定义与本地权重 model = torch.hub.load('WongKinYiu/yolov9', 'yolov9', 'yolov9.pt', device='cpu') model.eval() print("模型加载成功,参数量:", sum(p.numel() for p in model.parameters()))逻辑说明:torch.hub.load会从GitHub拉取yolov9的模型定义,并加载本地权重文件yolov9.pt。device='cpu'表示先用CPU做验证,避免一开始就卡GPU显存。参数量打印出来之后,可以确认模型是完整加载的。
参数说明:如果网络访问GitHub不稳定,可以把models和utils目录拷贝到本地,改成from models.yolo import Model这样的本地导入方式。权重文件路径按实际位置写,用os.path.abspath转成绝对路径最稳。
跑通加载这一步之后,用一张监控截图做推理测试。源码包里一般自带detect.py:
python detect.py --weights yolov9.pt --source sample.jpg --conf-thres 0.25 --device cpu逻辑说明:detect.py是推理入口,--weights指定权重,--source指定输入图片,--conf-thres是置信度阈值,低于这个值的检测框会被丢掉。device=cpu表示这次推理用CPU,一张图通常1到3秒出结果,环境没问题就能看到画了框的结果图。
参数说明:conf-thres默认0.25,实际监控场景建议放到0.4。原因在于玩手机动作的误报成本比漏报高——误报会让班组长频繁处理无效告警,时间长了就没人看告警了。iou-thres一般不用动,0.45是目标检测的默认值。
另一个常见误用是:不管机器有没有独显,代码里都写死device='cuda:0'。在纯CPU环境上这会直接抛错。我一般会在调用detect.py之前先跑一个环境探测脚本,根据torch.cuda.is_available()的结果动态选设备,这样同一套代码在测试机和现场服务器上都能跑。
源码包里除了train.py和detect.py,通常还有用来评估模型的val.py、导出模型的export.py和存放指标曲线的目录。这些脚本不用全都跑一遍,但val.py建议在训练后执行一次,它会输出mAP、Precision、Recall的数值版结果,比肉眼看曲线更精确。export.py则在需要把模型转成ONNX或TensorRT时才会用到,纯Python推理阶段可以先放着。
3. 数据准备与标注:把监控画面变成可训练的样本集
3.1 监控画面样本采集与类别定义
玩手机检测本质上是目标检测,模型只负责找“玩手机的人”以及“手机”。数据集的质量直接决定最终效果。常见做法是:从监控录像里抽帧,覆盖早中晚三个时间段,覆盖不同工位角度,覆盖站姿、坐姿、低头、举起手机等多种形态。如果监控覆盖了多个区域,每个区域都要抽一些帧,避免模型只在某个固定角度有效。
类别定义这里有个经验之谈。我建议只定义一个类别:phone_use,对应的标注框同时框住人手和手机。不要拆成“手机”“人手”“人脸”这样的多类别。原因有两个:监控画面里手机往往只有几十个像素,单独检测手机容易漏检;把“人手+手机+低头”整体框成一个目标,模型学起来更容易,类别单一也让告警逻辑更简单——只要出现框就说明有人在玩手机。
采集样本的推荐做法:
# 从每个录像文件中均匀抽帧,每两秒抽一帧,保存到images目录 ffmpeg -i camera1.mp4 -vf "fps=0.5" images_cam1/%04d.jpg逻辑说明:ffmpeg抽帧是最直接的样本生产方式。fps=0.5表示每两秒抽一帧,一个小时的录像能得到1800帧,人工筛选后留下600到800张有效画面就够了。监控画面里大量时间是没有玩手机动作的,均匀抽帧比连续抽帧更容易覆盖各种状态。
参数说明:如果录像分辨率高于200万像素,抽出的帧建议压缩到1280x720,统一jpg格式。分辨率太高会拖慢标注和训练速度,检测效果不会显著提升。如果录像是25帧每秒,抽帧间隔还可以根据目标出现的频率调整——目标出现频繁就fps=1,偶尔出现就fps=0.2。
然后做数据清洗:把没有人、没有手机、画面模糊的帧删掉。这个环节最耗时,但也是决定模型上限的环节——喂给模型的数据不对,后面调什么都白搭。我一般用FastStone或XnView快速翻图删图,一天能处理两千张。清洗完的图像按来源命名加前缀,比如cam1_0001.jpg,方便后续排查。
3.2 标注格式转换与数据集划分
标注工具用LabelImg或X-AnyLabeling都行。LabelImg是老牌工具,输出Pascal VOC的xml格式;X-AnyLabeling支持半自动标注,可以用一个初始模型先帮你框一遍再人工修正。对于监控场景,如果数据量超过1000张,我推荐半自动标注,自己给自己打数据能省一半时间。如果数据量小,LabelImg更简单直接,没有模型加载的额外配置成本。
标注框的边界也有讲究:玩手机动作的框,重点是把“手部到手机的区域”完整框住。不要只框手机不框手,因为模型学到的特征是“手拿着一个发光的方块”,而不是“一个方块”。框稍微比目标大一圈是可以接受的,但不要大到把整个上半身都框进去——那会让模型把“坐姿人手在桌面附近”都误判成玩手机。
标注完成之后,需要把VOC格式转成YOLO格式,因为YOLOv9训练读取的是txt标注文件。转换脚本是每个做目标检测的人都会遇到的坎,直接给一份最常用的:
import os import glob import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_dir, out_dir, classes): os.makedirs(out_dir, exist_ok=True) for xml_file in glob.glob(os.path.join(xml_dir, "*.xml")): tree = ET.parse(xml_file) root = tree.getroot() image_name = root.find("filename").text image_id = os.path.splitext(image_name)[0] out_txt = os.path.join(out_dir, image_id + ".txt") with open(out_txt, "w", encoding="utf-8") as f: for obj in root.iter("object"): class_name = obj.find("name").text if class_name not in classes: continue class_id = classes.index(class_name) xmin = float(obj.find("bndbox/xmin").text) ymin = float(obj.find("bndbox/ymin").text) xmax = float(obj.find("bndbox/xmax").text) ymax = float(obj.find("bndbox/ymax").text) # 转成YOLO的归一化中心坐标格式 x_center = (xmin + xmax) / 2 y_center = (ymin + ymax) / 2 width = xmax - xmin height = ymax - ymin f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n") print(f"转换完成,输出到 {out_dir}") if __name__ == "__main__": classes = ["phone_use"] convert_voc_to_yolo("annotations/xmls", "labels/train", classes)逻辑说明:这段脚本读取LabelImg生成的xml文件,把每个目标的类别名映射成class_id(从0开始编号),然后把xml里的左上角右下角坐标换算成YOLO需要的归一化中心坐标。归一化是坐标除以图片宽高,所以标注工具里图片尺寸必须和实际图片一致,否则画框位置会漂移。
参数说明:classes列表的顺序就是最终训练时的类别顺序,必须和之后data.yaml里的names保持一致。对玩手机项目,classes=["phone_use"]只有一类,class_id恒为0,模型只输出一种框。脚本对漏标或者类别名打错的xml会直接跳过,不会中断执行,但建议转换完抽查几个txt文件看看内容。
然后写数据集划分脚本:
import os import random import shutil image_dir = "images" label_dir = "labels" train_ratio = 0.8 images = os.listdir(image_dir) random.seed(42) random.shuffle(images) split = int(len(images) * train_ratio) train_images = images[:split] val_images = images[split:] os.makedirs("dataset/train/images", exist_ok=True) os.makedirs("dataset/train/labels", exist_ok=True) os.makedirs("dataset/val/images", exist_ok=True) os.makedirs("dataset/val/labels", exist_ok=True) for img in train_images: shutil.copy(os.path.join(image_dir, img), "dataset/train/images/" + img) base = os.path.splitext(img)[0] shutil.copy(os.path.join(label_dir, base + ".txt"), "dataset/train/labels/" + base + ".txt") for img in val_images: shutil.copy(os.path.join(image_dir, img), "dataset/val/images/" + img) base = os.path.splitext(img)[0] shutil.copy(os.path.join(label_dir, base + ".txt"), "dataset/val/labels/" + base + ".txt") print(f"训练集 {len(train_images)} 张,验证集 {len(val_images)} 张")逻辑说明:划分脚本把图片和对应的标注txt同步复制到YOLOv9要求的目录结构里——images和labels两个文件夹,train和val各自一对。之所以不用软链接而是直接复制,是因为训练时数据加载器的路径处理在Windows上有坑,绝对路径最稳。脚本假设每张jpg对应一个同名txt,如果没对应的txt文件,说明那张图没被标注,直接删掉或者标完再放进数据集。
参数说明:random.seed(42)固定随机种子,保证每次运行划分结果一致,方便复现。train_ratio取0.8,对3000张以下的监控数据集适用;如果数据超过1万张,可改成0.85或0.9。划分完之后检查一下val集里有没有漏标文件——验证集里出现空txt会让评估的mAP虚高,因为模型没有误报机会。
4. 训练参数与指标曲线:mAP、PR曲线怎么看、怎么调
4.1 yolov9训练命令与关键参数
数据准备好后训练。训练前先写data.yaml,YOLOv9靠它找数据集路径和类别信息:
# data.yaml train: D:/workspace/phonemonitor/dataset/train/images val: D:/workspace/phonemonitor/dataset/val/images nc: 1 names: ["phone_use"]逻辑说明:train和val指向训练集和验证集图片路径,绝对路径最省心。nc是类别数,names是类别名列表,顺序必须和标注脚本里的classes顺序一致。写错顺序,模型训练出来所有框的类别都是乱的。
参数说明:路径分隔符在Windows上要写正斜杠或双反斜杠,YAML解析器对单个反斜杠会报错。如果不确定路径,先把train下的图片拖到浏览器里确认。
然后启动训练:
python train.py \ --data data.yaml \ --imgsz 640 \ --batch-size 16 \ --epochs 150 \ --device 0 \ --weights yolov9.pt逻辑说明:train.py是YOLOv9工程的训练入口。--data指定配置文件,--imgsz统一缩放输入到640x640,这是速度和精度的平衡点。--batch-size 16在8GB显存上是安全值,显存更大的卡可以放到32。--epochs 150对玩手机检测够用,这个任务不算难,150轮一般能收敛。--device 0表示用第一块GPU。
参数说明:--weights指定yolov9.pt表示在官方预训练权重上微调,对小数据集非常重要,收敛快一大截。从零训练要写成--weights ""并加--scratch,但我基本不用,监控数据集再大也大不过COCO,迁移学习永远更优。
训练过程中有个玄学问题:loss曲线下降慢,不一定是模型问题,很可能是学习率没配对。train.py默认学习率0.01,配合warmup让前三个epoch预热。如果loss曲线出现锯齿状大幅震荡,优先考虑降低batch-size或检查标注里有没有空标签的图片。空txt文件会让模型在该图上没有任何监督信号,等价于把一张空白图塞进训练。
训练时的显存监控也是必须的。用nvidia-smi -l 1可以每秒刷新一次显存占用,看到显存用到95%以上,说明batch-size或imgsz开大了,赶紧Ctrl+C停掉调小,不要等它OOM。
4.2 指标曲线解读与调参策略
训练结束后,runs/train/exp/目录下会生成指标曲线图,包括PR曲线、F1曲线、混淆矩阵。这些曲线不是用来发朋友圈的,是用来决策怎么调参的。很多人训练结束直接把best.pt拿去用,完全没看指标,这属于黑匣子用法,效果不好也不知道为什么。
先看PR曲线(Precision-Recall Curve)。YOLOv9的PR曲线是一条随着置信度阈值变化的折线,曲线越靠近右上角,说明模型在保持高召回的同时还能维持高精确率。玩手机检测场景下,曲线右上角能到0.9甚至0.95附近,也就是阈值设成0.4或0.5时误报少、漏报也少。如果曲线右上角塌陷,说明模型在低置信度区域输出了一堆不靠谱的框,此时阈值要往高调。
mAP曲线的作用是判断训练是否收敛。mAP@0.5超过0.9属于优秀,0.8到0.9是能用的水平,低于0.7就要找原因。前三个排查点:第一,标注框有没有明显错位——比目标大一圈是常见现象,会让模型学偏;第二,训练集和验证集是否来自同一个摄像头角度——不是同一个,模型泛化不了是正常的,重新按角度划分数据集;第三,正样本密度——平均每张图只有0.2个目标,模型会漏检,补样本。
几个关键训练参数对结果的影响:
| 参数 | 取值范围 | 作用与影响 | 监控场景建议 |
|---|---|---|---|
| --imgsz | 320~1280 | 输入分辨率,越大小目标越清楚,显存和耗时同步上升 | 640起步,小目标多就960 |
| --batch-size | 4~64 | 梯度下降的稳定性,太小loss震荡,太大显存吃不消 | 16,OOM就减半 |
| --epochs | 100~500 | 训练轮数,过少欠拟合,过多浪费算力 | 150~200 |
| --conf-thres | 0.1~0.9 | 推理置信度阈值,越高误报越少但漏报增加 | 0.4~0.5 |
调参顺序我一般这样走:先epochs拉到150到200,监控场景数据量小,过拟合风险不高,多训练没有坏处。然后batch-size从16开始,显存不够就减半,4的倍数最稳。PR曲线右上角不够饱满,说明特征学习不充分,把imgsz从640提到960,代价是训练时间翻倍,小目标检测会有明显收益。最后才考虑修改数据增强参数,比如YOLOv9的hsv_h、hsv_s、mosaic等增强配置。
训练日志里还有三个关键loss:box_loss(框回归损失)、cls_loss(分类损失)、dfl_loss(分布聚焦损失)。YOLOv9用DFL(Distribution Focal Loss)细化边界框回归。这三个loss在训练前30个epoch下降明显,后期趋于平缓。如果box_loss在50轮后还明显上升,大概率是学习率没有按计划衰减导致震荡,可以用--cos-lr参数开启余弦学习率调度。这是我调参时常用的后悔药。
训练完用val.py做正式评估,不要只看自己的test视频:
python val.py --data data.yaml --weights runs/train/exp/weights/best.pt --batch-size 16逻辑说明:val.py会在验证集上跑完所有图片,输出mAP@0.5、mAP@0.5:0.95、Precision、Recall的数值结果。这比肉眼看曲线更精确,也方便做前后版本对比。如果这个数值明显低于训练时日志里打印的数值,说明验证集分布和训练集有偏差,需要回头检查数据集划分。如果现场对算力要求苛刻,也可以对比yolov5s这类轻量化模型的指标,看能否在更低功耗的硬件上达到相近效果。
5. 避坑指南:监控场景下YOLOv9的5个常见翻车点
5.1 坑一:小目标漏检——手机太小,模型压根看不见
现象:检测框频繁闪烁,玩手机动作真实存在,但模型只在人距离镜头很近时才输出框,远一点的工位完全不报,尤其是把手机握在手里放在桌面下方的动作,几乎全部漏检。
原因:监控摄像头视野大,人物在画面中往往只占100x200像素,手机只有30x50像素,YOLOv9在640x640输入下对这样的小目标特征提取不足。小目标的feature map响应太弱,经过下采样后特征基本被背景淹没。
解决:第一优先做的是把imgsz从640提到960甚至1280,小目标检测收益最直接,代价是训练和推理时间增加。第二,检查标注框是否把手机和手一起框住——只框手机等于强迫模型从极小的像素区域里提取特征,框住“手+手机”区域后目标尺寸能大一倍。第三,如果还不行,在采集阶段把远端工位的画面单独裁切放大作为独立训练数据,相当于给模型单独开小灶。
5.2 坑二:把“拿手机”当“玩手机”
现象:员工拿着手机放在桌面上、接听电话、用手机扫码,模型全报为玩手机,误报率居高不下,一天能推几十条无效告警。
原因:训练数据只标注了“手拿手机”的静止画面,模型没有学到“持续触摸、划动屏幕”的动态特征,静态帧上这两种状态确实难分。很多样本里人只是把手搭在手机上休息,也会被框出来。
解决:标注时把“拿手机但不操作”的样本单独标注为negative类,比如类名phone_hold,训练一个二分类逻辑。也可以在业务层加时间窗——连续N帧或累积M秒检测到才算告警,单帧检测结果不直接触发。这个方案比换模型更省成本,也是监控类项目最常见的处理方式。实际项目里我两种都会做,标注负样本能提升模型区分度,时间窗能过滤掉大部分瞬时误报。
5.3 坑三:训练集和验证集来自同段视频,指标虚高
现象:训练时mAP一直在0.9以上,自信满满去现场测,效果一塌糊涂,检测率直接掉到六成。回头看训练曲线,又找不到明显问题。
原因:数据抽样时随机划分,把同一段监控视频中相隔几秒的帧同时分进了训练集和验证集。这些帧背景、人物姿态几乎一模一样,模型相当于把“标准答案”背下来了,验证指标虚高。跑现场时画面一变,模型就露馅。
解决:划分数据集必须按时间段或视频文件分,不是按帧随机分。比如cam1的前5分钟做验证、后55分钟做训练,或者按日期切分。确保验证集和训练集没有来自同一段连续画面的帧。这个坑踩到的人非常多,属于血泪经验。划分完以后可以随机抽几对验证集和训练集的图,肉眼比对相似度,如果感觉像同一场景的近帧,就重新划。
5.4 坑四:推理卡顿,帧率跟不上监控需求
现象:GPU推理速度很快,单张图只要20毫秒,但实际视频流检测只有1到2 FPS,告警严重滞后,人走过去两秒了框才出来。
原因:推理脚本在逐帧读取视频,没有做跳帧处理;前后帧经过同样的预处理后重复送入模型;也没有用流式推理模式。这些因素叠加起来,GPU利用率上不去,大部分时间花在解码和IO上。监控摄像头通常是25帧每秒,逐帧推理本身就不合理。
解决:detect.py推理时加--vid-stride参数,取2或3表示每3帧处理1帧。需要实时性时,用cv2.VideoCapture读取时不要每帧都送模型,而是维护一个帧计数,等间隔处理。如果现场有多路摄像头,按路数排队送模型,避免同时把所有帧挤进GPU导致显存抖动。玩手机动作通常持续5秒以上,即使降到5 FPS也足够捕捉。
5.5 坑五:置信度阈值乱调,告警轰炸或静默
现象:阈值调到0.1,误报刷屏,一天上千条;阈值调到0.9,漏报警一大堆,领导来检查时发现真实玩手机没识别出来,整个系统被质疑。
原因:阈值和场景的误报代价没对齐。有的团队为了证明“模型很强”把阈值调低,结果现场误报太多被关停;有的为了减少打扰把阈值调高,结果真正的风险动作漏掉。还有一个隐藏问题:不同摄像头的画面质量差异大,同一个全局阈值在清晰的新摄像头上表现良好,在老旧模糊的摄像头上完全失效。
解决:调阈值前先跑一遍验证集,画出P-R曲线确定拐点,取精确率和召回率交点对应的值。然后结合业务按“漏报代价/误报代价”微调:宁可多发一条无效告警、不可漏掉一次违规,阈值就取低0.05到0.1;反之取高。另外别只依赖一个全局阈值,可以针对不同区域设置不同阈值——出入口区域阈值高,工位区域阈值低。这个思路同样适用于不同清晰度的摄像头。
6. 部署验证与进阶技巧:从单帧检测到实时视频流
最后一步是把训练好的模型接到实时视频流上验证,而不是停留在跑单张图片的阶段。最简单的做法是用OpenCV读摄像头或RTSP流,对每一帧做缩放、推理、画框、输出:
import cv2 import torch model = torch.hub.load('WongKinYiu/yolov9', 'yolov9', 'runs/train/exp/weights/best.pt', device='cuda') model.conf = 0.45 cap = cv2.VideoCapture("rtsp://192.168.1.20:554/stream1") frame_count = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break frame_count += 1 if frame_count % 3 != 0: # 跳帧,每3帧处理1帧 continue results = model(frame) for det in results.xyxy[0]: x1, y1, x2, y2, conf, cls = det.cpu().numpy() if conf >= 0.4: cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.imshow("monitor", frame) if cv2.waitKey(1) & 0xFF == 27: break cap.release() cv2.destroyAllWindows()逻辑说明:这段代码把推理过程串成一个循环。cap变量打开RTSP视频流,frame_count做跳帧控制,results.xyxy[0]拿到所有检测框的坐标和置信度,conf>=0.4的框才画出来。cv2.imshow把结果实时显示在窗口里,按ESC退出。从单帧到视频流,核心就是这一个循环。
参数说明:跳帧间隔3的意思是每秒25帧的视频实际只推理8帧左右,对玩手机这种持续数秒的动作完全够用,还能腾出GPU给别的路数。RTSP地址换成实际摄像头的地址,同一段代码也兼容本地avi文件或USB摄像头。模型加载时device='cuda',没有独显就改'cpu',CPU模式下把跳帧间隔调大到5,否则画面会很卡。
进阶方向有三个。第一个是导出TensorRT加速,用export.py把best.pt转成ONNX再转engine格式,推理速度能再提一倍,适合GPU资源紧张的项目,但要花时间处理动态尺寸的转换问题。第二个是告警联动,检测到目标后把当前帧截图存入本地,同时推送消息,在cv2循环里很容易扩展。第三个是保存统计日志,把每次告警的时间、摄像头通道号、置信度写入csv,作为绩效考核的依据——这才是监控场景真正需要的交付物。
我自己的习惯是:所有参数改动,包括阈值、跳帧间隔、ROI区域,都单独存一个配置文件,不要直接改代码里的魔法数字。训练和使用过程中把每个版本的权重和对应指标曲线归档,命名带上日期和混淆矩阵的假阳性率,这样模型效果波动时能快速回溯到是数据变了还是参数变了。希望这个方向的经验对你有用。
本文还有配套的精品资源,点击获取