简介:这是一套面向目标检测入门与智慧农业视觉应用的小规模草莓成熟度数据集,适合学习YOLO或Faster R-CNN等模型的标签格式转换与训练流程。压缩包内含1238个文件,由412张jpg原图、412个Pascal VOC格式xml标注和414个YOLO格式txt标注组成,内置labelImg标记结果,类别涵盖flower、growth、mature三个阶段,总标注框数1932个。数据量不大但类别分布较均衡,便于快速跑通数据加载、划分训练集/验证集和评估模型。压缩包整体约23.43MB,拷贝、解压与迭代训练都很轻量。当前已有508人学习下载,适合用来做草莓成熟度检测的基线实验、课程设计或作为自定义数据集的格式参考。
1. 草莓成熟度检测数据集:412张双格式图能把YOLOv8基线跑到什么程度
做农业视觉检测的工程师基本都卡在同一个环节:手头没有现成的标注数据。草莓成熟度检测数据集(VOC+YOLO格式、412张图、3个类别、7z压缩打包)能做的事,就是把这层门槛去掉——照片已经标好,格式是YOLO生态最常吃的VOC和txt两套,解压就能跑通一条从训练到验证的完整链路。
这套数据规模不大,但正好适合先跑基线:用412张图调通环境、验证模型结构和训练参数,再把增量数据补进来迭代。适合谁用?短期做课程设计、入门农业检测、或者想先评估草莓成熟度任务难度的人;也适合给正式项目做预实验,拿它定一个mAP参照线。下面按“格式怎么看、怎么训练、哪里会翻车、怎么扩数据”的顺序展开。
2. 先吃透VOC和YOLO两套标注格式:目录结构、XML字段与归一化坐标换算
拿到一个7z压缩包,最忌讳的做法是先解压出来直接丢给训练脚本,跑不通再回头查标注。YOLO生态里,标注格式决定一切:文件路径对不上、类别编号错一位、坐标归一化算错,训练出来的模型都是废的。所以第一步是把这个数据集的标注结构彻底看明白,再动手训练。
2.1 解压7z后先别急着训练:先检查目录是不是双格式并存
在 Linux 上解压 .7z 文件,需要 p7zip 工具集。我一般会先用7za l预览压缩包内容,确认是单层目录还是嵌套目录,再决定解压到哪,避免一上来全解出来发现目录嵌套了三层。下面这套命令是最常见的做法:
# 安装 7-Zip 命令行工具(Debian/Ubuntu 系) sudo apt update && sudo apt install -y p7zip-full # 列出压缩包内容,确认没有多余层级和分卷 7za l strawberry_maturity.7z # 解压到指定目录,-o 后面跟输出路径 7za x strawberry_maturity.7z -o./strawberry_data逻辑说明:7za l输出的文件树就是压缩包的真实结构,能提前看出是不是分卷包、有没有中文路径、是不是套了一层多余的根目录。-o参数指定解压输出目录,目录不存在会自动创建。命令里的文件名是示例,要用你实际拿到的包名替换。
解压完先看一眼顶层结构。常见的打包习惯是把图片、VOC标注和YOLO标注分三个目录放,解压后大概是这个布局:
strawberry_data/ ├── images/ # 412 张 JPEG 原图 │ ├── train/ # 部分数据集作者会提前分好 │ └── val/ ├── annotations/ # VOC 格式的 XML └── labels/ # YOLO 格式的 txt注意:不是所有打包者都会提前分 train/val,很多就是平铺 412 张图和对应标注。这种情况不用慌,后面第 3 章会讲怎么自己拆验证集。还有一类常见布局是 images 目录底下直接就是图,没有 train/val 二级目录,那拆分的活就交给你了。
2.2 VOC格式的XML里有哪些关键字段:读取XML并打印bbox的完整脚本
VOC 格式来自 PASCAL VOC 目标检测任务,是标注工具(比如 LabelImg)默认导出的格式之一。它的核心是每张图片对应一个同名的 XML 文件,根节点annotation包住图片文件名、尺寸和所有目标框。VOC 里的坐标是像素绝对坐标,原点是图片左上角,这点和 YOLO 完全不同。
我拿到 XML 会先写一个小脚本把所有框打印出来核对一遍,确认标注框贴不贴果实边界。下面这段脚本读取前 3 个 XML,输出每张图的尺寸和每个目标的类别、坐标:
import xml.etree.ElementTree as ET import glob xml_files = sorted(glob.glob("strawberry_data/annotations/*.xml")) for xml_path in xml_files[:3]: tree = ET.parse(xml_path) root = tree.getroot() filename = root.findtext("filename") width = int(root.findtext(".//size/width")) height = int(root.findtext(".//size/height")) print(f"图片: {filename} 尺寸: {width}x{height}") for obj in root.findall("object"): name = obj.findtext("name") box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) print(f" 类别: {name} bbox: ({xmin:.1f}, {ymin:.1f}) -> ({xmax:.1f}, {ymax:.1f})")逻辑说明:findtext(".//size/width")里的.//是相对路径缩写,意思是无论 size 节点嵌套在哪一层都能找到。打印尺寸这一步很关键,后面转 YOLO 格式、做滑窗切图都要用原图宽高当分母。xmin/ymin/xmax/ymax是整数像素值,但有些标注工具会导出带小数的 float,所以代码里统一用float()转换。
VOC 格式还有一个容易忽略的细节:object节点下的difficult和truncated字段。如果标注时把遮挡、模糊的果实标成了difficult=1,转 YOLO 时一般直接忽略这层信息,因为 YOLO txt 没有对应字段。如果你发现数据集里大量目标带difficult=1,训练时把它们删掉反而更干净。
2.3 YOLO格式的txt如何对应图片:归一化坐标换算与文件命名规则
YOLO 系列的 txt 标注和 VOC 是两种完全不同的思路:VOC 用绝对像素坐标,YOLO 用相对宽高的归一化坐标。txt 里每一行代表一个目标,固定五个值:类别ID 中心点x 中心点y 宽度 高度,全部归一化到 0 到 1 之间。
标题里说的“VOC+YOLO 双格式”,意味着两套标注都给你了,可以直接用 YOLO 那套。但你自己凑数据时大概率只有 VOC 的 XML,所以这段转换代码是长期要用的。把 VOC XML 转成 YOLO txt 的脚本如下:
import xml.etree.ElementTree as ET # 类名字符串 -> 数字ID,顺序必须和后续data.yaml里的names一致 class_name_to_id = { "immature": 0, "mid-ripe": 1, "ripe": 2, } def voc_to_yolo(xml_path, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() # 从原图尺寸读取宽高,这是归一化的分母 width = int(root.findtext(".//size/width")) height = int(root.findtext(".//size/height")) lines = [] for obj in root.findall("object"): name = obj.findtext("name") if name not in class_name_to_id: print(f"警告: 未知类别 {name} 出现在 {xml_path}") continue cat_id = class_name_to_id[name] box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) # YOLO需要中心点坐标和宽高,再除以原图宽高归一化 x_center = (xmin + xmax) / 2.0 / width y_center = (ymin + ymax) / 2.0 / height box_w = (xmax - xmin) / width box_h = (ymax - ymin) / height lines.append(f"{cat_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(out_txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines))逻辑说明:这段代码里class_name_to_id映射是整个流程最容易被忽略的点。VOC 的 XML 里类别是字符串,YOLO 的 txt 里类别是数字,如果映射表顺序和后面训练的data.yaml不一致,模型会把类别学得一团糟。我在代码里加了未知类别的警告,就是防止跑了半天发现某个类别被静默丢弃。
参数说明:归一化的分母必须用 XML 里size节点记录的原图宽高,不能用代码里假设的 640 或任何固定尺寸。只要图片被 resize 过,原标注坐标就要跟着换算,否则框的位置全偏。输出保留 6 位小数是 YOLO 生态的默认习惯,够用且不会让 txt 文件变得冗余。
2.4 3个类别的常见划分与类别映射表
草莓成熟度检测的 3 个类别,常见做法是按生长阶段拆成“未成熟、半成熟、成熟”三档,或者“未熟、成熟、过熟”三档。标题只写了 3 个类别,具体叫法以你解压出来的 data.yaml 或 labels 目录里实际出现的名字为准,但无论如何,训练前必须确认类别 ID 跟名字的对应关系。
下面这张映射表代表最常见的一种划分方式,实际以你手里的标注为准:
| 类别ID | 常见英文名 | 中文含义 | 典型外观特征 |
|---|---|---|---|
| 0 | immature | 未成熟 | 青绿或泛白,果实硬、小 |
| 1 | mid-ripe | 半成熟 | 局部转红,粉白相间 |
| 2 | ripe | 成熟 | 全红或深红,适合采摘 |
检查方法很简单:随机打开 labels 目录下几个 txt,看每行的第一个数字是不是只在 0、1、2 三个值里;再用第 2.2 节的脚本画一遍框,确认“第一个数字是 0 的框”框住的是不是青果。这一步花 10 分钟,能避免后面训练跑完才发现类别标签串位。
3. 用草莓数据集跑通YOLOv8最小训练:data.yaml、目录重组与参数设定
格式看懂之后,下一步就是把数据喂给 YOLOv8。这里用一套可复现的最小流程:把目录整理成 YOLOv8 要求的 images/labels 结构,写好 data.yaml,然后用一条训练命令跑起来。这套流程同时适用于标题里这个 412 张的草莓数据集,以及你自己攒的任何 VOC 格式数据。
3.1 先归一目录结构:train/val拆分与images/labels对齐
YOLOv8 的 ultralytics 训练器要求数据集目录必须是 images/train、images/val 和 labels/train、labels/val 这样的结构,图片跟同名 txt 标签所在层级一致。如果解压出来的数据是平铺的,第一件事就是按比例拆分。
拆分最好不要用ls | head -n 340这种按文件名排序的简单办法,因为文件名排序后可能全部是同一批采集环境的照片,验证集会失真。正确做法是先用随机种子打乱,再按比例切分,保证同一张图的图片和标签永远进同一个集合:
import os import random import shutil random.seed(42) # 固定随机种子,保证每次拆分结果一致 img_dir = "strawberry_data/images" label_dir = "strawberry_data/labels" out_root = "dataset" train_ratio = 0.8 imgs = sorted(os.listdir(img_dir)) random.shuffle(imgs) split = int(len(imgs) * train_ratio) train_imgs, val_imgs = imgs[:split], imgs[split:] for subset, img_list in [("train", train_imgs), ("val", val_imgs)]: out_img = os.path.join(out_root, "images", subset) out_lab = os.path.join(out_root, "labels", subset) os.makedirs(out_img, exist_ok=True) os.makedirs(out_lab, exist_ok=True) for img_name in img_list: base = os.path.splitext(img_name)[0] shutil.copy(os.path.join(img_dir, img_name), os.path.join(out_img, img_name)) txt_name = base + ".txt" if os.path.exists(os.path.join(label_dir, txt_name)): shutil.copy(os.path.join(label_dir, txt_name), os.path.join(out_lab, txt_name)) else: print(f"警告: {img_name} 缺少对应txt标签")逻辑说明:random.seed(42)这行是这个脚本的灵魂。不设种子的话,每次跑拆分结果都不一样,后面做对比实验时没法确定模型效果差异是数据变了还是模型参数变了。shutil.copy而不是move,是为了保留原始数据一份,万一拆分错了还能重来。
参数说明:train_ratio = 0.8意味着 412 张里大约 330 张训练、82 张验证。小数据集我建议验证集至少留 15% 到 20%,太少的话 mAP 波动会非常大。如果你的显卡连 batch=16 都跑不动,宁可降低图片尺寸也不要砍验证集。
3.2 data.yaml的写法:路径、names与类别数对齐
ultralytics 通过一个 yaml 文件描述数据集路径和类别名。这个文件本身不复杂,但路径写错是新手翻车的第一大原因。下面是一个能直接用的版本:
# strawberry.yaml # path 建议写绝对路径,避免不同工作目录启动训练时找不到数据 path: /home/user/strawberry_project/dataset train: images/train val: images/val names: 0: immature 1: mid-ripe 2: ripe逻辑说明:path是数据集根目录,train和val是相对这个根目录的图片路径,labels目录不需要写,ultralytics 默认找 images 同级的 labels 目录。names的顺序必须和上一章转换脚本里的class_name_to_id完全一致,否则类别 ID 错位。
参数说明:nc(类别数量)可以省略,ultralytics 会自动从 names 的长度推断。如果训练时报类别数对不上,八成是 yaml 里 names 只有 2 个但 txt 里有 3 类,或者反过来。写完后可以用 Python 验证一下数据集配置:
from ultralytics.data import check_det_dataset data = check_det_dataset("strawberry.yaml") print(data["nc"], data["names"])输出正确的话,这一关就过了。
3.3 最小训练命令与关键参数:imgsz、epochs、batch、patience
数据准备好了,训练命令本身很短,但参数要按这个数据集的体量来设。412 张小数据集和上万张的工业数据集,参数逻辑完全不同。下面是最小可用的训练命令:
cd /home/user/strawberry_project yolo detect train \ data=strawberry.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=150 \ batch=16 \ patience=20 \ project=runs/strawberry \ name=exp1逻辑说明:model=yolov8s.pt表示用 YOLOv8s 预训练权重作为起点。对 412 张的小数据集,强烈建议做迁移学习而不是从零随机初始化,否则这么少的数据量很难收敛。YOLOv8s.pt 会在第一次训练时自动下载,网络慢的话可以先手动下载放到当前目录。
参数说明里几个关键值:
imgsz=640:草莓果实通常不会特别小,640 是精度和速度的平衡点。如果训练集里果实只占画面很小区域,可以试imgsz=960,代价是显存和训练时间明显上涨。epochs=150:小数据集不需要一上来就训 300 轮。150 轮足够看出趋势,配合早停参数能省大量时间。batch=16:8GB 显存跑 YOLOv8s、imgsz=640 时,batch=16 比较稳。显存不够时优先降 imgsz,不要降到 batch=4 以下,否则 BatchNorm 统计量会不稳。patience=20:验证集 mAP 连续 20 轮不提升就提前结束。小数据集经常 30 到 60 轮就收敛,后面全是抖动和过拟合。
训练结束后,runs/strawberry/exp1 目录里会生成 weights/best.pt 和 last.pt,以及一批图表。不要只看 loss 曲线,loss 低不代表检测好。
3.4 训练完怎么验证:从results.png看曲线,用best.pt做预测
训练日志里最有价值的文件是results.png,它把 train loss、val loss、精度、召回、mAP50、mAP50-95、学习率全部画在了一张图上。我一般先看metrics/mAP50(B)那条曲线,确认它是先涨后平而不是一路走低。
推理验证用 best.pt,别用 last.pt。last.pt 是最后一轮的权重,早停时它往往是过拟合的那一版:
yolo predict model=runs/strawberry/exp1/weights/best.pt \ source=dataset/images/val \ save=True \ conf=0.25 \ project=runs/strawberry_pred逻辑说明:source指向验证集图片目录,save=True会保存画好框的图片。跑完后在 runs/strawberry_pred 目录里找几张典型图,肉眼看框的位置、类别标签是不是对的。这一步叫“眼睛验收”,任何 mAP 指标都替代不了。
如果发现半熟的草莓被标成成熟,不要急着改模型,先回到第 2.3 节的类别检查:大概率不是模型问题,是标签本身就有歧义,或者类别映射字典写反了。这类问题在标注边界模糊的数据集上很常见,后面第 4 章会展开说。
4. 避坑:7z解压、VOC转YOLO与训练阶段最容易翻车的5个细节
干活时让我最花时间的从来不是模型结构,而是数据链路和训练配置上的各种毛刺。这里挑 5 个在 VOC+YOLO 数据集上反复出现的坑,按“现象、原因、解决”的顺序写清楚,每一条都是我实际踩过或者帮别人排查过的。
4.1 7z压缩包密码正确却一直报错
现象:用7za x解压时输入密码,工具一直提示密码错误;或者明明显示解压完成,打开目录发现文件损坏、尺寸为 0。
原因:最常见的是文件名编码问题。Windows 中文环境下创建的 7z 包,内部文件名可能是 GBK 编码,而 Linux 上的 p7zip 默认按 UTF-8 解析,路径解析失败后报错信息会伪装成密码错误。另一个常见原因是压缩包是分卷的,你只拿到其中一卷。
解决:先运行7za l strawberry_maturity.7z查看文件列表,如果文件名显示乱码,基本可以断定是编码问题。指定代码页重新解压:
7za x strawberry_maturity.7z -p你的密码 -mcp=936 -o./strawberry_data逻辑说明:-mcp=936把文件名按 GBK 解码,这是 Windows 中文系统的默认代码页,绝大多数中文文件名乱码问题能靠它解决。如果还不行,再用 Python 的 py7zr 库解压,它对编码处理更宽容;分卷的话要把所有.7z.001、.7z.002放在同一目录再解压。
4.2 目录路径带中文或空格导致数据集加载失败
现象:check_det_dataset加载时报错,或者训练一开始就提示Dataset not found,但路径明明是对的。
原因:ultralytics 内部会用路径字符串拼接图片和标签目录,一旦路径里混入中文、空格或 Windows 风格的反斜杠,拼接出来的路径就找不到文件。这个问题在 Windows 上训练时尤其常见,因为数据集放到了C:\Users\张三\桌面这种路径下。
解决:把数据集放到纯英文、无空格的路径,例如D:\work\strawberry\dataset;yaml 里的path统一用正斜杠,即使系统是 Windows。我自己的习惯是项目根目录永远用英文命名,中文只出现在注释里。
4.3 VOC转YOLO时忘了除以原图宽高
现象:训练能跑通,loss 也在降,但预测出来的框要么偏大要么偏小,mAP 一直上不去,看起来像模型没学会。
原因:转换脚本里把归一化的分母写成了固定值,比如 640 或 416,而原图实际宽度可能是 1920。中心点坐标和框宽高都被压缩到了错误的尺度,模型学到的框自然不对。
解决:归一化分母必须从每张图的 XML 的size节点读取,一行一行核对。转换完随机挑 3 个 txt,手动把归一化坐标乘回原图宽高,看还原出来的像素坐标和 XML 里的bndbox是否一致。这一步能筛掉 90% 的转换错误。
4.4 类别ID和data.yaml对不上
现象:训练 loss 很正常,但预测时类别标签张冠李戴,比如把成熟草莓全部识别成未成熟,而成熟那一类的 mAP 是 0。
原因:VOC XML 里的类别是字符串,YOLO txt 里是数字,中间映射出错不会报任何错。最常见的错误是转换脚本里字典顺序和 data.yaml 里 names 顺序不一致,或者统计 txt 时把某个类别跳过了。
解决:训练前写一个统计脚本,把 labels 目录下所有 txt 的第一个数字汇总成集合,确认只有 0、1、2,再对照 data.yaml 里的 names 逐项检查“0 号是哪个类别”。我会把这个检查写进数据集预处理流程里,每次换数据集都跑一遍,属于“10 分钟换 1 天”的投入。
4.5 412张小数据集上bn崩溃和过拟合
现象:训练到中途 loss 突然变成 nan,或者 train loss 很低但 val loss 很高,验证集 mAP 曲线像心电图一样剧烈抖动。
原因:batch 太小导致 BatchNorm 的均值和方差估计不稳,是 nan 的主要来源;验证集太少则会让 mAP 对个位数的误检非常敏感。412 张数据集如果验证集只留 30 张,每次随机验证顺序不同,指标就会上下跳 3 到 5 个点。
解决:batch 下限设为 16,显存不够就降低 imgsz 而不是 batch;验证集至少保留 15% 以上的数据;把mosaic=1.0开起来,等于让模型每轮都看新组合图,缓解过拟合;最后用patience早停,让小数据集在过拟合前就停下。
提示:bn 崩溃还有一个隐蔽来源——数据里混入了一张纯黑或纯白的坏图。训练前遍历一遍图片,把尺寸异常或无法解码的文件删掉,再开始训练。
5. 412张不够用:在线增强、格式复用到滑窗切图的三条扩样路线
412 张图能把基线跑通,但离“采摘机器人实际部署”还有距离。这个量级的公开数据集更适合做方法验证和预实验,正式项目一般会在它基础上补数据。补数据不一定等于请人标注,先走这三条低成本路线,多数场景能顶一阵。
5.1 别急着加数据:先把YOLOv8自带的在线增强参数拉到合理位置
成熟度检测和通用目标检测有一点本质区别:颜色是草莓成熟度的核心语义,而 YOLO 自带的数据增强默认会大改颜色。所以增强参数要“保颜色、变姿态”,把增强火力集中在位置、尺度和光照鲁棒性上:
yolo detect train \ data=strawberry.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=200 \ batch=16 \ hsv_h=0.02 hsv_s=0.8 hsv_v=0.5 \ degrees=5 translate=0.1 scale=0.4 shear=2 \ fliplr=0.5 mosaic=1.0 mixup=0.2逻辑说明:mosaic=1.0表示每轮训练都有概率把 4 张图拼成 1 张,等于每轮数据量翻了 4 倍,是小数据集最容易吃到红利的一项增强。mixup=0.2是两张图按比例混合,对目标分布不均匀的场景有帮助。degrees=5把旋转限制在 5 度以内——草莓挂在植株上,大幅旋转在真实场景里不存在,只会造出模型学不会的样本。
参数说明里值得注意的三点:
hsv_h不要超过 0.05,否则红草莓可能被增强成黄草莓,成熟度颜色语义直接被破坏。hsv_s和hsv_v可以给大一点,大棚里不同时段的光照差异,用饱和度和明度扰动就能模拟。translate=0.1、scale=0.4是针对果实位置和大小扰动的,草莓在画面里出现的位置和距离变化很大,这两项比旋转更值得加。fliplr=0.5放心用,草莓没有左右语义,水平翻转不会改变成熟度属性。
5.2 VOC中间格式是后悔药:把XML转成COCO或其它框架的通用套路
标题里说数据集是 VOC+YOLO 双格式,意味着你手头有两份标注,随便用哪份都能喂给主流框架。但如果你自己准备数据,我强烈建议把 VOC 的 XML 当“母版”长期保留,因为它是纯 XML 加像素坐标,没有归一化依赖,以后转任何格式都有后悔药。
比如要转到 COCO 格式,用下面这段代码:
import xml.etree.ElementTree as ET import json def voc_to_coco(xml_files, categories): coco = {"images": [], "annotations": [], "categories": categories} img_id, ann_id = 0, 0 for xml_path in xml_files: tree = ET.parse(xml_path) root = tree.getroot() width = int(root.findtext(".//size/width")) height = int(root.findtext(".//size/height")) coco["images"].append({ "id": img_id, "file_name": root.findtext("filename"), "width": width, "height": height }) for obj in root.findall("object"): name = obj.findtext("name") cat_id = [c["id"] for c in categories if c["name"] == name][0] box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) w = float(box.findtext("xmax")) - xmin h = float(box.findtext("ymax")) - ymin coco["annotations"].append({ "id": ann_id, "image_id": img_id, "category_id": cat_id, "bbox": [xmin, ymin, w, h], "area": w * h, "iscrowd": 0 }) ann_id += 1 img_id += 1 return coco逻辑说明:COCO 的 bbox 格式是“左上角 x、左上角 y、宽、高”,和 VOC 的“左上右下”只差一步减法,注意别把xmax - ymin这样的混搭写进去。这段代码的意义在于:XML 是公共中间层,目标格式只是换壳,源数据不动,随时能再导出一份。
5.3 大图里的草莓靠滑窗切图扩样本:什么时候值得用
如果你手里的草莓原图是整棵植株的大图,单颗果实只有几十像素,YOLOv8 直接训练基本漏检。滑窗切图是这时候最实用的扩样手段:把大图切成若干 patch,每个 patch 当作一张新图,重新生成标注。
import cv2 def sliding_window(img_path, window_size=960, stride=480): image = cv2.imread(img_path) h, w = image.shape[:2] patches = [] for y in range(0, h - window_size + 1, stride): for x in range(0, w - window_size + 1, stride): patches.append(image[y:y + window_size, x:x + window_size]) # 处理图像边缘:右侧和下侧不足窗口大小的部分,取最右下角一块 if h % stride or w % stride: patches.append(image[h - window_size:h, w - window_size:w]) return patches逻辑说明:切出来的 patch 在送入训练前,要把原 VOC 标注里的像素坐标减去 patch 左上角偏移量,再把超出 patch 边界的框截断或丢弃。window_size=960、stride=480意味着相邻 patch 有一半重叠,同一个目标会出现在多个 patch 的不同位置,等于给目标位置加了平移扰动,这也是增强的一部分。
参数说明:滑窗切图有一个必须避开的坑——同一张大图切出的 patch 高度相关,拆分 train/val 时必须按原图划分,不能按 patch 划分。否则 validation 里会出现大量和训练 patch 重叠的内容,mAP 虚高,换到真实场景立刻现原形。
6. 检查这套数据集最终效果:直接看框、看混淆矩阵、再看漏检
先说结论:412 张 VOC+YOLO 格式的草莓成熟度数据集,最合适的定位是“固定基准”。你用它跑出来的 mAP 不是终点,而是对标后续所有改进的起点。所以训练完别急着打包模型,按下面三步验收。
第一步,用固定评估脚本跑一遍验证集,记录 mAP50、mAP50-95、precision、recall 四个数:
yolo detect val \ model=runs/strawberry/exp1/weights/best.pt \ data=strawberry.yaml第二步,把分类错误集中看一遍。val 结束后,ultralytics 会生成混淆矩阵图confusion_matrix.png。注意看对角线,再看第一行——第一行通常是背景被误检为目标的数量。如果背景误检太多,把 conf 阈值从 0.25 提到 0.4 能明显改善。
第三步,做漏检检查。跑完预测后,挑出置信度在 0.3 到 0.5 之间的检测结果,这类框是“模型不确定但又不算错”的灰色地带。在草莓成熟度这种细粒度分类场景里,半熟和成熟的边界本来就是模糊的,真正要警惕的不是边界模糊,而是模型系统性地漏检某一类——比如全绿的小果从来没被召回,那就要回到第 5.1 节的在线增强,把高饱和度的训练样本比例提上去。
如果要把这套模型真正落地成采摘机器人的感知模块,我的做法是导出 ONNX 再部署:
yolo export model=runs/strawberry/exp1/weights/best.pt format=onnx imgsz=640导出后用 onnxruntime 跑一遍推理,确认输出格式没变化再接控制流程。注意 YOLOv8 导出的 ONNX 默认输出形状是(1, 84, 8400),84 是 4 个框坐标加 80 个 COCO 类别数;换成你这个 3 类数据集后会变成(1, 7, 8400),后处理解析时别按固定 84 去切。
说一个我自己的血泪经验:有次用一个 200 来张的小数据集做验证,为了把 mAP 刷高,把验证集缩到 30 张。结果同一套权重换两次随机种子,mAP 能差出 5 个点,当时还以为是模型问题。后来把验证集固定下来,所有对比都用同一份 train/val 拆分,实验才稳下来。所以拿到这个 412 张的数据集,第一步别再重新随机拆分了——固定一套拆分,所有实验基于同一份数据,你的每个改进才有可比性。希望帮到你。
本文还有配套的精品资源,点击获取