news 2026/10/27 0:36:20

Labelme柑橘数据集转YOLO格式与YOLOv8训练避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Labelme柑橘数据集转YOLO格式与YOLOv8训练避坑指南

简介:这套柑橘果目标检测数据集专为农业视觉与智能农机研究而构建,包含580张真实果园场景图片及对应Labelme标注文件,可支撑树上挂果与树下落果的区分识别,适用于果园自动化收获、成熟度监测、产量预估以及农业机器人视觉导航等任务。压缩包内共1159个文件,其中579张JPG图像与580个JSON标注一一对应,标注格式通用易扩展,整体大小125.72MB,便于下载与数据集管理。标注内容覆盖On-tree与Under-tree两类目标,树上样本25319个、树下样本17334个,能帮助模型学习不同光照、姿态和遮挡条件下的果实特征,为算法训练提供扎实的数据基础。该资源已有438人学习,适合需要高质量目标检测标注数据的研究人员与开发者直接使用。

1. 先看清这个数据集在逼模型学什么:On-tree与Under-tree不是同一类难题

做果园视觉的工程师,看到这个标题应该先想到一件事:这个数据集故意没把两类果分开存放,而是让 On-tree 和 Under-tree 的标注出现在同一份 labelme 文件里。这等于要求模型在同一套权重下,同时处理被叶子遮挡的树上柑橘和堆在地上的树下柑橘果,两者的尺度、背景和光照差异,比很多跨数据集任务还要大。580 张图的数量不算多,但覆盖面刚好是果园机器人最需要的两条工况:树上巡检计数、树下落果识别。它适合拿来验证 YOLO 系列在果实检测上的迁移效果,也适合做数据增强策略的对比实验;如果你只是想跑通一个通用目标检测流程,这个数据集的细节你暂时用不上,等真正上了果园线再回头看,会发现每一处标注都在对工况说话。

2. labelme标注的黑匣子:先把JSON拆开再决定转不转YOLO

2.1 labelme格式到底存了什么

目标检测数据集里,labelme 格式看起来并不直观:它默认不给你矩形框,而是存多边形坐标。对于检测任务,多边形似乎多余,但它给了你很多“后悔药”。如果后来发现矩形框不够用,多边形的点还能重新生成分割掩码、旋转框,或者做果实着色的统计。我拿到这类数据集的第一动作永远是解析 JSON,而不是立刻转成 YOLO 训练。

labelme 的 JSON 结构大致是这样的:

{ "version": "5.2.0.post1", "flags": {}, "shapes": [ { "label": "On-tree", "points": [[168, 245], [182, 247], [199, 251]], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "IMG_0001.jpg", "imageData": null }

shapes 是标注实体的核心,每个元素对应一个实例。label 是类别名,points 是多边形的绝对像素坐标,坐标原点在左上角,y 轴向下,和 OpenCV、PIL 的读取结果一致。shape_type 常见的有 polygon、rectangle、circle。circle 要特别小心,它的 points 通常只有两个点,一个圆心一个圆周点,直接用 min/max 取外接矩形会把半径当成半条弦,后面转换阶段我会单列这个坑。

还有一个容易忽略的字段是 imageData。很多 labelme 版本导出时它都是 null,表示图片没有内嵌到 JSON 里;少数版本会塞一长串 base64。解析时不要碰 imageData,直接按 imagePath 去读原图,否则几百张图就能把内存撑爆。

2.2 先统计分布:类别比、实例密度与图片分辨率

转换之前,我习惯先把标注当成数据审视一遍,而不是直接写转换脚本。三个问题必须回答:两个类别的实例数量是否均衡、单张图片上最多有多少个框、图片分辨率大致落在什么范围。下面这个脚本只用了标准库加 PIL,遍历 JSON 输出统计结果:

import json import os import glob from collections import Counter from PIL import Image json_list = sorted(glob.glob("annotations/*.json")) class_counter = Counter() density_list = [] for json_path in json_list: with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) labels = [shape["label"] for shape in data["shapes"]] class_counter.update(labels) density_list.append((os.path.basename(json_path), len(labels))) img_path = os.path.join("images", data["imagePath"]) if os.path.exists(img_path): w, h = Image.open(img_path).size # 这里可以按图片名记录宽高,用于决定训练时的 imgsz print("类别实例数:", dict(class_counter)) print("最密集的10张图:") for name, cnt in sorted(density_list, key=lambda x: -x[1])[:10]: print(f" {name}: {cnt}个实例")

这个脚本回答两件事。第一,如果 On-tree 的实例数远大于 Under-tree,训练时 loss 会被多数类主导,少数类 AP 往往被压制,这会影响后面增强策略的选择。第二,如果 580 张图集中在 640×640 附近,训练时 imgsz 就定 640;如果大量是 1280×720 或更高,我建议先统一缩放到 960 左右再训练,而不是让 YOLO 内部强行 resize,那样既浪费显存还丢小果子细节。

2.3 文件路径与图片命名是隐性风险

labelme 保存的 imagePath 是标注时的路径,可能是D:\data\IMG_001.jpg,也可能只是一个文件名。跨机器拷贝数据集后,路径分隔符和盘符会让 YOLO 找不到图片。常见做法是在转换脚本里强制用os.path.join(img_dir, os.path.basename(data["imagePath"])),只取文件名部分。另一个隐患是中文名或带空格的文件名。Linux 下能读,但 ultralytics 某些版本在 dataloader 里拼接路径时对空格敏感,训练会随机跳过部分图片。我的习惯是把图片全部重命名为纯英文,例如citrus_0001.jpg,再开始标注和转换,避免后期返工。

提示:如果 JSON 里 shapes 为空,也就是这张图没有任何标注,转换时不要生成 txt。YOLO 会把没有 txt 的图当背景样本处理。如果图片本身不含果实,最好直接把它从训练集删掉,否则相当于给模型增加一张“空目标”干扰图。

3. 把labelme JSON转成YOLO格式:转换脚本与四个边界坑

3.1 转换脚本:多边形外接矩形加归一化

统计完分布之后,就可以做转换。这个脚本把 labelme 的多边形标注转成 YOLO 训练用的 txt 格式,每行五个值:class_id x_center y_center width height,全部归一化到 0~1。核心思路是对每个多边形的点取 min/max 得到外接矩形,再用图片宽高做归一化。

import json import os import glob import argparse from PIL import Image TARGET_LABELS = ["On-tree", "Under-tree"] def clamp(v, lo=0.0, hi=1.0): return max(lo, min(v, hi)) def convert_one(json_path, img_dir, out_dir): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_path = os.path.join(img_dir, os.path.basename(data["imagePath"])) if not os.path.exists(img_path): print(f"[skip] 图片不存在: {img_path}") return w, h = Image.open(img_path).size lines = [] for shape in data["shapes"]: label = shape["label"] if label not in TARGET_LABELS: continue # 跳过枝干、叶片等辅助标注 pts = shape["points"] xs = [p[0] for p in pts] ys = [p[1] for p in pts] xmin, xmax = min(xs), max(xs) ymin, ymax = min(ys), max(ys) # 先裁到图片边界内,防止越界框导致 w/h 异常 xmin = max(0.0, min(xmin, w - 1)) xmax = max(0.0, min(xmax, w - 1)) ymin = max(0.0, min(ymin, h - 1)) ymax = max(0.0, min(ymax, h - 1)) x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h if box_w <= 0 or box_h <= 0: continue cls_id = TARGET_LABELS.index(label) lines.append(f"{cls_id} {clamp(x_center):.6f} " f"{clamp(y_center):.6f} " f"{clamp(box_w):.6f} {clamp(box_h):.6f}") if lines: name = os.path.splitext(os.path.basename(data["imagePath"]))[0] os.makedirs(out_dir, exist_ok=True) with open(os.path.join(out_dir, name + ".txt"), "w", encoding="utf-8") as f: f.write("\n".join(lines)) def main(): parser = argparse.ArgumentParser() parser.add_argument("--json_dir", required=True, help="labelme JSON 目录") parser.add_argument("--img_dir", required=True, help="原始图片目录") parser.add_argument("--out_dir", required=True, help="输出 txt 目录") args = parser.parse_args() os.makedirs(args.out_dir, exist_ok=True) for jf in sorted(glob.glob(os.path.join(args.json_dir, "*.json"))): convert_one(jf, args.img_dir, args.out_dir) if __name__ == "__main__": main()

用法很简单:

python labelme2yolo.py --json_dir ./annotations --img_dir ./images --out_dir ./labels

脚本里 TARGET_LABELS 的顺序决定了 txt 里 class_id,后面 YAML 的 names 顺序必须和它保持一致,这是整个流程里最容易翻车的一处。对 circle 类型的标注,points 取 min/max 得到的是过圆心的外接矩形,虽然框可能偏小,但检测任务可以接受;如果圆形的圆心和圆周点顺序不确定,稳妥做法是拆开解析:以 points[0] 为圆心,points[1] 到圆心的距离为半径,再重建外接矩形。

3.2 转换阶段四个高频坑

第一个坑是 imageData 内嵌 base64 导致转换脚本变慢。现象是 CPU 飙升、内存翻倍,单个 JSON 偶尔几十 MB。原因是部分 labelme 版本保存时把图片一起写进 JSON。解决方法是完全不读 imageData,而是用 imagePath 重新读原图,脚本里已经这么处理。如果原图缺失,直接跳过,而不是解码 base64 去恢复图像。

第二个坑是覆盖类别的权重顺序。现象是训练时不报错,但 val 的 mAP 接近 0。原因往往是 YAML 里 names 列表顺序和转换脚本里 TARGET_LABELS 不一致,比如把 Under-tree 放在了第 0 位,模型学到的类别名和真实标签错位。解决方法是转换时把映射关系写到 classes.txt,训练前打开看一眼。

第三个坑是坐标越界。现象是训练到中途 loss 变 NaN,或者推理框位置明显偏移。原因是多边形某些点超出了图片边界,比如标注时拖拽出画布。虽然代码里用 clamp 做了防御,但只对转换后的中心坐标生效;如果原 JSON 里 xmin 本身就大于 xmax,比如手滑把点顺序填反,还是会产生负宽度框。我见过这种畸形标注,所以脚本里加了box_w <= 0的过滤。

第四个坑是空标注图被错误删掉。现象是训练时某张图始终没有 loss 贡献,模型对这块区域完全无感。原因是在划分训练集时,有人会把“没有 label 的图”和“标注丢失的图”混为一谈。实际上,没有 shapes 的 JSON 意味着这张图可能是负样本,也可能是漏标。如果图中明明有果实但 JSON 里为空,应当重新补标,而不是直接丢进背景。转换后花十分钟抽查 10 张 txt 的坐标是否和原图对齐,比训练后发现漏检要省事得多。

4. 用YOLOv8训练自己的柑橘数据集:目录组织、YAML与三个必调参数

4.1 目录结构与分层划分

转换完成后,需要把图片和 txt 整理成 YOLO 标准目录结构,也就是 images 和 labels 下都分 train、val 两级。Ultralytics YOLOv8 找标签时,会把图片路径里的 images 替换成 labels,再找同名 txt;如果目录结构不对,训练不会报错,但模型会一路学到空标签,最后什么都检不出来。

citrus/ ├── images/ │ ├── train/ │ │ ├── citrus_0001.jpg │ │ └── ... │ └── val/ │ └── ... └── labels/ ├── train/ │ ├── citrus_0001.txt │ └── ... └── val/ └── ...

划分时不能简单随机抽样。580 张图里可能只有少量图片同时包含 On-tree 和 Under-tree 两个类别。如果 val 集恰好没有 Under-tree,验证时的 mAP 对少数类就完全没有参考意义。我一般用分层抽样,按图片包含的类别组合分成四类:只有 On-tree、只有 Under-tree、两类都有、两类都没有,然后从每个桶里按比例抽 val。这样才能保证 val 和 train 的类别分布大致一致。

分层划分脚本的关键部分:

import json import glob import random from collections import defaultdict def label_mask(json_path): with open(json_path, encoding="utf-8") as f: data = json.load(f) labels = set(s["label"] for s in data["shapes"]) return (int("On-tree" in labels), int("Under-tree" in labels)) jsons = sorted(glob.glob("annotations/*.json")) random.Random(42).shuffle(jsons) groups = defaultdict(list) for jf in jsons: groups[label_mask(jf)].append(jf) val_ratio = 0.25 for mask, items in groups.items(): n = max(1, int(len(items) * val_ratio)) print(mask, "val:", n, "train:", len(items) - n)

实际复制文件时,直接根据这个分组结果,把图片和对同名 txt 一起移动到对应目录。分层抽样会把少数组合强制放进 val,即使该组合只有一两张图。这个强制很重要,否则模型对某个类别的泛化能力永远测不出来。

4.2 YAML配置:names顺序就是训练分类顺序

训练前写一个 data YAML,Ultralytics 会按它加载路径和类别名。

path: citrus train: images/train val: images/val nc: 2 names: 0: On-tree 1: Under-tree

names 的顺序必须和转换脚本里 TARGET_LABELS 一致,这是整个训练配置里唯一不能靠试错解决的点。如果把 names 写成{0: “Under-tree”, 1: “On-tree”}而 txt 里 0 仍然代表 On-tree,模型损失不会报错,但推理结果会被当成另一类。我的惯例是转换脚本把索引到类名的映射写到 classes.txt,YAML 名直接从文件复制,不手敲。

4.3 训练命令与三个最该动手的参数

目录和 YAML 都准备好后,直接用 YOLOv8 开训:

yolo detect train \ data=citrus.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ device=0 \ patience=30 \ mosaic=0.5 \ close_mosaic=10

这里有几个参数值得专门讲一下。model 选 yolov8n,是因为 580 张图这种量级用小模型更稳;yolov8l 参数太多,在数据量不够时更容易过拟合,曲线好看但不泛化。imgsz 取决于 2.2 节的统计结果,如果原图大多在 640 左右,就用 640;若原图是 1280×1080,我建议先缩到 960 再训练,而不是直接把 imgsz 设成 1280,显存压力和训练速度都会失控。batch 按显存来,消费级 8G 卡用 16 稳妥,OOM 时优先降 imgsz 而不是降 batch,因为检测模型对分辨率更敏感。

mosaic 这个增强,在果实检测上是个双刃剑。遥感目标检测里小目标密集拼图常靠 mosaic 提升性能,但柑橘树的叶子背景本来就是高纹理,再拼四张图会让模型看到大量边界被裁断的果实,训练分布和真实果园差距更大。我建议对这类数据集把 mosaic 降到 0.5 甚至 0,同时保留 hsv 色彩扰动和 scale 扰动,因为光照变化才是果园里最真实的干扰。如果全程开满 mosaic,训练 loss 降得很快,但 val mAP 反而会落后。

训练结束后,结果落在runs/detect/train下。重点看两件事:第一,results.png里 train/box_loss 和 val/box_loss 有没有在最后一二十轮分叉,分叉就是过拟合;第二,weights/best.pt是否比last.pt早出现很多,如果 early stop 在 40 轮以内触发,说明学习率偏大,可以把 lr0 从默认 0.01 降到 0.005 重跑。

5. 柑橘数据集的避坑排查:从标签校验到过拟合

5.1 标签与JSON数量对不上,训练却“正常”

现象:训练 loss 曲线很平滑,推理时大部分图片也出框了,但某些图片一个框都不出。去runs/detect/里翻标签,发现对应路径的 txt 行数明显少于源 JSON 的 shapes 数量。

原因:最常见的是类别名没匹配上,比如 JSON 里写的是On-tree,转换脚本里写的是on_tree,差一个分隔符就被过滤掉了;其次是图片文件名带下划线或中文,处理时被脚本跳过。

解决:训练前跑一次校验,按图片名逐一比较 JSON shapes 数量和 txt 行数,不相等就打印出来。这一步 5 分钟能写完,能省掉后面一整天的排查。

5.2 增强开满反而让mAP下降

现象:训练时加了 mosaic、rotation、scale,epochs 拉到 200,train loss 很低,val mAP 只有默认增强的一半。

原因:柑橘果实本身接近圆形,旋转增强不会改变它的形态,但会改变叶子阴影方向和果实之间的相对遮挡关系。模型学到的是“叶子纹理朝这个方向,阴影落在左边”,而不是“红色圆形物体是果实”,在真实果园里自然失效。

解决:把 degrees 降到 0 或 5,mosaic 降到 0.5。保留 hsv 色彩扰动,因为不同果园的叶色和果色差异才是真正需要在训练中模拟的变化。

5.3 实例不均衡导致Under-tree 被压制

现象:On-tree 的 mAP50 有 0.85,Under-tree 只有 0.6,且召回率偏低。

原因:图片数量相近,不代表实例数量相近。On-tree 一张图可能标 8 个果,Under-tree 一张图只有 1 到 2 个落果。检测模型的 loss 默认按实例数量加权,训练时多数类梯度占比大,少数类被压制。

解决:先按第 2 章脚本统计实例数,如果两类的实例 count 比例超过 3:1,就把少数类图片在原数据集里复制两到三份再做划分。这种过采样虽然会带来重复图片,但对 580 张图的小数据集来说,效果往往比换模型更立竿见影。

5.4 坐标归一化或class id从1开始导致NaN

现象:训练第一轮 loss 直接变成 nan,或者 loss 正常但推理框全部缩在图片左上角。

原因:转换时输出了负的 width 或 height,或者 class_id 从 1 而不是 0 开始写。YOLO txt 格式要求 class_id 是 0 开始的整数,y_center、h 也都必须为正数。

解决:转换后写一段防御性校验脚本,逐行读 txt,检查五个值的取值范围:

import glob for txt in glob.glob("labels/*.txt"): for line_no, line in enumerate(open(txt), 1): parts = line.strip().split() if len(parts) != 5: print(f"{txt}:{line_no} 字段数不对") continue cls, xc, yc, w, h = map(float, parts) if not (0 <= xc <= 1 and 0 <= yc <= 1): print(f"{txt}:{line_no} 中心点越界") if not (0 < w <= 1 and 0 < h <= 1): print(f"{txt}:{line_no} 宽高异常")

这个脚本要在训练前跑,而不是训练后。loss 变 nan 时再去翻 txt,往往要花半天。

5.5 验证集太小导致mAP像心电图

现象:训练 loss 曲线正常,val mAP 每轮都在 0.5 到 0.9 之间剧烈跳动,看不出收敛趋势。

原因:580 张图里抽 20% 作为 val,大概只有 116 张。如果这些图片里有一批特别难的 On-tree,某一轮验证刚好抽到(其实是固定集,但模型在早停后权重变化很大,导致波动),曲线就会大幅抖动。

解决:val 比例提高到 30%,同时保证每个类别在 val 里至少有 20 个以上实例。如果还抖,就直接做 5 折交叉验证,每折训练一次取平均。500 张图量级完全跑得起 5 折,一颗 GPU 一晚上能出 5 个模型,比单次 80/20 划分可靠得多。

6. 进阶验证:用per-class AP量化On-tree与Under-tree的检测差异

6.1 先看per-class mAP,而不是总mAP

训练完用验证命令看每类指标:

yolo detect val \ data=citrus.yaml \ model=runs/detect/train/weights/best.pt

终端会输出每个类别的 Instances、BoxP、BoxR、mAP50 和 mAP50-95。这里不要盯着总 mAP,直接把 On-tree 和 Under-tree 两行单拉出来对比。如果实例数接近但 AP 差很多,说明问题不在数据量,而在目标本身的视觉复杂度:On-tree 被叶子遮挡、密集重叠,Under-tree 前景背景分离更干净。如果 AP 差和实例数差成正比,优先补少数类数据,比如去果园补拍 100 张落果特写,效果肯定比换大模型明显。

6.2 针对性微调:关mosaic、加TTA、补难例

针对 On-tree 的漏检,我通常先做两步。第一步,把mosaic=0重训一版,确认 AP 变化;第二步,用带 TTA 的推理看密集遮挡场景是否改善:

yolo predict \ model=runs/detect/train/weights/best.pt \ source=test_on_tree/ \ augment=True \ nms_iou=0.5

augment=True 会让模型对同一张图做多尺度推理,对密集果实的召回率确实有帮助,但会把推理速度拉低 2 到 3 倍,部署时要做取舍。然后打开 val 输出目录里的val_batch*.jpg,把漏检图挑出来看一眼共同点。如果是果实被树叶遮挡超过一半,就到了数据层面,把漏检区域裁出来补标 50 张加入训练集,比换 yolov8l 更省钱。

我做这个数据集时最深刻的教训是:第一次转换只图快,没保留 classes.txt,第二次训练前手改了 YAML 里的 names 顺序,mAP 直接掉到 0.1,排查了大半天才发现是 0 和 1 两类的映射反了。后来我把“转换脚本输出 classes.txt,训练 YAML 直接引用它”写成固定流程,再没出过这类问题。五百张图的农业检测数据集,价值从来不在把流程跑通,而在你能把每一颗红色果实和背景的边界啃到什么程度。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 0:31:06

多语言语音模型的语言判别嵌入(LDE)架构设计

1. 项目概述&#xff1a;让语音模型真正“听懂”语言边界&#xff0c;而不是靠猜“语言判别增强多语言语音模型的语言学习能力”——这个标题乍看像论文摘要&#xff0c;但拆开来看&#xff0c;它直指当前多语言语音识别&#xff08;ASR&#xff09;和语音合成&#xff08;TTS&…

作者头像 李华
网站建设 2026/10/27 0:21:22

影刀RPA进阶教程:商品标题优化助手——竞品标题词频分析自动化

影刀RPA进阶教程&#xff1a;商品标题优化助手——竞品标题词频分析自动化 改标题全靠感觉&#xff0c;是大部分中小商家标题优化的现状。竞品前二十名的标题里"免烫"“冰丝”"阔腿"这些词到底出现了多少次&#xff0c;哪个词是流量密码哪个词纯属凑字数&a…

作者头像 李华
网站建设 2026/10/27 0:00:16

AI赋能知识产权供需匹配:专利评估、需求挖掘与多路召回工程实践

简介&#xff1a;《如何构建并实践AI赋能的精准知识产权运营供需匹配平台&#xff1f;》是一份面向政府科技管理部门、高校科研转化机构、技术转移服务商及企业创新负责人的专题文档&#xff0c;聚焦科技成果转化中信息不对称、资源分散、供需对接不畅等问题。压缩包共1个docx文…

作者头像 李华
网站建设 2026/10/26 23:58:27

BLDC电机PI控制仿真调试:从建模到波形收敛的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 23:56:57

AMD-Xilinx FPGA功耗优化实战:从RTL到布局布线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/26 23:54:16

pstack-claude 工程实践:Claude 工具链安装配置与多模型接入指南

1. 从"pstack-claude"这个名字说起&#xff1a;它到底想解决什么问题第一次看到pstack-claude这个项目名&#xff0c;很多人会愣一下——pstack 是什么&#xff1f;和 Claude 又是什么关系&#xff1f;我当初也是带着这个疑问去翻资料、跑代码的。先把结论摆在前面&a…

作者头像 李华