ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

监控场景玩手机检测实战:4974张数据集与VOC/YOLO/JSON格式转换指南

监控场景玩手机检测实战:4974张数据集与VOC/YOLO/JSON格式转换指南 简介这是一份用于岗位分心监测、玩手机识别等实际场景的室内监控摄像头目标检测数据集共4974张图片因包体过大分为两部分当前压缩包内含第一部分1375张标注图片。文件构成包含1375个jpg原图、对应1375个xmlVOC格式与1375个txtYOLO格式标签文件另附1个7z压缩包用于存放第二部分数据下载链接方便按需取用三种标签格式覆盖主流检测工具类别统一为“playphone”。压缩包整体约991.65MB文件总数4126个背景与拍摄角度多样包含室内不同光线、距离和姿态下的玩手机画面标注精准适合课程设计、算法训练或比赛项目直接使用。已有772人学习下载由博主实际项目验证质量可靠可用于模型拟合与效果调试。1. 玩手机检测为什么让监控视觉模型集体翻车先看清这份4974张数据集晚自习教室的监控画面里一个低头看手机的学生在检测模型里被标成了“趴着睡觉”。这个画面我见过不下十次。玩手机检测难不在算法而在数据——监控摄像头视角高、俯角大教室、工位、食堂多种背景轮换手机屏幕小、反光强、手部遮挡频繁常规数据集训练出来的模型拿到监控场景下几乎必翻车。这份4974张、由监控摄像头拍摄的多角度多背景数据集就是为这类问题准备的图片覆盖不同机位、不同时段和不同室内外场景标签同时给出VOC、YOLO、JSON三种格式解压后可以直接喂给YOLO系列做训练省掉最痛苦的标注环节。适合正在做智慧教室、安全生产、办公室行为检测的算法工程师也适合刚入门目标检测、想完整走一遍数据到训练流程的开发者。2. 三种标签格式逐个拆解VOC、YOLO、JSON的坐标系与相互转换一份数据集同时给三种标签格式不是冗余是省事。不同训练框架吃不同格式YOLO系列原生读txtMMDetection系偏好COCO JSON老一些的检测脚本还在用VOC XML。如果只给一种你换框架就得重写转换脚本三种都给反而逼着你先把坐标体系弄清楚。这一章我按实际解压后会看到的文件结构来讲再给出一个一致性校验脚本确保三种格式说的是同一批框。2.1 文件目录里实际会看到什么解压后常见的目录组织是图片目录、标注目录、划分说明三块。play-phone-dataset/ ├── images/ # 4974张jpg文件名通常是时间戳或摄像头编号 │ ├── cam_01_000123.jpg │ ├── cam_01_000124.jpg │ └── ... ├── annotations_voc/ # VOC格式每张图对应一个xml │ ├── cam_01_000123.xml │ └── ... ├── labels_yolo/ # YOLO格式每张图对应一个txt │ ├── cam_01_000123.txt │ └── ... ├── labels_json/ # JSON格式可能是单文件也可能是逐图 │ ├── annotations.json # COCO风格单文件 │ └── ... ├── train.txt # 训练集图片路径列表 ├── val.txt # 验证集图片路径列表 └── classes.txt # 类别名常见就一个: playing_phone拿到手第一件事不是开训而是先看 classes.txt 和 train.txt 的行数。classes.txt 里如果写着多个类别后面所有转换脚本都要按这个顺序映射train.txt 和 val.txt 的行数加起来不一定等于4974因为有些图可能被剔除过或没有目标这本身是正常的但你要心里有数。我一般还会顺手统计一下两张列表里有没有重复路径重复了说明划分脚本写得粗糙后面验证集指标会虚高。2.2 VOC与YOLO标签的本质差异绝对坐标与归一化坐标VOC XML 的核心是 object 节点下的一组 bndbox存的是像素绝对值annotation filenamecam_01_000123.jpg/filename size width1920/width height1080/height depth3/depth /size object nameplaying_phone/name bndbox xmin742/xmin ymin386/ymin xmax913/xmax ymax581/ymax /bndbox /object /annotationYOLO txt 每行五个数字类别ID、归一化中心点x、归一化中心点y、归一化宽度w、归一化高度h全部是0到1之间的小数。换算关系是x_center (xmin xmax) / 2 / width y_center (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height这里容易栽的跟头是宽高比。VOC里xmin/xmax是像素值YOLO里w/h是归一化后的比例两者互相转换时除法必须用原图尺寸不是用最大坐标值。如果某张监控图分辨率是1920x1080另一张是1280x720混在一起转换时尤其要小心——不能假设所有图一样大。2.3 JSON标签的两种可能结构COCO式数组与单图键值JSON格式的标签在数据集里有两种常见形态。第一种是COCO风格一个文件包含 images、annotations、categories 三个数组每个标注框有 image_id、category_id 和 bbox 四项bbox 是[x, y, w, h]绝对像素值第二种是逐图的键值结构文件名作为键值是框列表。拿到手先打开看一眼。{ images: [ {id: 0, file_name: cam_01_000123.jpg, width: 1920, height: 1080} ], annotations: [ {id: 0, image_id: 0, category_id: 1, bbox: [742, 386, 171, 195]} ], categories: [ {id: 1, name: playing_phone} ] }bbox 里的顺序是 x、y、w、h不是 xmin、ymin、xmax、ymax。这两个顺序在转YOLO时写错是最常见的翻车点前者要先算 xmax x w后者直接可用。每次写转换脚本前先打印一段样本确认别信文件名里的“coco”字样。2.4 一致性校验把三种格式对齐跑一遍三种格式本身可能来自不同的标注工具导出框数量不一致是常态。校验逻辑很简单同一张图三种格式解析出来的框数量应该相等中心点坐标应该几乎一样。import xml.etree.ElementTree as ET import json, os def parse_voc(xml_path): root ET.parse(xml_path).getroot() boxes [] for obj in root.iter(object): b obj.find(bndbox) boxes.append(( int(b.find(xmin).text), int(b.find(ymin).text), int(b.find(xmax).text), int(b.find(ymax).text) )) return boxes def parse_yolo(txt_path): boxes [] with open(txt_path) as f: for line in f: parts line.strip().split() boxes.append(tuple(float(x) for x in parts[1:])) return boxes def parse_json(json_path, img_name): data json.load(open(json_path)) boxes [] for ann in data[annotations]: img data[images][ann[image_id]] if img[file_name] img_name: x, y, w, h ann[bbox] boxes.append((x, y, x w, y h)) return boxes脚本本身不复杂核心是三个解析函数的返回值都统一成(xmin, ymin, xmax, ymax)的四元组再逐张比较长度和坐标偏差。跑完会出现三种结果一是三种格式框数一致说明导出没问题二是XML与JSON一致但YOLO多框说明YOLO文件里有重复行三是三边都不一致大概率是某几张图在标注后被裁剪过旧标签没同步。这种图要挑出来单独处理不然后面训练时Loss会莫名其妙跳高。3. 用YOLOv8训练这份数据集的完整装配流程从VOC到训练启动上一章讲清楚了三种格式的坐标系这一章直接落到YOLOv8训练。我一般不会直接吃数据集自带的YOLO目录——因为你不知道它的归一化是用哪张图的尺寸算的万一混入了不同分辨率的图标签就废了。所以哪怕已经有了YOLO格式我也会从VOC原始XML重新转一遍确认公式没被污染。3.1 写一个VOC转YOLO的转换脚本坐标越界一起处理转换脚本要处理的不仅是坐标归一化还有两类脏数据框坐标超出图像边界、框宽高为0。监控画面裁剪边缘经常留下半截框直接转成YOLO后框中心点落在0到1之外训练时会被当作异常样本。import xml.etree.ElementTree as ET import os def voc2yolo(xml_path, out_dir, class_map, img_w, img_h): root ET.parse(xml_path).getroot() lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: continue cls_id class_map[name] b obj.find(bndbox) xmin int(b.find(xmin).text) ymin int(b.find(ymin).text) xmax int(b.find(xmax).text) ymax int(b.find(ymax).text) # 夹紧到图像边界内避免归一化后出现负数 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) if xmax xmin or ymax ymin: continue x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_path os.path.join(out_dir, os.path.basename(xml_path).replace(.xml, .txt)) with open(out_path, w) as f: f.write(\n.join(lines))代码里做了两件容易被忽略的事一是坐标先夹紧再判空避免坐标越界样本被直接丢掉而不是纠正二是框退化到零宽高时跳过。前者应对的是标注边缘误超界后者应对的是标注员把点标成框的极端情况。参数方面class_map 需要你根据 classes.txt 手动构造比如{playing_phone: 0}img_w 和 img_h 是从同名图片读出来的不要用XML里 size 节点存的宽高因为 XML 的 size 偶尔会和实际图片不一致。3.2 按场景划分训练验证集避免同源图像泄漏这份数据集标称“多角度多背景”但同一摄像头连续帧的画面高度相似。如果按文件随机切分同一个摄像头、同一个背景的相邻帧会同时出现在训练集和验证集里验证指标会虚高到失真。我见过有人拿随机划分跑出 mAP 0.87换到真实场景直接掉到0.31问题就出在这里。正确做法是按摄像头编号或场景文件夹分组划分。文件名里通常带摄像头ID比如cam_01_000123.jpg直接用文件名前缀做分组。import glob, os, random from collections import defaultdict files glob.glob(images/*.jpg) groups defaultdict(list) for f in files: cam_id os.path.basename(f).split(_)[0] _ os.path.basename(f).split(_)[1] groups[cam_id].append(f) random.seed(42) group_names list(groups.keys()) random.shuffle(group_names) val_cams group_names[:int(len(group_names) * 0.2)] val_files [f for cam in val_cams for f in groups[cam]] train_files [f for cam in group_names[int(len(group_names) * 0.2):] for f in groups[cam]] with open(train.txt, w) as f: f.write(\n.join(train_files)) with open(val.txt, w) as f: f.write(\n.join(val_files))划分比例我一般取80/20但注意这里是按摄像头分组不是按图片数分组最终训练集和验证集的图片数比例不一定是精确的80比20。有5个摄像头时就可能验证集占到25%这没关系关键是验证集里不能出现和训练集同一机位的画面。另外固定 random.seed 很重要否则每次跑结果都不稳定没法对比实验。3.3 生成data.yaml并启动第一次训练YOLOv8的训练配置极简一个yaml文件加一条命令。data.yaml里只需要写清楚训练集、验证集路径、类别数和类别名。path: /your/absolute/path/to/play-phone-dataset train: train.txt val: val.txt names: 0: playing_phone启动训练前检查三件事train.txt里每行路径是否存在、val.txt里是不是空文件、类别数是否正确。这段检查在项目里遇到太多次了——数据集转手多次后路径前缀被改掉训练到一半才报FileNotFoundError白白浪费几小时。yolo detect train \ dataplay_phone.yaml \ modelyolov8n.pt \ imgsz640 \ epochs120 \ batch32 \ device0参数选择上监控画面分辨率高但手机目标小输入尺寸我建议至少640起步显存够就上960小目标AP会有肉眼可见的提升epochs从120起调玩手机检测不是复杂任务120轮足够收敛再多容易过拟合到背景纹理batch越大BN越稳但显存不够时优先降batch而不是降imgsz。4. 训练之前先做三件事可视化回放、类别与尺寸统计、漏检边界确认拿到数据集直接开训是新手最常见的操作但这份数据集的标签是人工标注的标注质量直接影响模型上限。训练前花四十分钟做三轮检查比训练后发现指标不对再回头查数据要快得多。这一章给出一套我在每次接到新数据集时都会跑的检查流程。4.1 把标签画回原图用抽检脚本抓标注错位可视化是检查标注质量最快的方式没有之一。从训练集和验证集各自随机抽200张把标签框画回原图存到一个目录里快速翻看。重点看三类错误框是不是框住了整个手机而不是只有屏幕、框是不是偏离目标半个身位、有没有漏标。import cv2, random, glob pairs [] for img_path in glob.glob(images/*.jpg): base os.path.basename(img_path).replace(.jpg, ) txt_path flabels/{base}.txt if os.path.exists(txt_path): pairs.append((img_path, txt_path)) random.seed(7) sample random.sample(pairs, 200) for img_path, txt_path in sample: img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path) as f: for line in f: parts line.strip().split() cls int(parts[0]) x_c, y_c, bw, bh map(float, parts[1:]) x1 int((x_c - bw / 2) * w) y1 int((y_c - bh / 2) * h) x2 int((x_c bw / 2) * w) y2 int((y_c bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) out fcheck_vis/{os.path.basename(img_path)} cv2.imwrite(out, img)这个脚本有几个细节值得说。反向还原坐标时用int()而不是round()因为画框时差一个像素不影响判断但round()在Python 3的银行家舍入规则下偶尔会出现反直觉的结果。宽度和高度乘以w和h时注意归一化坐标是相对于原始图尺寸的如果之前做过缩放预处理这里要同步缩放。抽检数量200张是经验值对4974张的数据集来说大约4%的覆盖率能发现大多数系统性问题如果这批里发现多于10张有明显错标说明标注质量不可靠需要全量检查。4.2 统计类别平衡与手机目标的像素占比玩手机检测的难点集中在两个数字上单张图目标数量分布、目标像素面积占整图的比例。前者决定要不要考虑密集检测后者决定小目标问题有多严重。import glob, numpy as np area_ratios [] count_per_img [] for txt_path in glob.glob(labels/*.txt): with open(txt_path) as f: lines f.readlines() count_per_img.append(len(lines)) for line in lines: parts line.strip().split() w float(parts[3]) h float(parts[4]) area_ratios.append(w * h) count_per_img np.array(count_per_img) area_ratios np.array(area_ratios) print(f单图框数均值: {count_per_img.mean():.2f}, 最多: {count_per_img.max()}) print(f目标像素占比 中位数: {np.median(area_ratios):.4f}, 90分位: {np.percentile(area_ratios, 90):.4f}) print(f占比小于0.01的目标比例: {(area_ratios 0.01).mean() * 100:.1f}%)监控画面里手机目标通常占整图的1%到3%中位数在0.02左右90分位可能到0.05。如果统计结果中位数低于0.01说明大量目标在100像素以下你得认真考虑用SAHI切图或者把输入分辨率拉到1280。这个统计还顺带暴露类别不平衡如果 classes.txt 里不止一个类别按类别分组统计框数某个类别框数少于总数5%时训练时考虑提高该类别的loss权重。4.3 明确“玩手机”的判定边界屏幕亮起才算这是标注语义问题也是我在这类项目里踩过最深的坑。同一批数据不同标注员对“玩手机”的理解完全不一样有人把“手里拿着手机但屏幕黑着”也算有人只标“屏幕亮着且在操作”还有人把“手机放在桌上但手搭在上面”也标了进去。三种语义混在一个数据集里模型学出来的特征会打架。建议训练前先做一个动作把数据集的JSON标签打开随机看50个标注框对应的图片确认这套数据的“正样本”到底是哪种状态。如果是监控场景的玩手机检测模型真正要抓的是“学生低头看手机屏幕”而不是“手里有个手机”——后者在教室里满地都是误报会高到没法用。确定语义之后再看标签和实际状态的匹配度如果发现标签确实混了多种语义宁可花两天时间把不一致的样本挑出去重标也不要带着脏标签硬训不然模型会陷入“黑匣子”状态你怎么调参都没用。5. 避坑监控场景训练手机检测的5个高频问题这一章写的是我在实际项目中反复遇到过的问题每条都是真金白银换来的。按“现象、原因、解决”三步来写你训练时碰到同款症状可以直接照方抓药。坑一标签坐标超出图像边界现象训练前可视化没做训练时Loss正常下降但验证集mAP始终在0.3附近上不去。排查时发现部分YOLO标签里出现1.02这样的归一化坐标或者0.4、-0.02。原因监控画面在标注阶段被裁切过标注框是按原图尺寸画的裁切后边界框的半截留在画面外转换脚本没有夹紧坐标。解决转换脚本里做 clamp把坐标强制限制在0到1之间同时记录坐标越界的图片路径。处理完统计一下有多少张被修正过这个数字告诉你数据集原始标注的边界质量。修正后重新训练mAP通常能涨5到8个百分点。坑二“玩手机”和“手持手机”语义混标现象模型训练完成后室内摄像头对着坐姿办公的人只要对方手靠近桌面就触发报警误检率超过70%。原因标注里混入了“拿着手机但没有看”的样本模型学到的是“手部附近有矩形物体”而不是“屏幕亮起”。监控画面里手部动作频繁误报被成倍放大。解决重新梳理标签语义只保留“屏幕可见且有操作行为”的目标。做法是把所有包含手机目标的图片单独导出来人工快速过一遍删掉语义不符的框。这一步在4974张的规模下大约需要两小时但能避免部署后不停的误报警。坑三夜间样本占比太少导致夜间直接漏检现象白天mAP测试0.82换到夜间监控画面玩手机目标直接漏掉一大半漏检率飙升到45%。原因数据集中白天样本占绝大多数模型把亮度特征当成了强判别线索。夜间屏幕亮光是唯一高亮区域白天学到的特征完全不适用。解决训练时做色彩和数据增强的针对性调整。YOLOv8训练参数里把 hsv_h 和 hsv_s 的增强幅度调大一些让模型不过度依赖色相同时在数据划分时按时段分层保证验证集里夜间样本占比不低于15%。如果数据集本身夜间样本极少老实去补拍夜间数据增强救不了样本分布本身的问题。坑四JSON解析报错json parse error或字段类型不对现象跑数据校验脚本时某几张图的JSON解析中断报错信息类似cannot deserialize value of type java.util.Date from str或者提示某个框的坐标字段是字符串不是数字。原因标注工具导出的JSON里混入了非标准字段坐标值可能被序列化成字符串如742.5也可能是空字符串。大多数脚本直接调json.load不会报坐标类型错报错的一般是下游框架在转对象时要求强类型。解决解析JSON后统一加一步类型清洗——遍历所有标注框把坐标字段用float()强转捕获异常后打印图片文件名和字段值。这步清洗虽然枯燥但能避免JSON转VOC或YOLO时出现整批数据被跳过的情况。坑五训练中BN崩溃Loss出现NaN现象训练到第30轮左右Loss突然变成NaN之后一直无法恢复模型权重保存后推理输出全是无效框。原因最常见的是学习率过高导致BN统计量发散其次是标签类别ID超出data.yaml里的names数量比如类别ID写成了2但names只有2个类别合法ID是0和1。解决先看标签里的类别ID范围确认没有越界然后调整训练参数把lr0降到0.001以下或直接使用YOLOv8默认的0.01配合小batch时减小到0.005。另一个实用手段是设置close_mosaic10最后10轮关闭马赛克增强让BN统计量稳定收敛这招能解决大部分训练中后段的NaN问题。6. 最后一跳用难例挖掘把新样本回流进训练集训练完一个基线模型后别急着部署先做一轮难例挖掘。这个动作的收益比调参高得多尤其适合监控场景把当前模型放到一个未参与训练的监控片段上跑推理把漏检的帧捞回来人工确认后补进训练集再迭代一个版本。这是监控视觉项目最常见的半自动化闭环。具体做法是构造一个难例挖掘脚本对未标注视频片段逐帧推理筛选出置信度在0.25到0.5之间的目标帧。置信度太低的是明显负样本太高的对训练没贡献中间这一段最有价值。把筛选出的帧导出为图片用模型已有的预测框初始化伪标签人工确认修正后合并进原始训练集。这里有个关键点伪标签只保留高置信度修正过的框不确定的框直接删掉不要存着“先练着后面再改”的心态——带错标签的样本比漏标更伤模型。合并之后用同样的参数重新训练把新旧两个模型在固定验证集上的漏检率对比一下。实践下来每次难例挖掘迭代通常能降低5到15个百分点的漏检率第一轮收益最大三轮之后逐渐饱和。我现在的习惯是每次部署新点位之前都跑一轮难例挖掘用训练集里的难例迭代模型而不是指望一版模型吃遍所有场景。这招反复用下来省掉的不只是部署后的返工更是挨骂。希望这条思路对你也帮得上忙。本文还有配套的精品资源点击获取
返回列表