
简介面向计算机视觉目标检测场景的苹果果实标注数据集整体规模适中基于Labelme工具为4430张苹果图像生成标注旨在帮助研究人员、开发者和学习者训练能够识别并定位苹果的检测模型。压缩包为7z格式体积约198.65MB内含2000个JSON标注文件每个文件记录苹果边界框的(x, y, width, height)坐标及类别属性等关键信息便于直接读取和转换。截至目前已有301人学习适用于算法对比、教学实验与二次开发。借助这些标注数据可省去手动标注环节直接用于卷积神经网络等深度学习模型的训练与评估配合数据增强、损失函数优化、模型参数调整等操作可有效提升识别精度并借助mAP等指标量化性能。在果园自动化监测、采摘机器人视觉导航等场景中具备实际落地价值。1. 苹果果目标检测绕不开这个 4430 张的 LabelMe 数据集做农业视觉的工程师找数据时都经历过类似的尴尬公开数据集里要么是 COCO 那种摆拍水果要么是无人机俯拍整片果园的大场景真正贴着单棵果树、近景拍摄、果实互相遮挡的数据少得可怜。苹果果目标检测 labelme-4430 的价值恰恰在于把场景收得很窄4430 张图片全是苹果树的近景视野果实堆叠、叶片遮挡、光照变化都在里面每个可见果实都被 LabelMe 多边形标注框住。它解决的是“把果实在复杂枝干背景下捡全、捡准”这个单一但高频的农业视觉诉求。适合用它训练 YOLO 做果园测产、套袋机器人或疏果作业的团队对只想泛泛了解目标检测的读者这个数据集反而用不上。搞清楚这 4430 张是怎么标出来的、怎么转成训练格式、坑在哪比纠结选哪个模型重要得多。2. 解开 4430 张标注图的真实结构LabelMe 格式与数据质量在把任何一张图送进训练脚本之前都得先搞清楚数据集底朝哪儿开。苹果果目标检测这个 labelme-4430 数据集标注全用 LabelMe 完成保存格式是一张图对应一个 JSON 文件。很多人拿到手第一反应是“直接转成 YOLO 开训”结果转换脚本跑出几百个空标签或者类别数突然多出五六个问题几乎都出在没看懂 JSON 结构。这一章把数据格式和验证动作拆开讲半小时内能确认这套数据能不能直接进训练管线。2.1 LabelMe JSON 里到底存了什么LabelMe 的保存逻辑是“一张图片一个 JSON”图片和 JSON 文件名相同、后缀不同放在同一目录。打开一个文件结构大致长这样{ version: 5.3.1, flags: {}, shapes: [ { label: apple, points: [[512.3, 284.1], [530.0, 300.7], [508.6, 312.4]], group_id: null, shape_type: polygon, flags: {} }, { label: apple_unripe, points: [[402.1, 319.4], [421.8, 335.0]], group_id: null, shape_type: polygon, flags: {} } ], imagePath: apple_1024.jpg, imageWidth: 1024, imageHeight: 768 }这段 JSON 不是工具随手写出来的每个字段都跟后续转换直接相关。shapes 是标注对象数组每个对象里 label 是类别名points 是多边形的顶点坐标shape_type 是标注形状imagePath 是图片文件名imageData 多数情况是 null因为图片以独立文件存在数据集目录里。上面第二个 shape 故意只留了两个点这是标注时漏点的脏数据后面转换脚本会按顶点数少于 3 直接过滤掉。参数说明shape_type 有 polygon 和 rectangle 两种常见取值。polygon 用一串顶点围出果实轮廓贴合度最高但转换成外接框时需要自己算 min/maxrectangle 只存左上和右下两个对角点转换快但两个紧挨的果实很容易被框进同一个矩形里。对苹果这种圆形又经常重叠的目标polygon 明显更适合。group_id 是 LabelMe 给人头、人体这类多部件标注用的分组字段果实检测用不上保持 null 就好。flags 里存的是标注时手动打上的标志位大多为空对象不用管它。看到这里你应该明白一件事LabelMe 的标注本质是“矢量的轮廓点集”而 YOLO 系训练格式是“归一化的外接矩形”两者之间必须有一层转换这也是后面要说的核心步骤。转换之前先做数据体检否则脏数据会被原样带进训练集。2.2 4430 张这个规模先判断数据质量再谈训练4430 张听起来是个不错的数字但张数本身不能代表质量。拿到数据后我会先抽看 30 张用 LabelMe 打开重点确认三个维度。第一看单图目标数。如果大多数图里有 5 到 30 个果实属于密集场景训练时要注意遮挡和小尺寸如果目标数不到 3 个模型很容易过拟合“空背景”。第二看分辨率。数据集的 JPG 分辨率如果是 768x1024 或更高小果实的像素直径足够用 640 输入训练不会丢太多信息如果原图只有 320x400那 4430 张的量也要把输入分辨率拉满不然小果子全是噪点。第三看光照分布。苹果是强反射表面直射光下高光区会淹没红色纹理逆光下果实边缘发暗如果数据里绝大多数是中午的顶光模型在早晨、傍晚的拍摄场景会明显掉点。把这三个维度记下来能直接指导后面的增强参数和分辨率设置。比如我发现这份数据里果实重叠比例高就会在训练时加大 copy-paste 增强而不是盲目堆 mosaic。另外值得看的是标注风格的一致性同一个数据集里如果有的标注员把果实轮廓圈得极细、顶点放到三四十个有的只画四五个点模型学到的形状先验就不统一后面做分割或转矩形都会有偏差。这种差异用肉眼扫几十张就能看出来。2.3 用 LabelMe 打开校验装环境与 3 个必做动作LabelMe 本身是个桌面标注工具用 Python 环境安装。常见的做法是新建一个干净环境再装避免和项目依赖打架conda create -n labelme python3.8 -y conda activate labelme pip install labelme装完后命令行直接输入 labelme 就能打开主窗口。注意如果是在 Windows 上装Python 版本别用最新的3.8 或 3.9 配合 pyqt5 5.15 是踩坑最少的组合遇到 pyqt5-sip 相关报错后面避坑章里有具体解法。打开数据集目录后做三件必做动作。第一逐类别核对 label 名称看看有没有出现 “apple”、“Apple”、“苹果” 这种大小写或语言混写类别不统一是后续训练掉点最隐蔽的原因。这一步花了五分钟后面能省五小时。第二检查有没有空标注文件即 shapes 数组为空的 JSON这类文件要么是漏标要么是特意保留的负样本需要自己确认用途。第三核实图片和 JSON 是否一一对应数据集里常有图片没标注、或 JSON 指向不存在的图我用一段小脚本快速扫描import json import os data_dir apple_4430 missing_img [] empty_shapes [] shape_counts [] for name in os.listdir(data_dir): if not name.lower().endswith(.json): continue with open(os.path.join(data_dir, name), encodingutf-8) as f: data json.load(f) img_name data.get(imagePath, ) if not os.path.exists(os.path.join(data_dir, img_name)): missing_img.append((name, img_name)) if not data.get(shapes): empty_shapes.append(name) shape_counts.append(len(data.get(shapes, []))) print(fJSON 总数: {len(shape_counts)}) print(f缺图片的 JSON: {len(missing_img)}) print(f空标注 JSON: {len(empty_shapes)}) print(f平均每张标注目标数: {sum(shape_counts) / len(shape_counts):.1f})逻辑说明脚本遍历数据集根目录下所有 JSON读取 imagePath 和 shapes 两个字段。imagePath 指向的图片不存在代表这个标注是孤儿文件训练时会白扫一条路径shapes 为空代表这张图没有任何目标如果它不是特意留的负样本建议直接剔除。输出中的平均目标数可以快速判断数据密度这个数字决定训练时的 batch 和增强策略。我在一份相似结构的果树上跑这个脚本通常能找出 1% 到 3% 的缺图或空标文件不清理掉它们训练日志会持续出现难排查的警告。提示不要用文件管理器的“查看大图标”方式肉眼校验上千张图效率低且容易漏。上面的扫描脚本跑一遍几秒钟就能定位缺图和空标注。3. 从 LabelMe JSON 到 YOLO 训练集转换脚本与数据划分LabelMe 的 JSON 是矢量标注主流检测器训练时并不直接消费它。拿 YOLO 系来说训练数据要求每一张图对应一个同名 txt每行代表一个目标实例格式是“类别ID 中心x 中心y 宽 高”坐标全部用图片宽高归一化到 0~1。这一步不做好后面训练再努力也是白搭——模型读到的坐标错位、类别错乱loss 曲线看着在降实际指标全崩。这一章给出可直接复用的转换脚本以及划分数据集时最容易踩的坑。3.1 为什么要转成 YOLO 的 txt 格式LabelMe 的 polygon 是任意多边形YOLO 的框是轴对齐矩形两者语义不同。更直接的原因YOLO 系列的 dataset parser 根本不认多边形它只读 txt 框文件。所以转换不是可选优化而是必经预处理。转换要做三件事把多边形顶点坐标换算成外接矩形的两个角点把角点坐标除以图片宽高得到 0~1 的归一化值把字符串类别名映射成从 0 开始的整数 ID。这三件事任何一件做错体现在训练里都是隐蔽的错。类别名没映射对训练不会报错但推理出来所有框的标签全是乱的坐标忘了归一化换一张不同分辨率的图推理直接全部偏出目标位置。我见过有人图省事直接手写 txt结果类别列表顺序和 yaml 不一致整个项目白跑一周所以建议老老实实走脚本转换。3.2 转换脚本多边形转外接框并过滤脏标注下面这个脚本是我处理这份 4430 张数据时最常用的版本直接跑就能出 YOLO 格式。它把 classes 列表放在脚本顶部避免代码里到处硬编码类别名import json import os classes [apple, apple_unripe] class_dict {name: i for i, name in enumerate(classes)} def convert_labelme_to_yolo(data_dir, json_name, out_dir): json_path os.path.join(data_dir, json_name) stem os.path.splitext(json_name)[0] with open(json_path, encodingutf-8) as f: data json.load(f) img_w data.get(imageWidth, 0) img_h data.get(imageHeight, 0) if not img_w or not img_h: return lines [] for shape in data[shapes]: label shape[label] if label not in class_dict: continue points shape[points] if len(points) 3: continue xs [p[0] for p in points] ys [p[1] for p in points] x_min max(0, min(xs)) y_min max(0, min(ys)) x_max min(img_w, max(xs)) y_max min(img_h, max(ys)) if x_max - x_min 2 or y_max - y_min 2: continue x_center (x_min x_max) / 2 / img_w y_center (y_min y_max) / 2 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h lines.append(f{class_dict[label]:d} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if not lines: return os.makedirs(out_dir, exist_okTrue) with open(os.path.join(out_dir, stem .txt), w, encodingutf-8) as f: f.write(\n.join(lines) \n) data_dir apple_4430 out_dir labels for name in os.listdir(data_dir): if name.lower().endswith(.json): convert_labelme_to_yolo(data_dir, name, out_dir)逻辑说明脚本按 JSON 文件名遍历整个数据集目录。对每个 shape 做四件事把 label 通过 class_dict 映射成整数 ID把多边形所有顶点的 x、y 分别取 min 和 max 得到外接矩形将矩形坐标夹取在图片范围内再归一化到 0~1 后写入 txt。其中 label 不在 classes 白名单里的直接跳过顶点数小于 3 的视为误标注跳过宽高小于 2 像素的目标也丢弃。参数说明classes 的顺序至关重要它必须与后面 data.yaml 里的 names 完全一致顺序一旦错位整数 ID 和类别名的对应关系就是乱的训练不报错但推理标签全部错位。x_min max(0, min(xs)) 这条 clamp 是防止标注员把果实截边处的顶点画到图片外导致框范围越界YOLO 训练时对越界框只会打警告但警告攒多了会拖慢训练而且这些框参与 loss 计算时坐标不准确。宽高 2 像素的过滤阈值是按经验取的小于 2 像素的目标在 640 输入下已经不足 3 个像素留着只会干扰学习。提示转换完不要急着训练先抽几个文件用文本编辑器打开 txt 看看前几行的坐标是否都在 0~1 区间内。我的脚本没有对“坐标整体越界”做二次校验如果 min(xs) 大于 imageWidth 这种极端情况出现转换结果会是负数所以先人工抽查最稳妥。3.3 按 70/20/10 划分数据集目录对齐与随机种子4430 张图转完标签后下一步是划分 train/val/test。常见但错误的方式是从 0 到 4430 按序号切三段。如果原始文件名的序号暗含拍摄时间或果树编号这种切法会让验证集和训练集的光照、品种分布完全不一致训练结果虚高或虚低。正确做法是先把所有图片名整体随机打乱再按比例切片并固定随机种子保证别人能复现同一份划分import os import random from shutil import copy2 random.seed(42) data_dir apple_4430 images [f for f in os.listdir(data_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(images) n len(images) train_imgs images[:int(n * 0.7)] val_imgs images[int(n * 0.7):int(n * 0.9)] test_imgs images[int(n * 0.9):] def copy_split(img_list, split_name): img_out fdataset/{split_name}/images lbl_out fdataset/{split_name}/labels os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for img in img_list: stem os.path.splitext(img)[0] copy2(os.path.join(data_dir, img), os.path.join(img_out, img)) txt stem .txt if os.path.exists(os.path.join(data_dir, txt)): copy2(os.path.join(data_dir, txt), os.path.join(lbl_out, txt)) copy_split(train_imgs, train) copy_split(val_imgs, val) copy_split(test_imgs, test) print(ftrain: {len(train_imgs)}, val: {len(val_imgs)}, test: {len(test_imgs)})逻辑说明random.shuffle 把图片顺序彻底打乱再按 0.7/0.2/0.1 切片。copy_split 把图片和同名 txt 分别复制到 images 和 labels 目录YOLO 训练器要求在同一个 split 下 images/labels 两目录结构并存。txt 不存在时只拷图片这张图会被当作背景样本参与训练在果实检测里反而是有用的负样本。参数说明random.seed(42) 里的 42 只是个约定俗成的种子换成其他整数也可以关键是固定后复现实验时大家拿到的 train/val/test 完全一致。比例 0.7/0.2/0.1 对 4430 张来说大约是 3100/886/444 张验证集和测试集都有足够规模来评估 mAP。如果后续要做超参调优建议把 test 单独留着只在 val 上调参否则测试集被看多了最终指标会失真。还有一种坑是按果园目录分组如果数据是按树编号的随机切分后同一棵树的图会同时出现在 train 和 val 里评估结果会乐观部署到新果园就打折。3.4 写 data.yaml 时容易忽视的两个字段转换和划分做完还要写一个数据集描述文件。YOLO 系项目都靠它找路径、读类别数。这里放一个典型配置train: dataset/train/images val: dataset/val/images test: dataset/test/images nc: 2 names: [apple, apple_unripe]逻辑说明train/val 是训练和验证的图片目录YOLO 训练器会自动在同一级目录下找同名的 labels 目录所以不需要在 yaml 里写标签路径。nc 是类别数names 是类别名列表顺序必须和第 3.2 节转换脚本里的 classes 列表一致。参数说明最容易踩的坑有两个。第一个是 names 顺序和 class_dict 不一致训练正常、loss 正常但推理时输出“类别 0”实际对应的是“apple_unripe”这种问题只在部署阶段暴露。第二个是路径直接写 dataset/train/images 这种相对路径时执行训练命令的工作目录必须在 yaml 所在的项目根目录如果跑脚本的地方不对YOLO 会报数据集找不到或者静默读到空目录生成一堆背景样本。我的习惯是先用最小命令把测试集跑通一遍再全量训练yolo train datadataset.yaml modelyolov8n.pt epochs5 imgsz640 batch16这里 epochs5 只是验证管线确认能正常读图、算 loss、出权重再拉长 epochs。modelyolov8n.pt 是 YOLOv8 的 nano 预训练权重跑通后可以换 s 或 m 规模果类目标不算特别难n 到 m 之间基本够用。4. 苹果果检测落地避坑标注、转换、训练、评估的 5 个血泪经验数据标注到模型落地翻车点往往不在模型本身而在数据和工具链。以下是处理 LabelMe 标注、YOLO 训练时反复踩过的几个坑每一条按现象、原因、解决来写。4.1 pyqt5-sip 版本冲突LabelMe 打不开现象Python 3.10 环境里 pip install labelme 后命令行输入 labelme报 ModuleNotFoundError: No module named PyQt5.sip或者直接是 pyqt5-sip 相关错误标注工具整个不可用。原因LabelMe 通过 PyQt5 提供图形界面pyqt5-sip 是 PyQt5 和 Python 解释器之间的胶水层版本不匹配时这个模块装不上常见于较新的 Python 版本和 pypi 同期发布的 pyqt5 包版本冲突。解决不跟它搏斗直接新建一个 Python 3.8 环境先固定装 pyqt5 5.15.7 和 pyqt5-sip 12.12.1再装 labelmeconda create -n labelme python3.8 -y conda activate labelme conda install pyqt55.15.7 pyqt5-sip12.12.1 -y pip install labelme这套组合里 Python 3.8 是 LabelMe 5.x 时代最常用的开发环境pyqt5 5.15 是兼容文件最丰富的一版。装完可以执行 python -c from labelme import utils; print(ok) 验证能打印就说明环境正常。用 3.11、3.12 的读者装不上不要死磕版本换环境是最快的后悔药。4.2 转换后标签大面积越界或空白现象YOLO 训练启动时打印大量 warnings: some labels are out of bounds或者某些图训练时没有任何目标参与计算loss 曲线在那个 batch 突然跳一下。原因转换时没对多边形坐标做边界防护。标注员框截断在图像边缘的果实会习惯性把顶点点到图片外面还有一种是 shape 只有两三个点转出的矩形宽高接近 0被 YOLO 判定为无效标签。解决转换脚本里对坐标做 clamp把 x_min、x_max 限制在 [0, imageWidth]y 轴同理小于 2 像素的直接丢弃就是我 3.2 节脚本里的处理。另外转换完用脚本统计每个 txt 的行数行数为 0 的文件要确认是不是负样本如果不是把对应图片也从训练集移除否则它每个 epoch 都充当背景样本白白消耗训练时间。4.3 类别名不统一模型多出 5 个“苹果”现象训练日志里打印的类别统计原本期望只有 apple 一个类结果出现 apple、Apple、apples、apple_fruit 好几种mAP 看起来还行业务上根本没法用。原因多人协作标注时每个人写 label 字符串的习惯不同。LabelMe 不提供统一的类别约束只要标注员手动键入这种混乱就会发生。数据集里还有可能夹着苹果花、树枝之类的干扰标注标签名稍一不同就变成新类。解决在转换脚本里按白名单归一化不要按原样映射。比如把 label.strip().lower() 后统一为 apple或者检测 label 是否在 [apple, apple_unripe] 白名单里不在的丢弃并打印警告。训练前再跑一遍类别统计确认最终类别数等于 yaml 里的 nc。把这一步放在转换阶段处理比训练后处理省事得多。4.4 外接矩形框进半个邻居mAP 上不去现象训练收敛后 val mAP50 在 0.9 上下但可视化检测结果发现两个紧挨的苹果经常被一个框包住或者在遮挡边缘框住半个果实和半个树枝看起来挺准实际框没落位。原因LabelMe 里画的是多边形转成外接矩形后两个贴得近的果实的矩形会互相重叠一个果实被另一个遮挡时外接矩形含大量背景模型学到的是“矩形包含的整片区域都算目标”评估时中心点在两个果实中间IOU 算不高。解决如果业务要求框出单个果实有两条路。一条是直接用多边形顶点做训练改用 YOLOv8-seg 这类带 mask 头的模型绕开多边形转矩形的信息损失另一条在转矩形后做一次裁剪增强训练时随机 crop 把单个果实尽量撑满画面让模型少依赖整片背景。前者效果更好但配置和推理成本都高一层量力而行。4.5 预训练权重迁移loss 冲高直接 NaN现象用 COCO 预训练权重在自己的 4430 张数据上微调前两个 epoch loss 直接冲上几十然后变成 NaN训练进程崩掉。原因COCO 预训练模型输出层类别数是 80自己的类别数是 2输出层维度不匹配加上默认学习率 0.01 对少量私有数据来说偏大一步梯度更新就把权重推到数值溢出。解决在训练命令里明确指定预训练权重让模型自动对齐类别数或者用不带最后输出头的预训练权重做迁移学习率从默认的 0.01 降到 0.001打开 warmup 让前几个 epoch 用小学习率热身。我的习惯是 lr00.001、warmup_epochs3、配合 cos_lr 衰减这个组合在小规模果类数据集上极少崩。崩了也不用重新标注数据把 last.pt 加载进来接着跑权重会逐步收敛。5. 把 4430 张用到上限数据增强与模型验证的进阶玩法数据集本身是死的拉开差距的是增强参数和验证方式。苹果这种高饱和红色目标最容易翻车的操作是无脑开 hsv_h 大抖动。YOLO 训练参数里 hsv_h 默认 0.015 是安全的苹果的红色调偏移太大就变成橙色模型学到的是“橙色球体”而不是苹果hsv_s 可以放到 0.3果园光照差异本来就大hsv_v 保持 0.3 或略加。mosaic 在这个数据集上要谨慎四张图拼在一起会把苹果缩到四分之一太小的果实直接变噪点。我的习惯是 mosaic1.0、copy_paste0.5 搭配copy_paste 把同批图里的果实贴到空背景区域补的是遮挡和密集的多样性。验证阶段mAP 只给全局回执决定能否上场的是局部的失败形态。我训练完必做两步第一步跑一批测试集推理把结果图按 mAP 从低到高排序人工看最差的 50 张第二步统计漏检目标里小目标占比和遮挡目标占比这两个数字直接决定下一步调分辨率还是调增强。不同输入分辨率在 4430 张规模上的经验区间可以参考下表。输入分辨率单卡训练耗时mAP50 典型区间适合场景640x6406~10 小时0.82~0.92边缘盒子、实时花果识别1280x128024~36 小时0.90~0.96测产、单棵树拍照分析按 4430 张、单卡、batch 16 估算实际随增强强度浮动。如果验证集中小目标占三成以上且漏检集中在小目标把输入分辨率从 640 提到 960 往往比调任何增强都有效。如果漏检集中在两三个果子贴一起的案例换成 YOLOv8-seg 直接学多边形反而更对症。我自己的习惯是每次训练只改一个变量记录 val mAP 和 F1不并存太多新增强否则根本说不清哪个环节有效。这个数据集的坑我基本都踩了一遍最想说的一条先花半小时验证数据完整性再花两小时训练永远比反过来省时间。转换脚本、固定随机种子、类别白名单这三样是任何数据集项目都值得提前做好的后悔药。希望上面的经验和做法能帮你少走一段弯路。本文还有配套的精品资源点击获取