☰
基于YOLOv5的茶叶目标检测实战:从数据标注到树莓派部署
2026/10/2 13:06:51 网站建设 项目流程

简介:这份资源面向计算机视觉入门与进阶学习者,以及需要落地农产品检测场景的开发者,提供一套基于YOLOv5的茶叶目标检测完整项目实战方案。包内共95个文件,以41个yaml配置文件、34个Python源码为主,辅以5个shell脚本、4份markdown说明文档、2个txt依赖清单及Dockerfile等部署文件,压缩包约242KB,结构紧凑、便于快速搭建训练与推理环境。内容涵盖模型结构定义、数据增强、损失计算、指标评估、训练与检测脚本等核心模块,并附带流程教程与notebook示例,可帮助读者理解从数据配置、模型训练到推理部署的完整链路,掌握目标检测在茶叶识别任务中的调参思路与排错方法。目前已有513人学习下载,适合希望以真实项目巩固YOLOv5实践能力的中高级学习者参考。

1. 茶叶目标检测到底在检什么:从一片鲜叶到 YOLOv5 的落地边界

茶叶目标检测,说白了就是让模型在一张茶园或传送带照片里,把「嫩芽」「一芽一叶」「一芽二叶」「老叶」这些目标框出来并分类。它和通用目标检测最大的区别在于:目标极小、颜色高度接近、遮挡密集,而且同一株茶树上不同等级嫩芽的形态差异可能只有几毫米。基于 YOLOv5 实现茶叶目标检测算法,是当前性价比最高的一条路——YOLOv5 的 anchor 机制对小目标友好,训练速度快,部署到树莓派 5 或 Jetson 这类边缘设备也有成熟路径。这套方案适合三类人:想用 YOLOv5 训练自己数据集的算法初学者、做智慧农业采摘机器人的嵌入式工程师、以及需要茶叶分级质检的产线开发者。但我要先把边界说清楚:YOLOv5 不是万能的,茶叶目标检测的难点不在模型结构,而在数据标注一致性和小目标特征保留,这两点决定了你的 mAP 能不能过 0.7。

2. 数据准备与标注:茶叶数据集怎么建才不白干

2.1 茶叶目标检测的类别定义与采集策略

茶叶目标检测翻车最多的环节不是训练,而是标注。我见过太多人拿手机在茶园随手拍几百张,标完训练出来 mAP 只有 0.3,回头一看类别定义就错了。茶叶的检测类别必须和你的下游任务绑定:如果是采摘机器人,类别应该是「可采摘芽头」和「不可采摘」两类;如果是分级质检,类别要细到「单芽」「一芽一叶」「一芽二叶」「对夹叶」「老叶」五类。类别定义一旦确定,整个数据集不能中途改,改一次就得全部重标。

采集时要注意三个硬指标。第一,光照覆盖要全,晴天顺光、晴天逆光、阴天散射光、傍晚弱光各占一定比例,否则模型一到傍晚就瞎。第二,拍摄距离要固定,建议 30cm 到 50cm,太远目标只有几个像素,YOLOv5 的 P3 特征图也救不回来。第三,背景要多样,纯茶园背景训练出来的模型换到竹筛或传送带上直接崩。常见做法是每个类别至少 300 张有效样本,五类就是 1500 张起步,低于这个数别指望 mAP 好看。

2.2 用 labelImg 标注茶叶并转成 YOLO 格式

标注工具用 labelImg 就够了,别一上来就上 CVAT,学习成本不划算。安装和启动命令如下:

pip install labelImg labelImg

启动后按w画框,选类别,保存为 Pascal VOC 的 XML 格式。但 YOLOv5 要的是归一化的 txt 格式,每行是类别索引 x_center y_center width height,全部除以图像宽高归一化到 0 到 1。转换脚本我一般自己写一个,逻辑清晰可控:

import os import xml.etree.ElementTree as ET # 类别顺序必须和 data.yaml 里的 names 完全一致 classes = ["single_bud", "one_bud_one_leaf", "one_bud_two_leaves", "mature_leaf", "old_leaf"] def convert(xml_dir, txt_dir, img_w, img_h): os.makedirs(txt_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() # 图像宽高优先从 XML 读,读不到再用传入值兜底 size = root.find("size") w = int(size.find("width").text) if size is not None else img_w h = int(size.find("height").text) if size is not None else img_h lines = [] for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name not in classes: continue # 类别写错直接跳过,避免索引越界 cls_id = classes.index(cls_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) # 归一化并转成中心点加宽高 x_center = (xmin + xmax) / 2.0 / w y_center = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") with open(os.path.join(txt_dir, xml_file.replace(".xml", ".txt")), "w") as f: f.write("\n".join(lines)) convert("./annotations", "./labels", 640, 640)

这段代码的关键参数是classes列表,顺序必须和后面data.yaml的names一一对应,错一位整个训练就废了。归一化用六位小数是 YOLOv5 官方推荐精度,少了会有累积误差。转换完一定要抽查几张,用cat看 txt 内容,确认坐标都在 0 到 1 之间,出现大于 1 的值说明 XML 里的宽高读错了。

2.3 数据集划分与 data.yaml 配置

划分比例我一般用 8:1:1,训练集、验证集、测试集。茶叶数据量本来就不大,验证集别低于 10%,否则 mAP 波动大到没法判断模型好坏。目录结构按 YOLOv5 的约定来:

dataset/ images/ train/ val/ test/ labels/ train/ val/ test/

然后写data.yaml:

path: ./dataset train: images/train val: images/val test: images/test nc: 5 names: ["single_bud", "one_bud_one_leaf", "one_bud_two_leaves", "mature_leaf", "old_leaf"]

nc是类别数,必须和names长度一致。这里有个血泪经验:path用相对路径时,训练命令必须在path的上一级目录执行,否则 YOLOv5 找不到图片,报错信息还特别隐晦,只说找不到 label,其实是路径拼接错了。

3. YOLOv5 环境配置与训练:从 conda 到第一个权重文件

3.1 conda 环境与 YOLOv5 依赖安装

环境配置是新手第一道坎。我推荐 conda 建独立环境,别在 base 里折腾:

conda create -n tea_yolo python=3.8 -y conda activate tea_yolo git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt

Python 3.8 是 YOLOv5 兼容性最好的版本,3.10 以上有些 torch 版本会出玄学问题。requirements.txt里会自动装 torch,但如果你有 GPU,建议先单独装对应 CUDA 版本的 torch,再装其余依赖,否则可能装成 CPU 版,训练时torch.cuda.is_available()返回 False,白等一晚上。验证环境:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU only")

三行输出确认版本、CUDA 可用性、显卡型号。如果第三行是 CPU only,先解决驱动和 torch 版本匹配问题,别急着开训。

3.2 用 YOLOv5 训练茶叶数据集的完整命令

训练命令看着简单,参数才是决定成败的地方:

python train.py \ --data ./data/tea.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --hyp data/hyps/hyp.scratch-low.yaml \ --project runs/train \ --name tea_exp1 \ --cache

逐项说。--weights yolov5s.pt用预训练权重,茶叶数据量小,从头训基本没戏,迁移学习是标配。--img 640是输入分辨率,茶叶目标小,理论上 1280 更好,但显存翻四倍,8G 显存跑 640 是稳妥选择。--batch 16根据显存调,显存不够就降到 8,但 batch 太小 BN 层统计不稳,mAP 会抖。--epochs 200是上限,实际看早停,YOLOv5 默认 patience 是 100 轮没提升就停。--hyp用 low 增强配置,茶叶数据集小,强增强容易过拟合到增强后的伪特征。--cache把图片缓存到内存,小数据集能明显加速,但数据集超过 10G 别开,内存会爆。

训练过程中重点看三个指标:box_loss应该稳定下降,如果震荡剧烈说明学习率太大;mAP@0.5是主指标,茶叶检测能到 0.75 以上算合格;precision和recall要平衡,recall 太低说明漏检多,采摘场景漏检比误检更致命。

3.3 超参数怎么调:茶叶小目标的三个必调项

YOLOv5 超参数几十个,茶叶检测真正要动的就三个。第一,anchor尺寸。茶叶目标普遍偏小,默认 anchor 是基于 COCO 的,偏大。用python utils/autanchor.py --data data/tea.yaml重新聚类,生成的 anchor 替换模型配置里的默认值,小目标召回能涨 3 到 5 个点。第二,lr0初始学习率,默认 0.01,茶叶数据集小,我一般降到 0.005,配合lrf0.1 做余弦退火,训练更稳。第三,mosaic增强概率,默认 1.0,茶叶目标密集时 mosaic 拼四张图会让目标更小更难学,我一般降到 0.5,后期再关掉。

改超参在hyp.scratch-low.yaml里改,别直接改源码。改完重新训,对比 mAP 曲线,一次只改一个参数,否则你不知道是哪个起了作用。这是调参的基本纪律,很多人一次改五个,涨了不知道为啥涨,跌了不知道为啥跌,纯靠运气。

4. 推理、部署与效果验证:模型训完只是开始

4.1 用 detect.py 跑通茶叶图片和视频推理

训练完拿到best.pt,先别急着部署,用detect.py验证:

python detect.py \ --weights runs/train/tea_exp1/weights/best.pt \ --source ./test_images \ --img 640 \ --conf-thres 0.4 \ --iou-thres 0.45 \ --save-txt \ --project runs/detect \ --name tea_test

--conf-thres 0.4是置信度阈值,茶叶检测我一般设 0.4 到 0.5,太低误检多,太高漏检多,按场景权衡。--iou-thres 0.45是 NMS 的 IoU 阈值,茶叶目标密集时这个值要调低到 0.3 到 0.4,否则相邻芽头会被 NMS 误删。--save-txt把检测框坐标存下来,方便后续做计数或分级统计。推理结果重点看两类错误:把老叶误检成嫩芽,说明类别特征没学好,回去检查标注;密集区域漏检,调低 iou-thres 或重新聚类 anchor。

4.2 树莓派 5 上部署自己训练的 YOLOv5 模型

边缘部署是茶叶检测真正落地的场景。树莓派 5 性能比 4 强不少,但直接跑 PyTorch 版 YOLOv5 还是吃力,常见做法是转 ONNX 再用 onnxruntime 推理:

python export.py --weights best.pt --include onnx --img 640 --opset 12

导出 ONNX 后,在树莓派上装onnxruntime,写推理脚本。关键参数是opset 12,版本太高树莓派的 onnxruntime 可能不支持。树莓派 5 上 640 分辨率单帧推理大概 200 到 400ms,做实时采摘引导勉强够用,要更快就降到 416 分辨率,精度会掉几个点。部署时注意散热,树莓派 5 满载跑推理温度能上 80 度,不加风扇会降频,推理时间翻倍。

4.3 茶叶检测模型的验证指标与误检分析

验证不能只看 mAP。茶叶检测要额外看三个指标:单类 AP,看哪个类别拖后腿;混淆矩阵,看类别之间怎么混的;以及实际业务指标,比如采摘场景的漏检率。跑验证:

python val.py \ --weights best.pt \ --data ./data/tea.yaml \ --img 640 \ --conf-thres 0.001 \ --iou-thres 0.6 \ --task test

--conf-thres 0.001是验证时的低阈值,为了算完整的 PR 曲线,别用推理时的 0.4。--task test指定在测试集上跑,别用验证集,验证集参与过调参,指标会虚高。跑完看runs/val下的混淆矩阵图,如果one_bud_one_leaf大量被预测成one_bud_two_leaves,说明这两类标注边界模糊,回去统一标注标准,而不是调模型。

5. 避坑与排查:茶叶目标检测最常见的五个翻车现场

5.1 训练 loss 不降反升

现象:训练前几十轮box_loss和obj_loss都在涨,mAP 一直是 0。原因通常是学习率太大或者标注文件格式错。先检查 labels 目录下的 txt 是不是归一化坐标,有没有出现大于 1 的值。再检查data.yaml的nc和names长度是否一致。如果都正常,把lr0从 0.01 降到 0.001 再试。还有一种隐蔽情况:图片和 label 文件名没对上,YOLOv5 找不到 label 会当成背景图训练,loss 也会异常。

5.2 mAP 卡在 0.5 上不去

现象:训练正常,loss 在降,但 mAP@0.5 到 0.5 就平了。原因多半是 anchor 不匹配小目标。茶叶目标在 640 分辨率下可能只有 20 到 40 像素,默认 anchor 最小是 10x13,勉强够,但聚类后的 anchor 更贴合。跑 autanchor 重新生成,替换yolov5s.yaml里的 anchors 字段。另一个原因是数据量不够,五类各 300 张是底线,低于这个数模型学不到类间差异,加数据比调参有用。

5.3 推理时类别全预测成同一类

现象:detect.py 跑出来所有框都是single_bud。原因是data.yaml的names顺序和训练时不一致,或者转换脚本里classes列表顺序变了。YOLOv5 的类别索引是硬编码的,顺序错一位,所有预测都错位。解决方法是训练和推理用同一份data.yaml,转换脚本的classes从data.yaml读,别手写两份。

5.4 树莓派上推理报 onnxruntime 版本错误

现象:导出 ONNX 后在树莓派上加载报Unsupported model IR version。原因是导出时 opset 版本太高,树莓派上装的 onnxruntime 版本旧。解决方法是导出时指定--opset 12,或者升级树莓派的 onnxruntime。如果还不行,用onnxsim简化模型,去掉冗余算子。树莓派 5 的 ARM 架构对某些算子支持不好,简化后兼容性更好。

5.5 傍晚或逆光图片漏检严重

现象:白天图片检测正常,傍晚或逆光图片大量漏检。原因是训练集光照分布不均,模型没学过弱光特征。解决方法是补充弱光样本,或者在推理前做直方图均衡化预处理。我一般会在推理脚本里加一步 CLAHE 增强,对茶叶这种低对比度目标效果明显。但注意,预处理要和训练时的增强保持一致,训练没用 CLAHE,推理加了反而可能掉点。

6. 把茶叶检测做到产线可用:一个提升小目标召回的技巧

模型训完、部署跑通,离产线可用还差一步:小目标召回。茶叶检测最要命的是漏检,一个嫩芽没检出来,采摘机器人就少摘一个,产线质检就漏判一个。我试过最有效的一个技巧是推理时用 TTA(测试时增强),简单说就是把图片翻转、缩放后分别推理,再融合结果。YOLOv5 的 detect.py 自带 TTA 开关:

python detect.py \ --weights best.pt \ --source ./test_images \ --img 640 \ --augment \ --conf-thres 0.35 \ --iou-thres 0.4

--augment就是开启 TTA,会对每张图做翻转和缩放推理再 NMS 融合。代价是推理时间翻三倍左右,树莓派上可能从 300ms 涨到 900ms,实时场景要权衡。但在离线质检场景,这个技巧能把小目标召回率提升 5 到 8 个点,非常值。

另一个技巧是切片推理,针对超高分辨率图片。茶叶航拍图或产线高清相机图可能有 4000x3000,直接缩到 640 目标只剩几个像素。做法是把大图切成 640x640 的小块,每块带 20% 重叠,分别推理后再把框映射回原图做 NMS。这个逻辑要自己写,核心是滑动窗口加坐标还原:

def slice_inference(img, model, slice_size=640, overlap=128): h, w = img.shape[:2] stride = slice_size - overlap all_boxes = [] for y in range(0, h, stride): for x in range(0, w, stride): # 切块,边界不足时贴边补齐 x2 = min(x + slice_size, w) y2 = min(y + slice_size, h) x1 = max(0, x2 - slice_size) y1 = max(0, y2 - slice_size) patch = img[y1:y2, x1:x2] # 推理并把框坐标加回偏移量 boxes = model(patch) for box in boxes: box[0] += x1 box[1] += y1 box[2] += x1 box[3] += y1 all_boxes.append(box) # 最后对所有框做一次全局 NMS return nms(all_boxes, iou_thres=0.4)

slice_size和overlap是核心参数,overlap 太小边缘目标会被切断,太大推理次数暴增。我一般用 128 像素重叠,640 的块,一张 4000x3000 图大概切 35 块,推理时间可接受。这个方案在茶叶航拍计数场景实测比整图缩放召回高 15 个点以上。

最后说个我自己的习惯:每次训完模型,我都会挑 20 张最难的图——逆光、密集、遮挡——单独存一个hard_cases文件夹,每次改完参数先跑这 20 张,看漏检和误检有没有改善。这比看整体 mAP 敏感得多,整体 mAP 涨 0.5 个点可能只是简单样本的贡献,难样本才是产线真正的考验。茶叶目标检测这行,模型结构不是壁垒,数据和验证方法才是。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询