简介:面向目标检测算法训练与验证的数据集资源,围绕手机目标识别场景构建,适合使用YOLO、Faster-RCNN等主流框架的算法工程师、科研人员及计算机视觉学习者。包内共一万五千余个文件,包含五千余张真实场景jpg图片,以及配套的VOC格式xml标注和YOLO格式txt标注,两类标签文件分目录存放,下载后按需选用即可直接训练。所有图片均经过LabelImg逐张标注,类别统一为phone,场景丰富,包含不同背景、光线与拍摄距离,可有效支撑模型训练、验证与效果对比。资源以rar压缩包形式提供,整体大小约704MB,包内文件按图片、VOC标注和YOLO标注分类存放,结构清晰,定位方便;当前已有1211人浏览学习。借助该数据集,可免去数据采集与人工标注的大量时间成本,快速搭建手机检测实验,也可作为学术实验或工程落地的数据基础。
1. VOC手机检测识别数据集:它不是格式,是你要攒的资产
手机回收流水线上要按型号分拣、会议系统要识别参会者是否在看手机、产线要确认手机外壳有没有装配到位——这些场景落到算法侧,核心任务都是同一个:在画面里把手机“框出来”。很多人一上来就问该用哪个模型,结果在数据这一关就翻了车:手里一堆照片,标签格式五花八门,训练时模型根本吃不进去。VOC手机检测识别数据集,说的就是用 Pascal VOC 格式组织一批带边界框标注的手机图片,供检测模型训练和评估。它适合想自建目标检测数据集的从业者,尤其是刚接触检测、要把“手机”这个具体目标做扎实的人。这篇文章我按自己做数据集的顺序,把目录结构、标注规范、格式转换和踩坑点一次讲完。
2. 拆开VOC格式:目录树、XML标签与手机检测的适配逻辑
2.1 一个VOC数据集的目录结构:JPEGImages、Annotations、ImageSets/Main
Pascal VOC 格式看着死板,但正因为死板,几乎所有检测框架都认识它。一个标准的 VOC 数据集目录长这样:
VOCdevkit/ ├── VOC2007/ │ ├── JPEGImages/ # 原始图片,jpg或png │ ├── Annotations/ # 每张图片对应的xml标注 │ └── ImageSets/ │ └── Main/ # train.txt、val.txt,一行一个文件名JPEGImages 放原图,Annotations 放同名 XML,ImageSets/Main 里是训练集和验证集的划分清单。这个结构是二十年前 Pascal VOC 比赛定下来的,今天 YOLO、MMDetection、Ultralytics 这些框架的工具链都能直接消化它。
我自己做手机检测数据集时,见过最省事的做法是把 CCDP(车牌数据集)、CrowdHuman(行人密集数据集)里带手机标签的图片抽出来拼进去,但注意:这些数据集标签字段不一定完全兼容 VOC,要先用脚本过一遍。如果你准备用 YOLOv8 训练自己的数据集,VOC 也不是终点——多数框架要的是 YOLO 的 txt 格式,所以 VOC 更像一个中间交换格式:标注时用 VOC,训练前转成 YOLO。第 4 章我会给转换脚本。
2.2 annotation XML里的六个关键字段:从filename到bndbox
一张phone_0012.jpg对应一个phone_0012.xml,打开后核心字段就六个:
<annotation> <folder>JPEGImages</folder> <filename>phone_0012.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>cellphone</name> <difficult>0</difficult> <bndbox> <xmin>120</xmin> <ymin>200</ymin> <xmax>360</xmax> <ymax>420</ymax> </bndbox> </object> </annotation>folder和filename告诉代码去哪找图片;size里的宽高是坐标归一的基准,训练时如果图片被 Resize,框和图片是一起缩放的,所以 XML 里的原始像素坐标反而更可靠;object是重头戏,name是类别名,bndbox是目标左上角和右下角的像素坐标。
要注意difficult这个字段。Pascal VOC 当年定义它为“难以识别”的目标,计算 mAP 时默认不算分。手机检测里还有个容易翻车的点:bndbox的坐标必须落在图片范围内,如果标注时手抖把 xmin 拖出画面,训练时边界框会变负值,部分框架直接抛错,或者静默丢掉这张图,最后你发现数据集“少”了图片还没日志提示。我的习惯是每次标注完跑一遍脚本,检查 XML 里坐标是否越界、xmin 是否小于 xmax,别把时间浪费在玄学排错上。
2.3 手机检测为什么适合VOC:与COCO、YOLO txt的取舍
COCO 格式是 JSON 嵌套结构,适合大规模多类别;YOLO txt 每行一个目标,紧凑但没有图片元信息;VOC 是 XML,冗余但在早期调试阶段最舒服——任何一步出错,打开 XML 就能看到原始坐标。做手机检测这种单类别或少数类别任务,VOC 的“笨”恰恰是优点。
| 特性 | VOC XML | COCO JSON | YOLO txt |
|---|---|---|---|
| 人类可读性 | 高 | 低 | 低 |
| 坐标类型 | 像素xmin/ymin | 像素x/y/w/h | 归一化x_center/y_center |
| 是否含图片尺寸 | 是 | 是 | 否 |
| 多类别扩展 | 加多个object | 改categories | 改数字索引 |
| 主流框架兼容 | 好 | 好 | 最好 |
我的建议是:标注阶段锁死 VOC,训练前再转 YOLO txt。原因很简单,用 YOLO 格式做标注,坐标一归一化,后期检查边界框是否越界、面积是否合理,都得乘回图片尺寸,徒增一步换算。而 VOC 的原始像素坐标,一眼能看出问题。
3. 从零搭一套手机检测数据集:采集、标注与首轮质检
3.1 三条采集路径:公开数据集借力、自采、半自动生成
做手机检测数据集,图片来源我一般分三条路,按性价比排序:
第一是公开数据集里捞。COCO 里有cell phone类,CrowdHuman 行人密集场景里大量存在手持手机的目标,这些数据直接拿来当种子。但注意,公开数据集里的手机图片普遍是“手持状态”,桌面摆放、床上散落、充电场景覆盖面很弱。如果你要识别的是流水线上的手机,纯靠公开数据远远不够。
第二是自采。手机检测的特殊性在于目标高度反光、屏幕内容千变万化,自采时用 1080p 以上的分辨率,涵盖不同光照、不同桌面材质、不同品牌。每个场景拍 30~50 张,改变拍摄角度和距离,比一场拍 500 张同机位照片有效得多。
第三是半自动生成。把手机贴图到真实背景上做合成,生成大量带真值标签的图片。这条路适合补充“极端角度”和“遮挡”样本,但合成数据会让模型学到贴图的边缘纹理,必须混入真实数据一起训练,不能纯用合成数据。我自己做过一次纯合成的手机检测实验,训练时 loss 降得漂亮,一到真实场景 AP 直接腰斩,血泪经验。
3.2 手机标注规范:类别划分、贴边原则与遮挡处理
标注质量直接决定模型上限,这一点在手机检测上尤其明显。先定类别:是做二分类“手机/背景”,还是细分成“手持手机”“桌面手机”“手机屏亮/屏灭”?类别粒度越细,标注工作量越大,但下游应用越精准。如果你只要一个“手机在哪”的框,建议先只建一个cellphone类别,训练跑通后再细分,不要第一步就想吃成胖子。
边界框贴边原则是:框住手机本体,不含手指、桌面倒影、充电线。手机是规整矩形,标注时四个边贴着机身外沿即可。但手持有遮挡时,框要包含被遮挡部分的“虚拟轮廓”——比如手指挡住屏幕下沿,框的下边界仍要画到完整的手机机身位置,否则模型学到的是“被挡住的手机只有半个框”。
遮挡处理分三档:轻微遮挡(手指、线缆)正常标注,算正样本;严重遮挡(手机被书本盖住大半、只露一个角)建议直接不标注这张图,留作负样本;完全看不见的不要硬标。difficult字段在这里有用途:标注员拿不准但能看出是手机的目标,标上difficult=1,训练时排除它,避免噪声。
3.3 用LabelImg标注并导出VOC XML的完整流程
LabelImg 是老牌标注工具,支持 PascalVOC 格式输出。安装和跑起来的常用做法是:
# 创建虚拟环境,避免依赖冲突 conda create -n labelimg python=3.9 -y conda activate labelimg # 安装 PyQt5 和 labelimg pip install pyqt5 pip install labelimg # 启动标注工具 labelimg启动后,左侧打开目录选 JPEGImages 文件夹,打开标注目录选 Annotations 文件夹,左下角PascalVOC表示输出 XML 格式。画框快捷键是W,画完选类别、存盘。要提前在data/predefined_classes.txt里写好类别名,比如cellphone,不然每次都要手输。
一轮标注下来,我推荐立刻做首轮质检,别等全部标完再查。质检脚本做三件事:检查所有 XML 是否能被xml.etree.ElementTree解析;检查bndbox坐标是否越界、xmin 是否大于 xmax;统计每张图的标注框数量、每个类别的总样本数。发现问题当场改掉,标注拖得越久,返工成本越高。这一轮质检能拦掉 80% 的数据集低级错误。
4. 把VOC转成YOLO能吃的格式:转换脚本与划分策略
4.1 转换脚本:XML坐标归一化成YOLO txt
YOLOv8 训练自己的数据集时,标签默认放在labels/目录下,每个 txt 文件一行对应一个目标,格式是class_id x_center y_center width height,坐标全部除以图片宽高归一化。下面这段脚本是我常用的转换逻辑:
import os import xml.etree.ElementTree as ET from pathlib import Path # 类别清单:顺序与你训练时data.yaml里的classes一致 CLASSES = ["cellphone"] def convert_xml_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): # 跳过difficult=1的样本 difficult = obj.find("difficult") if difficult is not None and int(difficult.text) == 1: continue name = obj.find("name").text if name not in CLASSES: continue cls_id = CLASSES.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # VOC是xmin/ymin/xmax/ymax,YOLO要的是中心点坐标和宽高 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h # 越界保护:坐标可能略超[0,1],裁到合法范围 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) width = min(max(width, 0.0), 1.0) height = min(max(height, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") out_name = Path(xml_path).stem + ".txt" out_path = os.path.join(out_dir, out_name) with open(out_path, "w") as f: f.write("\n".join(lines)) # 批量转换:遍历Annotations下所有xml xml_dir = Path("Annotations") out_dir = Path("labels") out_dir.mkdir(exist_ok=True) for xml_file in xml_dir.glob("*.xml"): convert_xml_to_yolo(xml_file, out_dir)这段脚本的逻辑分三层:先解析 XML 拿到图片尺寸和所有object;再把每个目标的四边形坐标换算成归一化中心点与宽高;最后按class_id写入对应 txt。参数上要注意三点:CLASSES的顺序一旦定下来就不能乱改,训练时data.yaml里的类别顺序必须和它一致,否则标签张冠李戴;difficult样本跳过还是保留,取决于你数据集太小还是对噪声零容忍,样本多就跳,样本少就留;归一化坐标做了min/max裁切,虽然标注规范要求不能越界,但转换时加一道保护是零成本的后悔药。
4.2 train/val划分:随机切分之外的人为约束
很多人在划分这一步偷懒,直接random.shuffle后按比例切。但手机检测场景有一个典型问题:同一部手机在同一个桌子、同一个角度连拍 10 张,如果随机切分,train 里有 8 张、val 里有 2 张,模型其实记住了背景和光照,而不是手机本身,val 指标虚高得一塌糊涂。划分要按“场景组”来,而不是按单张图。
import random from pathlib import Path image_dir = Path("JPEGImages") annotation_dir = Path("Annotations") output_dir = Path("ImageSets/Main") output_dir.mkdir(parents=True, exist_ok=True) # 按前缀分组:同一拍摄场景的文件名前缀相同,比如scene01_001.jpg groups = {} for img in image_dir.glob("*.jpg"): scene_id = img.stem.split("_")[0] # 取前缀作为场景ID groups.setdefault(scene_id, []).append(img.stem) scene_ids = list(groups.keys()) random.seed(42) random.shuffle(scene_ids) # 按场景数划分,而不是按图片数 val_ratio = 0.2 val_count = max(1, int(len(scene_ids) * val_ratio)) val_scenes = set(scene_ids[:val_count]) train_scenes = set(scene_ids[val_count:]) def write_split(scene_set, out_file): with open(out_file, "w") as f: for scene in scene_set: for stem in groups[scene]: xml_path = annotation_dir / f"{stem}.xml" if xml_path.exists(): f.write(stem + "\n") write_split(train_scenes, output_dir / "train.txt") write_split(val_scenes, output_dir / "val.txt")这段脚本的关键参数是scene_id的提取规则:如果你的文件名是scene01_001.jpg这种规范命名,按第一个下划线前缀分组就没问题;如果文件名没有规律,就得维护一个场景清单列表,手动指定哪些图属于同一场景。random.seed(42)保证每次划分结果一致,方便复现实验。另外建议再分一个test.txt,比例 5% 左右,只在最后评估时用,调参阶段不碰它。
4.3 训练前最后一道校验:把标签画回图片上看
转换和划分完成不等于数据集能用,最后一道校验是把 txt 里的标签画回图片上,肉眼确认坐标没有整体偏移。这个步骤花不了几分钟,但能拦住最隐蔽的错误——比如图片被框架自动 Resize 成正方形,而你的标签是按原图尺寸归一化的,如果数据加载时没有对应处理,画出来的框就会错位。
import cv2 from pathlib import Path label_dir = Path("labels") image_dir = Path("JPEGImages") out_dir = Path("check_vis") out_dir.mkdir(exist_ok=True) scale = 2 # 放大两倍便于观察小目标 for txt in label_dir.glob("*.txt"): img_path = image_dir / (txt.stem + ".jpg") img = cv2.imread(str(img_path)) if img is None: continue h, w = img.shape[:2] img = cv2.resize(img, (w * scale, h * scale)) with open(txt) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id, xc, yc, bw, bh = parts xc = float(xc) * w * scale yc = float(yc) * h * scale bw = float(bw) * w * scale bh = float(bh) * h * scale x1 = int(xc - bw / 2) y1 = int(yc - bh / 2) x2 = int(xc + bw / 2) y2 = int(yc + bh / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(cls_id), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) out_path = out_dir / (txt.stem + "_check.jpg") cv2.imwrite(str(out_path), img)这段脚本把归一化坐标乘回原图尺寸,用 OpenCV 画框并保存到check_vis目录。我一般随机抽 30 张图,扫一遍确认:框是否紧贴手机边缘、有没有框到背景、类别 ID 是否正确。如果发现系统性偏移——比如所有框都整体往右偏了几个像素,多半是标注时工具设置了图片缩放,或者转换脚本里坐标除错了基准,回头查size字段和归一化公式。这一步不做,训练到一半才看出标签有问题,返工成本翻倍。
5. 手机检测数据集最常见的五个坑:现象、原因、解决
5.1 小目标大面积漏检:先查标注框面积不是先换模型
现象:模型对近距离、大屏手机检测很准,但画面里远处桌面上的一部手机经常漏掉,val 的 mAP 卡在 0.7 上不去。
原因:手机在画面里只占几十像素,标注框面积过小。YOLO 系列下采样 32 倍后,小目标在特征图上只有一两个像素点,模型根本“看不见”。更隐蔽的是,标注时小目标容易被漏标,负样本比重过大,模型学到的全是“没有手机”。
解决:统计标注框的像素面积分布,把面积小于 32x32 的目标单独考虑。常用做法有两种:一是提高采集图片分辨率,让手机在画面上至少占 100 像素以上;二是对小目标图片做过采样——训练时让含小目标的图片多出现几次。换模型(比如从 YOLOv8 换到 YOLO11)能缓解,但根源在数据,先查标注再换网络。
5.2 反光幕和玻璃把边框框到整面屏:标注噪声怎么定责
现象:训练后模型输出很多“虚胖”的框,把手机两侧的空白桌面、反光区域都包进去。人工看一眼检查图,发现标注员的框本身就框大了。
原因:手机是强反光物体,尤其在深色桌面上,屏幕边缘的反光会让标注员误判机身宽度,把反光晕染的区域也当作手机本体。这个噪声不是类别错误,而是边界框标注误差,训练时模型被“错误真值”拽偏。
解决:定标注规则,反光不含在框内;再对已有的标注做修正,凡是框宽超过手机实际宽度 10% 以上的,重新标注。实操里我见过一个取巧做法:用分割工具先标出手机 mask,再取 mask 的外接矩形作为检测框,这样反光区域会因为不是同一连通域而被排除。
5.3 手持手机和桌面手机样本数量悬殊:不平衡怎么掰回来
现象:模型对“桌面静止手机”检测很准,对“人手持手机打电话”的场景漏检率翻倍,但 val 指标没异常,因为 val 里大部分也是桌面样本。
原因:类别定义粗(都是cellphone),但内部子场景分布不均。采集时顺手拍桌面手机容易,拍手持手机需要有人配合,样本自然少。模型学到的是“手机最常见的姿态”,而不是广义的手机,这就是不平衡的隐形影响。
解决:先按子场景统计样本量——手持、桌面、充电、暗光各有多少图,哪类少于总数 10% 就重点补采。补不了就做复制增强和随机旋转、翻转,让模型见过的形态平衡些。比如手持样本多来自发布会图片,角度单一,自己补拍时多拍几个仰角、侧角的画面。
5.4 模型记住桌子没记住手机:背景过拟合的信号
现象:训练集里手机基本出现在棕色木质桌面,测试时换成白色大理石桌面,漏检率明显上升;把同一张手机图贴到新背景上,模型输出框位置漂移。
原因:手机和桌面纹理在训练图里总是同时出现,模型把“棕色区域的矩形暗色块”当作手机的特征,而不是真去学屏幕、边框、听筒这些手机通用特征。背景过拟合是所有目标检测数据集的大敌,手机这种小目标尤其严重。
解决:采集时故意增加背景多样性,至少 10 种桌面材质、5 种光照方向。训练时配合随机 HSV 扰动和 Mosaic 增强,让模型去学目标本身。如果有人力,用随机背景做数据合成,把手机目标贴到完全不相关的背景上,是打断背景关联的最直接手段。
5.5 train和val串场:数据集泄露的隐蔽形式
现象:val 的 mAP 有 0.9,训练 loss 也很收敛,但部署到现场效果惨不忍睹。回头检查划分脚本,发现同一次拍摄的连拍照片被随机切成了 train 和 val 两部分。
原因:同一场景、同一光照下连拍的图片高度相似,模型在 train 里见过几乎一样的画面,val 等于开卷考试。这不是标签泄露,是图片内容泄露,比类别错标更难排查。
解决:回到第 4.2 节的按场景分组划分,保证同一次拍摄、同一个手机、同一个背景的所有帧只出现在 train 或只出现在 val。还有一个加强版做法:用图像相似度做查重,把结构相似度超过 0.9 的图片合并到一个分组,防止同一个场景被拆散。
6. 让数据集持续变强的闭环:难例回炉与评估基准
数据集不是一次性做完就扔的东西,它是需要迭代的。跑完第一版训练后,我的固定动作是把验证集里所有漏检和误检的图片挑出来,按“类型”归档:是远处小手机没检出,还是反光误检,还是遮挡目标被跳过。每一类难例补 50~100 张真实图片,放进训练集重新训练。这一轮操作下来,AP 涨幅通常比换任何数据增强参数都明显。
难例回炉的具体流程是:先用当前模型对一批未标注的现场图片做推理,把置信度在 0.3~0.7 之间的“模糊预测”挑出来,人工确认后补标,这些样本正好落在决策边界上,对模型最有价值。这个做法有个朴素的名字叫 hard example mining,但很多人只在论文里见过它,在实际项目中没坚持做。我见过一个筛选手持手机样本的项目,连续做三轮难例回炉,mAP 从 0.72 涨到 0.85,没有再调过任何网络结构。
评估基准也要固定下来:每轮迭代用同一批 val 图片、同一个随机种子、同一个输入尺寸训练,比较才有意义。我习惯把 val 的 mAP、每类 AP、以及一个“最小可检目标尺寸”指标一起记录——三分钟能写完,但有了它你才能回答“这轮改动到底有没有用”,而不是凭感觉拍板。
最后说个我的教训:刚开始做手机检测数据集时,我急着训模型,用劣质标注硬跑,结果后续所有优化都像是在歪地基上盖楼。后来我老实花了两周时间清洗、补标、按场景重划分,训练一次就达到之前调参一周的效果。数据集的整理和迭代永远比调参性价比高,这个坑,希望帮到你。
本文还有配套的精品资源,点击获取