ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

柑橘果目标检测实战:labelme数据集转YOLO训练与避坑指南

2026/10/2 2:44:33 拓冰建站 浏览量
柑橘果目标检测实战:labelme数据集转YOLO训练与避坑指南 简介面向智慧农业与计算机视觉研究的柑橘果目标检测数据集包含579张真实果园场景JPG图片与580个Labelme JSON标注文件资源包共1159个文件、约125.72MB。标注目标分为树上柑橘On-tree与树下柑橘Under-tree两类其中树上标签25319个、树下标签17334个合计42653个边界框实例覆盖健康挂果和已掉落或采摘后放置两种状态可为果园自动化收获系统、成熟度与掉落率监测、视觉产量预估及农业机器人导航等任务提供真实训练数据。所有文件按“同名JPGJSON”方式成对存放JSON记录每个目标的类别与坐标信息方便直接读取或载入主流目标检测框架完成模型训练与评估也能为自建数据集的标注规范提供参考。目前已有437人学习浏览适合具备一定目标检测基础、从事智慧农业算法开发的科研人员和算法工程师使用。1. 拿到580张柑橘果标注图先别急着训练做目标检测的朋友看到“数据集-柑橘果目标检测数据集-labelme,580张目标检测图片,包含树上柑橘On-tree和树下柑橘果Under-tree的标注”这个标题第一反应多半是“图不多但类别分得细”。确实580张图在目标检测领域只能算小规模但它把目标拆成了On-tree和Under-tree两类这个划分本身就是农业采摘机器人视觉方案里最关键的决策点。这个数据集能解决什么问题直接说结论它适合做采摘场景下的目标检测方案验证也适合用来训练一个区分树上果和落地果的检测模型。对于做农业机器人、果园产量估算、采摘机械臂视觉系统的工程师来说这类带语义位置信息的数据集比单纯标“柑橘”要值钱得多。对新手来说这580张图配合labelme标注格式刚好能走通一条“标注→转换→训练→验证”的完整链路。需要先说明白标题里没有给训练结果、没有给精度指标、也没有给数据采集场景的详细描述。所以这篇文章不打算假装见过数据集内部文件而是按这类数据最常见的落地方式把标注格式怎么读、怎么转YOLO、训练时两类样本怎么处理、哪些环节最容易翻车全部拆开讲清楚。2. 树上柑橘和树下柑橘果两类目标为什么值得分开标注2.1 两类果实在视觉特征上的本质差异On-tree柑橘长在树上背景通常是枝叶、树干和天空。果实被叶片遮挡是常态光线从树冠缝隙穿过会有大量局部过曝和阴影。果实的颜色从青绿到橙黄都有而且很多果实只露出半个甚至三分之一标注框的边界天然就带有模糊性。Under-tree柑橘是落在地上的果子背景是泥土、杂草、落叶。这类果实的遮挡主要来自草叶覆盖和果实堆叠。落果经常是成片出现的几个果子挤在一起标注框之间的IoU很高如果标注时一个框框住了多个果实训练出来的模型就会把“多个果实粘连”当成一个目标。这两类目标在检测器看来是完全不同的任务。On-tree考验的是模型在复杂背景下的局部特征提取能力Under-tree考验的是分离密集目标的能力。把它们合并成一个大类“柑橘”当然也能训练但模型在推理时无法区分“树上的果”和“地上的果”下游的采摘机械臂就没法决定该执行“采摘”还是“捡拾”动作。2.2 标注类别设计什么情况下建议合并什么情况下必须分开常见的做法是把On-tree和Under-tree做成两个类别而不是合并成一个。因为采摘机器人的执行逻辑完全不同树上的要用机械臂加吸盘或者剪切机构地上的要用收集机构或者吸果装置。有一种情况可以合并你只是做产量预估不关心果子在哪里。那用单一类别标注会得到更高的标注速度580张图一个人一天半就能标注完。但如果做的是采摘决策合并类别等于把决策信息丢掉了。这里有个折中办法保留两类但允许模型输出时做后处理合并。也就是说训练时用两类推理时如果不需要区分就把两个类别的检测框合并到一个列表里用。我一般会建议保留两类。原因有两个一是在标注阶段标注员分两类并不比标一个类多花多少时间因为视觉差异太明显了二是一旦后续业务需要区分采摘和捡拾你再回去补标成本比现在高很多。2.3 580张图片的规模评估够不够训练一个能用的模型580张图如果两类目标每张平均有10个那就是接近6000个标注实例。对YOLOv8或者YOLOv5这类模型来说这个量级只能算“能跑通流程精度看运气”。深色的背景不可怕可怕的是背景纹理跟果实纹理相近。果园里最常见的情况是枯叶的颜色和落地柑橘的颜色在暗光条件下非常接近模型几乎无法区分。下面这段代码处理多边形的第一步——读取labelme JSON并过滤空标注import json import glob labelme_files glob.glob(path/to/labelme/*.json) print(f共找到 {len(labelme_files)} 个 labelme 文件) valid_count 0 for file in labelme_files: with open(file, r, encodingutf-8) as f: data json.load(f) shapes data.get(shapes, []) if len(shapes) 0: print(f警告: {file} 中没有标注跳过) continue valid_count 1 print(f{file}: {len(shapes)} 个标注类别: {set(s[label] for s in shapes)})这段代码做的事情很简单扫描目录下所有labelme标注文件统计每个文件的有效标注数并打印类别分布。逻辑上注意两点一是labelme JSON的编码可能是UTF-8打开文件时指定encodingutf-8否则Windows下会报UnicodeDecodeError二是shapes字段是list[dict]每个dict包含label、points、shape_type三个关键键points是一个由[x, y]坐标对组成的列表。3.2 多边形标注的核心操作闭合、密集度和边界点用labelme给柑橘果标注时我建议默认用多边形不要用矩形。柑橘是圆形果实但被树叶遮挡后实际轮廓是各种不规则形状。矩形框会把大量背景包进去特别是On-tree场景框内可能有一半是叶子模型学习时会被误导。标注操作规范是这样的# 多边形标注沿着果实可见边缘打点 # 密集度要求 # - 轮廓平滑的果实6~10个点即可 # - 被遮挡严重的果实沿可见边缘打点遮挡处直接用直线连接 # - 单果标注禁止一个框包含多个果实密集度是有讲究的。点太少轮廓形状失真标注框算出来的中心点偏移点太多标注效率极低而且相邻点之间产生的锯齿会被模型当成特征。常见做法小尺寸果实30×30像素以内用4~6个点大尺寸果实用8~12个点。这种密集度在转换YOLO格式时包围盒的误差可以控制在2个像素以内。这里说一个必须养成的好习惯标注完成后逐张点击labelme界面左侧的“Edit”模式检查多边形是否闭合、有没有错误的多余点。步骤在labelme主界面点击“Edit Polygons”然后拖拽每个多边形的顶点删除明显偏离果实边缘的点。3.3 从JSON到YOLO格式转换脚本和边界计算逻辑labelme的JSON格式不能直接喂给YOLO训练必须转换成YOLO需要的txt格式每行class_id x_center y_center width height归一化到0~1的浮点数。转换脚本的编写逻辑是从shapes里取第一个点的坐标作为顶点依次计算多边形的外接矩形然后做归一化。完整转换脚本import json import os import glob def convert_labelme_to_yolo(json_path, output_dir, class_mapping, img_width, img_height): with open(json_path, r, encodingutf-8) as f: data json.load(f) shapes data.get(shapes, []) if not shapes: return None txt_filename os.path.basename(json_path).replace(.json, .txt) txt_path os.path.join(output_dir, txt_filename) lines [] for shape in shapes: label shape[label] if label not in class_mapping: print(f未知类别 {label}跳过) continue class_id class_mapping[label] points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x_min min(xs) x_max max(xs) y_min min(ys) y_max max(ys) x_center (x_min x_max) / 2.0 y_center (y_min y_max) / 2.0 width x_max - x_min height y_max - y_min x_center_norm x_center / img_width y_center_norm y_center / img_height width_norm width / img_width height_norm height / img_height lines.append(f{class_id} {x_center_norm:.6f} {y_center_norm:.6f} {width_norm:.6f} {height_norm:.6f}) if lines: with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) return txt_path return None img_width, img_height 1920, 1080 class_mapping {On-tree: 0, Under-tree: 1} json_files glob.glob(path/to/labelme/*.json) for json_file in json_files: txt_path convert_labelme_to_yolo(json_file, yolo_labels, class_mapping, img_width, img_height) if txt_path: print(f转换成功: {txt_path})这段代码的注意事项坐标归一化必须用原始图片的宽高不能用一个统一的固定值。如果数据集里图片尺寸不统一每个JSON文件里没有记录图片尺寸转换前需要单独读取图片获取宽高否则框的位置会整体偏移训练时损失函数计算的IoU全部偏大模型收敛曲线看起来正常实际推理结果却一塌糊涂。这是一个非常隐蔽的坑检测到图片尺寸不一致时应该单独读取图片信息# 不要这样 - 所有图片用同一个尺寸会导致标注偏移 # img_width, img_height 1920, 1080 # 这样做 - 每张图片独立获取尺寸 from PIL import Image image_path os.path.splitext(json_path)[0] .jpg with Image.open(image_path) as img: img_width, img_height img.size4. 把580张标注图组织成YOLOv8训练集目录结构与参数设置4.1 数据集目录组织标准结构YOLOv8要求的数据集结构相对宽容但有一个公认的规范结构。把图片和标签放在同一个目录下YOLO会自动匹配同名的txt文件。一个标准的目录组织方式是citrus_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml └── README.mddata.yaml内容path: /absolute/path/to/citrus_dataset train: images/train val: images/val test: images/test names: 0: On-tree 1: Under-tree这里有个细节path字段建议写绝对路径。相对路径在你自己电脑上能跑换机器后很可能报错Dataset not found。更稳妥的做法是在训练命令中直接覆盖data参数或者在YAML里写成与训练脚本的相对位置。我习惯在YAML里写相对路径train: ../citrus_dataset/images/train这样可以避免部署路径变化导致的数据集读取失败。4.2 样本划分策略580张图怎么分才能让验证集有参考意义580张图片的分割比例通用做法是80%训练、20%验证。对580张来说训练集464张、验证集116张。如果图里每张平均有10个目标训练集有4600个目标勉强够用。划分时注意先按图片划分再按目标数检查。因为同一张图里的目标高度相关如果按目标划分训练集和验证集会各拿到同一张图的一部分验证结果看起来漂亮实际上模型没见过这张图但见过高度相似的目标评测结果虚高。正确做法是把整张图所有目标都放到同一个集合里。划分脚本import random import os import shutil random.seed(42) dataset_root citrus_dataset image_files [f for f in os.listdir(images/all) if f.endswith(.jpg)] random.shuffle(image_files) train_ratio 0.8 train_count int(len(image_files) * train_ratio) train_files image_files[:train_count] val_files image_files[train_count:] for file in train_files: shutil.copy(fimages/all/{file}, fimages/train/{file}) lbl file.replace(.jpg, .txt) if os.path.exists(flabels/all/{lbl}): shutil.copy(flabels/all/{lbl}, flabels/train/{lbl}) for file in val_files: shutil.copy(fimages/all/{file}, fimages/val/{file}) lbl file.replace(.jpg, .txt) if os.path.exists(flabels/all/{lbl}): shutil.copy(flabels/all/{lbl}, flabels/val/{lbl})random.seed(42)很重要。没有固定随机种子每次划分结果不同后续做实验对比时就说不清精度差异是模型改进带来的还是数据划分变化带来的。固定种子后同一份数据在任何机器上划分结果一致。4.3 YOLOv8训练参数设置针对小数据集的五个关键参数580张图训练YOLOv8参数默认值是针对大规模数据集调的直接跑的效果远低于可接受水平。需要手动调整的参数如下epochs: 300 batch: 16 imgsz: 640 patience: 50 cache: Trueepochs300是为了让模型充分收敛。小数据集训练时200~300轮是常见区间少于200轮模型欠拟合明显多于500轮开始过拟合如果开了patience50验证集连续50轮不提升就提前停止实际轮数会自动控制在400以内。batch16取决于GPU显存8G显存跑YOLOv8s这个值安全12G以上可以到32。cacheTrue把图像加载到内存里580张图约2~3GB内存够建议开启能显著提升训练速度。训练命令yolo detect train \ datacitrus_dataset/data.yaml \ modelyolov8s.pt \ epochs300 \ batch16 \ imgsz640 \ patience50 \ cacheTrue \ projectcitrus_yolo \ namerun1训练结束后检查runs/segment/train/目录下的results.csv和confusion_matrix.png。results.csv里看val_box_mAP50-95这个指标如果从0.1涨到0.4以上再进入平台期说明标注质量够用如果整个训练过程指标都在0.1附近震荡优先怀疑标注框有坐标错位或者类别标签标反了。4.4 推理时类别合并的后处理逻辑训练完成后如果实际使用场景不需要区分On-tree和Under-tree可以在后处理时把两类的检测框合并from ultralytics import YOLO model YOLO(citrus_yolo/run1/weights/best.pt) results model.predict(test.jpg, conf0.25) all_boxes [] for result in results: boxes result.boxes for box in boxes: x1, y1, x2, y2 box.xyxy[0].tolist() cls_id int(box.cls[0].item()) conf float(box.conf[0].item()) all_boxes.append([x1, y1, x2, y2, conf])这种做法保留了训练时两类分开的信息推理时按需合并灵活性最好。如果你直接把模型输出的两个类合并得到的效果等同于只训练一个类但训练时的精度比单类还高因为模型内部的分类分支把两类硬生生分开bbox回归分支则各自只处理一类样本回归压力更小。5. 柑橘果数据集避坑清单5条血泪经验5.1 现象读取labelme生成的JSON时报编码错误现象用open(json_path)打开文件后执行json.load报UnicodeDecodeError提示gbk codec cant decode byte。这在Windows系统上特别常见因为labelme默认以UTF-8写入JSON文件而Windows的Python默认以GBK读取。原因labelme写入文件时使用的是UTF-8编码但Windows系统区域设置是中文时open()函数的默认编码是GBK导致解码失败。解决所有读取JSON文件的操作显式指定编码with open(json_path, r, encodingutf-8) as f: data json.load(f)5.2 现象转换后的YOLO标注文件里出现了负数或者大于1的坐标值现象在YOLO训练时日志里持续输出WARNING: Invalid box coordinates或者训练完了做可视化发现标注框超出图片边界有的框中心点不在图内。原因labelme标注时如果标注框的某个顶点超出了图片边缘鼠标拖到边界外points里的坐标就会出现负值或者超过图片宽高的值。转换脚本直接做归一化没有得到正常范围。解决在转换脚本里加一个坐标裁剪逻辑x_min max(0, min(xs)) y_min max(0, min(ys)) x_max min(img_width, max(xs)) y_max min(img_height, max(ys))注意这个操作不是简单的“修数据”而是“裁剪标注框”。如果橙色果实的框超出边界2%被裁掉的2%正好是果实最亮的部分会对模型学到完整特征有一定影响。所以我的建议是裁剪后检查这张图的标准差如果标注框被裁剪超过5%或者裁掉的部分包含果实边缘特征就应该重新标注这张图而不是直接转换。5.3 现象标注的类别名里带空格或特殊符号现象标注时图省事把类别写成orange on tree或者under_tree转换脚本里类别映射表写的是On-tree训练时不断报class not found错误或者模型把自己的训练标签全部当成一个未知类。原因类别名不一致同一个数据集的标注里出现了多种写法。labelme不限制类别命名但YOLO要求标注文件里的类别ID是一个从0开始的整数类别名只存在于映射表里。解决数据集的标注名单要提前定义好并且在标注中途不要改。如果已经标了一半发现命名不一致写个脚本做一次批量替换把所有的orange on tree统一成On-tree把under_tree统一成Under-tree。5.4 现象580张图训练出来的模型验证集mAP很高实地测试一塌糊涂现象在验证集上mAP达到0.75看着很棒。但到了真实果园模型不是漏检就是误检尤其On-tree的果树在强烈阳光下几乎全部漏掉。原因验证集的116张图和训练集的464张图可能来自同一天同一个果园同一个时间段光照条件、果实成熟度、背景类型高度相似模型把背景特征也当成判别特征了。迁移到新果园背景变化模型直接失效。解决这是最小数据集最典型的“假精度”问题。有两个缓解手段第一训练前用albumentations做强数据增强特别是RandomBrightnessContrast和HueSaturationValue这能模拟不同光照条件下的果园实景第二模型推理时把置信度阈值调低到0.15左右先保证召回率再靠后续的tracking或temporal consistency去过滤误检。如果做好了增强和阈值调节实测还是有明显偏差就需要补采集其他果园、其他天气的数据没有别的办法。5.5 现象Labelme装了但打不开报PyQt5相关错误现象pip install labelme成功但命令行输入labelme提示类似ModuleNotFoundError: No module named PyQt5.sip或者启动后黑窗口直接闪退。原因labelme依赖PyQt5而PyQt5的部分子模块PyQt5.sip在部分Python版本下不能直接导入。很多情况下是因为conda环境中numpy或sip版本不兼容。解决常见做法是先用pip安装PyQt5再装labelmepip install pyqt5 pyqt5-tools pyqt5-sip pip install labelme如果还是不行用conda安装conda install -c conda-forge labelmeconda-forge会解析依赖我试过的最省事的方法。还有一个备选是使用labelme的Web版本但这个涉及部署复杂度一般本机装不上才考虑。6. 一个验证技巧用混淆矩阵揪出模型分不清哪类果训练完成后不要只盯着mAP。对柑橘果数据集这种二分类场景混淆矩阵是更好的诊断工具。YOLOv8训练目录里自带confusion_matrix.png但它是所有类别的汇总看不出单类别的细节。我习惯另外做一次独立的验证评估from ultralytics import YOLO model YOLO(citrus_yolo/run1/weights/best.pt) results model.val(datacitrus_dataset/data.yaml, splitval, conf0.25) # 提取每个gt框的预测结果 for r in results: if r.boxes is None: continue cls r.boxes.cls.cpu().numpy() conf r.boxes.conf.cpu().numpy() for c, cf in zip(cls, conf): class_name model.names[int(c)] if cf 0.3: print(f低置信度检测: {class_name}, conf{cf:.2f})这个脚本的用法把置信度阈值从默认的0.25改成0.3、0.35、0.4分别跑一遍看每个类别的召回率怎么变化。如果On-tree在置信度0.3时召回率只有0.6说明这类果树的特征区分度不够需要回到标注阶段检查是不是On-tree的标注框把太多树叶包进去了导致模型学到的特征本身就含有大量背景信息。另一个技巧是对出错样本做可视化。把预测结果画到原图上用不同颜色区分On-tree和Under-tree的框同时标注置信度。这一步能快速发现两类标注错误一种是两棵树上的果子被标成了同一个框另一种是落地的柑橘被标成了On-tree。这类标注错误在转换脚本阶段无法检测——坐标是合法的类别映射也存在但语义是错的。建议逐张看一下可视化输出重点看置信度低于0.3的检测框找规律。做自动驾驶感知那会儿我的习惯是每次训练完不看mAP先看坏case这个习惯后来也带到了农业视觉项目上。虽然580张图的柑橘数据看起来简单但果园的光照、遮挡和果子密集程度对检测模型的挑战完全不亚于城市街道场景。但是标准的做法是把它作为起点用这套数据跑通流程找到标注规范和训练参数等需要部署到真实果园时再按同样的流程去扩充数据训练迭代。希望这个方向帮到正好在跟农业视觉数据较劲的你。本文还有配套的精品资源点击获取