YOLOv5打电话行为检测落地全指南:数据集、PyQt界面与部署避坑
发布时间:2026/9/26 12:57:20来源:尧图网络
简介这套YOLOv5打电话行为检测方案面向需要快速落地行为识别项目的开发者与算法学习者解决模型训练门槛高、数据标注繁琐的问题。包内含训练好的.pt权重文件、YOLOv5工程源码、配套数据集标签同时提供txt与xml两种格式可灵活用于YOLO系列训练或VOC格式处理PyQt5界面支持图片、视频和摄像头实时检测开箱即用适合课堂实训、毕业设计或安防场景二次开发。资源共165个文件以Python脚本、pyc、yaml配置、jpg/png图像样本、xml/txt标签、pt模型和ui界面文件为主另有mp4演示视频整体约433.91MB。目前已有604人学习/浏览完整工程结构便于按目录复现训练流程参考博客还可查看数据集与检测结果能帮助节省大量整理和调参时间。1. 一套能直接跑的YOLOv5打电话行为检测方案难的不是训练而是这三样先说结论YOLOv5打电话行为检测这个组合项目结构拆开就是“YOLOv5模型 训练好的权重 打电话数据集 PyQt界面”四块听着像拼积木真正落地时模型训练只占三分之一工作量数据集的质量和PyQt界面能不能流畅跑起来才是大头。我见过不少团队拿开源模型跑两天就换方案翻车点根本不在mAP而在“打电话”这个动作怎么定义——是手机出现在画面里就算还是手机必须贴着耳朵才算这个标准没定清楚训练出来的模型就处在薛定谔的可用状态。这篇文章把我做过的方案讲透从打电话数据集的构成、标注规则、YOLOv5训练参数到PyQt界面怎么不卡壳地实时出框再到验收时看什么指标、发布埋了哪些雷。适合做运输安全监控、工地纪律检测、考场行为分析以及想把手头YOLOv5模型接进桌面程序的工程师照着复现。2. 打电话行为检测的技术选型为什么用YOLOv5而不是图像分类2.1 打电话检测不是图像分类先定检测目标再谈类别“检测打电话”在计算机视觉里其实是一个复合任务它同时包含两个子问题人在哪里以及这个人是否处于打电话状态。纯图像分类模型只能回答“这张图里有没有人打电话”回答不了“画面里三个正在打电话的人分别在哪个位置”更扛不住摄像头视野里同时出现多人、多姿态的场景。YOLOv5是单阶段目标检测模型一个模型同时输出目标框、类别和置信度天然适合“定位分类”的需求所以拿来做打电话行为检测是合理的。真正让这个任务难的不是模型选型而是“打电话”这个类别的类内差异。打电话的姿态至少有三种手机紧贴耳朵、手机举在耳边还没贴上、拿着手机低头看屏幕负样本更麻烦手拿饮料、手夹烟、手扶方向盘、摸头发都可能被模型误判成“手持手机”。也就是说模型要学的是“手机与头部区域的空间关系”而不是“画面里有没有一块矩形物体”。很多新手直接把公开的人手检测模型拿来当打电话检测用结果误报率高得没法看原因就在这个建模思路上。所以在做数据集标注之前得先把类别体系想清楚。我的做法是拆成三个类别phone手持手机但没打电话、call手机贴耳或举在耳边、person人体全身框。不要只标一个“打电话”类别因为标注的人对“打电话”的判断标准不一致模型学到的边界就会漂移。多一个phone做硬负样本等于主动告诉模型“手里有东西和正在打电话是两回事”。2.2 YOLOv5s、v5m、v5l怎么选显存、帧率与精度的天平YOLOv5官方仓库提供了s、m、l、x四档模型对应参数量和推理速度的取舍。打电话检测场景通常部署在监控工控机或者普通办公电脑上很少用得上x档。我一般只在s和m之间选训练好的模型权重文件也不大分发成本低。模型参数量权重大小640分辨率算力我的最低显存建议YOLOv5s7.2M14MB16.5 GFLOPs4GB可跑batch 16YOLOv5m21.2M42MB49.0 GFLOPs6GB以上稳妥YOLOv5l46.5M92MB109.1 GFLOPs8GB勉强不推荐这里的GFLOPs是YOLOv5官方给出的640分辨率下的理论计算量实际部署时还受显卡、输入分辨率影响。选择逻辑很简单打电话检测对框的精细程度要求不高但对帧率敏感因为后续还要做时序判定帧率低于10FPS会导致“打电话”这个状态在连续帧上断断续续。摄像头分辨率是1080P时推流到模型前通常要缩到640×640YOLOv5s在GTX 1660上能跑到40FPS以上m档会掉到25FPS左右。如果部署机器只有核显或者老式工控机s档几乎是唯一选择。低显存环境下还有一条路训练时用s档推理时把输入分辨率从640降到480显存占用能再降三分之一。代价是小目标远处的手机漏检率上升适合人物离摄像头较近的室内场景。如果你手里的机器连4GB显存都没有那别碰m和l老老实实s档加CPU推理后面讲PyQt界面时会给出对应的优化策略。2.3 三件套的分工检测模型负责看见逻辑层负责判读界面负责交互整个YOLOv5打电话检测项目的主流程可以画成一条线视频帧进入YOLOv5模型输出每个人的框、每个手机的框以及它们的类别接着进入一段后处理逻辑判断手机框和人体框或头部区域之间的位置关系记录连续多帧的检测结果最后PyQt界面负责把画面、检测框、状态文字实时渲染出来。这里有个关键设计决定打电话判定的空间基准到底用人体框还是头部框。如果摄像机安装角度是平视或微俯视头部区域大约在人体框的上三分之一处直接用人体框的比例去估算耳朵位置即可如果摄像头是高位俯拍比如走廊顶装摄像头手机遮挡头部严重人体框的上边缘根本对不准耳朵这时候得单独训练一个人头检测器配合使用。做运输监控时我常用的是人体框方案因为道路枪机抓的是车身中景人头上方有较多留白估出来的头部区域还算稳定。时序判定是容易被忽略的一层。单帧模型输出噪声很大一个“手拿手机”的姿态可能连续几帧被误判为“call”单帧就报警的话系统会不停误报。常见的做法是维护一个长度为10帧的滑动窗口窗口内call类别出现7帧以上才确认一次“正在打电话”连续3帧无call则解除状态。这一层逻辑放在检测模型的输出之后、PyQt界面渲染之前代码量不大但能把误报率压下去一大截。3. 从打电话数据集到能用的模型数据清洗、标注规则与训练参数全流程3.1 数据从哪里来、怎么筛公开数据集与自采数据混排打电话数据集可以从两个渠道凑齐公开数据集和自采数据。公开的驾驶分心数据集比如Kaggle上的State Farm Distracted Driver Detection里有大量“打电话”和“发短信”的标注图片画面是车内摄像头对着驾驶员拍的类别和“打电话”高度相关另一种常见来源是国内博客和网盘上有人整理过的“打电话行为检测”数据集下载后注意看清楚标注格式有的是VOC的XML有的是COCO的JSON需要统一转成YOLO需要的txt格式才能训练。先把公开数据集拉过来看一遍筛掉清晰度太差、过曝、目标被大面积遮挡的图片。自采数据的价值在于对齐部署场景。公开数据集大多是车内视角如果你要监控的是办公室或者工地画面里人物大小、摄像头俯仰角都和公开数据集差很远直接用公开数据训练换个场景就漏检。我的做法是拿手机在学校附近天桥、办公室通道录几段5到10分钟的视频人走动、打电话、玩手机、正常行走混着来抽帧后加入数据集让模型见过目标场景的真实光线和视角。自采数据不用太多占到总量的20%到30%就能明显改善场景迁移的落差。数据清洗定三条硬规则第一删除有明显噪点、运动模糊到看不出手机轮廓的图片这类样本会让模型在训练时学习到错误的纹理特征第二检查有没有“标签与画面完全对不上”的脏数据公开数据集里时常混着标签错位的样本不筛掉的话训练损失会异常抖动第三正负样本比例控制住正样本call和负样本没在打电话的人至少要做到1比3纯正样本训练出来的模型会把一切手部动作都当成打电话。3.2 标注规则三个类别的边界怎么划才不翻车标注环节是整个项目里最耗时也最影响上限的步骤。我推荐用CVAT或者labelImg工具不重要规则才重要。三个类别的判定标准在动手标注前就要和参与标注的人对齐否则每个人标出来的框五花八门。phone类手机出现在画面里无论手持、放在桌上、放在腿上都算但只要手机清晰可见就得框。这个类别存在的意义是给模型提供“手机”这个物体的基础特征让模型先学会找手机再去判断手机和头部的关系。call类手机贴耳、手机与耳朵在同一焦平面并发生遮挡关系、手机举着但明显停在耳边位置这三种姿态都算call。关键判断点是“手机是否在头部区域内或紧贴头部边缘”而不是“手有没有抬起来”因为戴耳机打电话手可以完全垂着。person类全身框不需要框得太紧背后的一些背景可以带进来这有助于模型理解人物和远景之间的关系。还有一个容易犯的错误是框的范围。手机框一定要只标手机本体不要包住整只手。很多人图省事把手机和手一起框成一个大矩形训练出来的模型会对“手的形状”产生过拟合换个人戴不同颜色的手套就开始漏检。call类的框也是这样只需要把手机框住不需要体现手或脸的位置空间关系由模型自己去学习。一张图里界乎于“靠近但没贴耳”的模糊姿态宁可标成phone也不要标call这样可以训练出更严格的分类边界。3.3 数据划分脚本一份可以抄的YOLO格式切分代码YOLOv5训练时要求图片和标签文件同名图片放在images目录标签放在labels目录目录结构按数据集划分成train和val即可。这个脚本把原始图片目录和标注目录按比例随机切分到新的数据集目录固定随机种子保证每次划分结果一致。import os import random import shutil def split_dataset(image_dir, label_dir, output_dir, val_ratio0.15, test_ratio0.05, seed42): random.seed(seed) images [f for f in os.listdir(image_dir) if f.endswith((.jpg, .png, .jpeg))] random.shuffle(images) n len(images) n_val int(n * val_ratio) n_test int(n * test_ratio) splits { train: images[n_val n_test:], val: images[:n_val], test: images[n_val:n_val n_test], } for split_name, imgs in splits.items(): img_out_dir os.path.join(output_dir, split_name, images) lbl_out_dir os.path.join(output_dir, split_name, labels) os.makedirs(img_out_dir, exist_okTrue) os.makedirs(lbl_out_dir, exist_okTrue) for img_name in imgs: base os.path.splitext(img_name)[0] shutil.copy2(os.path.join(image_dir, img_name), os.path.join(img_out_dir, img_name)) shutil.copy2(os.path.join(label_dir, base .txt), os.path.join(lbl_out_dir, base .txt)) for split_name, imgs in splits.items(): print(f{split_name}: {len(imgs)} 张) if split_name val: print(f验证集图片总数: {len(imgs)}) if __name__ __main__: split_dataset(raw_images/, raw_labels/, phone_dataset/)脚本逻辑不复杂但有几个细节值得说明。随机种子固定为42这样反复执行切分结果一致团队协作时不会各切各的。图片和标签用copy2而不是rename保留原始数据做备份因为后期发现标注错误还要回头改。验证集比例取15%对几千张的中等规模数据集够用了测试集单独切出一份5%放一边最后验收模型时才用避免训练过程里反复看测试集导致过度拟合。3.4 写data.yaml并启动训练这些超参值得一个个调数据准备好后写一个data.yaml描述数据集路径和类别信息train: ./phone_dataset/train/images val: ./phone_dataset/val/images test: ./phone_dataset/test/images nc: 3 names: [phone, call, person]路径用相对路径时要确保是相对于你执行训练命令的目录而言建议直接在YOLOv5仓库根目录下运行data.yaml放在根目录或指定路径都可以。names的顺序必须与标注txt里的类别编号一一对应这里phone是0call是1person是2标注脚本里生成txt时就要按这个编号写入。训练命令如下python train.py \ --data phone_data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache ram \ --patience 20逐项拆解参数的含义。--weights yolov5s.pt表示加载COCO预训练权重这不是偷懒而是用别人已经学好的底层视觉特征做初始化能大幅缩短训练时间在中小规模数据集上几乎不会带来负面效果。--img 640是训练时随机缩放的基准分辨率如果摄像头画面里手机目标很小可以提高到960或1280但显存占用和推理延迟都翻倍建议先用640跑通全流程再调这个值。--cache ram把训练集图片一次性缓存到内存中避免每个epoch都从磁盘重新读图训练速度提升明显前提是内存够大几千张图大概需要8GB到12GB内存。--patience 20是早停机制连续20个epoch验证集损失没有下降就自动停止训练防止模型过拟合之后还继续空转。关于超参数YOLOv5默认的anchor基本不用调打电话数据集的物体比例非常稳定默认anchor能覆盖大部分框的形状。真正值得动的是mosaic增强的开启程度如果训练数据来自同一个监控摄像头场景高度重复可以把mosaic保持默认并在训练后期用--multi-scale增强尺度多样性如果数据来自多个不同场景mosaic的作用就没那么大了。3.5 训练后看什么指标别只盯着loss训练结束后YOLOv5会在runs/train/exp/weights/目录下生成best.pt和last.ptbest.pt是验证集上表现最好的权重last.pt是最后一个epoch的权重部署时一定选best.pt。打开runs/train/exp目录下的results.png重点看三条曲线训练损失、验证损失、验证集mAP0.5。对打电话检测这个任务我通常把mAP0.5作为主要验收指标0.5的交并比阈值意味着不需要框非常精准只要大概框住手机就能算对的框。实际部署时置信度阈值会卡在0.25到0.4之间所以mAP0.5比mAP0.5:0.95更有参考意义。另一个要重点看的是混淆矩阵YOLOv5训练结束会在runs/train/exp目录下输出confusion_matrix.png如果call类别大量被识别成phone说明标注里“贴耳”和“没贴耳”的边界样本太少回去补充这类数据比调参有用得多。4. 把训练好的YOLOv5接进PyQt界面视频流、QThread与实时画框4.1 PyQt界面的三个基本件窗口、画面标签、控制按钮PyQt部分不用做得多花哨一个能工作的桌面工具只需要三样东西主窗口QMainWindow、显示视频画面的QLabel、控制启停的QPushButton。主窗口左侧放按钮和状态信息右侧放画面标签布局用QVBoxLayout或者QHBoxLayout都可以。摄像头来源可以直接用OpenCV的VideoCapture设备号0表示第一个摄像头也可以支持传入视频文件路径方便直接用录制好的测试视频验证模型效果。界面逻辑上要注意的一点模型加载放在界面初始化阶段而不是点击“开始检测”之后。YOLOv5模型初始化需要加载权重、构建网络结构耗时从几秒到十几秒不等放在按钮响应里会让界面陷入无响应状态。更规范的写法是初始化界面时就把模型加载成全局或类成员变量点击按钮后只是拉起一个视频循环线程。这样用户打开程序后点击开始就能立刻看到画面体验好很多。状态信息区可以放两个标签一个是实时推理耗时显示每帧处理多少毫秒另一个是检测状态显示当前是否判定为“正在打电话”。这两个数据对调试和验收都非常重要推理耗时能直观反映模型和分辨率选型是否合理检测状态则能验证时序判定逻辑是否生效。4.2 用QThread跑推理为什么不能把detect写在UI线程PyQt最经典的翻车现场就是新手把while循环和模型推理直接写进按钮的槽函数结果程序一启动摄像头画面就卡住窗口拖不动、按钮点不了。原因是UI线程被推理循环阻塞住了Qt的事件循环无法处理窗口绘制和鼠标事件。解决办法是把视频读取和模型推理放进QThread子线程通过信号把结果传回主线程更新界面。import cv2 import time from PyQt5.QtCore import QThread, pyqtSignal class DetectWorker(QThread): frame_ready pyqtSignal(object, list, float) error pyqtSignal(str) def __init__(self, model, source0, parentNone): super().__init__(parent) self.model model self.source source self.running True def run(self): cap cv2.VideoCapture(self.source) if not cap.isOpened(): self.error.emit(无法打开摄像头或视频文件) return while self.running: ok, frame cap.read() if not ok: break t0 time.time() results self.model(frame) # 提取 x1, y1, x2, y2, confidence, class_id dets results.xyxy[0].cpu().numpy() spend time.time() - t0 self.frame_ready.emit(frame, dets, spend) cap.release() def stop(self): self.running False self.wait()这段代码里值得注意的地方有几个。results.xyxy[0]返回的是一个包含所有检测结果的tensor维度是N×6分别是x1、y1、x2、y2、置信度、类别索引。.cpu().numpy()把它转成numpy数组方便在主线程直接遍历。这里没有用pandas().xyxy[0]因为pandas解析会慢一些在实时推理循环里每帧多花几毫秒对帧率敏感的场景属于不必要的开销。frame_ready信号携带原始帧、检测结果、推理耗时三个数据一次性传给主线程避免多信号之间的时序错乱。线程停止的写法也需要注意。self.running False只是一个标志位如果模型推理一帧超过几秒比如CPU推理大图stop()方法里的wait()会等到当前循环结束才返回界面会短暂无响应。改进方式是设置一个超时时间或者在run循环里多次检查running标志但实际场景中几百毫秒的等待用户基本无感可接受。4.3 主线程画框坐标映射的坑检测结果里的坐标是相对于原始视频帧的而QLabel显示的尺寸很可能和原始帧不匹配尤其是用户拖拽窗口改变布局后。直接在原始坐标上画矩形再缩放显示会导致框的偏移和变形。必须先计算显示区域的缩放比例再映射坐标。def update_frame(self, frame, dets, spend): h, w frame.shape[:2] label_w self.video_label.width() label_h self.video_label.height() scale_x label_w / w scale_y label_h / h # OpenCV读进来的是BGRQImage需要RGB display cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) for det in dets: conf det[4] cls_id int(det[5]) if conf 0.35: continue x1, y1 int(det[0] * scale_x), int(det[1] * scale_y) x2, y2 int(det[2] * scale_x), int(det[3] * scale_y) # 类别1对应call绿色显示其他类别黄色 color (0, 255, 0) if cls_id 1 else (0, 255, 255) cv2.rectangle(display, (x1, y1), (x2, y2), color, 2) label_text f{self.names[cls_id]} {conf:.2f} cv2.putText(display, label_text, (x1, max(0, y1 - 8)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) h2, w2 display.shape[:2] qimg QImage(display.data, w2, h2, 3 * w2, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))坐标映射的关键在于scale_x和scale_y这两个值是用显示控件当前尺寸除以原始帧的宽高得到的。使用QLabel的当前宽高而不是固定值是为了支持窗口缩放后框依然对齐。颜色按类别区分很实用call类用绿色phone类用黄色一眼就能看出模型当前状态下正在检测什么。cv2.putText的坐标Y方向需要减去一个像素偏移否则文字会盖在框线上阅读体验差。QImage构造函数的第三个参数传入3 * w2表示每一行像素占用的字节数。这里最容易踩坑的是忘了把BGR转成RGB直接拿OpenCV的原始帧构造QImage画面会出现偏蓝和偏橙的诡异颜色。转换那行cv2.cvtColor不能省。4.4 打电话状态的时序确认逻辑模型输出的是单帧的检测结果直接用会导致报警状态频繁抖动。我在前面提到滑动窗口的时序判定这里给出具体代码实现class CallStateJudge: def __init__(self, window_size10, threshold7): self.window [] self.window_size window_size self.threshold threshold def update(self, has_call): 每帧调用一次传入本帧是否检测到call类别 self.window.append(has_call) if len(self.window) self.window_size: self.window.pop(0) confirmed sum(self.window) self.threshold return confirmed使用方式是在主线程的update_frame槽函数里判断当前帧是否存在类别为call的检测结果把布尔值传给CallStateJudge.update()。返回True时界面状态栏显示“正在打电话”同时可以触发写入日志或抓图保存。窗口大小10帧、阈值7帧这两个参数对应的是“10帧里至少7帧检出call才确认”的规则既过滤了偶发误检又能容忍单帧漏检。如果摄像头帧率是30FPS这个窗口对应约0.33秒确认延迟是可以接受的。5. 避坑YOLOv5打电话检测项目里最具迷惑性的5个坑5.1 “手机放桌上”也被识别为call类别边界标歪了现象模型训练完推理时一个放在桌面上、没人碰触的手机被标成call触发报警。最开始我以为是模型过拟合把置信度阈值从0.25调到0.5也挡不住。原因查了训练集的标注后发现标注的人把“手机在画面中靠近人物”的样本都标成了call而没有严格按照“手机与头部接触”的标准。模型的注意力被带偏学到的是“画面里有手机且离人近”就是call桌面手机自然中招。解决把这类样本的标注全部改回phone同时补充一批“手机在桌面、在手里但没有举到耳边”的负样本。改完之后重新训练call混淆到phone的量明显下降。这个事说明标注规则手册必须细化到“贴耳才算call”这种程度否则交付的模型就是薛定谔的。5.2 低头看手机完全漏检手机屏幕被遮挡现象正面机位拍到的画面里人物低头看手机模型既没检出phone也没检出call直接放过去了。从人体姿态来看人确实在操作手机但目标检测模型依赖的是手机这个物体本身的外形特征。原因低头场景下手机屏幕与摄像头视角接近垂直屏幕纹理丢失手机的轮廓几乎是一条线加上手部遮挡模型提取不到足够特征。这不是模型训练的问题是成像角度造成的物理限制。解决常见做法是调整摄像头安装位置用下压式俯拍或侧方机位让手机屏幕不被完全遮挡如果机位固定没法调整就要在界面逻辑里加入姿态辅助判定比如“长时间低头且双手靠拢”也作为疑似打电话的提示但结果标记为待人工确认而不是直接报警。将检测和判定做成两个层面能降低漏报带来的安全风险。5.3 训练loss能降到0.02测试集上误报却一个接一个现象训练过程看起来非常漂亮loss曲线一路下降mAP0.5超过0.9但换一段真实摄像头录的视频跑误报率高到不堪入目。这应该是很多人在训练自己的数据集时碰到过的最困惑的问题。原因把验证集和测试集切成同分布了验证集图像和训练集图像来自同一批数据模型从这里学到了复制粘贴式的记忆而非泛化能力。前面切分脚本里专门保留5%的test目录就是用来做这个隔离的。解决训练完成后把test目录里的图片单独跑一遍推理统计结果。如果test上的mAP明显低于val说明模型过拟合训练集分布回去补数据或增强正则化。更进一步用录制的一段真实场景视频跑端到端测试不只看mAP还要统计误报数和漏报数这才是发布的真实基线。5.4 PyQt界面花屏或颜色怪异RGB与BGR没转现象界面能显示画面但整个图像颜色严重偏蓝、发黄或者出现像素错位的花屏。有时窗口一拖动画面直接变成横条纹像是编码格式错了。原因OpenCV读取视频帧返回的是BGR三通道排列而QImage默认接收的是RGB排列直接构造QImage不转换通道顺序就会偏色QImage构造时第三个参数每行字节数写错比如写成3而不是3*w就会导致每一行像素错位成花屏。解决构造QImage之前先cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)。每行字节数写成3 * w对RGB888格式来说是固定的。如果用了QPixmap.fromImage(qimg)显示后画面方向不对检查VideoCapture的宽高设置再不行就用qimage.mirrored()修正镜像。5.5 推理循环越来越慢最终只剩3FPS现象开头的50帧运行流畅后面越来越卡接近一分钟时帧率掉到个位数CPU占用接近满载。看起来像是模型推理本身的问题其实是内存管理问题。原因YOLOv5模型每帧推理产生的tensor对象没有得到有效的回收累计在内存里。另一个可能因素是OpenCV的VideoCapture缓冲队列积压摄像头帧不断往内存塞推理速度跟不上读取速度积压越来越多。解决在run循环里把每帧的中间变量用局部变量接收不持有引用推理结果通过signal传给主线程后立即释放。对视频文件读取用cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)把缓冲队列压到最小让摄像头帧只保留最新一帧丢掉处理不过来的旧帧。还有一个实用的做法是每次循环末尾调用cv2.waitKey(1)释放底层资源这个函数会触发OpenCV内部事件处理帮助释放部分缓存。6. 发布前的最后一公里精度复测、ONNX导出与模型瘦身6.1 用一段没见过的真实视频做端到端验收测试集图测完不等于模型可以上线真正的验收是录一段部署场景的视频逐帧跑完整推理链路统计两个核心数字误报次数和漏报次数。我的习惯是录5分钟正样本视频和5分钟负样本视频正样本视频里人在正常打电话、玩手机、喝水负样本视频里人只是走路交谈、看文件。跑一遍后如果负样本视频里触发报警超过2次就需要回到时序判定层去提高确认阈值如果正样本视频里“打电话”状态的检出帧数低于80%就说明模型的召回不够要考虑降低置信度阈值或补充训练数据。这个阶段要记录的信息不止报警次数还有一个容易被忽略的指标确认延迟。从画面里人物第一次把手机举到耳边到界面亮出报警中间经过了多少帧。延迟过长会让保安或管理人员觉得系统不灵敏一般目标控制在0.5秒以内。如果延迟超标检查是不是时序确认窗口设得太长或者推理帧率太低导致状态切换变慢。6.2 导出ONNX并切换成CPU推理PyTorch模型文件在部署机器上跑推理需要安装对应版本的PyTorch依赖体积大且显存占用高。导出成ONNX后可以用ONNX Runtime做推理CPU和GPU都能跑依赖小很多启动速度也更快。YOLOv5官方仓库直接提供导出脚本。python export.py --weights best.pt --include onnx --opset 12opset 12是ONNX算子集的版本新版ONNX Runtime对opset 12的支持非常成熟不会出现算子兼容问题。导出后会生成best.onnx文件在Python里加载推理import onnxruntime as ort import numpy as np session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) def infer_onnx(frame): # 预处理缩放、归一化、通道转换 img cv2.resize(frame, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255.0 outputs session.run(None, {session.get_inputs()[0].name: img}) return outputs[0]这段推理代码返回的outputs[0]形状是1×25200×6其中25200是YOLOv5在640分辨率下的默认anchor数量需要经过非极大值抑制才能得到最终的框。ONNX Runtime在无显卡的工控机上跑YOLOv5s的640分辨率通常能达到15到25FPS对打电话检测这种人员动作变化慢的场景已经够用。如果你部署的机器是树莓派或其他ARM开发板这个导出方案同样适用性能取决于内存带宽和CPU核心数。6.3 模型瘦身与帧间隔策略模型部署的前沿优化有三个方向按投入产出比排序。第一是降低输入分辨率把640降到480推理时间能缩短三成到四成代价是远距离小目标的召回下降。在做通话检测时如果摄像头距离人物在3米以内降到480完全可接受。第二是跳帧检测每2帧做一次完整推理中间帧直接复用上一帧的检测框配合简单的IoU追踪用户看起来画面是连贯的实际推理量减半。这个方案部署成本最低普通工控机也能跑出实时效果。第三是INT8量化用ONNX Runtime的量化接口把模型从FP32压到INT8体积缩小为原来的四分之一推理速度在CPU上能再翻一倍打电话检测对框精度不敏感量化掉的那点精度损失几乎无感推荐做。每次模型更新迭代之后保留上一次的best.pt和量化后的ONNX文件在验证视频上做回归测试确认新版本没有引入新的误报类型。这个习惯帮我挡掉过好几次上线前才发现的反向回归。最后说一个个人习惯交付给别人的项目我一定会在界面里留下一个debug模式开关打开后显示原始检测框和置信度方便现场调试时一眼看出是模型漏检还是后处理逻辑在捣乱。这套YOLOv5打电话检测方案做完之后最大的体会是工业场景里模型的mAP不是核心数据边界定义、线程稳定性、坐标转换这些摆在明面上的工程细节才是决定项目能不能用起来的关键。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网