简介:基于YOLOv5的草莓目标检测项目,源自“猛犸杯”比赛赛题,因赛事主办方提供的数据量非常少,数据预处理与增强便成为提升模型准确率的关键。项目定位于人工智能、计算机、通信工程、自动化等专业方向,适用于在校学生、教师或企业开发者的毕业设计、课程设计、作业、竞赛练习及初期项目演示,同时对目标检测初学者也足够友好。压缩包共包含507个文件,整体大小约979.54MB,其中最主要的是402张草莓图像和100个XML格式标注文件,可直接组成训练集与验证集;另附1个Jupyter Notebook主程序、1个Markdown说明文档,以及少量DB缓存文件;图像与标签一一对应,目录结构清晰,便于快速启动YOLOv5训练流程。已有200人浏览学习,代码均经过完整测试并成功运行,项目在答辩评审中平均分达到96分,可靠性较高;针对赛题数据不足的问题,源码中给出了具体的数据预处理与增强方案,能有效改善小样本下的检测效果,帮助使用者复现赛题结果;用户也可在此基础上修改和扩展,用于其他目标检测任务或教学展示。
1. 为什么是 YOLOv5 + 几百张草莓图,而不是一上来换 YOLOv8
参加过这类“给定几百张草莓图像”的赛题选手大多有个同感:跑通 YOLOv5 只是开胃菜,真正卡人的是数据标注边界、小目标漏检和超参数选择。草莓果实小、青果与叶片颜色接近、果子之间互相遮挡,如果直接照搬默认配置训练,mAP@0.5 可能只有 0.35 上下,而同组选手却能冲到 0.75 以上。拉开差距的往往不是网络结构,而是有没有把“目标检测流程”里数据准备和验证调参两个环节做扎实。
YOLOv5 在这类基于 YOLOv5 的水果识别场景里仍然是最稳的选择。它有成熟的数据增强管线、自动 anchor 计算、轻量级权重和多尺度训练开关,对几百张的小样本数据集非常友好;直接换 YOLOv8 反而要处理更多如果没有源码级修改就不直观的分布式训练配置。这篇文章按比赛赛题项目落地路径来写:从数据集整理、VOC/COCO 转 YOLO 标签,到环境配置、最小训练命令、验证指标解读,再到比赛提交前的结果导出与小目标后处理,每一步都给可以直接复制的命令和参数。
2. 草莓图像数据集整理与 YOLO 标注格式转换
2.1 训练集/验证集目录结构与 data.yaml 先定好
YOLOv5 训练自己的数据集时,不会自动读 XML 或 JSON,它期望images/和labels/两个目录,且同名图片和 txt 标签一一对应。我一般会在比赛数据下发后先建好固定目录,避免后面训练时反复改路径。目录结构如下:
datasets/strawberry/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ └── val/训练和验证图片分别放在images/train与images/val,对应的标注文本放在labels/train与labels/val。测试集可以不放标签,因为最终要用训练好的权重去 predict。几百张数据不要直接按文件名乱序划分,建议按“同株草莓的连续帧”分到同一集合,避免模型在验证时撞见训练图片的相似背景,导致指标虚高。
接着在yolov5项目根目录下建一个数据集配置文件,比如strawberry.yaml:
path: ../datasets/strawberry # 相对于 yolov5 项目目录的路径 train: images/train val: images/val nc: 2 names: 0: green_strawberry 1: red_strawberry这里path指向图片根目录,train和val是相对于该根目录的子目录名,也可以直接写绝对路径。nc是类别数,names里的顺序必须与标注 txt 中第一列的类别 ID 严格一致;如果比赛赛道只要求检测“草莓”一个类别,就把nc改为 1,names只写strawberry。类别 ID 从 0 开始,这一步出错时训练不会报错,但验证阶段的混淆矩阵会变得不可解释。
2.2 用脚本把 VOC 或 COCO 标注转成 YOLO txt 标签
比赛给的原始数据不一定直接是 YOLO 格式,常见的是 VOC 的 XML 文件。手工在 LabelImg 里重标一遍几百张图耗时太长,正确做法是写个转换脚本。下面这段 Python 可以将 VOC XML 转为 YOLO 需要的归一化 txt:
import xml.etree.ElementTree as ET import os classes = ["green_strawberry", "red_strawberry"] def convert_annotation(xml_path, out_dir, classes): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = float(size.find("width").text) img_h = float(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in classes: continue cls_id = classes.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) xmax = float(box.find("xmax").text) ymin = float(box.find("ymin").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") txt_path = os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] + ".txt") with open(txt_path, "w") as f: f.write("\n".join(lines)) xml_dir = "datasets/strawberry/annotations" label_dir = "datasets/strawberry/labels/train" for xml_file in os.listdir(xml_dir): if xml_file.endswith(".xml"): convert_annotation( os.path.join(xml_dir, xml_file), label_dir, classes )脚本先把图片宽高从<size>节点中读出来,再将xmin/xmax/ymin/ymax像素坐标换算为中心点加宽高的归一化值。YOLO label 的每个字段是类别ID x_center y_center width height,其中坐标全部除以原图宽高。如果原始数据是 COCO 的 JSON,bbox字段为[x, y, width, height],此时只需要把x_center换成x + width/2再除以图片宽,y_center同理,不要直接把 json 的x当成归一化中心。
转换后随机抽查几个 txt 文件,确认没有空文件。另外要注意标签里有difficult=1的物体:比赛评测一般不区分难度,建议保留并且正常归一化,删掉会让模型在困难样本上的召回率虚高。
2.3 数据集质量检查的 3 个常见坑
表格里列出的三个问题,是我在处理几百张小样本数据集时最容易遇到的,每类都会直接影响训练收敛:
| 常见问题 | 表现 | 检查方法 |
|---|---|---|
| 坐标越界(大于 1 或小于 0) | 训练 loss 震荡,mAP 不升 | 直接用脚本遍历所有 txt 判断每行的第 2~5 个字段是否在[0,1] |
| 空标签文件 | 图片参与训练但没有监督信号 | find labels -name "*.txt" -size 0列出并删除空文件 |
类别 ID 与data.yaml不一致 | 混淆矩阵出现跨类别噪点 | 统计每个 txt 第一个字段,额外检查是否有值等于nc |
第二点比较容易忽略:YOLOv5 会跳过没有标签的图片,但如果你在训练前已经用--data指向了包含空标签的目录,它默认仍会用空目录构建 dataset cache,导致部分图片意外参与 mosaic 增强但不产生任何 loss。我一般在训练前直接清掉空文件:
find datasets/strawberry/labels -name "*.txt" -size 0 -delete第三点中的“ID 等于 nc”意味着标注文件里出现了 2,但nc却配置成 2,这样索引越界,训练会在正常迭代几十个 epoch 后突然报错。解决方法是统一改用脚本遍历标签,不要靠人工记类别表。
3. YOLOv5 环境配置与草莓训练最小命令
3.1 YOLOv5 环境配置:Python 版本与 PyTorch 安装顺序
无论你手头是 NVIDIA 显卡还是纯 CPU 环境,都要先把 Python 环境隔离开。我习惯用 conda 创建yolov5专用环境,Python 版本不必追求最新,3.9 最稳,因为部分老版本requirements.txt里的lap、tensorboard在 Python 3.11 上容易出现预编译包缺失。
conda create -n yolov5 python=3.9 -y conda activate yolov5 cd yolov5 # 进入你已经 clone 下来的 YOLOv5 项目目录 pip install -r requirements.txtrequirements.txt里已经列好 torch、torchvision、opencv-python、numpy 等依赖。需要注意的是,这里默认安装的 PyTorch 版本会从 pip 源拉取 CPU 或 CUDA 版本的二进制文件,但 CUDA 驱动与 PyTorch 的版本若不对齐,torch.cuda.is_available()会返回 False。常见做法是:先确认nvidia-smi能输出驱动版本,再根据对应 CUDA 版本到 PyTorch 官网选择安装命令。例如本机是 CUDA 11.8,就执行:
pip install torch==2.0.0 torchvision==0.15.0 --index-url https://download.pytorch.org/whl/cu118这里的--index-url指定了 PyTorch 的 CUDA 预编译索引,后续pip install -r requirements.txt时会自动忽略已经被满足的 torch 包,不会重复安装成 CPU 版。装完用python -c "import torch; print(torch.cuda.is_available())"确认输出为 True,再进入训练环节。
3.2 准备预训练权重与修改草莓数据集配置
YOLOv5 的迁移学习思路是从 COCO 预训练权重出发,再在自己的草莓数据集上微调。对几百张的赛题数据来说,预训练权重是稳住收敛的重要保障,不要从随机权重开始从头训练。下载yolov5s.pt放在项目根目录;如果你显存不大,用yolov5s已经足够,因为它比yolov5m快很多,而小样本场景下大模型反而更容易过拟合。
在训练前确认strawberry.yaml中path路径写对。我将上一章配置的yaml放在yolov5项目根目录下,因此path: ../datasets/strawberry可以直接从项目目录跳到同一级数据集目录。train和val的路径不要再加images/前缀,因为 YOLOv5 会自动拼接path与train/val字段,若重复写会得到不存在的目录。
3.3 启动训练的最小命令与参数含义
数据准备完成后,用下面的最小命令训练:
python train.py \ --data strawberry.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 120 \ --cache \ --workers 4--img 640表示输入分辨率长边为 640,这是 YOLOv5 的默认值,对中等大小的草莓图像足够;如果草莓在原始图像里只占 20 像素左右,可以把--img 896或--img 1280,但显存占用会明显上升。--batch 16在 8GB 显存的 RTX 3060/3070 上比较安全;显存小的机器降到--batch 8同时开启梯度累积,或者关掉--cache。--epochs 120对小数据集偏多,但 YOLOv5 默认有patience=100的早停机制,实际未必跑满。--cache把图片提前加载进内存,几百张图内存占用不大,能省去每轮读取磁盘的时间。
训练开始后,项目根目录会生成runs/train/exp,里面包含results.png、weights/best.pt和weights/last.pt。last.pt是最后一个 epoch 的权重,best.pt是验证集 mAP 最高时的权重,比赛提交一律用best.pt。如果训练中 loss 曲线出现先降后升的“沟状反弹”,说明学习率偏大,应当调低--hyp里的lr0。
3.4 训练日志里最值得盯的两个指标
训练过程中不建议频繁看图片输出,重点看两条曲线:训练边界框损失box_loss和验证集mAP@0.5。box_loss反映了预测框与真实框的坐标回归误差;草莓检测里大量小目标,box_loss容易在小数点后三位波动,只要整体趋势向下就不用干预。mAP@0.5在头 20 个 epoch 通常会快速爬到 0.5 以上,随后进入平台期;如果它在某个阈值附近反复震荡,说明目标尺度分布不均匀,需要调整训练分辨率或数据增强参数。
4. 小目标检测场景下的验证与 YOLOv5 超参数调优
4.1 用 val.py 输出验证集 mAP 与 per-class 指标
训练完成后不要直接去跑 detect.py,先对验证集做一次完整评估。val.py会输出每个类别的 Precision、Recall 和 mAP,还会生成混淆矩阵图confusion_matrix.png,这对找草莓检测的误报来源非常直接。推荐命令:
python val.py \ --data strawberry.yaml \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --task val终端会打印一张表,含义如下:
| 指标 | 说明 | 草莓赛题里怎么用 |
|---|---|---|
| Precision | 预测框中有多少是真的草莓框 | 青果误检高时偏低 |
| Recall | 真实草莓中有多少被召回 | 遮挡严重时偏低 |
| mAP@0.5 | IoU=0.5 下的平均 AP | 比赛通常以它排名 |
| mAP@0.5:0.95 | IoU 从 0.5 到 0.95 取平均 | 小目标评测更严格 |
mAP@0.50是比赛成绩最直接的参考,但如果赛题规定用 COCO 的mAP@0.5:0.95,你就要把小目标检测的质量放在第一优先级。查看这次验证的日志会发现green_strawberry与red_strawberry两行的 AP 差很大;绿色草莓与叶片颜色太近,误检多,调参方向应侧重提升召回率,比如降低置信度阈值时做额外的 NMS 合并。
4.2 修改 YOLOv5 超参数提升草莓召回率
YOLOv5 的数据增强开关集中在data/hyps/hyp.scratch-low.yaml中。训练时用--hyp指定这个文件,直接修改里面对应的字段即可。针对草莓小目标且背景复杂的特点,我一般会做如下调整:
lr0: 0.003 # 默认0.01,小数据集用更小学习率防震荡 lrf: 0.12 # 最终学习率为初始学习率乘0.12 mosaic: 1.0 # Mosaic 增强,把4张图拼接再缩放,对小目标有帮助 mixup: 0.2 # 混合两张图训练,默认通常为0 copy_paste: 0.1 # 把草莓实例复制到新图上,增加遮挡样本其中mosaic对草莓检测提升最明显,它把四张训练图缩放后拼成一张,让模型在训练时看到大量“小草莓”语义,缓解小目标占比不足的问题。mixup会把两张图的像素按比例混合,标签也做同比例的混合;它适合叶片遮挡严重的场景,但mixup过大会让模型学到不真实的纹理,因此 0.2 左右比较安全。copy_paste是 YOLOv5 7.0 里已有的实例复制增强,它会从本批次里随机挑选草莓 mask 粘贴到其他图片上,相当于免费扩充几百张数据集。
修改之后重新训练时,不要再覆盖原来的runs/train/exp,可以在train.py后加上--name strawberry-tune:
python train.py \ --data strawberry.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 120 \ --hyp data/hyps/hyp.scratch-low.yaml \ --name strawberry-tune4.3 针对草莓目标过小的两个进阶开关
如果验证集上大量草莓框被漏掉,检查一下标签里目标框的像素宽度分布。常见做法是写个小脚本统计所有标注框的宽高,如果中位数小于 15 像素,说明原始分辨率不够,需要打开 YOLOv5 的多尺度训练:
python train.py --img 896 --batch 8 --multi-scale --name strawberry-ms--multi-scale会在每个 epoch 随机缩放输入尺寸,让模型见过更大范围的尺度空间。代价是训练速度变慢,所以 batch 要相应降低。另一个开关是关闭自动 anchor 计算,使用手动 Kmeans 重新生成针对草莓尺度的 anchor:在train.py命令后加--noautoanchor即可。
锚框对密集小目标的影响经常被人忽略。YOLOv5 默认 COCO anchor 偏向中等物体,自动计算会基于你的标签分布重新聚类,但它只统计训练集的框,如果训练集里草莓有大有小且数量不均,聚类出来的 anchor 可能全压在大目标上。用--noautoanchor后,可以结合utils/autoanchor.py里的check_anchors输出查看 anchor 与标签的匹配度,匹配度低于 0.8 时手动增加一组更小的 anchor。
5. 比赛提交前的结果导出与最后处理
5.1 用 detect.py 批量输出草莓检测到 txt
比赛测试集通常不带标注,我们需要用训练好的best.pt跑一遍推理。YOLOv5 的detect.py默认会画框保存图片,但提交评测只需要标签,因此运行时加上--save-txt和--save-conf:
python detect.py \ --weights runs/train/exp/weights/best.pt \ --source ../datasets/strawberry/test/images \ --img 640 \ --save-txt \ --save-conf \ --project runs/detect \ --name submission每张测试图片会在runs/detect/submission/labels下生成同名 txt,每一行是class_id conf x_center y_center width height。注意这里的class_id与训练时的names对应,比如 0 代表green_strawberry,1 代表red_strawberry,提交前要确认比赛方需要的类别顺序是否一致。
5.2 把归一化 txt 转换成比赛 JSON 格式
多数比赛要求提交 COCO 风格的 JSON,而不是 YOLO txt。常见做法是在推理后做一次字段拼接:txt 里是归一化中心坐标,需要拿着原图宽高还原成像素级别的[x, y, w, h]。下面这段脚本把labels目录下的 txt 转换成可提交的 JSON:
import os import json IMG_DIR = "../datasets/strawberry/test/images" LABEL_DIR = "runs/detect/submission/labels" results = [] for txt_file in sorted(os.listdir(LABEL_DIR)): image_name = txt_file.replace(".txt", ".jpg") img_path = os.path.join(IMG_DIR, image_name) from PIL import Image w, h = Image.open(img_path).size with open(os.path.join(LABEL_DIR, txt_file)) as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) score = float(parts[1]) cx, cy, bw, bh = map(float, parts[2:]) x = (cx - bw / 2) * w y = (cy - bh / 2) * h box_w = bw * w box_h = bh * h results.append({ "image_id": int(image_name.split(".")[0]), "category_id": cls_id + 1, "bbox": [x, y, box_w, box_h], "score": score }) with open("submission.json", "w") as f: json.dump(results, f, indent=2)脚本里category_id加 1,是因为 COCO 类别从 1 开始,而 YOLO 类别从 0 开始。image_id必须对应测试图片名中的数字,如果文件名是test_001.jpg,可以使用正则提取数字,但一定保证 json 里的image_id与评测系统一致,否则所有框都会被丢弃。
5.3 推理加速与空结果兜底
比赛时间紧张时,可以用--half开启 FP16 推理,在支持半精度的 GPU 上能带来接近一倍的提速:
python detect.py --weights best.pt --source test/images --save-txt --save-conf --halfbest.pt如果是在 CPU 上训练出的,半精度推理可能不稳定,建议只对 GPU 训练后的权重使用。另一个容易出问题的地方是模型在部分测试图上没有输出任何框。评测系统一般不允许该图片的预测结果为空,这时可以保留所有预测里置信度最高的框,并把score改成极小值如 0.01,避免整图被判漏。处理完 json 后再检查一遍category_id是否从 1 开始,否则评测会全部判 0 分。
本文还有配套的精品资源,点击获取