news 2026/9/26 12:57:15

YOLOv5打电话行为检测落地全指南:数据集、PyQt界面与部署避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv5打电话行为检测落地全指南:数据集、PyQt界面与部署避坑

简介:这套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 16
YOLOv5m21.2M42MB49.0 GFLOPs6GB以上稳妥
YOLOv5l46.5M92MB109.1 GFLOPs8GB勉强,不推荐

这里的GFLOPs是YOLOv5官方给出的640分辨率下的理论计算量,实际部署时还受显卡、输入分辨率影响。选择逻辑很简单:打电话检测对框的精细程度要求不高,但对帧率敏感,因为后续还要做时序判定,帧率低于10FPS会导致“打电话”这个状态在连续帧上断断续续。摄像头分辨率是1080P时,推流到模型前通常要缩到640×640,YOLOv5s在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_ratio=0.15, test_ratio=0.05, seed=42): 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_ok=True) os.makedirs(lbl_out_dir, exist_ok=True) 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是0,call是1,person是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.pt,best.pt是验证集上表现最好的权重,last.pt是最后一个epoch的权重,部署时一定选best.pt。打开runs/train/exp目录下的results.png,重点看三条曲线:训练损失、验证损失、验证集mAP@0.5。

对打电话检测这个任务,我通常把mAP@0.5作为主要验收指标,0.5的交并比阈值意味着不需要框非常精准,只要大概框住手机就能算对的框。实际部署时置信度阈值会卡在0.25到0.4之间,所以mAP@0.5比mAP@0.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, source=0, parent=None): 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读进来的是BGR,QImage需要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_size=10, threshold=7): 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曲线一路下降,mAP@0.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 12

opset 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不是核心,数据边界定义、线程稳定性、坐标转换这些摆在明面上的工程细节才是决定项目能不能用起来的关键。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 12:56:47

产品质量策划总结与认定报告编写指南:APQP框架与数据校验

简介&#xff1a;这份专题资料为2021至2022年产品质量策划总结和认定报告文档&#xff0c;面向制造企业质量工程师、体系审核人员及质量管理培训学员&#xff0c;用于梳理产品从设计到交付全过程的质量控制要点。压缩包内共1个doc文件&#xff0c;约52KB&#xff0c;可直接编辑…

作者头像 李华
网站建设 2026/9/26 12:56:18

保健食品广告语合规红线与赛道评估方法(2026版)

保健食品广告语的合规要求&#xff0c;比普通食品更严格。 保健食品不得使用医疗用语、不得做功效对比、不得对安全性做断言&#xff0c;要遵守批准功效范围等规定。 本文梳理三类高频合规红线&#xff0c;并拆解保健食品广告语在功能市场、品质市场、礼品市场三个赛道的评估方…

作者头像 李华
网站建设 2026/9/26 12:54:33

图论PDF到生产代码:NetworkX实战避坑指南

简介&#xff1a;本资源是一份面向计算机科学、网络工程及运筹学初学者与进阶学习者的图论核心入门讲义&#xff0c;聚焦图与网络分析的基础理论与经典应用。内容系统涵盖图论起源&#xff08;如哥尼斯堡七桥、哈密尔顿环球旅行、中国邮递员问题&#xff09;、基本概念&#xf…

作者头像 李华
网站建设 2026/9/26 12:53:32

SQL Server数据库设计实战:从表结构到索引优化的完整指南

做SQL Server这套东西十几年&#xff0c;每次接手一个新项目&#xff0c;我第一件事不是写代码&#xff0c;而是先看数据库设计。很多人觉得这是小题大做&#xff0c;觉得CRUD嘛&#xff0c;表随便建一建就行了。但恰恰是这个"随便"&#xff0c;后面会让你付出成倍的…

作者头像 李华
网站建设 2026/9/26 12:53:28

L2线性回归全解析:从最小二乘原理到正则化实战避坑指南

在头歌平台上和线性回归死磕了整整一个周末之后&#xff0c;我才后知后觉地发现&#xff1a;这门课最磨人的不是推导公式&#xff0c;而是把公式转化为能跑通的代码时那些“理所当然”的细节。如果你也在国科大的《模式识别与机器学习》L2线性回归上被卡住&#xff0c;或者正在…

作者头像 李华