
简介面向果园自动化与农业视觉研究的柑橘果目标检测数据集包含579张JPG原图与580个JSON标注文件覆盖树上柑橘On-tree与树下柑橘Under-tree两类目标适用于果园收获系统开发、成熟度与掉落率监测、视觉产量预估算法研究及农业机器人导航训练等场景。数据集中树上标签累计25319个、树下标签17334个标注信息完整可直接用于YOLO、Mask R-CNN等目标检测模型的训练与评估。压缩包整体约125.72MB共1159个文件文件类型以jpg图像和json标注为主目录结构清晰便于按需取用。已有437人学习下载对于需要真实农田环境图像数据的计算机视觉开发者而言是一份具有明确类别区分和较大标注规模的基础资源可有效支撑从数据预处理到模型迭代的完整实验流程。1. 做柑橘检测不是缺模型是缺一套能下地的标注数据我自己拆过不少果园场景的目标检测项目最深的感受是模型结构早就不是瓶颈标注数据才是。这套柑橘果目标检测数据集一共 580 张实拍图片覆盖两个关键类别——树上柑橘On-tree和树下柑橘Under-tree标注工具是 labelme原始格式为 JSON。它在解决一个非常具体的问题果园环境里果实和目标之间的遮挡、光照、叶片干扰以及树下落果与土壤背景的混淆用通用数据集训练出来的模型一进果园就原形毕露。适合谁用做农业检测、果实计数、落果分拣的算法工程师以及需要一套可靠标注做迁移学习或基准验证的研究者。它不算海量但场景属性明确能帮你把标注体系和训练流程完整跑通。2. 看懂这套数据的底细580 张图的布局与 labelme 的 JSON 结构拿到手的是一份标注好的工程数据不是一堆散图就完事。你首先要做的事情是把它读透搞清楚每张图背后对应了什么信息。很多人拿到数据集直接扔进训练脚本结果发现类别名对不上、坐标读不出来、甚至有的 JSON 里字段被改过白白浪费时间。我先带你把它翻一遍。2.1 目录结构与命名规律先建立你的数据地图常见做法是数据集压缩包解压之后会有一个images目录放原始图片一个annotations目录放 labelme 生成的 JSON 文件图片文件名和 JSON 文件名一一对应。比如IMG_0234.jpg对应IMG_0234.json。这样做的最大好处是后续无论转成 YOLO 还是 COCO 格式程序都可以通过读取文件名前缀完成匹配不需要额外维护映射表。我一般解压后会先跑一个快速统计脚本重点看三件事图片总数、每张图的标注框数量分布、两个类别的样本量比例。命令很简单# 先看目录结构是否完整 find . -type f | sort | head -20 # 统计图片和 JSON 数量是否一一对应 ls images/ | wc -l ls annotations/ | wc -l如果两边数量不一致说明要么有漏标的图片要么有标了但没有原图的孤儿 JSON。后一种情况在训练时会导致索引错位YOLO 在加载数据时候可能会报错也可能不报错但悄悄少算了一张图。所以这个步骤别跳过消耗一分钟能省后续一天的排查时间。具体到这张数据集580 张图意味着至少产生 580 个 JSON 标注文件。但有些 JSON 里可能包含多张图的信息吗不会labelme 天生就是一个 JSON 对应一张图这是它的设计模型所以你可以放心按 1:1 的关系去处理。不同图片的分辨率不需要预先统一YOLO 训练时可以靠rect或者letterbox来做适配但转换坐标时必须按每张图的原始宽高归一化这是后面容易踩坑的地方。2.2 JSON 字段逐项拆解不是所有字段都对训练有用labelme 的 JSON 结构属于那种“看着简单细节不少”的类型。核心字段就几个imagePath、imageWidth、imageHeight、shapes其中shapes是一个数组每一项记录一个标注对象。每个标注对象里最要紧的是label和points。{ version: 5.0.1, flags: {}, shapes: [ { label: On-tree, points: [ [342.5, 187.2], [410.3, 218.7], [398.6, 289.4], [329.8, 260.1] ], group_id: null, shape_type: polygon, flags: {} }, { label: Under-tree, points: [ [100.2, 450.3], [168.9, 442.8], [172.4, 500.6], [104.1, 508.2] ], group_id: null, shape_type: polygon, flags: {} } ], imagePath: IMG_0234.jpg, imageData: null, imageWidth: 640, imageHeight: 480 }看这个结构标的是polygon多边形而不是矩形框。这意味着你在转换时必须自己根据多边形顶点计算外接矩形。labelme 里也有画矩形框的选项但这份数据用的是 polygon对果实这种不规则椭圆目标来说多边形比矩形更贴近实际轮廓尤其是树上柑橘被叶子遮挡时矩形框会框进去大量的叶片背景导致训练时正样本噪声变大。group_id这个字段往往被忽略但它有实际用途。如果同一串柑橘被拆成了多个多边形你想让它们在检测后被归为一组就需要用到它。在后续做计数任务时group_id可以帮你把重叠视角里的同源目标合并掉减小重复计数误差。但注意转换到 YOLO 格式时这个信息会丢失因为 YOLO 的 txt 只写类别和坐标。如果想保留分组关系建议在转换之前先把group_id导出成一份独立 JSON否则后面计数阶段想用就没得用了。imageData字段在 labelme 保存时默认为 null表示图像数据不嵌进 JSON 里。但有些版本的 labelme 会把图片 base64 编码塞进来导致 JSON 体积巨大。打开 JSON 文件如果发现imageData不是 null而是一长串字符串转换脚本需要处理这种冗余数据否则内存占用会涨得很快。2.3 类别的实际含义On-tree 与 Under-tree 并不是简单的两种状态很多第一次拿到这份数据的人会问树上柑橘和树下柑橘不都是柑橘吗为什么拆成两类这个问题问到点子上了。从目标检测模型的角度看这两类目标的形态特征、尺度分布、遮挡程度是完全不同的。树上柑橘通常挂在枝头背景是叶片和枝干颜色和叶片有区分但遮挡严重小目标占比高树下柑橘是落果通常躺在土壤、枯叶或杂草里部分果皮已经破损或沾土颜色偏暗轮廓和背景反差更大。所以这两个类别不能合并成一个“柑橘”类去训练。如果合并模型会学到一套矛盾的纹理和尺度特征树上果的小尺度模式和树下果的大尺度模式互相干扰最终精度反而不如分开训练。这一点在做数据规划时就要想明白类别定义决定了模型的上限后处理再花哨也补不回来。3. 从 labelme 转成 YOLO 格式转换脚本与四个边界坑labelme 的 JSON 不能直接喂给 YOLO。YOLO 系列的训练格式要求每张图对应一个同名 txt 文件每行内容为class_id x_center y_center width height其中坐标全部是归一化到 [0, 1] 的浮点数。转换本身不算难但里面藏着几个容易翻车的细节。这一章我直接给出一份能跑的转换脚本然后逐个讲清楚边界坑。3.1 转换脚本从多边形到归一化矩形框下面这份脚本是我在处理 labelme 数据时常用的骨架核心逻辑是读取 JSON → 提取 shapes → 计算外接矩形 → 归一化 → 写 txt。你可以直接保存为labelme2yolo.py使用。import json import os from PIL import Image def poly_to_bbox(points): 由多边形顶点计算外接矩形返回 x_min, y_min, x_max, y_max xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) return x_min, y_min, x_max, y_max def convert_labelme_json(json_path, img_width, img_height, out_txt_path, class_map): with open(json_path, r) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label].strip() if label not in class_map: print(f[警告] 未知标签: {label}, 文件: {json_path}) continue class_id class_map[label] points shape[points] if len(points) 3: continue # 点数不够忽略该标注 x_min, y_min, x_max, y_max poly_to_bbox(points) # 边界裁剪避免坐标超出原图 x_min max(0, min(x_min, img_width - 1)) y_min max(0, min(y_min, img_height - 1)) x_max max(0, min(x_max, img_width - 1)) y_max max(0, min(y_max, img_height - 1)) if x_max x_min or y_max y_min: continue x_center (x_min x_max) / 2.0 / img_width y_center (y_min y_max) / 2.0 / img_height width (x_max - x_min) / img_width height (y_max - y_min) / img_height lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) with open(out_txt_path, w) as f: f.write(\n.join(lines) \n)这段脚本的输入参数有三个地方需要你根据自己的情况调整class_map字典、图片宽高来源、输出路径的拼接逻辑。其中class_map决定类别编号顺序建议固定不要今天用{On-tree: 0, Under-tree: 1}明天改成{Under-tree: 0, On-tree: 1}否则训练权重和标签对应关系全乱掉。调用时你要从 JSON 里读取imageWidth和imageHeight字段不要用Image.open去读实际图片尺寸。原因是 labelme 保存时写入的尺寸就是标注时的画布尺寸如果图片后来被压缩过实际尺寸可能变了但 JSON 里记录的还是旧值混用会导致坐标换算错误。3.2 坑一坐标越界与裁剪策略第一个高频问题是坐标越界。标注时手滑把多边形顶点拖到了画布外面或者标注时图片边缘的果实只标了一半这些情况都会让计算出来的矩形框超出[0, img_width]范围。如果不处理YOLO 训练时会报出类似All bounding boxes should have positive width and height的警告严重的直接整个样本被丢弃。我的做法是在脚本里加显式裁剪也就是上面代码里那四行min/max操作。但注意裁剪只是止血不是根治。更合理的处理方式是统计一下越界标注的比例如果超过 5%说明标注阶段画布尺寸设置有问题需要返工而不是靠脚本硬撑。3.3 坑二标签名空格与大小写不一致labelme 里每个人输入习惯不同有人写On-tree有人写On - tree有人写成ontree。这些在 JSON 里都是合法字符串但一旦与你的class_map不匹配脚本就会静默跳过该目标导致标签数量比实际少了很多。我在处理时会在转换前先跑一个标签汇总脚本cat annotations/*.json | grep label | sort | uniq -c把出现过的所有标签名和次数列出来。这一步能很快发现有没有拼写不一致的“隐藏类别”。查出露头标签后可以在脚本里做一层归一化处理比如去掉空格、统一小写再映射到class_map。千万别小看这个坑标注几千个框的时候哪怕 1% 的标签错位最后的 mAP 都可能掉几个点。3.4 坑三小目标的坐标精度丢失YOLO 归一化坐标保存的是 6 位小数看起来精度够但遇到小目标时就危险了。一颗距离镜头较远的树上柑橘在 640×480 的图像里可能只有 20×20 像素归一化后宽度是 0.03125这个量级下6 位小数的精度损失通常是可接受的但如果你把归一化坐标换算回像素时会发现误差达到 0.5 像素左右。对正常检测任务这个误差不致命但对分割任务或小目标计数任务需要再精确一点。更稳妥的做法是在转换脚本里统一用整数像素坐标参与计算最后一步再归一化不要中间过程反复转换。上面脚本的做法就是先算像素级外接矩形再归一化这样能最大限度保留原始标注信息。3.5 坑四图像尺寸不是整数倍时的归一化误差YOLO 官方要求的图像输入通常会被 resize 到 640×640 或 416×416而原始图片可能不是正方形比如 640×480 或者 1280×720。转换脚本按原图尺寸归一化是没问题的训练时模型会自动做 letterbox 填充。但如果你在转换阶段图省事先把图片 resize 成 640×640 再计算坐标就会引入额外的拉伸误差矩形框位置会偏移。常见做法是在转换脚本里保持原图尺寸计算训练阶段用一个rect参数让 YOLO 按原始宽高比加载图片这样精度损失最小。或者用letterbox填充而不是直接拉伸。4. 训练与验证从划分到超参让模型真的能在果园里跑起来转换完成之后训练环节的坑不比标注阶段少。数据集只有 580 张这个量级在深度学习里属于小样本划分比例、数据增强、超参设置都会直接影响最终结果。这一章我把整个流程捋一遍。4.1 数据集划分别让同一棵树出现在两个集合里580 张图我一般建议按 8:1:1 划分训练集、验证集、测试集也就是 464 张训练58 张验证58 张测试。但划分时有一条铁律同一个场景、同一棵树拍的多张连续帧只能出现在同一个集合里不能一部分进训练集一部分进测试集。原因很好理解连续帧的差异极小如果训练集里已经见过了测试集里再来一遍等于开卷考试测出来的 mAP 虚高。果园数据通常是连拍的你拿到的 580 张里可能有一大半是同一棵树不同角度的相片不做场景分组直接随机划分结果会非常乐观但一部署就失灵。划分脚本很简单但注意要基于“场景分组”而不是单张图片做随机import random import shutil from collections import defaultdict # 假设图片文件名格式为: 场景ID_序号.jpg image_files os.listdir(images/) grouped defaultdict(list) for f in image_files: scene_id f.split(_)[0] grouped[scene_id].append(f) scene_ids list(grouped.keys()) random.shuffle(scene_ids) train_scenes scene_ids[:int(len(scene_ids) * 0.8)] val_scenes scene_ids[int(len(scene_ids) * 0.8):int(len(scene_ids) * 0.9)] test_scenes scene_ids[int(len(scene_ids) * 0.9):]如果你的文件名里没有场景信息那就需要人工根据图片内容做聚类划分或者用图像相似度计算来辅助划场景。这一点是很多人忽略的也是最容易导致模型“看起来很好落地就废”的元凶之一。4.2 YOLOv8 训练参数小数据集别硬上大模型我用 YOLOv8n 或 YOLOv8s 来示范训练命令先不碰大模型。580 张图的数据量上 YOLOv8x 纯属自找麻烦参数量大收敛慢还容易过拟合最后的精度未必比小模型高多少。yolo detect train \ datacitrus.yaml \ modelyolov8n.pt \ epochs120 \ imgsz640 \ batch16 \ lr00.01 \ augmentTrue \ patience20 \ projectruns/citrus \ nameexp1 \ device0citrus.yaml文件写成这样path: /path/to/citrus_dataset train: images/train val: images/val test: images/test names: 0: On-tree 1: Under-tree参数配置要讲几个点。imgsz640不是拍脑袋定的得看你这 580 张图里小目标占比。如果果实像素尺寸普遍小于 32×32建议把 imgsz 调大到 1280虽然训练时间会长但小目标特征能保留更多。batch16在 580 张图的情况下已经不算小了跑不动就降到 8梯度累积可以弥补。patience20的意思是验证集指标 20 个 epoch 不提升就早停对小数据集很友好避免后面纯粹在拟合噪声。如果你的机器显存只有 8Gimgsz640batch8yolov8n是能跑起来的组合。如果显存再小那就把batch降到 4不要一开始就砍imgsz小目标检测任务里分辨率比模型宽度值钱得多。4.3 验证阶段看什么mAP50 和 mAP50-95 要分开读训练完看指标很多人只盯 mAP50这是个误区。mAP50 表示 IoU 阈值在 0.5 时候的平均精度对位置精度的要求很宽松框偏了很大一截照样能算命中。在柑橘检测这种任务里如果你只是数个数mAP50 够用如果你要做定位分析比如果实成熟度监测需要精确框住每个果实就得认真看 mAP50-95。yolo detect val \ modelruns/citrus/exp1/weights/best.pt \ datacitrus.yaml \ imgsz640 \ batch16验证结束后要重点看两类输出一是 per-class 的 precision 和 recall二是 PR 曲线。如果 On-tree 的 recall 明显低于 Under-tree说明树下果容易漏检这时候优先调的是数据增强里针对小目标的策略而不是盲目加深网络。5. 避坑与常见问题标注数据里最常见的那几个翻车点这一章我整理几个我自己踩过、也帮别人排查过的典型问题。每条都是“现象 → 原因 → 解决”的结构你可以按图索骥在自己的数据上检查一遍。5.1 标签总数和图片数对不上训练集里有一部分图是空文件现象训练时日志显示实际参与训练的图片数量比数据集里的图片数量少了几张或者验证集 mAP 忽高忽低找不到规律。原因最常见的是 labelme 标注过程中保存失败JSON 文件损坏或内容为空导致转换脚本输出空的 txt 文件。YOLO 加载时认为该图片没有标注训练时会出现异常有时它会静默跳过最终表现为指标不稳定。解决转换完成后立刻跑一个全量校验脚本检查每一张图片对应的 txt 是否为空文件、是否包含非法数值。for f in labels/train/*.txt; do if [ ! -s $f ]; then echo 空标签文件: $f fi done5.2 类别不平衡导致模型只会检测树上果现象训练完验证集里 Under-tree 的 mAP 只有 0.3On-tree 却有 0.9模型对树下果视而不见。原因580 张图里标注的果实数量本身不平衡。树上柑橘往往长在枝条上密度高一框能标几十个而树下落果分布稀疏一框可能只有三五个。模型在训练时见到的树下果样本太少学不到有效特征。解决不要急着删数据先做类别权重调整。YOLO 训练时可以直接指定每个类别的 loss 权重或者在citrus.yaml里通过cls参数调整。更简洁的做法是给 Under-tree 类做在线增强把它的图像做额外旋转、裁剪、亮度扰动增加有效样本量。5.3 转换后矩形框整体偏移到图片右上角现象跑完转换脚本用可视化脚本画框检查发现所有框的位置都往右上角漂移了大概几十像素。原因这个基本可以锁死在坐标归一化时用错了宽高。要么是imageWidth和imageHeight读反了要么是读取了imageData里嵌的 base64 图片的实际尺寸而这个图片尺寸和标注画布尺寸不一致。labelme 有个版本会在导入图片时默认缩放到界面显示大小如果保存 JSON 时没有同步更新尺寸字段标注坐标记录的就是显示尺寸下的坐标。解决转换前对每个 JSON 做一次字段校验打印imageWidth、imageHeight与原图的真实宽高做对比。不一致的批量修正再转。永远不要假设 JSON 里的尺寸是对的要校验。5.4 多个标注框重叠严重模型预测结果出现大量重复框现象验证集里同一个果实被预测出三个框置信度都挺高NMS 消不掉最后精度看着还行但计数彻底不能看。原因原始标注时同一个果实被不同的人标了两次或者同一个人在复标时没有对之前的框做筛选导致标签本身就出现重叠框。模型学到了这种重复输出的模式遇到果实时也会输出多个高置信框。解决在转换之前做一次标注重叠检查。计算所有标注框之间的 IoU如果 IoU 大于 0.5 且属于同一类别就保留面积更大的那个框删掉另一个。这个脚本不复杂但能显著降低模型输出的重复框比例。5.5 imageData 嵌了 base64 导致内存爆炸现象转换脚本跑着跑着内存占用飙到几个 GB速度越来越慢甚至直接被杀掉。查看 JSON 文件大小每个都超过 1MB。原因imageData字段把图片的 base64 编码写进了 JSON一长串字符串在json.load时全部加载进内存。580 个 JSON 同时处理内存直接不够用。解决转换脚本里在读取后立刻判断imageData是否为 None不是就置空删除然后再处理shapes。如果原 JSON 里已经嵌了数据删除后建议保存一份清理版本后续所有脚本都基于清理版跑。6. 进阶技巧用多边形面积与矩形框面积之比做标注质量二审这一个阶段我一般会在训练一轮之后做用数据本身的几何特征来反查标注质量。具体做法是观察每个标注对象的多边形面积与它的外接矩形面积的比值这个比值反映的是标注形状与矩形的贴合度。树上柑橘被遮挡时标出来的多边形往往是不规则的贴合度比值常在 0.5 到 0.7 之间而树下落果相对完整多边形接近椭圆比值通常能到 0.75 以上。如果你发现某张图里大量目标的贴合度比值异常低比如小于 0.4那大概率不是果实形状不规则而是标注时把叶片或树枝也框进去了。把这些候选样本挑出来重新检查效率比肉眼过一遍全部图片高很多。import json import math def polygon_area(points): n len(points) area 0.0 for i in range(n): x1, y1 points[i] x2, y2 points[(i 1) % n] area x1 * y2 - x2 * y1 return abs(area) / 2.0 def bbox_area(points): xs [p[0] for p in points] ys [p[1] for p in points] return (max(xs) - min(xs)) * (max(ys) - min(ys)) with open(annotations/IMG_0234.json) as f: data json.load(f) for shape in data[shapes]: pts shape[points] pa polygon_area(pts) ba bbox_area(pts) ratio pa / ba if ba 0 else 0 print(f{shape[label]}: {ratio:.3f})跑完这个脚本把比值小于阈值的结果单独存一个名单然后打开对应 JSON 快速复查。你会发现有些框确实标歪了有些则是目标本身被严重遮挡这种属于正常情况不需要处理。真正要修的是那些占了较大面积但贴合度极低的框它们往往是标成了叶片边缘或者树枝。用这个方法我可以在一两小时内把 580 张图的标注质量摸一遍底比从头到尾人工翻阅快得多。从那以后我每次拿到新数据集都会先跑一遍这个检查再决定要不要直接进训练流程。这个习惯帮我躲掉了很多“训练到一半才发现数据有问题”的尴尬局面希望帮到你。本文还有配套的精品资源点击获取