
简介一份面向草莓成熟度检测任务的YOLO格式数据集包含训练集、验证集、测试集以及对应标签和class文件。数据按照YOLOv5文件夹规范保存标签采用相对坐标格式依次为类别、中心点横坐标、中心点纵坐标、宽度和高度共两个类别分别对应成熟与未成熟草莓可无缝接入主流YOLO检测流程。资源包共1063个文件其中530张jpg图片、531个txt标签另附1个Python脚本和1张类别示意png总大小约22.96MB。训练集约400张验证集约100张测试集约50张图片多来自田间草莓植株包含成熟与未成熟果实同框、花朵、枝叶等多样场景便于练习数据划分、模型训练、参数调优及成熟度识别效果评估。当前已有259人学习下载适合入门YOLO检测的开发者快速获得规范数据集也方便需要二次标注或扩展类别的研究者直接复用。1. 草莓成熟度检测为什么绕不开这一步先把数据集“体检”再做训练很多人第一次拿到草莓成熟度检测数据集时习惯性先设好训练参数再开工结果不是标签和 class 文件对不上就是验证集 mAP 高得离谱、一实拍就翻车。问题往往不在模型而在数据集本身训练集、验证集、对应标签、class 文件这四样东西没有校准就喂给了 YOLO。这篇文章会用一套包含训练集和验证集的草莓成熟度数据集做底稿从目录体检、标签解析、可视化校验讲到训练参数和五个高频踩坑最后给出一套让成熟度检测从“能跑”变得“能用”的置信度分级策略。2. 数据集解剖训练集、验证集、标签与 class 文件在配合什么2.1 一个成熟 YOLO 数据集的目录布局与命名约定拿到手的数据集如果按 YOLO 惯例整理通常是同一套图片同时拆成 train 和 val 两个互不相交的子集图片放在 images 下标签放在 labels 下图片名与标签名一一对应。常见的目录布局是这样strawberry_dataset/ ├── classes.txt # 类别清单一行一个类名 ├── images/ │ ├── train/ │ │ ├── 0001.jpg │ │ ├── 0002.jpg │ │ └── ... │ └── val/ │ ├── 1001.jpg │ └── ... └── labels/ ├── train/ │ ├── 0001.txt │ ├── 0002.txt │ └── ... └── val/ ├── 1001.txt └── ...如果你打开后看到的是这种结构说明数据集已经按 YOLO 的读取要求整理好了。里面没有单独的 test 目录因为 YOLO 训练时只需要 train 和 valtest 一般留给你自己做最终评测很多公开数据集并不会额外提供。另一种常见变体是直接用一份data.yaml代替classes.txt里面写train、val两个路径和names列表。这种情况更接近原生 YOLO 的接入方式。建议你把data.yaml当作文档来读把路径改成本机绝对路径后直接使用。2.2 标签 txt 里那五行数字类别、中心点、宽高与归一化YOLO 系列沿用同一个标签格式每行表示一个目标框共五个数字。草莓成熟度检测的标签文件不长这样0 0.482031 0.523438 0.156250 0.210938 2 0.714844 0.386719 0.121094 0.183594 1 0.310938 0.601562 0.136719 0.183594按顺序解释这五个数字见下表字段含义取值范围说明第一列类别编号0 到类别数-1对应 class 文件中的行号从 0 开始第二列框中心点 x 坐标0 到 1除以图片宽度归一化第三列框中心点 y 坐标0 到 1除以图片高度归一化第四列框宽度0 到 1除以图片宽度归一化第五列框高度0 到 1除以图片高度归一化这套归一化格式是 YOLO 训练时的硬性要求。如果你发现某个 txt 里出现大于 1 的坐标值基本可以判定它要么没归一化要么是从别的检测格式直接改了后缀名需要转格式而不是硬训。我处理这类数据集时第一步永远是随机挑几行算算坐标回乘图片宽高确认落在合理位置。2.3 为什么必须让 train/val 严格互斥从过拟合角度理解验证集训练集和验证集不能有交集这是检测任务里最容易被忽视却影响最大的一条。如果同一棵草莓、同一个拍摄机位的图片同时出现在两份清单里模型在训练时相当于见过验证集的答案验证时 mAP 会虚高等到了农田或大棚里换上新的拍摄角度检测效果立刻打回原形。要判断一个数据集是否泄漏可以比较 train 与 val 的图片文件名。如果两边出现大量文件名相同或仅大小写不同的图片就需要重做划分。常见做法是按“果实所在的拍摄场景”分组而不是按图片个体随机划分同一个场景的所有图片一起进入 train 或一起进入 val。对成熟度这种受光照影响很大的任务这点尤其重要因为同一株草莓在早中晚拍出来的颜色差异会直接改变标签难度。3. 本地跑通这个数据集目录校验、标签解析与可视化3.1 第一步用脚本校验目录结构与类别号开始训练前先把目录结构和图片与标签的对应关系跑一遍。我习惯写一个小脚本检查每个 split 下图片和标签数量是否一致、有没有缺标签的孤儿图片# 校验目录结构和图片-标签对应关系 import os from pathlib import Path data_root Path(strawberry_dataset) for split in [train, val]: img_dir data_root / images / split lbl_dir data_root / labels / split img_files list(img_dir.glob(*.jpg)) list(img_dir.glob(*.png)) lbl_files list(lbl_dir.glob(*.txt)) img_names {p.stem for p in img_files} lbl_names {p.stem for p in lbl_files} missing_lbl img_names - lbl_names missing_img lbl_names - img_names print(f[{split}] 图片数: {len(img_files)}, 标签数: {len(lbl_files)}) print(f 缺标签的图片: {len(missing_lbl)}) print(f 无图片的标签: {len(missing_img)}) if missing_lbl: print( 示例:, list(missing_lbl)[:3])这里glob(*.jpg)只匹配了 jpg 和 png 两种常见格式如果你的数据集里含有 bmp、jpeg 等扩展名需要把它一并加进去。文件名是否带前缀不与目录挂钩判断依据是.stem属性它只取文件名去掉后缀的部分不关心图片所在子目录。3.2 第二步把标签画到图上肉眼检查标注质量目录校验只能证明文件有对应关系不能证明标注内容正确。我要把 txt 里的归一化坐标还原成像素坐标画到原图上人工看一眼。这一步能筛掉坐标系混乱的标签、框错位置的果实、类别标反的样本# 把 YOLO 标签画回图片检查标注是否贴合果实 import cv2 img_path strawberry_dataset/images/train/0001.jpg lbl_path strawberry_dataset/labels/train/0001.txt img cv2.imread(img_path) h, w img.shape[:2] # 成熟度类别 - 颜色三分类示例 colors {0: (0, 255, 0), 1: (0, 200, 255), 2: (0, 0, 255)} with open(lbl_path) as f: for line in f: cls, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) color colors.get(int(cls), (255, 255, 255)) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(int(cls)), (x1, max(0, y1 - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(check_0001.jpg, img) print(已输出 check_0001.jpg请人工核对框与果实的位置关系)坐标回算的过程就是把归一化中心点和宽高分别乘以图片宽高。需要注意cv2.rectangle的坐标是左上角和右下角所以要先由中心点算出 x1、y1再算 x2、y2。如果一帧图里有多个目标脚本里循环读行的方式天然支持逐行处理不必写额外逻辑。这一步能提前暴露一种常见病有人用 OpenCV 保存图片时把 BGR 通道搞乱导致可视化颜色完全错位。所以建议不要只看一两张每类成熟度状态各抽 5 张快速过一遍。3.3 第三步确认 class 文件与标签类别号一一对应class 文件里每一行的顺序就是类别编号的顺序行号从 0 开始。如果 class 文件写的是三行类别名而某个标签文件里出现类别号 3那就意味着越界。更隐蔽的问题是类别顺序和你的预期不一致比如原数据集按“半熟、成熟、未熟”排序而你写 yaml 时按“未熟、半熟、成熟”排序训练出来的输出顺序就全乱了。# 打印 class 文件内容并统计标签中出现过的类别编号 from pathlib import Path from collections import Counter class_file Path(strawberry_dataset/classes.txt) print(class 文件映射:) for i, name in enumerate(class_file.read_text().splitlines()): print(f {i}: {name}) used_cls Counter() for txt in Path(strawberry_dataset/labels/train).glob(*.txt): with open(txt) as f: for line in f: cls int(line.split()[0]) used_cls[cls] 1 print(训练标签中出现的类别编号:, sorted(used_cls.keys()))对比输出就能发现是否存在类别号缺失或越界。常见情况是类别号不连续比如只有 0 和 2 出现1 完全缺席。编号不连续本身不影响训练——YOLO 会把编号当成索引使用但如果你打算迁移预训练权重预训练模型输出头是按 80 类设计的和你本地数据集类别数不一样时需要让 YOLO 自动重置输出层。此时类别号是否连续会影响重置逻辑的判断强烈建议重排成从 0 开始的连续编号。4. 训练之前的数据体检成熟度分布、样本量与类别均衡4.1 统计类别分布未成熟、半熟、成熟各自占多少草莓成熟度的三个类别天然存在数量落差大田里随手拍成熟红果和半熟果多刚坐果的绿果明显少。如果直接训练模型会对样本量大的类别过拟合对绿色小果几乎不响应。先把分布统计出来心里有数再定对策from pathlib import Path from collections import Counter counter Counter() for txt in Path(strawberry_dataset/labels/train).glob(*.txt): with open(txt) as f: for line in f: cls int(line.split()[0]) counter[cls] 1 for cls in sorted(counter): print(f类别 {cls}: {counter[cls]} 个标注框)我见过一个典型的草莓数据集成熟果框有 8000 多个半熟果 3000 多个未熟果只有 500 多个。这种情况下如果只按原始分布训练验证集里未熟果的召回率通常在 0.2 以下因为模型几乎没见过“绿色草莓被框起来”的样子。4.2 样本不均衡的两种处理损失函数权重与简单过采样处理不均衡先做损失函数权重再做数据层面的过采样两者可以叠加。损失函数权重按“总框数 / 类别框数”的粗略反比来设不用精算让少数类在反向传播时获得更大梯度即可。# 以三类别为例按上一节统计结果给 loss 权重 # 例如总框数 12000未熟果 500 # weight total / cls_count 近似24, 4, 1.5 cls_loss_weights [24.0, 4.0, 1.5]实际在 YOLO 配置里做权重调整时通常是通过修改损失函数中 cls 分支的 BCELoss 权重或者在数据采样器里给少数类的图片设置更高的采样概率。我更推荐后者把未成熟果的图片复制两份放进训练目录但要注意只复制图片和标签文件不要复制进验证集否则泄漏问题会在后面反咬一口。过采样的应用边界是少数类样本本身太少比如不足 200 个框时盲目复制容易让模型对着重复样本硬背。此时应该优先补数据去棚里补拍绿色果、或者让标注人员把漏标的绿色小果补上这比任何权重都有效。4.3 数据增强要适度成熟度是细粒度特征过度增强反而丢精度常规目标检测训练喜欢开大风味的增强比如随机旋转 90 度、色相大幅偏移、马赛克增强。但成熟度检测的核心判据是果实颜色的连续变化绿色、黄绿、泛红、深红是一个渐变光谱。如果把色相偏移开到 0.1 以上一颗成熟红果在增强后可能变成黄绿色标签还写着“成熟”这就等于给模型喂错误标注。我一般会把增强参数调到偏保守的档位# 成熟度检测建议的起步增强参数 hsv_h: 0.01 # 色相偏移极小颜色是成熟度主要特征 hsv_s: 0.3 hsv_v: 0.3 degrees: 10 # 轻微旋转即可草莓姿态变化不大 flipud: 0.0 # 上下翻转对贴近地面的草莓意义不大 mosaic: 0.8 # mosaic 保留对丰富背景有效这里hsv_h只有 0.01意味着训练时色相几乎不动保护红色与绿色的区分度。flipud关掉是因为大棚或地膜场景里上下翻转容易把阴影方向搞乱造成果实明暗关系失真。这不是说增强越多越好而是要让增强不破坏与任务强相关的视觉特征。5. 训练与评估阶段的高频踩坑从欠拟合到误检5.1 踩坑记录一img_size 设置过大导致显存溢出、反而掉点现象训练在第一个 epoch 就报 CUDA out of memory或者勉强跑起来后 val mAP 比小分辨率的还低。原因草莓果实小且密集总想用 1280 甚至 1536 的大输入尺寸来保住小目标细节但输入尺寸翻倍显存占用近似翻四倍batch size 又没降必然溢出。即使没溢出过大的输入尺寸也会让模型过度关注表面纹理反而忽略整体颜色分布。解决先看标签里目标框的尺寸分布用 max 和均值决定输入尺寸。如果绝大多数框边长大致落在图片宽高的 5% 到 15%用 640 完全够只有框占比低于 3% 时再考虑升到 1024并同步把 batch size 减半。5.2 踩坑记录二标签类别号与 class 文件错位训练 acc 为 0现象训练日志里 cls loss 不降分类 acc 一直等于 0或者训练能完成但输出类别永远只有某一个。原因数据集最初标注时用的类别顺序是固定的但你在整理 class 文件时重排了类别名顺序导致标签里的 0 在 class 文件里变成了别的类。比如原数据集 0 是半熟1 是成熟class 文件里却写成 0 未熟、1 半熟、2 成熟那所有半熟标签的声音都落到“未熟”上。解决写一个重映射脚本把旧类别号映射到新类别号并重写标签。这一步要在训练前做不要想着在训练时动态改。# 类别重映射示例旧顺序 [半熟, 成熟, 未熟] - 新顺序 [未熟, 半熟, 成熟] old_to_new {0: 1, 1: 2, 2: 0} for txt in Path(strawberry_dataset/labels/train).glob(*.txt): lines [] with open(txt) as f: for line in f: parts line.split() old_cls int(parts[0]) new_cls old_to_new[old_cls] lines.append(f{new_cls} { .join(parts[1:])}\n) with open(txt, w) as f: f.writelines(lines)这段脚本会把每个标签文件的类别号替换成新编号其余四个坐标值原封不动。执行前一定先备份标签目录这种批量改写一旦出错数据集的原始标注就丢了没有后悔药。5.3 踩坑记录三验证集 mAP 很高但实拍误检多——背景阈值没调现象验证集 mAP50 到了 0.85拿到棚里实拍模型把地膜反光、水滴反光、远处红色物体全框成成熟草莓。原因验证集里的图片和训练集来自同一个拍摄设备、同一个光线环境模型学到了背景特征与前景特征的关联。当部署到新的光照条件时它把“红色且接近圆形”的纹理误判为成熟果实因为训练时这类纹理几乎都带标签。解决推理时把置信度阈值从默认的 0.25 提到 0.5 以上并观察 Precision-Recall 曲线。更好的办法是用验证集统计每个类别的独立置信度阈值而不是统一用同一个值。对成熟度检测成熟类的置信度通常偏高可以给更严格的阈值未熟类因为样本少、特征弱阈值应适当放宽否则召回率会崩。5.4 踩坑记录四成熟度相邻类别混在一起丢失不均衡问题现象混淆矩阵显示未熟与半熟之间误判严重半熟与成熟之间也有大片交叉模型似乎学不出清晰的边界。原因成熟度本身是连续渐变过程同一颗草莓在转色期既不像绿果也不像红果。标注人员如果没有统一判定标准就可能把同一种状态标出不同类别模型只能学到一个模糊的边界。解决与标注人员定死判定标准比如“红色面积占比低于 30% 为未熟30% 到 70% 为半熟超过 70% 为成熟”。然后在训练前审查边界样本把标签明显矛盾的样本剔掉或重标。这个清洗过程很耗时但对细粒度检测的收益远大于调参。5.5 踩坑记录五训练集里出现了带标注框的“脏图片”现象训练 loss 一直很大且降不下去每隔几个 epoch 突然抖动一下验证集 mAP 时高时低。原因数据集打包时混入了奇怪的图片比如带水印的截图、文字图表、纯色背景图甚至分辨率极低的缩略图。这些图片的标签要么是乱标要么是复制粘贴出来的训练时相当于给模型灌噪声。解决在体检阶段加一步异常样本探测统计所有训练图片的尺寸、宽高比、像素均值和方差把尺寸异常小、像素方差接近 0 的图片挑出来人工看。另一个办法是利用第 3 章的可视化脚本跑一批随机样本人工翻图检查。大多数脏图片在第一轮人工抽查里就能被发现。6. 把成熟度检测从“能跑”做到“能用”迁移学习与分级置信度先别急着从头训练。用官方预训练权重做迁移学习可以大幅压缩收敛时间而且对中小规模的草莓数据集尤其友好。常见做法是保留预训练 backbone 的前几层把检测头重置为本地类别数用较小学习率微调。对成熟度检测backbone 提取的纹理和边缘特征非常通用真正需要重新学的是“颜色过渡区间”这种细粒度语义。# 迁移学习起步配置示例 pretrained: yolov8m.pt lr0: 0.0005 # 迁移学习用更低学习率防止破坏预训练特征 epochs: 60拿到模型后再叠一层成熟度分级逻辑。YOLO 输出的每个框带一组类别概率比如[0.05, 0.80, 0.35]分别对应未熟、半熟、成熟。常规做法是取概率最大值对应的类别作为结论。但成熟度边界本来就是渐变的概率分布经常出现“半熟 0.45、成熟 0.40”这种胶着状态此时硬选最大值很容易在临界果实上翻车。我习惯用 top-2 概率的差值做一次软化决策如果两个最高概率差小于某个阈值就把结果标成过渡状态返回给业务端做人工复核。这是我从一个果蔬分级 Demo 里学到的习惯阈值一般取 0.15 到 0.2按验证集上的分布微调。# 用 top-2 概率差判断成熟度是否处于过渡状态 def classify_maturity(probs, threshold0.15): top2 sorted(probs, reverseTrue)[:2] if top2[0] - top2[1] threshold: return transition return int(probs.index(top2[0]))另一件能提升“能用感”的事是给预测框按成熟度着色输出。成熟标红、半熟标黄、未熟标绿农技人员看输出图就能直接判断模型在哪些果实上犹豫不定这些犹豫样本非常适合回填训练集做增量标注。我在某次选果场景里就是用这种半自动极速标注的方式补了 200 多张难例边界混淆立刻比调了三天参数还明显。现在我拿到任何检测数据集都会先跑一遍第 3 章的体检再谈训练。这个习惯救过我很多次省下的重训时间远大于体检那几分钟。希望帮到你。本文还有配套的精品资源点击获取