
简介这份手机识别数据集面向计算机视觉初学者、目标检测算法练习者及需要扩充手机类样本的开发者可用于训练与验证手机目标检测模型或作为数据增强、标注格式转换的练习素材。资源包共约2000个文件以1997张jpg原始图片为主另附3个json标注文件整体压缩后约82.97MB图片覆盖多种拍摄角度与场景json文件采用coco格式可直接对接主流检测框架的数据加载流程。目前已有715人学习下载说明其在手机识别这一细分任务中具备一定参考价值。读者可借助这批原始图片与coco标注快速搭建训练集与验证集省去自行采集与标注的时间成本同时便于对比不同模型在手机目标上的检测效果或用于格式转换、可视化检查等预处理环节的练手。1. 手机识别数据集2628 张原始图片配 COCO JSON 标注能直接喂给哪些模型手上有一批 2628 张手机拍摄的原始图片标注是 COCO JSON 格式这个组合在目标检测和图像分类的落地里其实相当实用。手机拍摄意味着视角杂、光照乱、背景脏跟实验室里摆拍的数据集完全不是一个难度而 COCO JSON 又是目前主流框架兼容性最好的标注格式之一从 YOLOv8 到 MMDetection 再到 Detectron2基本都能直接吃。很多人拿到数据集第一反应是「图片够不够多」但真正决定这批数据能不能用的是标注质量、类别分布和格式转换链路。2628 张不算大但如果类别集中、标注干净微调一个预训练检测模型完全够用。这篇文章面向的是手里已经拿到或准备整理这类手机识别数据集的工程师从格式解析、清洗、转换到训练验证把整条链路拆开讲清楚让你拿到数据当天就能跑起来而不是卡在「json 用什么打开」这种问题上。2. COCO JSON 标注结构拆解从 json 数组到可训练张量2.1 COCO JSON 的五个顶层字段到底存了什么COCO 格式的标注文件本质是一个大 JSON 对象顶层通常包含images、annotations、categories、info、licenses五个字段。真正跟训练相关的是前三个。images是一个 json 数组每个元素记录一张图的id、file_name、width、heightannotations记录每个标注框的image_id、category_id、bbox、area、iscrowdcategories则是类别 id 到类别名的映射。理解这三者的关联是后续所有操作的基础——annotations里的image_id指向images里的idcategory_id指向categories里的id任何一环对不上训练时就会报 key error 或者类别错乱。用 Python 快速把结构摸清楚比用编辑器硬翻 JSON 高效得多import json from collections import Counter # 加载标注文件注意用 utf-8手机数据集类别名常含中文 with open(annotations/instances.json, r, encodingutf-8) as f: coco json.load(f) # 顶层字段一览 print(顶层字段:, list(coco.keys())) # 图片数量与标注数量 print(图片数:, len(coco[images])) print(标注框数:, len(coco[annotations])) print(类别数:, len(coco[categories])) # 每个类别的框数量分布判断类别是否均衡 cat_map {c[id]: c[name] for c in coco[categories]} counter Counter(cat_map[a[category_id]] for a in coco[annotations]) for name, cnt in counter.most_common(): print(f{name}: {cnt})这段代码做了三件事确认顶层字段是否完整、统计图片与标注总量、按类别统计框数量。参数上唯一需要注意的是encodingutf-8手机数据集如果类别名是中文用默认编码在某些系统上会直接抛UnicodeDecodeError。跑完这段你就能判断这批数据是否存在严重的类别不均衡——如果某个类别只有个位数框训练时基本学不出来要么补标要么合并类别。2.2 bbox 坐标系与手机图片 EXIF 旋转的隐藏坑COCO 的bbox格式是[x, y, width, height]其中x, y是左上角坐标单位是像素原点在图片左上角。这一点跟 YOLO 的[x_center, y_center, w, h]归一化格式不同转换时必须做坐标变换。但手机数据集有一个比坐标格式更隐蔽的问题EXIF 旋转。手机竖拍的照片实际像素存储可能是横的靠 EXIF 的 Orientation 字段告诉查看器旋转 90 度显示。标注工具如果读取的是显示后的图而训练框架读取的是原始像素两者就会差一个 90 度旋转框全歪。先检测这批图有没有 EXIF 旋转问题from PIL import Image, ImageOps import os def check_exif_rotation(img_dir, sample50): rotated [] files [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg))] for name in files[:sample]: path os.path.join(img_dir, name) img Image.open(path) exif img.getexif() orientation exif.get(274) # 274 是 Orientation 的 tag if orientation and orientation ! 1: rotated.append((name, orientation)) print(f抽样 {min(sample, len(files))} 张存在 EXIF 旋转的有 {len(rotated)} 张) for name, o in rotated[:10]: print(name, orientation , o) return rotated check_exif_rotation(images/)orientation值为 1 表示正常3 表示旋转 180 度6 表示顺时针 90 度8 表示逆时针 90 度。如果检测出大量非 1 的图片处理方式有两种一是用ImageOps.exif_transpose把所有图物理旋转到正确方向并重存同时标注框坐标不变因为标注工具通常基于显示图标注二是确认标注工具当时读的就是原始像素那就不要动图。判断依据是拿一张有旋转的图把标注框画上去看是否对齐对齐了就别转没对齐就转图。这一步不做后面训练 loss 会莫名其妙降不下去属于典型的玄学问题。2.3 用 pycocotools 做一次标注合法性体检在转换格式之前强烈建议用官方pycocotools跑一遍合法性检查。它能发现框越界、面积为 0、image_id 悬空等问题这些问题在训练时往往表现为难以定位的报错。from pycocotools.coco import COCO coco COCO(annotations/instances.json) # 检查每个标注框是否越界 img_ids coco.getImgIds() bad 0 for img_id in img_ids: info coco.loadImgs(img_id)[0] W, H info[width], info[height] for ann in coco.loadAnns(coco.getAnnIds(imgIdsimg_id)): x, y, w, h ann[bbox] if x 0 or y 0 or x w W or y h H or w 0 or h 0: bad 1 print(f越界框 img{info[file_name]} bbox{ann[bbox]}) print(越界或非法框总数:, bad)loadImgs按 image_id 取图片元信息loadAnns取对应标注逐个比对 bbox 是否落在[0, W] × [0, H]范围内。越界框数量如果超过总量的 1%说明标注工具或导出流程有问题需要回溯修正。合法性问题不解决转换到 YOLO 格式后训练会直接报normalized coordinates out of range之类的错误。3. 把 COCO JSON 转成 YOLOv8 训练格式脚本与四个边界坑3.1 转换脚本从 instances.json 到 labels/*.txtYOLOv8 的检测训练需要每张图对应一个同名.txt文件每行是class_id x_center y_center width height全部归一化到 0~1。同时需要一个data.yaml描述训练集路径和类别名。下面这个脚本把 COCO JSON 一次性转到位import json import os from PIL import Image def coco_to_yolo(json_path, img_dir, out_label_dir, out_yaml): with open(json_path, r, encodingutf-8) as f: coco json.load(f) # 类别 id 重映射为 0 起始的连续整数YOLO 要求 class_id 从 0 开始 cats sorted(coco[categories], keylambda c: c[id]) cat_id_map {c[id]: i for i, c in enumerate(cats)} names [c[name] for c in cats] # 按 image_id 聚合标注 img_map {img[id]: img for img in coco[images]} ann_by_img {} for ann in coco[annotations]: ann_by_img.setdefault(ann[image_id], []).append(ann) os.makedirs(out_label_dir, exist_okTrue) for img_id, anns in ann_by_img.items(): info img_map[img_id] W, H info[width], info[height] # 用实际图片尺寸兜底防止 json 里的宽高与真实文件不一致 real_path os.path.join(img_dir, info[file_name]) if os.path.exists(real_path): with Image.open(real_path) as im: W, H im.size stem os.path.splitext(os.path.basename(info[file_name]))[0] lines [] for ann in anns: x, y, w, h ann[bbox] # 裁剪到图像范围内避免归一化后越界 x max(0, min(x, W)) y max(0, min(y, H)) w min(w, W - x) h min(h, H - y) if w 1 or h 1: continue # 过滤掉过小的无效框 xc (x w / 2) / W yc (y h / 2) / H nw w / W nh h / H cid cat_id_map[ann[category_id]] lines.append(f{cid} {xc:.6f} {yc:.6f} {nw:.6f} {nh:.6f}) with open(os.path.join(out_label_dir, stem .txt), w) as f: f.write(\n.join(lines)) # 生成 data.yaml with open(out_yaml, w, encodingutf-8) as f: f.write(fpath: {os.path.abspath(img_dir)}\n) f.write(train: images\n) f.write(val: images\n) f.write(fnc: {len(names)}\n) f.write(names:\n) for i, n in enumerate(names): f.write(f {i}: {n}\n) print(f转换完成类别数 {len(names)}标注文件输出到 {out_label_dir}) coco_to_yolo(annotations/instances.json, images/, labels/, data.yaml)脚本里几个关键处理值得说明。类别 id 重映射是必须的COCO 的 category_id 经常不连续比如 1、3、7YOLO 要求从 0 开始的连续整数不重映射会直接报类别越界。用真实图片尺寸兜底是因为部分标注工具导出的width/height跟实际文件对不上以真实文件为准最稳。坐标裁剪和过小框过滤是防御性编程手机数据集里偶尔会有误标的一两个像素的框留着只会干扰训练。归一化保留 6 位小数足够再多没必要。3.2 四个边界坑中文路径、空标注、类别名空格、图片格式第一个坑是中文路径。YOLOv8 在部分环境下读取含中文的路径会失败尤其是 Windows 下配合某些版本的 OpenCV。稳妥做法是把图片和标签目录都改成纯英文路径类别名里的中文保留在 yaml 里没问题但路径别用中文。第二个坑是空标注图片。有些图在 COCO 里没有任何 annotation转换后不会生成对应的 txt 文件。YOLO 默认会忽略没有标签的图但如果你希望这些图作为负样本参与训练需要手动生成一个空的 txt 文件。判断标准是如果空标注图占比超过 10%建议保留为负样本低于 5% 直接忽略。第三个坑是类别名带空格或特殊字符。data.yaml里如果类别名是mobile phone这种带空格的YOLO 解析时可能出问题。建议在 yaml 里用引号包起来或者干脆把类别名改成下划线连接。第四个坑是图片格式混用。手机数据集里常见 jpg、jpeg、png 混在一起甚至还有 HEIC。YOLO 对 HEIC 支持不好转换前统一转成 jpg# 用 ImageMagick 批量把 png 和 heic 转成 jpg统一格式 mogrify -format jpg -quality 92 images/*.png mogrify -format jpg -quality 92 images/*.heic-quality 92是压缩质量手机数据集本身有噪点质量给到 90 以上基本无损感知。转换后记得核对图片数量别把原图覆盖丢了。3.3 划分训练验证集别用随机划分糊弄手机数据手机数据集如果是在不同时间、不同场景下拍的随机划分训练验证集会导致同一场景的图同时出现在两边验证指标虚高。正确做法是按场景或拍摄批次划分。如果数据里没有场景标签至少按文件名前缀或拍摄时间分组后再划分。import os import random import shutil def split_dataset(img_dir, label_dir, out_root, val_ratio0.2, seed42): random.seed(seed) files [f for f in os.listdir(img_dir) if f.lower().endswith(.jpg)] # 按文件名前缀分组模拟按场景划分 groups {} for f in files: key f.split(_)[0] if _ in f else f[:4] groups.setdefault(key, []).append(f) keys list(groups.keys()) random.shuffle(keys) val_keys set(keys[:max(1, int(len(keys) * val_ratio))]) for split in [train, val]: os.makedirs(os.path.join(out_root, images, split), exist_okTrue) os.makedirs(os.path.join(out_root, labels, split), exist_okTrue) for key, fs in groups.items(): split val if key in val_keys else train for f in fs: shutil.copy(os.path.join(img_dir, f), os.path.join(out_root, images, split, f)) stem os.path.splitext(f)[0] src_lbl os.path.join(label_dir, stem .txt) if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(out_root, labels, split, stem .txt)) print(划分完成验证集组数:, len(val_keys)) split_dataset(images/, labels/, dataset/)按前缀分组再划分能保证同一场景的图不会跨集。seed42固定随机种子保证可复现。验证集比例 20% 对 2628 张来说约 500 张够评估用。划分完记得把data.yaml里的train和val路径改成images/train和images/val。4. 用这批数据微调 YOLOv8参数怎么设、指标怎么看4.1 从预训练权重起步的最小训练命令2628 张图从零训练不现实必须用 COCO 预训练权重微调。YOLOv8 的最小训练命令如下yolo detect train \ datadataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/mobile \ nameexp1modelyolov8n.pt选 nano 版本是因为数据量不大大模型容易过拟合。imgsz640是手机图片的常用输入尺寸如果原图分辨率远大于 640可以适当提到 960但显存占用会翻倍。batch16在 8G 显存上比较稳显存不够就降到 8 并配合accumulate。lr00.01是微调的常用初始学习率比从头训练的 0.01 略低更稳。patience20表示 20 轮指标不升就早停省时间。4.2 关键参数对手机数据的影响与调整方向手机数据的特点是目标尺度变化大、背景复杂几个参数需要针对性调整。imgsz如果手机拍的目标普遍较小建议提到 960 甚至 1280小目标在 640 下容易丢。mosaic数据增强默认开启对手机数据有帮助但如果目标经常被截断可以调低mosaic概率。hsv_h、hsv_s、hsv_v控制颜色抖动手机数据光照差异大适当调高hsv_v比如 0.5能提升泛化。degrees旋转增强对手机竖拍横拍混用的情况有用设 10 左右即可太大反而引入不真实样本。训练过程中重点看三个指标mAP50、mAP50-95和每类的precision/recall。手机数据集常见的问题是某些类别 recall 低说明漏检多可能是该类样本太少或目标太小。这时候优先补标该类而不是盲目加 epoch。4.3 验证与推理把预测框画回手机原图确认训练完必须做可视化验证光看指标不够。用训练好的权重在验证集上推理并画框from ultralytics import YOLO import cv2 model YOLO(runs/mobile/exp1/weights/best.pt) results model.predict(dataset/images/val, conf0.25, saveTrue, projectruns/vis) # 单独看一张打印每个框的类别和置信度 r results[0] for box in r.boxes: cls model.names[int(box.cls)] conf float(box.conf) xyxy box.xyxy[0].tolist() print(f{cls} {conf:.2f} {[round(v) for v in xyxy]})conf0.25是推理置信度阈值验证阶段可以调低到 0.1 看漏检部署时再调回 0.25~0.5。saveTrue会把画框后的图存到runs/vis一定要人眼过一遍重点看有没有框歪、框到背景、类别标错的情况。指标高但可视化一塌糊涂的情况在手机数据上并不少见通常是标注坐标系或 EXIF 问题没处理干净。5. 手机识别数据集避坑与排查五条血泪经验5.1 训练 loss 不降反升先查标注坐标系现象训练几轮后 box loss 震荡不收敛mAP 一直在 0 附近。原因标注框坐标系与图片实际像素不一致最常见的是 EXIF 旋转没处理或者标注工具用了归一化坐标但导出时没还原成像素。解决抽 5 张图把标注框画上去人眼核对不对就回到 2.2 节处理 EXIF确认标注工具导出的是像素坐标。5.2 验证 mAP 虚高训练集和验证集撞场景现象验证集 mAP 0.9 以上但换一批新拍的手机图推理效果很差。原因随机划分导致同场景图片同时进了训练和验证集模型记住了背景而不是目标。解决按 3.3 节的方式按场景或文件名前缀分组划分重新训练后再看指标通常会降一些但更真实。5.3 报 normalized coordinates out of range现象训练启动即报坐标越界错误。原因转换时 bbox 超出图片范围归一化后大于 1 或小于 0。解决在转换脚本里加坐标裁剪3.1 节已包含并跑一遍 2.3 节的合法性检查把越界框修掉或删掉。5.4 某些类别完全学不出来现象训练完发现某个类别 mAP 为 0其他类别正常。原因该类样本数量太少比如只有十几张或者类别 id 在转换时映射错了。解决先确认data.yaml里类别顺序和转换脚本一致再统计该类框数量低于 50 个框的类别考虑补标或与其他相似类别合并。5.5 推理速度慢部署到手机端跑不动现象训练指标不错但导出到手机端推理一帧要几百毫秒。原因用了 yolov8l 或更大模型或者输入尺寸设了 1280。解决手机端部署优先选 yolov8n 或 yolov8s输入尺寸 640导出时用yolo export modelbest.pt formatonnx或直接导出 ncnn/tflite量化到 int8 还能再快一倍。精度和速度的平衡点需要在自己的设备上实测。6. 让 2628 张手机图发挥更大价值半自动标注与增量迭代2628 张图训练一个能用的检测模型只是起点真正拉开差距的是后续的迭代效率。我的习惯是第一版模型训出来后不要急着补标新数据而是先用模型对未标注的手机图做一遍推理把高置信度的预测框导出成预标注人工只做修正。这样标注效率能提升三到五倍。具体做法是用model.predict跑一批新图把结果按 COCO 格式写回去再导入 LabelStudio 或 LabelImg 做人工校验。import json from ultralytics import YOLO model YOLO(runs/mobile/exp1/weights/best.pt) results model.predict(new_images/, conf0.4, saveFalse) coco_out {images: [], annotations: [], categories: []} for i, name in enumerate(model.names): coco_out[categories].append({id: i 1, name: name}) ann_id 1 for r in results: h, w r.orig_shape img_id len(coco_out[images]) 1 coco_out[images].append({ id: img_id, file_name: r.path.split(/)[-1], width: w, height: h }) for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() coco_out[annotations].append({ id: ann_id, image_id: img_id, category_id: int(box.cls) 1, bbox: [x1, y1, x2 - x1, y2 - y1], area: (x2 - x1) * (y2 - y1), iscrowd: 0 }) ann_id 1 with open(pre_annotations.json, w, encodingutf-8) as f: json.dump(coco_out, f, ensure_asciiFalse) print(f生成预标注 {ann_id - 1} 个框导入标注工具人工修正)conf0.4是预标注的置信度门槛设太低会引入大量误检增加人工负担设太高会漏掉难样本。这个值需要根据第一版模型的 precision 来调precision 高就设 0.5一般就 0.4。导出的 COCO JSON 可以直接被 LabelStudio 导入人工只改错的框比从零画快得多。增量迭代的节奏我一般是这样第一版用全部 2628 张训第二版把人工修正后的预标注数据加进去第三版再针对验证集里 recall 低的类别定向补拍补标。每一版都保留data.yaml和权重方便回滚对比。手机识别这类场景数据分布会随拍摄设备、环境变化而漂移定期用新数据微调比一次性训一个大模型更实用。踩过的最大坑是早期图省事用随机划分指标好看但上线就翻车后来老老实实按场景分组划分指标降了但真实场景稳了。希望帮到你。本文还有配套的精品资源点击获取