简介:目标检测是计算机视觉的核心任务之一,其模型效果高度依赖训练数据的质量与匹配度。通用公开数据集虽然在类别和规模上有优势,但在面对特定场景如桌面物品盘点、智能家居或办公巡检时,往往存在类别覆盖不全、视角不匹配、类内差异大等问题,导致模型泛化能力不足。构建自有的日常用品数据集,需要从类别边界定义、多环境采集、样本清洗到标注规范全流程把控,并使用YOLOv8进行模型训练与参数调优。通过合理的数据增强、分层抽样和针对性的数据迭代,可有效提升小目标与遮挡场景下的检测精度。结合ONNX/OpenVINO导出与部署,最终可在边缘设备上实现实时推理,支撑仓储盘点、辅助视障人群等实际应用。本文分享一套完整的数据集构建与YOLOv8训练部署链路,为需要训练自定义目标检测模型的开发者提供可复现的工程实践参考。 前两天清理硬盘,翻出一个叫“日常用品目标检测数据集.zip”的压缩包。这个包是我断断续续攒了三个月的数据集,里面是十几类日常用品的目标检测标注数据。恰好最近又在调YOLOv8,就顺手把这套数据集的构建、标注、训练、部署整个链路再走了一遍。今天这篇东西,就聊聊这套数据集到底包含什么、我是怎么从零把数据攒起来并喂给模型的,以及中间踩过的那些坑。
目标检测这个方向,网上公开数据集一抓一大把,COCO、VOC、Open Images,类别齐全、标注质量高。但真落到具体场景里,你会发现公共数据集往往不顺手。我当时的场景很简单:想做一个桌面物品自动盘点的小工具,让手机或摄像头拍一张照片,就能识别出桌面上有哪些日常用品,并统计数量。COCO里虽然有“杯子”“手机”这些类别,但类别覆盖不全,而且很多类别是生活场景中概率极低的(比如“大象”)。与其满世界找数据集,不如自己攒一个,把标注规范、类别边界、训练流程都攥在自己手里。这篇文章就把整条链路拆开讲,适合刚入坑目标检测、想训练自己数据集的朋友参考。
1. 为什么我会去做一个“日常用品”数据集
1.1 真实需求:从“找东西”到“自动盘点”
触发我做这个数据集的原因很日常——我桌子乱,而且到了那种“东西明明就在眼前但就是找不到”的程度。当时想:如果有一个模型能直接告诉我“桌上有手机、钥匙、眼镜、水杯”,并且给我画个框框出来,那就省事了。把这个需求再放大一点,就是仓储盘点、智能家居、盲人辅助、办公场景巡检这些更完整的业务场景。
日常用品目标检测跟通用目标检测最大的区别在于:类别是高度自定义的,且场景相对固定。这意味着我不能直接拿预训练权重去推理,必须用自己采集、自己标注的数据做一次微调,才能保证检测效果符合预期。也正是因为类目少、场景近,这类模型的准确率可以做得比通用模型高很多,推理速度也更容易跑到实时级别。
1.2 看起来简单,实际坑不少
很多人觉得“日常用品嘛,不就是杯子、手机、书本,简单得很”。真做起来才发现问题一堆:
- 同一类东西形态差异巨大。一个马克杯和一个保温杯外形天差地别,“杯子”这个类别的类内方差很大。
- 遮挡和堆叠严重。桌面上的遥控器往往压着一支笔,眼镜可能半摞在书本上,目标不是完整露出来的。
- 尺度变化大。手机和钥匙在画面里属于小目标,水杯和笔记本则是大目标,检测器要同时兼顾不同尺度。
- 背景干扰。桌面的木纹、键盘的格纹、屏幕反光,都可能被模型误认成目标边界。
这些坑不是训练时才冒出来的,而是从数据采集阶段就开始埋雷了。如果采集时光源单一、角度固定、背景全是同一种桌面,模型训练出来的效果会很“虚”,换一个环境立马现原形。
1.3 和公开数据集的区别
我对比过COCO、Objects365和Open Images上的相关类别。COCO的“cup”类主要覆盖的是酒吧、餐桌场景,视角以平视为主;而我要的场景是俯拍桌面、侧方台面,视角差异导致直接用COCO预训练权重效果并不理想。至于Objects365,类别颗粒度太粗,“daily necessities”这种没有精确到单品。
自己攒数据集的好处就是可以把场景、视角、类别边界全部控制住。说白了,公共数据集解决的是“能不能检测”的问题,自建数据集解决的是“检测得准不准、稳不稳”的问题。两者是互补关系,而不是替代关系。
2. 数据从哪来:采集、筛选与类别边界
2.1 类别定到13类,我做了哪些取舍
类别定义是整个数据集的灵魂,这一步改起来成本最高,越往后越难动。我当时折腾了三个版本,最后定了13类:
| 序号 | 类别ID | 类别名称 | 包含对象 |
|---|---|---|---|
| 1 | mug | 马克杯/陶瓷杯 | 各种带柄杯子 |
| 2 | bottle | 饮料瓶/矿泉水瓶 | 塑料瓶、玻璃瓶 |
| 3 | cellphone | 手机 | 各种智能手机 |
| 4 | laptop | 笔记本电脑 | 含平板形态的合盖/开盖设备 |
| 5 | glasses | 眼镜 | 近视镜、太阳镜 |
| 6 | book | 书本/杂志 | 书、本子、杂志、文件夹 |
| 7 | scissor | 剪刀 | 各种剪刀 |
| 8 | remote | 遥控器 | 电视、空调、风扇遥控器 |
| 9 | key | 钥匙 | 单把钥匙、钥匙串 |
| 10 | pen | 笔 | 签字笔、圆珠笔、钢笔 |
| 11 | bowl | 碗 | 各类家用碗 |
| 12 | charger | 充电头/数据线 | 充电器、USB线 |
| 13 | earphone | 耳机 | 有线、无线耳机 |
这三个版本的取舍逻辑供大家参考:
- 第一版我把“马克杯”和“保温杯”拆成了两类,训练后发现混淆严重。因为很多保温杯和马克杯在外形、颜色上太接近,模型经常把保温杯标成马克杯。后来干脆合并成一个
mug类,准确率反而上去了。 - 第二版加了“纸巾盒”“台灯”这些类别,后来发现样本量不够且形态过于多样,先砍掉了。类别定得越多,每类需要的最小样本量就越高,这是数据成本问题。
- 第三版把“充电器”和“数据线”合并成一个
charger类,因为很多充电头和线是连在一起的,强行拆开会导致标注边界怎么画都不对。
2.2 采集策略:光源、角度与负样本
采集直接影响模型泛化能力,这块我总结成这样一句话:宁可图片数量少一点,也要让环境的多样性足够丰富。
具体来说,我做了这几件事:
- 场景铺开:在书桌、餐桌、茶几、厨房台面、床上、办公桌上分别拍摄,而不是只在自己那一张桌子上拍。
- 光源覆盖:自然光、白炽灯、暖黄灯光、屏幕补光都拍一些。目标检测对光照极其敏感,同一部手机在冷光和暖光下颜色差异很大,如果训练集只有一种光,实际使用基本会翻车。
- 角度混合:以俯拍为主(约70%),再加入平视、斜视角度。纯俯拍训练的模型,一旦摄像头位置偏低,精度会迅速下降。
- 数量变化:每张图里的目标数量从单目标到多目标都要有。很多开源数据集单张图只有一个目标,模型调成
batch=16训练时其实没太大感觉,但到实际场景里满桌子都是东西,会疯狂漏检。 - 负样本采集:专门拍了上百张没有这些目标的场景图(空桌面、地毯、床面),这样训练时模型才能学会“这些东西不该被识别出来”,减少误检。这一点经常被忽略,但特别重要。
2.3 清洗环节:这步真不能省
采集完的原始图片不能直接进标注工具,必须清洗。我第一个版本就是把所有拍到的图一股脑丢进去标注,结果训练出来的模型在几张图上表现诡异,排查半天发现是其中一两张图本身有问题:
- 图片严重过曝,目标轮廓完全看不清,标注框的位置基本靠猜;
- 图片里目标被大范围遮挡,只露出一个角,标注出来没有学习价值;
- 连续帧相似度极高,等于同一张图被复制了几遍,训练集被“注水”了。
清洗逻辑其实很简单:一张张快速扫过去,模糊的删掉,重复的删掉,遮挡超过50%且无明显特征的删掉。我最终从原始采集的4000多张里筛出了约3600张可用的,这个量级对于13类目标检测来说算是及格线。如果你要检测的类别更多,或者目标形态更复杂,样本量还需要往上加。
3. 标注规范与目录结构:让数据能直接喂给YOLO
3.1 标注工具选择与bbox标注注意点
标注工具我用过LabelImg、Labelme,也用过半自动的X-AnyLabeling。现在最推荐的是X-AnyLabeling,因为它内置了YOLOv8预训练模型做自动标注辅助。你先手动标几十张图让它学习一下场景,后面大部分图就能“粗标+人工修正”了,效率至少翻倍。如果是纯手动标注,4000张图会让人崩溃。
标注bbox时有几个原则,这些原则直接决定模型输出质量:
- 紧贴可见外轮廓:标注框要尽量贴合目标可见部分,不要把遮挡物也框进去。
- 遮挡只按可见部分处理:如果一支笔被遥控器压住一半,只标注露出的那一半。不要凭“脑补”把完整笔身画进去,否则模型学习到的边界就是错的。
- 类别归属要定死:比如“数据线和充电头连着”这种情况,标注时以充电头为主框,如果数据线明显伸出来了,就单独再画一个
charger框。 - 小目标宁可框大点,让模型能“看清”它的上下文,比框得极精准但特征太少要好。
3.2 目录结构设计:一个zip解压就能用
这套数据集最终导出成zip,解压后结构如下:
daily_items_dataset/ ├── images/ │ ├── train/ # 训练图片 │ ├── val/ # 验证图片 │ └── test/ # 测试图片(可选) ├── labels/ │ ├── train/ # 训练标签(YOLO格式 txt) │ ├── val/ # 验证标签 │ └── test/ # 测试标签 ├── data.yaml # YOLO数据配置文件 ├── dataset_description.md # 数据集说明文档 └── classes.txt # 类别列表这里面容易被忽略的是data.yaml。很多人把图片和标签放好了,却忘了写YAML,或者路径写错,导致训练的时候报“no labels found”。YAML内容简明扼要:
path: C:/datasets/daily_items_dataset # 数据集根目录,改成你的实际路径 train: images/train val: images/val test: images/test names: 0: mug 1: bottle 2: cellphone 3: laptop 4: glasses 5: book 6: scissor 7: remote 8: key 9: pen 10: bowl 11: charger 12: earphone如果图片和标签路径写绝对路径,换机器训练时记得同步修改path字段,否则会踩“明明数据都在但就是找不到”的坑。
3.3 格式转换与数据集划分脚本
标注工具导出的是VOC XML或JSON格式,YOLO训练需要txt格式,每行一个目标,格式是:类别ID 中心点x 中心点y 宽度w 高度h,所有数值都归一化到0~1。
这里分享我当时转换用的Python脚本(针对LabelImg的XML转YOLO txt):
import os import xml.etree.ElementTree as ET from pathlib import Path def convert_label(xml_path, out_dir, class_list): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) out_lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in class_list: continue cls_id = class_list.index(cls_name) 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) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h out_lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') out_name = Path(xml_path).stem + '.txt' with open(Path(out_dir) / out_name, 'w') as f: f.write('\n'.join(out_lines))数据集划分我用的是分层随机抽样,保证每类目标在训练集和验证集中占比一致。直接用sklearn的train_test_split,按图片文件名做分层,shuffle=True,random_state=42。划分比例我选的是train:val:test = 8:1:1。对日常用品这类小数据集来说,8:1:1比9:0.5:0.5更稳妥,验证集太薄的话指标波动会很大,一个批次的好坏就能让mAP上蹿下跳。
4. YOLOv8训练自己的数据集:完整参数与调优过程
4.1 环境与配置:先跑通再谈调优
训练环境方面,我建议直接用Ultralytics官方仓库,也就是pip install ultralytics。版本固定很重要,我当时用8.0.43跑通后,没锁版本升级到8.1.x,结果个别参数的行为发生了变化,训练日志对不上,排查了很久,最后回滚版本才正常。搞深度学习项目,尽量用一个干净的虚拟环境,把关键依赖版本记录在requirements.txt里。
初始阶段我建议直接用yolov8n.pt预训练权重跑通整个流程,看数据加载、标注解析、训练循环是否正常。如果连最小的模型都跑不通,那问题基本出在数据集本身,而不是模型太大。
yolo detect train \ data=dataset/data.yaml \ model=yolov8n.pt \ epochs=50 \ imgsz=640 \ batch=16 \ device=0这句命令跑完,如果训练过程没有报错,并且runs/detect/train/下出现了weights/best.pt,说明数据链路已经通了。
4.2 训练参数说明:不是所有参数都值得调整
从nano模型切到正式训练时,我用了这套参数,跑出来的效果比较稳定:
yolo detect train \ data=dataset/data.yaml \ model=yolov8s.pt \ epochs=150 \ imgsz=640 \ batch=32 \ optimizer=AdamW \ lr0=0.001 \ lrf=0.01 \ weight_decay=0.0005 \ mosaic=1.0 \ close_mosaic=10 \ cos_lr=True \ device=0逐项说明一下这几个参数背后的逻辑:
imgsz=640:YOLOv8的默认训练尺寸。我的数据里有钥匙、笔这类小目标,640是底线。后面为了冲小目标精度,我试过imgsz=960,mAP确实涨了2到3个点,但训练和推理耗时明显变长。日常用品检测不是自动驾驶,对实时性的要求没那么极限,可以接受稍高的分辨率。mosaic=1.0:前140个epoch开启Mosaic增强,把4张图拼成一张训练,能显著提升模型对遮挡和拥挤场景的鲁棒性。但最后10个epoch要关掉,即close_mosaic=10,否则模型在纯增强数据上训练太久,对真实图像的分布拟合不够。cos_lr=True:学习率用余弦退火。日常用品数据集不大,余弦退火能比固定步长衰减更平滑地收敛。optimizer=AdamW:小数据集上AdamW比SGD收敛更快、更稳。如果你的数据量上万张,可以换回SGD。
4.3 训练阶段怎么判断模型状态
训练过程中不能傻等,看日志要会抓关键信息。每次epoch结束会打印一行指标,我主要看这几个:
box_loss、cls_loss:训练损失。正常应该持续下降,如果loss震荡剧烈或到后面不降反升,考虑是不是学习率太大或者增强过猛。mAP50:IoU阈值0.5下的平均精度。这项指标代表“框大致画对了没”,对日常用品检测来说,mAP50在0.9以上才算合格。mAP50-95:IoU从0.5到0.95的均值。这项更严格,反映框的精细度。日常用品检测一般要求到0.7以上,低于0.6说明定位精度还有问题。
训练到60~80个epoch时,mAP50通常会先起来,而mAP50-95涨得慢,这是正常的。如果到了120个epochmAP50还在明显上涨,说明150个epoch不够,可以适当延长。我最终在约140~145个epoch时收敛,后面几个epoch基本平台期。
5. 训练结果分析:那些指标之外的隐藏问题
5.1 指标里能看出的问题
训练结束后,runs/detect/train/目录下会生成一堆分析文件。按优先级看这几个:
混淆矩阵(confusion_matrix.png):这张图能直接暴露出类别之间的混淆情况。我第一版训练结果里,mug和bowl之间有一小块非零的混淆块——模型把一些矮胖的马克杯框标成了碗。这类问题单看mAP数值是看不出来的,必须靠混淆矩阵定位。
结果曲线(results.png):里面包含train/box_loss、val/box_loss等曲线。如果验证损失在后期拉高而训练损失还在降,说明过拟合了,需要加数据增强、加样本量或者降低模型复杂度。
单类指标:用下面这句命令可以打印每个类别的详细指标:
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=dataset/data.yamlUltralytics会输出一个表格,包含每个类别的Precision、Recall、mAP50、mAP50-95。我当时那张表里,key(钥匙)的mAP50只有0.72,远低于其它类别的0.9+。这直接把后面的工作重心带到了“小目标优化”上。
5.2 指标里看不出的问题
指标只是第一道关,真正决定模型能不能用的是实拍测试。
我做了两组针对性测试:一组是单物品特写,一组是桌面上10+物品堆叠。单物品特写效果不错,框得干净利落;一到堆叠场景就露馅了:
- 遥控器压着笔的时候,
pen的框会漏检,因为模型没见过这种遮挡强度的正样本; - 桌面反光严重时,手机屏幕那一块会被框成
book; - 充电线缠绕状态下,
charger的框会只框住充电头,线体完全不检。
这些问题指标上其实有征兆,比如Recall掉到80%以下,但只看mAP分不出来。我总结的经验是:不能全信指标,要拿真实场景中的难例去“折磨”模型,然后再回头补数据。这样一来,迭代方向才不会被指标带偏。
5.3 针对性的数据迭代
针对测试暴露的问题,我做了三轮补数据:
- 第一轮补的是堆叠遮挡:不整理桌面,随手扔几样东西,让目标互相遮挡,连拍几百张,全部标注。这轮补完后,
pen的Recall从0.74提到了0.86。 - 第二轮补的是复杂背景:把目标放到不同颜色、不同纹理的桌布上拍。这轮直接解决了“把桌布上的花纹识别成目标”的误检。
- 第三轮补的是小目标:把手机放远一点拍,让它在640分辨率下只占几十个像素。同时我花了一些时间查小目标检测的方案,发现可以用SAHI(切片辅助推理),即把大图切成小图分别推理再合并结果。对钥匙、笔这类小目标效果很明显。
三轮补完后,最终验证集指标如下(部分类别):
| 类别 | Precision | Recall | mAP50 | mAP50-95 |
|---|---|---|---|---|
| mug | 0.98 | 0.96 | 0.99 | 0.87 |
| cellphone | 0.95 | 0.93 | 0.97 | 0.82 |
| key | 0.84 | 0.79 | 0.88 | 0.61 |
| pen | 0.88 | 0.86 | 0.92 | 0.65 |
| charger | 0.91 | 0.88 | 0.95 | 0.72 |
整体mAP50从最初的0.83提到0.96,mAP50-95从0.55提到0.74。这里给大家一个参考:日常用品这类类内差异大、小目标多的数据集,mAP50-95能到0.7以上就算很能打的了。如果只看mAP50,会被它的“高分”误导,其实模型对小目标还是弱。
6. 封装成可用的检测器:导出与部署实测
6.1 ONNX/OpenVINO导出
训练完之后肯定不能只在命令行里看效果,要封装成可用的检测器。YOLOv8导出很顺手:
yolo export model=runs/detect/train/weights/best.pt format=onnx opset=12 \ imgsz=640 simplify=True导出时两个参数值得注意:
opset=12:兼容性最好的版本,太新可能导致部分ONNX Runtime版本不支持。simplify=True:开启后会把模型计算图做简化,我的模型从约700MB参数量的PyTorch权重导出成约200MB的ONNX,减负相当明显。
如果要在边缘设备上推理,我还会导出OpenVINO格式:
yolo export model=best.pt format=openvino imgsz=640OpenVINO格式在Intel CPU和核显上跑得非常快,我这套模型在i5-1240P的核显上能达到25ms/帧左右,延迟比纯ONNX Runtime快一倍还多。
6.2 实测效果与性能数据
部署后我专门做了一个小工具:手机摄像头对准桌面,实时画框显示物品名称和数量。实测下来:
- CPU(Intel i5-1240P,ONNX Runtime):单帧推理约42ms,加上前后处理约50ms,基本是20FPS。
- 核显(OpenVINO):推理约25ms,可以做到30FPS左右,已经具备实时性。
- 精度上,对单物品、中度堆叠场景表现很好;极端堆叠(10件以上相互遮挡)时,
pen、key还会漏,但误检基本没有。
这个过程中我最大的体会是:模型精度不足时,先别急着换更大的模型,先回头补数据;模型跑得太慢时,也别急着换设备,先导出ONNX+OpenVINO试试。很多时候调参数省下的时间,远不如把部署流程吃透来得高效。
另外还有一个实测中很有效的技巧,在摄像头取流时给检测结果加一个时间平滑。具体是每帧都跑模型,但对同一目标做跟踪式的去抖,一个框如果连续3帧不变就稳定显示,这样就避免了单帧误检一闪而过或者框抖动的问题。这个在很多“数据检测demo”里看起来不明显,但实际用起来观感差别很大。
7. 最后再给想复现的朋友一点建议
整个流程走下来,我发现做“日常用品目标检测数据集”这件事,真正难的不是训练,而是前面那些繁琐、机械、不显眼的工作:定类别、拍照片、清洗、标注、格式转换。这些环节占了整个项目80%的时间,但它们才是决定模型上限的地方。训练只是把数据里的信息“榨”出来而已。
如果你也想尝试做类似的数据集,我的建议是:第一,先把类别边界定义清楚,不要做太多容易混淆的类;第二,采集时一定要覆盖多种环境,不要在同一个场景里拍到天荒地老;第三,训练前给自己留出至少两轮“测试-补数据-重训”的迭代时间,第一版模型往往只是起点。
这套数据集后来还被我用在了一个小实验里:接入开放词汇检测模型,比如Grounding DINO,用自然语言描述直接检测“桌上的红色马克杯”,效果比固定类别的YOLOv8灵活很多。如果你对目标检测有兴趣,在自己的日常场景里攒一套数据,跑通一遍全流程,比看十篇教程都管用。
本文还有配套的精品资源,点击获取