免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

监控视角玩手机数据集实战:标注转换与YOLO训练避坑指南

监控视角玩手机数据集实战:标注转换与YOLO训练避坑指南 简介面向室内玩手机识别检测任务这份数据集由监控摄像头多角度、多背景抓拍采集完整图片规模达4974张适用于岗位分心监测、打电话/玩手机识别等实际项目以及课程设计、竞赛训练。当前压缩包内含4126个文件包含1375张JPG图像、1375个VOC格式的XML标注文件、1375个YOLO格式的TXT标注文件并附带第二部分数据的7z下载链接整体包体约991.65MB。数据集标注类别为playphone标签覆盖VOC、YOLO、JSON三种格式可直接接入主流目标检测算法训练。所有图片均来自博主实际项目使用场景玩手机姿态多样、拍摄角度丰富标注精准可靠已有773人学习下载。适合具备一定目标检测基础、需要真实场景数据完成算法验证或项目落地的开发者使用。1. 玩手机数据集值得用吗先看它是不是“监控视角”的再看标签齐不齐做安防、工地和教室场景的推理项目时最缺的不是模型是标注数据。网上能搜到的手机检测集大多是手机随手拍或者网图拼出来的拿去做监控视频推理经常翻车因为图像角度和真实摄像头完全不是一回事。玩手机数据集多角度多背景-监控摄像头拍摄-实际项目所用4974张这个资源一眼看就是给监控场景准备的一套数据摄像头固定视角、俯拍为主、背景从办公桌到教室再到园区出入口都有同时还把voc、yolo、json三种格式的标签一股脑打包进了zip里。如果你是做边缘盒子或后端视频分析、需要训练一个能识别“有人在玩手机”这个动作的yolo模型这套数据天生就比爬图数据集省掉一大截清洗成本。但拿到zip先别急着解压丢进训练脚本里面有几个只有动手才会碰到的坑值得先看完再决定怎么用。2. 先把zip解开、把三种标注格式核对一遍再谈训练2.1 监控摄像头拍的“玩手机”长什么样小目标、俯视角和光斑干扰标题提到“多角度多背景”“监控摄像头拍摄”这两个定语决定了数据分布。摄像头不像人手那样拿着手机贴近拍摄而是挂在三四米高的墙上俯拍画面里一个成年人坐着手机在胸口高度实际像素通常只有几十到一百属于典型的小目标场景。这个特性直接影响后面选模型和设anchoryolov8n在640输入下能框但框的置信度普遍不高。背景多意味着桌面反光、屏幕亮光、玻璃倒影都会混进前景模型很容易把屏幕发光区域当成手机轮廓。另一个容易被忽略的点是监控的“动作姿态”与网图完全不同。网图里“玩手机”大多是手持举在面前监控画面里常见姿态是低头刷屏幕、手机平放桌面单手操作、打电话贴耳。如果标注人员只标了“手里拿手机”的形态训练出来模型对“手机放桌上被手遮挡一半”这类情形会漏检。所以拿到zip后第一件事不是训练是先抽查图片看你这个项目里需要识别的玩手机姿势和数据集里标注的姿势是不是同一类。2.2 解压后先跑一遍文件清单确认目录结构和数量对得上zip里到底装了什么要以解压后的实际目录为准。先建一个干净的目录把zip解开然后列出顶层结构、统计图片数、统计每个标签目录下的文件数确认4974张图和三个标签目录能对得上。mkdir -p phone_dataset cd phone_dataset # 解压时要留意zip内是否带顶层目录 unzip -q ../玩手机数据集*.zip # 如果没有顶层目录文件散落在当前目录需要先建目录归类 ls -la # 统计图片数量jpg/png/jpeg三种常见后缀都数一遍 find . -type f \( -name *.jpg -o -name *.png -o -name *.jpeg \) | wc -l # 统计vocxml、yolotxt、json三种标签文件数量 echo xml files: $(find . -type f -name *.xml | wc -l) echo txt files: $(find . -type f -name *.txt | wc -l) echo json files: $(find . -type f -name *.json | wc -l)这套命令下来能发现不少问题。如果是散文件find在解压目录里直接数能第一时间看到每个子目录里图片和xml数量是否对得上如果图片和xml数量差几十张说明部分原图被删但标注没清理训练时yolo会直接报找不到标注文件。还有一种常见情况是yolo的txt文件放在images同级目录下训练时如果data.yaml里路径忘记写正确会出现“all images have no labels”的报错白白浪费半天。参数说明-q是静默解压避免文件多时刷屏find按后缀统计是最稳的方式不要信压缩包名称里写的“4974张”实际数出来可能因为Mac系统自动生成的__MACOSX目录导致数量偏大或偏小。如果统计出来图片数量超过标题说的4974多半是zip里混了缩略图或重复文件后面划分数据集时需要去重。2.3 用脚本统计标注质量目标尺寸、类别分布、异常标注一次看清文件数量对得上只是第一步还需要确认标注内容本身没有畸形。VOC的xml里存的是xmin/ymin/xmax/ymax绝对坐标YOLO的txt存的是归一化后的中心点和宽高JSON如果按COCO格式存的是bbox加area。三种格式侧重点不同但可以用同一个思路检查解析标注后统计每张图的目标数量、目标宽高占图像的比例、以及类别分布。import os import xml.etree.ElementTree as ET from collections import Counter xml_dir labels_voc # 根据实际解压路径修改 img_w, img_h 1920, 1080 # 先假装统一分辨率后面出错会暴露 cls_counter Counter() target_count [] tiny_boxes 0 for name in os.listdir(xml_dir): if not name.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, name)) root tree.getroot() objs root.findall(object) target_count.append(len(objs)) for obj in objs: cls obj.findtext(name) cls_counter[cls] 1 bbox obj.find(bndbox) xmin int(float(bbox.findtext(xmin))) ymin int(float(bbox.findtext(ymin))) xmax int(float(bbox.findtext(xmax))) ymax int(float(bbox.findtext(ymax))) w xmax - xmin h ymax - ymin if w 32 or h 32: tiny_boxes 1 print(类别统计:, cls_counter) print(每张图目标数分布: min{}, max{}, avg{:.2f}.format( min(target_count), max(target_count), sum(target_count) / len(target_count))) print(小于32像素的小目标框数量:, tiny_boxes)这段脚本能暴露出三类典型问题。第一类别名是不是统一的同一套数据里可能一部分xml写phone另一部分写cellphone统计出来后两个类别计数都非零如果不合并训练时相当于把同一类拆成了两类mAP永远上不去。第二画框是不是有大量重叠或十字交叉每张图目标数量如果平均超过3个说明有些“玩手机”场景里一个框套了两个人后面用yolo默认NMS参数时会把重复框一并输出。第三小目标比例如果小于32像素的框占三成以上直接用yolov8默认anchor训练会严重漏检需要开mosaic增强或改用P2检测头。统计完不要只看终端输出建议把tiny_boxes比例和类别数记在项目README里因为这些参数在后面训练调优时要反复回看。特别是当模型在验证集上出现类别遗忘时基本都能追溯到标注统计阶段就存在的类别不平衡。3. 把voc和json标签统一成yolo格式转换脚本与两个必改参数3.1 三种格式的本质差异和转换方向标题里“vocyolojson三种格式”是打包者给使用者的便利但实际训练yolo只需要txt文件。VOC格式存在xml的bndbox节点记录绝对像素坐标yolo格式要求每行一组class x_center y_center width height全部归一化到0~1之间json如果是COCO格式则用annotations数组保存每个目标的bboxx, y, w, h和category_id。方向很清楚把xml和json都转成txt然后按yolo要求的目录树组织起来。转换前先确认一件事yolo txt里的类别编号必须和data.yaml里的names顺序一致。比如data.yaml里names: [phone]那txt里0就代表phone转xml时findtext(name)读出 “cellphone” 的话需要先做字符串映射不能直接拿原始字符串当类别号。这也是大多数人转换完直接训练的翻车点——模型训出来了但预测的框一直被标成错误类别。3.2 VOC转YOLO解析xml和读取图片尺寸的正确顺序VOC转YOLO的脚本核心是同时知道xml里的框坐标和图片的宽高因为要做归一化。实际数据集里xml偶尔不带size节点或者图片在预处理阶段被缩放过了这时候从root.find(size)读出来的宽高不一定是真实宽高。更稳的做法是直接读同名的jpg/png图片头用cv2.imread拿实际尺寸。import os import cv2 import xml.etree.ElementTree as ET voc_dir labels_voc img_dir images yolo_dir labels_yolo os.makedirs(yolo_dir, exist_okTrue) class_map {cellphone: 0, phone: 0, 玩手机: 0} IMAGE_EXTS (.jpg, .jpeg, .png) for xml_name in os.listdir(voc_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(voc_dir, xml_name) tree ET.parse(xml_path) root tree.getroot() # 先找同名图片直接用图片真实宽高归一化 img_path None base os.path.splitext(xml_name)[0] for ext in IMAGE_EXTS: candidate os.path.join(img_dir, base ext) if os.path.exists(candidate): img_path candidate break if img_path is None: print(f[跳过] 找不到图片: {xml_name}) continue img cv2.imread(img_path) if img is None: print(f[跳过] 图片读取失败: {img_path}) continue h, w img.shape[:2] txt_name base .txt lines [] for obj in root.findall(object): cls obj.findtext(name) if cls not in class_map: class_map[cls] len(class_map) cid class_map[cls] bbox obj.find(bndbox) xmin float(bbox.findtext(xmin)) ymin float(bbox.findtext(ymin)) xmax float(bbox.findtext(xmax)) ymax float(bbox.findtext(ymax)) # 计算归一化中心点和宽高yolo格式要求全部除以图片宽高 cx ((xmin xmax) / 2.0) / w cy ((ymin ymax) / 2.0) / h bw (xmax - xmin) / w bh (ymax - ymin) / h # 夹取到0~1之间防止标注越界导致训练loss异常 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) bw min(max(bw, 0.0), 1.0) bh min(max(bh, 0.0), 1.0) lines.append(f{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(yolo_dir, txt_name), w) as f: f.write(\n.join(lines)) print(类别映射:, class_map)脚本里两个容易被忽略的细节。第一用cv2.imread读真实尺寸而不是抄xml里的size节点是因为很多数据集做过压缩预处理但标注没同步更新直接用xml尺寸归一化会导致框错位一整片。第二越界夹取min(max())这段是针对标注时手抖把坐标写出图外的情况虽然会缩小框面积但比让yolo收到负坐标直接报错强得多。另外代码里class_map用了一个字典加len(class_map)的写法意思是如果遇到没有预先定义的类别就自动分配一个新id。这在数据集类别多时省事但有一个副作用不同文件的类别分配顺序取决于遍历顺序可能出现同一个类在不同文件里id不一样。所以推荐的做法是先遍历一遍xml收集全部类别名确认没有拼写差异后再跑转换比转换中动态分配更可控。3.3 JSON转YOLO先判断是COCO还是labelme格式再动手JSON转YOLO比VOC转YOLO多了一道工序先看JSON结构。COCO格式顶层有images、annotations、categories三个数组bbox是[x, y, width, height]而常见的labelme或标注平台导出格式顶层可能是shapes数组每组有label和points。这两种格式的解析逻辑完全不同不能直接套同一个脚本。import json import os import cv2 json_file annotations.json img_dir images yolo_dir labels_yolo os.makedirs(yolo_dir, exist_okTrue) with open(json_file, r) as f: data json.load(f) # 判断是不是COCO格式有images字段和annotations字段 if images in data and annotations in data: # 建立图片id到文件名的映射 img_id_to_name {img[id]: img[file_name] for img in data[images]} # 建立类别id到类别名的映射0是background通常从1开始 cat_id_to_name {cat[id]: cat[name] for cat in data[categories]} class_map {} lines_by_img {} for ann in data[annotations]: img_id ann[image_id] img_name img_id_to_name[img_id] cat_id ann[category_id] cls_name cat_id_to_name.get(cat_id, unknown) if cls_name not in class_map: class_map[cls_name] len(class_map) cid class_map[cls_name] x, y, w, h ann[bbox] # COCO的bbox是 [x, y, w, h] 绝对像素 img_path os.path.join(img_dir, img_name) img cv2.imread(img_path) if img is None: continue ih, iw img.shape[:2] cx (x w / 2.0) / iw cy (y h / 2.0) / ih bw w / iw bh h / ih if img_name not in lines_by_img: lines_by_img[img_name] [] lines_by_img[img_name].append(f{cid} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) for img_name, lines in lines_by_img.items(): txt_path os.path.join(yolo_dir, os.path.splitext(img_name)[0] .txt) with open(txt_path, w) as f: f.write(\n.join(lines)) else: print(不是COCO格式先人工看一眼顶层字段再写对应的解析分支)COCO转换时最容易踩的坑是category_id不从0开始。COCO官方格式里背景占了0所以第一个实际类别通常从1开始而yolo的类别必须从0开始。脚本里用class_map重新编号就是为了把这个差消除掉。还有一种情况是json里多个标注框对应同一张图但不同框在不同进程里被往txt追加写入导致同文件重复行最终训练时一个图片对应多份重复标注损失曲线会出现周期性跳变。这里用lines_by_img按图片名聚合后再统一写文件就是为了避免追加写入造成的重复。3.4 划分训练集和验证集按摄像头场景分比按图片随机分靠谱很多同行在训练时直接拿随机种子划分train/val这对爬图数据没问题但对监控数据集是灾难。监控视频是连续帧抽出来的同一段视频的前后帧高度相似同一张桌子上同一个角度同一部手机。随机划分会把同一段抽帧图同时分进train和val让你在验证集上看到虚高的mAP真正部署到新摄像头时性能直接掉十个点。更好的做法是先看文件名前缀。如果文件名带摄像头编号或者时间段信息比如CAM_01_xxx.jpg、2024-05-01_14-30-xx.jpg就按前缀分组把不同摄像头或者不同时间段的图片整体划分确保验证集里出现的场景是训练阶段完全没见过的。如果没有显式的前缀信息可以用粗聚类按图片拍摄角度xml里没有透视信息就只能人工抽看或者按图片平均亮度粗分。最简单可行的方案是先按文件名中的时间戳取小时字段把白天和夜间的场景分到两边第二天发现夜间验证集的mAP比白天低一截就是划分正确的最好证明。import os import random from collections import defaultdict img_dir images train_dir train.txt val_dir val.txt random.seed(42) # 按文件名前9个字符例如CAM_01_前包含摄像头编号分组 groups defaultdict(list) for name in os.listdir(img_dir): if not name.endswith(.jpg): continue prefix name[:9] groups[prefix].append(os.path.join(img_dir, name)) train_lines [] val_lines [] for prefix, paths in groups.items(): random.shuffle(paths) split_idx int(len(paths) * 0.8) train_lines.extend(paths[:split_idx]) val_lines.extend(paths[split_idx:]) with open(train_dir, w) as f: f.write(\n.join(train_lines)) with open(val_dir, w) as f: f.write(\n.join(val_lines)) print(ftrain: {len(train_lines)}, val: {len(val_lines)})4. 训练时最容易翻车的五个场景漏检、误判、暗光和类别错位的花式踩坑4.1 现象远处的手机在画面里不到40像素车底的手机和地上的手机都没框住监控摄像头区别于手持拍摄的最大特点是距离远。教室场景下一排人坐着靠窗那排人的手机缩成一个小亮斑模型直接漏检。训练时如果数据增强没开mosaic或者输入分辨率设了480小目标信息在前处理环节就被下采样丢了。原因要从yolo的检测原理说起下采样倍数决定了特征图最小能感知的目标尺寸。yolov8在640输入下P3特征图的步长是8单个像素对应原图8个像素一个40像素的手机在P3上只有5个像素宽特征极弱。解决方法是先把训练分辨率提到960做一个高分辨率版本或者在预处理阶段做滑窗切图把原图切成四块各训练一张部署时同样切图再合并结果。切图方案会牺牲一定帧率但小目标召回率立竿见影。4.2 现象模型把保温杯、小风扇和手机全框出来了置信度还很高玩手机检测里误检最多的是“反光物体”和“圆柱形物体”。办公桌上保温杯的反光区域形状和手机屏幕趋同特别是训练集里大量出现手机平放桌上的样本模型容易学会“台面上有一块亮色矩形就是手机”这种偷懒的判别逻辑。原因是训练集中“手机放桌上”的正样本里背景反光也被标注进去了。解决方法是先看一遍训练集标注检查有没有把手机的倒影、屏幕反射光也标进bbox然后清洗掉这些边界样本或者干脆把“手机平放桌上”单独拆成一个类让模型把“手持手机”和“桌上手机”分别学减少互相干扰。另一个方法是给负样本单独建一个背景类加入一些没有手机的桌面、书堆、水杯图片让模型见过足够多的“非手机”反光物体。4.3 现象傍晚和夜间场景漏检率翻倍同一段视频白天能框住晚上就放飞标题写的是“多背景”背景多了意味着光照分布天然不均匀。监控画面在傍晚之后强光变少手机屏幕成了画面里唯一的高亮区域但模型如果只在白天样本上训练会把屏幕高亮当作过曝而丢掉。更隐蔽的是夜间监控的红外补光会让手机屏幕产生摩尔纹形状特征和白天完全不一样。解决思路有三个。第一训练时把hsv增强的饱和度抖动和曝光抖动开大hsv_h0.015, hsv_s0.7, hsv_v0.5让模型见过更极端的光照。第二给夜间样本单独设置标注而不是统一白天夜间一起训。第三个最笨但最有效的办法从监控视频里抽夜间帧用已有模型做半自动标注人工修正后补进训练集。没有夜跑数据的模型不是一个能上监控的方案。4.4 现象训练时loss正常下降但验证集上把手机标成水杯的置信度很高类别错位问题往往出在第3章的转换环节。VOC转YOLO时如果多个xml里同一类写了两三种拼写phone、cellphone、手机脚本把它们归成不同类别训练时输出头就有四五个类别模型强行记忆每个拼写的分布验证时看到手机框就输出“最高概率的那个类别”而这个类别恰好可能是水杯。还原现场的方法是训练结束后打印模型预测的类别名和对应置信度发现错位后重新清洗类别名。另一种情况是JSON转YOLO后类别id偏移1json里phone的id是1转成yolo后直接拿1当类别号而data.yaml里names: [phone]只有0号类别这个1越界后模型预测时会把背景当成目标。所以每次转换后必须随机抽十个txt文件对照原标注人工核对一次类别编号。4.5 现象训练集很小收敛慢但val_loss后期不降反升4974张图片对目标检测来说不算大。监控场景高度冗余一批图里如果重复帧多有效信息更少。训练到第80个epoch附近发现train_loss还在降val_loss停住甚至回升就是经典过拟合特征。解决方法是把epochs从300降到100同时给ultralytics的patience参数设一个10~20的早停窗口让它在val_loss连续不下降时自动断点。另一个有效做法是把mosaic增强在最后10个epoch关掉作者提供的close_mosaic参数设为10避免训练后期模型还在吃合成噪声这能显著拉平val_loss曲线。如果用了yolov5那套超参对应调整mosaic0.0和anchors的自动重算群体小目标多时把anchor重新聚成3组小尺度簇。5. 用yolo训练这套监控视角数据集的实战参数与验证步骤5.1 训练命令与必调参数转换完txt、划分好train/val之后直接用ultralytics的yolo命令启动训练。模型建议从yolov8n或yolov8s的预训练权重起步不要用yolov8x原因有两个监控视频分析多数跑在边缘设备上x系列张量太大推理帧率不达标小数据集上大模型更容易过拟合n系列的泛化能力在监控场景里反而更稳。# 首轮训练用yolov8s开mosaic增强关闭余弦退火的最后一个阶段 yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs150 \ imgsz640 \ batch16 \ device0 \ patience15 \ close_mosaic10 \ hsv_h0.015 \ hsv_s0.7 \ hsv_v0.4 \ fliplr0.5这里的参数含义patience15是连续15个epoch验证集指标无提升就自动停防止无效消耗算力close_mosaic10在最后10轮关闭mosaic增强让模型从“识别拼接块”回归到“识别真实场景”hsv_v0.4是把亮度抖动开大因为监控场景里光照差异大。如果训练卡里显存紧张batch从16降到8同时把workers4调低避免数据加载瓶颈。这个命令跑完会输出best.pt和last.pt。best.pt是验证集上mAP最高的权重部署时优先用best.pt不要迷信last.pt——最后一个epoch不一定是泛化最好的。5.2 验证不能只看mAP要回放真实监控视频mAP是统计指标但监控项目最终看的是“视频里到底有没有漏报”。我把建议的验证步骤拆成三件事。第一打开训练生成的PR_curve.png看曲线右端的最大召回率如果最大召回率低于0.85说明存在系统性漏检优先回去检查小目标增强是不是没开。第二看confusion_matrix.png的最后一列background如果背景类占比超过10%说明误检严重需要追加负样本或调高置信度阈值。第三拿一段真实监控视频抽帧用best.pt按阈值0.25跑一遍推理重点看三处画面边缘的人、逆光座位、手机屏幕亮度异常的区域。推理时把conf阈值调低到0.15专门看有没有多余的高置信度误检框。如果误检集中出现在桌面反光处就在数据增强里加hsv_s0.8把饱和度抖动进一步调大如果漏检集中出现在画面远端小目标就切到960分辨率训练。5.3 一档进阶技巧清洗后补训比换模型更见效如果首轮训练后对误检率不满意不要急着换yolov8m或者换yolov5先尝试做一次“难样本挖掘”用best.pt对训练集之外的监控视频抽帧推理把置信度在0.3~0.6之间的“不确定框”和所有误检框收集出来人工修正后合并进训练集再训一轮。这个循环跑两三次对监控场景误检率的压制效果比把模型加大一个规格明显得多。我一般会把第二次训练时的hsv_h调回0.01因为补训阶段不希望模型再被颜色抖动干扰同时fliplr也关掉一半避免左右翻转把屏幕姿态改得不合理。实际项目里这套流程保底能让误检率降到可部署水平。如果你手里的场景和数据集标的“玩手机”姿态不完全一致记得先抽50张图人工核对一遍再决定要不要补标一轮。做模型通常不是最花时间的事和数据打交道才是希望帮到你。本文还有配套的精品资源点击获取
返回列表