尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

草莓成熟度目标检测数据集实战:3类530张图训练与调参指南

发布时间:2026/9/27 23:05:27

资讯中心
01
ARTICLE

草莓成熟度目标检测数据集实战:3类530张图训练与调参指南

草莓成熟度目标检测数据集实战:3类530张图训练与调参指南
简介这份草莓成熟度目标检测数据集面向农业视觉算法研究者、高校实验室与农业机器人团队针对红色果实与背景区分困难、成熟度过渡模糊、果实成簇遮挡及公开数据类别过粗等痛点提供细粒度三分类标注方案。资源包共1060个文件由530张高分辨率田间JPG图像与530个同名JSON标注文件组成压缩包约35.62MB标注覆盖unripe、semi-ripe、fully-ripe三个等级可直接用于YOLO、RT-DETR、Faster R-CNN等检测模型训练。图像包含叶片遮挡、果实重叠与多角度拍摄场景有助于提升模型在真实采摘环境中的鲁棒性其中半熟状态对判断最佳采摘窗口尤具决策价值。该数据集规模适中既适合从头训练轻量级检测器也可作为大模型微调的领域数据帮助资源有限的团队快速建立基线。目前已有69人学习下载。1. 草莓成熟度目标检测数据集3 类 530 张图能训出什么去年帮一个做设施农业的朋友调采摘机器人的视觉模块他拿来的数据只有一百多张手机拍的草莓标注还是用画图工具手绘的矩形框训练出来的模型在棚里跑把青果全认成了熟果机械臂一爪子下去捏烂半垄。这件事让我意识到小样本、类别少、场景单一的数据集恰恰是农业视觉落地里最容易被低估的一环。草莓成熟度目标检测数据集3 类 530 张图这个规格听起来不大但它对应的是一个非常具体的任务在叶片遮挡、光照不均、果实重叠的棚内环境里把草莓按成熟度分成未熟、半熟、成熟三类并给出边界框。它适合两类人一是想快速跑通一个农业目标检测闭环的算法工程师二是手里有采摘设备、需要验证视觉方案可行性的集成商。530 张图不算多但足够让你在半天内完成一次从标注检查到模型推理的完整验证而不是卡在数据准备阶段空转。这一篇就围绕这个数据集把选型、训练、调参和踩坑讲透。2. 三类成熟度怎么定义标注口径决定模型上限2.1 成熟度分类的边界为什么比想象中难定拿到 530 张图第一件事不是急着写 dataloader而是把三类标签的定义对齐。草莓的成熟过程是连续的从青绿到白绿、浅粉、全红中间没有物理上的硬边界。如果标注时凭感觉画框同一种颜色在不同标注员手里可能被分到不同类模型学到的就是噪声。常见做法是给每一类写一句可操作的视觉判据比如未熟定义为果面 90% 以上为青绿色、无红色晕染半熟定义为果面出现明显粉红或浅红色斑块但红色面积不超过 50%成熟定义为果面 80% 以上呈鲜红色、果肉有光泽。这些阈值不是绝对的但必须写下来让所有参与标注的人用同一把尺子。另一个容易被忽略的点是遮挡。棚里的草莓经常被叶片挡住一半或者几颗果子挤在一起。标注时如果只框可见部分模型在推理时会倾向于把叶片边缘当成目标边界如果框完整果实又需要标注员脑补被遮挡的轮廓。我的建议是遮挡面积小于 30% 的按完整果实框超过 30% 的直接标为忽略区域不参与训练。这样虽然会损失一些样本但能避免模型学到错误的边界特征。530 张图里如果忽略区域超过 15%就要考虑补拍或者换一批数据否则类别不平衡会很明显。2.2 用脚本检查标注一致性三个必查项标注完成后不要直接开训。先跑一个检查脚本把三类框的数量、宽高比、面积分布拉出来看。下面这段 Python 代码假设标注是 YOLO 格式的 txt 文件每行是class_id x_center y_center width height归一化到 0 到 1 之间。import os import numpy as np from collections import defaultdict def check_yolo_labels(label_dir, img_size640): stats defaultdict(list) for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue path os.path.join(label_dir, fname) with open(path, r) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) ! 5: print(f格式异常: {fname} - {line.strip()}) continue cls_id int(parts[0]) w float(parts[3]) * img_size h float(parts[4]) * img_size area w * h ratio w / h if h 0 else 0 stats[cls_id].append((area, ratio)) for cls_id in sorted(stats.keys()): arr np.array(stats[cls_id]) print(f类别 {cls_id}: 框数{len(arr)}, f面积均值{arr[:,0].mean():.1f}, f面积中位数{np.median(arr[:,0]):.1f}, f宽高比均值{arr[:,1].mean():.2f}) # 检查异常小框 small arr[arr[:,0] 100] if len(small) 0: print(f 警告: 有 {len(small)} 个框面积小于 100 像素可能是误标) if __name__ __main__: check_yolo_labels(./labels/train)这段脚本的逻辑很直接遍历所有标注文件按类别统计框的面积和宽高比。面积均值和中位数能告诉你目标尺度分布如果某一类的面积中位数明显偏小可能是把远处的小果子也标进去了训练时会被下采样吃掉。宽高比均值用来判断框的形状是否合理草莓大致是圆锥形宽高比通常在 0.7 到 1.3 之间如果出现大量 2.0 以上的框大概率是标注时把叶片或相邻果实一起框进去了。面积小于 100 像素的框要特别警惕在 640 输入尺寸下这些目标经过 backbone 下采样后可能只剩几个像素模型根本学不到有效特征建议直接过滤掉或者放大图像重新标注。参数方面img_size要和你训练时的输入尺寸保持一致YOLO 系列常用 640如果你打算用 1280 训练就把这个值改成 1280 再跑一遍。label_dir指向存放 txt 标注的目录不要和图片目录混在一起。跑完这个脚本你会对 530 张图的质量有一个量化判断再决定要不要清洗。2.3 类别不平衡的处理过采样还是加权损失530 张图里三类样本的数量很难刚好均等。成熟草莓通常最多因为拍摄者会下意识多拍好看的果子未熟和半熟可能偏少。如果直接用原始分布训练模型会偏向多数类把半熟也预测成成熟。常见做法有两种一是对少数类做过采样在 dataloader 里对包含少数类的图片提高采样概率二是在损失函数里给少数类更高的权重。我一般会先跑一遍基线看混淆矩阵如果少数类的召回率低于 0.5再考虑加权。YOLOv8 的cls损失权重可以通过cls_pw参数调整但更直接的办法是在数据集配置文件里复制少数类图片的路径让采样器自然看到更多次。注意不要简单复制图片文件那样会污染验证集正确做法是在训练脚本里用WeightedRandomSampler或者自定义 sampler。提示过采样倍数不要超过 3 倍否则少数类会过拟合验证集上的表现反而下降。3. 从 530 张图到可训练集划分、增强与格式转换3.1 训练验证测试怎么分别用随机划分530 张图如果按 8:1:1 随机划分训练集 424 张验证集 53 张测试集 53 张。听起来合理但农业数据有个特点同一株草莓可能被连续拍摄多张随机划分会让同一株的图片同时出现在训练和验证集里导致验证指标虚高。正确的做法是按拍摄批次或植株编号划分把不同植株的图片分到不同集合。如果数据里没有植株编号至少按拍摄时间或文件名的连续段来切避免相邻帧泄漏。具体操作上我一般会先把所有图片按文件名排序然后按 7:2:1 的比例切分但切分前会打乱一次确保每个集合里都有三类样本。如果某一类在验证集里少于 5 个框就调整切分点把更多该类样本放进验证集。下面是一个简单的划分脚本按类别分层抽样。import os import random from collections import defaultdict from sklearn.model_selection import train_test_split def split_dataset(img_dir, label_dir, output_txt, val_ratio0.2, test_ratio0.1): # 收集每张图包含的类别 img_cls {} for fname in os.listdir(img_dir): if not fname.lower().endswith((.jpg, .png, .jpeg)): continue base os.path.splitext(fname)[0] label_path os.path.join(label_dir, base .txt) classes set() if os.path.exists(label_path): with open(label_path, r) as f: for line in f: parts line.strip().split() if parts: classes.add(int(parts[0])) img_cls[fname] classes if classes else {0} # 按主类别分层 stratify [min(cls) for cls in img_cls.values()] files list(img_cls.keys()) train_files, temp_files, _, temp_strat train_test_split( files, stratify, test_sizeval_ratio test_ratio, random_state42) val_files, test_files train_test_split( temp_files, test_sizetest_ratio / (val_ratio test_ratio), stratifytemp_strat, random_state42) with open(output_txt, w) as f: for name, subset in [(train, train_files), (val, val_files), (test, test_files)]: for fn in subset: f.write(f{subset}/{fn}\n) print(f训练集 {len(train_files)} 张验证集 {len(val_files)} 张测试集 {len(test_files)} 张) if __name__ __main__: split_dataset(./images, ./labels, ./split.txt)这段代码用train_test_split做了两次分层抽样第一次分出训练集和临时集第二次从临时集里分出验证集和测试集。stratify参数取每张图里最小的类别 ID这样能保证稀有类别在三个集合里都有分布。输出文件split.txt每行是子集/文件名后续训练脚本可以直接读这个文件来加载数据。注意random_state固定为 42方便复现如果你换了数据集这个值可以改但同一批实验里不要变。3.2 增强策略哪些能用哪些会帮倒忙农业场景的增强和通用目标检测不太一样。水平翻转、垂直翻转、随机裁剪、亮度对比度调整这些常规操作都可以用但要注意几点草莓的颜色是成熟度判据的核心色相偏移hue jitter不能太大否则半熟可能被调成成熟。我一般把色相偏移限制在 ±10 度以内饱和度偏移 ±20%亮度偏移 ±30%。马赛克增强mosaic在 YOLOv8 里默认开启对小数据集很有效但 530 张图本身就不多mosaic 会把四张图拼成一张进一步降低单张图的有效信息量建议把mosaic概率从 1.0 降到 0.5后期再关闭。另一个容易翻车的是随机旋转。草莓在棚里通常是垂挂的旋转角度过大比如 ±90 度会产生现实中不存在的姿态模型学到之后在真实场景里反而容易误检。我的经验是旋转角度限制在 ±15 度以内或者干脆不用旋转靠翻转和裁剪来增加多样性。如果你用的是 YOLOv8可以在data.yaml同级的hyp.yaml里改这些参数或者直接在训练命令里覆盖。yolo detect train \ datastrawberry.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ mosaic0.5 \ degrees15 \ hsv_h0.015 \ hsv_s0.2 \ hsv_v0.3 \ fliplr0.5 \ flipud0.0 \ namestrawberry_v1这条命令里mosaic0.5把马赛克增强概率减半degrees15限制旋转角度hsv_h0.015对应色相偏移约 ±5 度YOLO 的 hsv_h 是比例值0.015 乘以 360 约 5.4 度hsv_s0.2和hsv_v0.3分别控制饱和度和亮度。flipud0.0关闭了上下翻转因为草莓垂挂的姿态上下翻转后不自然。batch16在 8GB 显存的卡上跑 640 输入比较稳如果显存不够就降到 8。epochs150对小数据集来说足够收敛但要看验证集损失曲线如果 100 轮后还在下降可以加到 200。3.3 格式转换从 VOC 到 YOLO 的四个边界坑很多公开数据集或者自建标注工具导出的是 VOC 格式的 XML而 YOLO 训练需要归一化的 txt。转换脚本网上一搜一大把但实际用的时候有四个坑第一VOC 的坐标是左上角和右下角的绝对像素值YOLO 需要中心点和宽高并且要除以图片宽高做归一化除零错误要处理第二图片尺寸要从 XML 里读不能硬编码因为 530 张图可能来自不同设备第三类别名到 ID 的映射要固定不能按字母序自动排否则训练和推理的类别顺序会错位第四有些 XML 里会有difficult标记这些框应该跳过不参与训练。import xml.etree.ElementTree as ET import os from PIL import Image CLASS_MAP {unripe: 0, semi_ripe: 1, ripe: 2} def voc_to_yolo(xml_path, img_dir, out_dir): tree ET.parse(xml_path) root tree.getroot() fname root.find(filename).text img_path os.path.join(img_dir, fname) if not os.path.exists(img_path): print(f图片不存在: {img_path}) return with Image.open(img_path) as im: iw, ih im.size lines [] for obj in root.findall(object): if obj.find(difficult) is not None and obj.find(difficult).text 1: continue name obj.find(name).text if name not in CLASS_MAP: print(f未知类别: {name} in {xml_path}) continue cls_id CLASS_MAP[name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 裁剪到图像边界 xmin max(0, min(xmin, iw - 1)) ymin max(0, min(ymin, ih - 1)) xmax max(0, min(xmax, iw - 1)) ymax max(0, min(ymax, ih - 1)) if xmax xmin or ymax ymin: continue xc (xmin xmax) / 2.0 / iw yc (ymin ymax) / 2.0 / ih w (xmax - xmin) / iw h (ymax - ymin) / ih lines.append(f{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) out_path os.path.join(out_dir, os.path.splitext(fname)[0] .txt) with open(out_path, w) as f: f.write(\n.join(lines)) if __name__ __main__: for xml in os.listdir(./annotations): if xml.endswith(.xml): voc_to_yolo(os.path.join(./annotations, xml), ./images, ./labels)这段脚本里CLASS_MAP显式定义了类别名到 ID 的映射顺序要和训练时的data.yaml一致。difficult标记为 1 的框直接跳过避免模型被难样本带偏。坐标裁剪到图像边界内防止标注时手抖画出界。归一化后的值保留 6 位小数足够精度。跑完转换后建议随机抽 10 张图用可视化脚本画框检查一遍确认没有坐标错位。4. 训练参数怎么设小数据集的收敛技巧4.1 模型选型nano 还是 small530 张图、3 类目标属于典型的小数据集。模型越大过拟合风险越高。YOLOv8n 参数量约 3.2MYOLOv8s 约 11.2M在 530 张图上nano 版本通常就能达到不错的精度而且推理速度快适合后续部署到边缘设备。如果你用 YOLOv8s建议冻结 backbone 的前几层只训练 head 部分或者用更低的初始学习率。我一般先用 nano 跑一个基线看 mAP50 能不能到 0.85 以上如果达不到再考虑换 small 或者加数据。不要一上来就用 large那是在浪费算力。学习率方面YOLO 默认的lr00.01对 530 张图来说偏大容易在早期震荡。我一般降到 0.005 或者 0.001配合余弦退火调度。warmup_epochs设 3 到 5让模型先在小学习率下预热。权重衰减weight_decay0.0005可以抑制过拟合。如果你发现训练损失下降但验证损失上升就是过拟合的典型信号这时候要么加增强要么减模型容量要么早停。4.2 用验证集曲线判断何时停三个观察点训练过程中不要只看最终的 mAP要盯着验证集的三条曲线val/box_loss、val/cls_loss和metrics/mAP50。如果val/box_loss在 50 轮后还在下降说明模型还在学习定位可以继续训如果val/cls_loss开始上升说明分类过拟合了该停了。mAP50的曲线如果出现平台期超过 20 轮也可以停。YOLOv8 默认的patience50是连续 50 轮没有提升就早停对 530 张图来说太宽松了建议改成 20 到 30。另一个技巧是保存最佳模型和最后模型训练结束后用测试集分别评估选泛化更好的那个。有时候最后模型在验证集上表现更好因为早停点不一定最优。YOLO 会自动保存best.pt和last.pt你只需要在评估时指定权重路径。4.3 推理阶段置信度阈值和 NMS 怎么调训练完模型部署到实际采摘设备上时置信度阈值conf和 NMS 的 IoU 阈值iou需要根据场景调。默认conf0.25、iou0.7在草莓场景下可能偏松因为果实密集时 NMS 会误删相邻框。我的经验是如果草莓之间重叠严重把iou降到 0.5 到 0.6让 NMS 更激进地合并框如果漏检多把conf降到 0.15 到 0.2但要注意误检会增加。最好用测试集画一条 PR 曲线找到 F1 分数最高的阈值点。from ultralytics import YOLO import numpy as np model YOLO(runs/detect/strawberry_v1/weights/best.pt) results model.val(datastrawberry.yaml, conf0.001, iou0.6, plotsTrue) # 从 results 里提取不同 conf 下的 precision 和 recall # 实际使用时可以直接看生成的 PR_curve.png这段代码用conf0.001跑验证目的是让模型输出所有可能的检测框然后 YOLO 会自动生成 PR 曲线图。你从图上找到 F1 最大的点对应的 conf 就是最优阈值。iou0.6是 NMS 的阈值根据你的场景调整。注意val模式下的conf和推理时的conf不是一回事验证时设低是为了画完整曲线实际部署时用曲线上找到的最优值。5. 避坑与排查530 张图训练中最容易翻车的五件事5.1 现象mAP 很高但实际推理全是错框原因验证集和训练集来自同一批连续拍摄的图片数据泄漏导致指标虚高。解决按植株或拍摄批次重新划分确保验证集里的草莓和训练集不重叠。如果无法追溯来源至少按文件名的时间戳切分不要随机打乱。5.2 现象训练损失正常下降但验证损失从第 10 轮开始飙升原因学习率太大或者增强太弱模型在 530 张图上快速过拟合。解决把lr0降到 0.001增加mosaic和mixup的概率或者冻结 backbone 前 5 层。如果还不行就换更小的模型。5.3 现象半熟类别的召回率始终低于 0.4原因半熟样本太少或者标注边界模糊模型学到的特征不一致。解决检查半熟类的框数如果少于 50 个用过采样把采样概率提高到 2 到 3 倍同时重新审视标注判据把模棱两可的样本剔除或重新标注。5.4 现象推理时同一颗草莓出现多个重叠框原因NMS 的 IoU 阈值太高或者模型对同一目标输出了多个类别的框。解决把iou降到 0.5并在推理后处理里加一个跨类别 NMS只保留置信度最高的框。如果模型经常把成熟和半熟同时预测出来说明分类头没学好需要检查标注一致性。5.5 现象换一台设备推理结果完全不对原因训练时的输入尺寸、归一化参数或者类别顺序和推理时不一致。解决把训练时的data.yaml、hyp.yaml和权重文件一起打包推理脚本里显式指定imgsz和class_names不要依赖默认值。特别是类别顺序YOLO 的names字典必须和训练时完全一致。6. 把 530 张图用出 5300 张的效果三个进阶技巧第一个技巧是伪标签半监督。先用 530 张图训一个基线模型在未标注的棚内视频帧上跑推理把置信度高于 0.8 的框作为伪标签人工抽检修正后加入训练集。这样可以把数据量扩到几千张但要注意伪标签的噪声会累积每轮加入的新样本不要超过原始数据的 50%。我一般会跑两轮第一轮加 300 张第二轮再加 300 张然后停。第二个技巧是切片推理。草莓在 640 输入下可能只占几十个像素如果原图是 4K 分辨率直接缩放会丢失细节。可以把原图切成 640×640 的块每块单独推理再合并结果。YOLOv8 支持imgsz设大但显存吃不消切片是更实际的办法。切的时候要有重叠重叠率 20% 到 30%避免目标被切断。第三个技巧是用测试时增强TTA提升召回。推理时对同一张图做水平翻转和轻微缩放分别推理后合并框再做 NMS。YOLO 的augmentTrue参数可以开启 TTA但速度会慢 2 到 3 倍。如果采摘设备对实时性要求不高比如每秒处理 2 到 3 帧就够TTA 是值得的。下面是一个简单的 TTA 推理示例。from ultralytics import YOLO import cv2 import numpy as np model YOLO(best.pt) def tta_predict(img_path, conf0.25, iou0.5): img cv2.imread(img_path) all_boxes [] # 原图 r1 model(img, confconf, iouiou, verboseFalse)[0] all_boxes.append(r1.boxes.data.cpu().numpy()) # 水平翻转 img_flip cv2.flip(img, 1) r2 model(img_flip, confconf, iouiou, verboseFalse)[0] boxes2 r2.boxes.data.cpu().numpy() if len(boxes2) 0: boxes2[:, 0] img.shape[1] - boxes2[:, 0] # x1 翻转 boxes2[:, 2] img.shape[1] - boxes2[:, 2] # x2 翻转 boxes2[:, [0, 2]] boxes2[:, [2, 0]] # 交换 x1 x2 all_boxes.append(boxes2) # 合并后做 NMS merged np.vstack([b for b in all_boxes if len(b) 0]) if len(merged) 0: return [] # 简单 NMS keep [] order merged[:, 4].argsort()[::-1] while len(order) 0: i order[0] keep.append(i) if len(order) 1: break xx1 np.maximum(merged[i, 0], merged[order[1:], 0]) yy1 np.maximum(merged[i, 1], merged[order[1:], 1]) xx2 np.minimum(merged[i, 2], merged[order[1:], 2]) yy2 np.minimum(merged[i, 3], merged[order[1:], 3]) w np.maximum(0, xx2 - xx1) h np.maximum(0, yy2 - yy1) inter w * h area_i (merged[i, 2] - merged[i, 0]) * (merged[i, 3] - merged[i, 1]) area_j (merged[order[1:], 2] - merged[order[1:], 0]) * (merged[order[1:], 3] - merged[order[1:], 1]) iou_arr inter / (area_i area_j - inter 1e-6) order order[1:][iou_arr iou] return merged[keep] if __name__ __main__: boxes tta_predict(./test/strawberry_001.jpg) print(fTTA 后保留 {len(boxes)} 个框)这段代码做了原图和水平翻转两次推理把翻转后的框坐标映射回原图再合并做 NMS。conf和iou参数和单次推理保持一致。注意翻转后的 x 坐标映射公式是new_x width - old_x并且要交换 x1 和 x2 的顺序否则框会变成负宽度。NMS 部分用 numpy 手写方便你理解每一步实际部署可以用torchvision.ops.nms加速。TTA 的代价是推理时间翻倍但如果你的场景对漏检容忍度低这点开销值得。最后说一个我自己的习惯每次训完模型不管指标多好我都会拿 20 张完全没参与训练的棚内实拍图跑一遍人工数漏检和误检。530 张图的数据集指标再漂亮也要过这一关。有一次 mAP50 到了 0.92实拍图里半熟草莓还是漏了三分之一后来发现是验证集里半熟样本的拍摄角度太单一。数据集的坑永远在数据本身。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。