简介本资源面向计算机视觉学习者与行为分析研究者提供一套基于YOLOv13的手机使用检测完整方案可用于课堂专注度观察、办公场景手机使用习惯研究等目标检测实践。压缩包共约2000个文件以1980个txt标注文件为主另含18个md说明文档、1个yaml数据配置与1份pdf资料整体约345MB目录结构清晰便于按模块查阅。数据集已划分train、val、test并附data.yaml可直接用于yolov5至v12等系列算法训练类别为phone手机。资源同时包含训练好的模型与PyQt5可视化界面并配有使用教程读者可快速完成检测演示与结果查看。目前已有35人学习适合希望从数据标注、模型训练到界面部署完整走通一遍的中级开发者参考。1. 手机使用检测这套资源到底能帮你省掉哪些重复劳动实验室里做行为分析最耗时的往往不是模型调参而是从零攒一份能直接开训的数据集。这套资源把「手机使用检测」这个具体场景的标注数据、训练好的权重和 PyQt5 可视化界面打包在一起拿到手就能跑推理、看效果、改界面。它解决的是「有想法但缺数据缺界面」的卡点适合做课堂行为分析、驾驶分心监测、办公场景手机使用习惯研究的人。数据集标注了 phone 这一类YOLO 格式的 txt 和 VOC 格式的 xml 都给了train/val/test 已经切分好data.yaml 也配好了yolov5 到 yolov12 系列都能直接接。PyQt5 界面负责把检测结果可视化出来不用自己从 QLabel 开始搭。一句话这是一份能让你跳过数据清洗和界面搭建、直接进入模型验证和业务逻辑调试的工程包。2. 数据集结构与 YOLO 格式转换从 xml 到 txt 的完整链路2.1 为什么同时给 YOLO 和 VOC 两套标签YOLO 系列训练读的是归一化后的 txt每行class_id x_center y_center width height坐标都是相对图像宽高的比例值。VOC 的 xml 则是绝对像素坐标xmin ymin xmax ymax四个角点。两套都留着是因为不同工具链吃的东西不一样labelImg 默认存 xml而 ultralytics 的 DataLoader 只认 txt。资源里两套并存意味着你可以用 xml 做标注复核和可视化画框用 txt 直接喂训练不用来回转。常见做法是拿 xml 当「原始档案」txt 当「训练副本」改标注时先动 xml 再重新导出 txt避免直接改 txt 导致坐标对不上。2.2 目录组织与 data.yaml 的字段含义拿到包之后先别急着训把目录结构看清楚。典型布局是这样dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamlimages 和 labels 下的子目录名必须一一对应train 的图配 train 的标签错一个就全乱。data.yaml 里通常长这样path: ./dataset train: images/train val: images/val test: images/test nc: 1 names: [phone]path是数据集根目录train/val/test是相对 path 的路径nc是类别数names是类别名列表。这里 nc1、names 只有 phone说明是单类检测。如果你后面要加「人脸」或「手持姿态」这类新类改 nc 和 names 的同时txt 里的 class_id 也要跟着改否则模型会把新类当成 phone 学。2.3 用脚本把 VOC 的 xml 批量转成 YOLO 的 txt资源里虽然两套都有但如果你自己补标了一批 xml就得转。下面这个脚本我常用读 images 目录找同名 xml输出到 labels 目录import os import xml.etree.ElementTree as ET from PIL import Image # 类别映射顺序必须和 data.yaml 的 names 一致 CLASS_MAP {phone: 0} def convert(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_okTrue) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 用对应图像的真实宽高做归一化不能拿 xml 里的 size 凑合 img_name xml_file.replace(.xml, .jpg) img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): print(f缺图跳过: {img_name}) continue w, h Image.open(img_path).size lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_MAP: continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 转成中心点加宽高的归一化格式 xc (xmin xmax) / 2.0 / w yc (ymin ymax) / 2.0 / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{CLASS_MAP[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(out_dir, xml_file.replace(.xml, .txt)), w) as f: f.write(\n.join(lines)) convert(dataset/xml, dataset/images/train, dataset/labels/train)逻辑上就三步解析 xml 拿绝对坐标用图像真实宽高归一化按类别映射写 txt。参数上注意CLASS_MAP的 value 必须和 data.yaml 的 names 索引对齐Image.open读的是原图尺寸而不是 xml 里声明的 size因为有些标注工具会写错 size 字段。跑完抽查几个 txt用cat看一眼数值是不是都在 0 到 1 之间出现大于 1 的基本就是宽高取反了。2.4 划分 train/val/test 的注意点资源已经切好了但如果你自己扩数据别用随机切分。手机使用检测的图像往往来自连续视频帧相邻帧几乎一样随机切会让同一场景的帧同时进 train 和 val验证指标虚高。常见做法是按视频源或时间段切前 70% 时间段的帧进 train中间 15% 进 val最后 15% 进 test。这样 val 的指标才接近真实泛化能力。切完检查一下三个子集的类别分布phone 框的数量比例别差太多差太多就在 yaml 里加augment或者对少数子集过采样。3. 用 YOLOv13 训练手机检测模型参数怎么设、日志怎么看3.1 环境准备与 ultralytics 的版本选择这套资源标称支持 yolov5 到 yolov12实际训练时我一般用 ultralytics 这个统一接口它把 v5/v8/v11 的 API 拉齐了换模型只改一行。装的时候注意 Python 版本别太低3.9 以上比较稳pip install ultralytics8.3.0 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121第二行是按 CUDA 12.1 装的 torch如果你机器是 CUDA 11.8把 cu121 换成 cu118。装完跑yolo checks看环境重点看 CUDA 是否可用、显存多大。显存 8G 以下建议把 batch 降到 8 或 4不然第一个 epoch 就 OOM。3.2 训练命令与关键参数逐项说明假设 data.yaml 在 dataset 目录下模型权重用资源里给的预训练权重yolo detect train \ datadataset/data.yaml \ modelyolov13n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/phone \ nameexp1data指向 yamlmodel是初始权重epochs是总轮数imgsz是输入分辨率batch是批大小lr0是初始学习率patience是早停耐心值连续 20 轮 val 指标不升就停。project和name决定日志和权重的落盘位置。手机检测这种单类任务100 轮通常够如果 val 的 mAP 在 60 轮后还在涨可以加到 150。lr0 默认 0.01 对单类偏大如果 loss 震荡厉害降到 0.005 再试。3.3 训练日志里该盯哪几个数跑起来之后终端会刷一堆指标别只看 mAP50。重点看三个box_loss是否稳定下降cls_loss是否收敛到接近 0mAP50-95是否在涨。如果 box_loss 降但 mAP 不涨多半是标注框质量有问题回去抽查 txt 和原图的对齐情况。如果 cls_loss 一直不降检查 data.yaml 的 nc 和 names 是否和 txt 里的 class_id 对得上。资源里只有 phone 一类cls_loss 理论上应该很快压下去压不下去就是标签有脏数据。3.4 用训练好的权重做推理验证训完或者直接用资源里给的权重跑单张图看效果yolo detect predict \ modelruns/phone/exp1/weights/best.pt \ sourcetest_images/ \ conf0.25 \ saveTrueconf是置信度阈值0.25 是常用起点。手机检测场景里如果画面里手机小且多conf 可以降到 0.15 召回更多但误检也会涨。跑完看runs/detect/predict下的图重点看漏检和误检分别出现在什么场景逆光、遮挡、手持角度偏的时候漏检多说明训练集缺这类样本得补数据而不是调阈值。4. PyQt5 可视化界面把检测结果接进桌面应用4.1 界面整体结构与信号槽机制PyQt5 做检测界面核心就三块左边 QLabel 显示图像右边 QPushButton 触发检测底部 QTextEdit 打日志。信号槽是 Qt 的命脉按钮的clicked信号连到你的检测函数检测函数跑完再发一个自定义信号把结果传回主线程更新 QLabel。别在按钮回调里直接跑推理推理耗时几百毫秒到几秒会把界面卡死用户以为程序崩了。常见做法是开一个 QThread 跑推理通过pyqtSignal把带框的图像和统计信息发回主线程。4.2 加载模型与图像显示的代码骨架下面这段是界面里最核心的推理加显示逻辑import sys import cv2 from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class InferThread(QThread): # 自定义信号传回带框图像和检测数量 result_ready pyqtSignal(object, int) def __init__(self, model, img_path): super().__init__() self.model model self.img_path img_path def run(self): results self.model(self.img_path, conf0.25) # plot 返回带框的 numpy 数组BGR 格式 annotated results[0].plot() self.result_ready.emit(annotated, len(results[0].boxes)) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.model YOLO(best.pt) self.label QLabel(等待检测) self.btn QPushButton(选择图片并检测) self.btn.clicked.connect(self.on_click) layout QVBoxLayout() layout.addWidget(self.label) layout.addWidget(self.btn) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def on_click(self): # 实际项目里用 QFileDialog 选文件这里写死路径演示 self.thread InferThread(self.model, test.jpg) self.thread.result_ready.connect(self.show_result) self.thread.start() def show_result(self, img, count): # OpenCV 是 BGRQt 要 RGB转换顺序别搞反 rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.label.setPixmap(QPixmap.fromImage(qimg)) self.setWindowTitle(f检测到 {count} 个手机) app QApplication(sys.argv) win MainWindow() win.show() sys.exit(app.exec_())逻辑上InferThread继承 QThreadrun里调模型plot()直接返回画好框的图省得自己写画框代码。result_ready信号带两个参数一个是图像数组一个是检测框数量。主线程的show_result负责把 BGR 转 RGB 再塞进 QLabel。参数上注意QImage的构造需要传 bytesPerLine也就是ch * w传错会花屏。conf和训练时保持一致不然界面里看到的框和验证时对不上。4.3 把检测数量和行为统计接进界面光显示框还不够行为分析要的是统计。在show_result里可以顺手算当前帧检测到几个 phone连续多少帧都有 phone用来判断「持续使用」还是「短暂拿起」。常见做法是维护一个滑动窗口比如最近 30 帧里有 20 帧检测到 phone就标记为「正在使用」。这个逻辑放在主线程里做因为推理线程只管出框业务判断放主线程方便改阈值。界面底部加个 QTextEdit每次检测完追加一行「时间戳 数量 状态」导出成 CSV 就是使用习惯的原始记录。5. 避坑与排查标注、训练、界面三段的血泪经验5.1 现象训练 loss 正常但 mAP 一直是 0原因data.yaml 里的names和 txt 里的 class_id 对不上或者 txt 路径和 images 路径没对齐。YOLO 读标签是按 images 路径替换成 labels 去找的目录名差一个字母就全读不到读不到标签时 loss 会算成背景mAP 自然为 0。解决先跑一遍yolo detect train加--verbose看它打印的train: Scanning...后面跟的图片数和标签数是否一致。不一致就检查 images 和 labels 的子目录名是否严格对应再抽查一个 txt 的 class_id 是否在 names 范围内。5.2 现象PyQt5 界面点按钮后直接卡死原因推理函数写在主线程的按钮回调里模型加载和推理都是阻塞操作主线程被占住Qt 的事件循环转不动界面就不刷新了。解决把推理放进 QThread 或 QRunnable通过信号把结果发回主线程。注意 QThread 的对象要在主线程创建run里别碰界面控件只发信号。如果推理要连续跑视频流用 QTimer 定时触发或者用线程池别在run里写死循环。5.3 现象界面显示的框位置偏移或颜色错乱原因OpenCV 读图是 BGRQt 的 QImage 默认按 RGB 解析不转换的话红蓝通道互换框的颜色和原图都对不上。另外QImage构造时 bytesPerLine 传成w * 3在某些宽度下会错位因为 Qt 要求按 4 字节对齐。解决统一在显示前做cv2.cvtColor(img, cv2.COLOR_BGR2RGB)bytesPerLine 用rgb.strides[0]而不是自己算。如果还偏检查plot()返回的图是否被 resize 过QLabel 的setScaledContents(True)会拉伸图像但不会改坐标框会跟着拉伸看起来偏但实际是显示缩放导致的。5.4 现象验证集 mAP 很高实际用的时候漏检严重原因train/val/test 是按随机切分做的同一段视频的相邻帧被分到了不同子集验证集里全是训练集见过的场景指标虚高。解决按视频源或时间段重新切分确保 val 和 test 里的场景在 train 里没出现过。切完再训一遍mAP 会掉几个点但那个数才是真实水平。如果掉太多说明模型泛化不够加数据增强或者换更大的模型。5.5 现象推理时显存够但速度很慢原因imgsz设得太大或者没开半精度。手机检测这种单类任务640 通常够设到 1280 速度直接砍半。另外 PyTorch 默认 FP32开 FP16 能快 30% 左右。解决推理时加halfTrueimgsz从 640 起步不够再往上加。如果用的是 CPU 推理把devicecpu显式写上别让它自动选有时候自动选会走到集显上反而更慢。6. 进阶把检测结果变成使用习惯的时间序列界面跑通之后真正有价值的是把逐帧检测变成行为指标。我一般会在 PyQt5 里加一个后台线程按固定间隔比如每秒 2 次抓帧推理把「时间戳、检测数量、置信度均值」写进 SQLite。攒够一天的数据用 pandas 做滑动平均就能看出「上午 10 点手机使用最频繁」这类规律。下面这段是统计逻辑的骨架import sqlite3 import pandas as pd conn sqlite3.connect(usage.db) # 按分钟聚合算每分钟平均检测数量和最大置信度 df pd.read_sql(SELECT ts, count, conf FROM detections, conn) df[ts] pd.to_datetime(df[ts]) df.set_index(ts, inplaceTrue) per_min df.resample(1min).agg({count: mean, conf: max}) # 标记持续使用连续 5 分钟平均数量大于 0.5 per_min[using] (per_min[count] 0.5).rolling(5).sum() 5 print(per_min[per_min[using]].head(20))resample(1min)把逐帧记录压成分钟级rolling(5).sum() 5判断最近 5 分钟是否持续有手机。阈值 0.5 和 5 这两个数按场景调课堂场景可以放宽到 0.3驾驶场景要收紧到 0.8。导出的 CSV 可以直接喂给可视化库画热力图按小时和星期几交叉看使用习惯的峰谷就出来了。验证模型有没有过拟合我习惯留一段完全没参与训练的视频跑完统计漏检率和误检率。漏检率高于 15% 就补对应场景的数据误检率高于 10% 就检查是不是把手机壳、遥控器这类相似物体标成了 phone。这套流程走下来从数据到界面到行为分析就闭环了。从那以后我每次拿到新的检测包都先跑一遍单张推理确认标签对齐再开界面最后才上统计顺序反了就得返工。希望帮到你。本文还有配套的精品资源点击获取