简介:针对农业生产中麦穗人工计数效率低、误差大的痛点,这份基于 YOLOv10 的麦穗计数系统资料提供了从环境搭建、模型准备、代码编写到 GUI 呈现的完整实现流程,面向熟悉 Python 与深度学习基础的科研工作者和工程开发人员,适用于麦类作物产量估算与精准管理场景。资源以 1 个 docx 文档集中呈现,文件大小约 46KB,包含环境依赖说明、YOLOv10 模型导出、检测代码解释、数据示例以及完整的代码整合模块。目前已有 143 人学习下载。读者可通过这份文档快速掌握 YOLOv10 在目标检测任务中的实际落地方法,包括 PyTorch 环境配置、ONNX 模型导出、PyQt5 图形界面开发,以及不同光照、角度和密度条件下数据集泛化能力的处理思路,为自动化麦穗计数和农业智能化管理提供可直接参考的技术方案。
1. 麦穗计数不用再靠人眼:YOLOv10 端到端检测怎么落地
调研时,我们通常需要在一幅 6000×4000 的田间照片里数出几百个麦穗。过去靠人眼加计数器,一个样本动辄半小时,眼睛酸不说,不同人的结果还差 10%~15%。基于 YOLOv10 的麦穗计数系统,把这个过程压缩到毫秒级:先由端到端检测器直接输出每个麦穗的边界框和置信度,再做后处理统计数量。它同时覆盖了数据标注、模型选型、训练推理和计数统计,是植物表型、育种筛选、产量预估里很实用的一条流水线。这篇文章适合两类读者:一类是要在自采数据上搭建系统的开发者和农学研究者,另一类是只想复现一个“检测+计数”完整项目的学习者。下面从数据准备开始,把每一步的参数和踩坑点讲清楚。
2. 麦穗数据集准备:标注格式转换与增强的完整路径
2.1 数据从哪来:公开数据集打底,田间自采补难例
麦穗检测领域里,公开数据集首选 GWHD(Global Wheat Head Detection),它收集了多个地区、多种生育期的麦穗图像,密度、光照、背景差异都很大。直接拿它当训练底稿,能避免小样本带来的过拟合。我一般先去 GWHD 把整体流程跑通,再在自己的地块上补充拍摄,重点补三类画面:逆光下穗头与背景对比度低的、分蘖期穗头互相遮挡严重的、灌浆后期穗头弯曲角度大的。只拍顺光的整齐麦田,模型看起来精度很高,一到实际场景就翻车。
如果只想快速验证,用手机拍 600~1000 张也可以起步,但后面要接受一个现实:泛化能力不会太好。标注时统一用单类“麦穗”即可,不要把芒、叶片、茎秆混进来。这些目标边界模糊,混标只会让检测头学出更多假阳性。标注工具用 LabelImg 或 X-AnyLabeling 都行,保存成 PascalVOC 格式,后面脚本统一转成 YOLO 需要的 txt。
2.2 把 VOC 标注转成 YOLO 格式:转换脚本与几个边界坑
YOLO 系列训练时读的是纯文本标注,每行一个目标,格式是class x_center y_center width height,全部归一化到 0~1。下面这段脚本是 VOC XML 转 YOLO txt 的常用写法,我每次换数据集都会复用,只改class_names列表。
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) bbox = obj.find('bndbox') x1 = float(bbox.find('xmin').text) y1 = float(bbox.find('ymin').text) x2 = float(bbox.find('xmax').text) y2 = float(bbox.find('ymax').text) # 坐标越界会直接污染训练,先夹紧到图像范围内 x1 = min(max(x1, 0), img_w) x2 = min(max(x2, 0), img_w) y1 = min(max(y1, 0), img_h) y2 = min(max(y2, 0), img_h) if x2 <= x1 or y2 <= y1: continue x_c = (x1 + x2) / 2 / img_w y_c = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {x_c:.6f} {y_c:.6f} {w:.6f} {h:.6f}") out_name = os.path.splitext(os.path.basename(xml_path))[0] + '.txt' with open(os.path.join(out_dir, out_name), 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) # 用法示例:class_names 顺序必须和后续 yaml 里 names 一致 voc_to_yolo('annotations/001.xml', 'labels/train', ['wheat_ear'])这段脚本做了三件重要的事:类别名到 id 的映射、坐标越界裁剪、空框过滤。其中越界裁剪最容易被忽略,标注时手一抖就可能画出图像边界,不裁剪的话归一化坐标会大于 1,训练时 loss 直接爆炸。class_names列表的顺序决定cls_id,它必须和训练配置文件里的names顺序严格一致,否则类别就错位了。另外注意,LabelImg 如果直接选择 YOLO 格式保存,确实省掉转换步骤,但它不保证坐标上下界,我还是习惯统一跑一遍这个脚本。
2.3 划分与增强:train/val 怎么切,mosaic 什么时候该关
数据准备好后,按 8:1:1 或者 8:2 划分训练集和验证集。划分时注意按整张图来切,不要同一张图的内容既出现在 train 又出现在 val,否则验证指标会虚高。下面这个脚本把图片和对应的 txt 一起拷到目标目录:
import os import random from shutil import copy2 random.seed(42) img_dir = 'images_all' label_dir = 'labels_all' train_ratio = 0.8 imgs = [f for f in os.listdir(img_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))] random.shuffle(imgs) n_train = int(len(imgs) * train_ratio) for i, img in enumerate(imgs): tag = 'train' if i < n_train else 'val' label = os.path.splitext(img)[0] + '.txt' copy2(os.path.join(img_dir, img), f'dataset/mc/images/{tag}/{img}') if os.path.exists(os.path.join(label_dir, label)): copy2(os.path.join(label_dir, label), f'dataset/mc/labels/{tag}/{label}')划分完检查一下:labels 目录里不能有空 txt,更不能出现图片存在但 txt 缺失的情况。ultralytics 训练时如果找不到标签,会给出“label not found”的提示,但有时候只是警告,会把这张图直接跳过,最终导致有效训练样本变少而不自知。
增强参数上,我的经验是:fliplr=0.5放心开,麦穗左右翻转不影响语义;flipud要谨慎,麦穗的生长方向大多是竖直的,上下翻转会引入不真实的形态;hsv_h=0.015、hsv_s=0.3这类颜色增强对光照变化很有用。最值得说的是mosaic,它在 YOLOv8 时代默认 1.0,但对麦穗这种密集小目标不是越大越好。mosaic 会把四张图缩放到小尺寸再拼接,麦穗缩小后可能只剩几个像素,检测头根本学不到有效特征。我一般设成 0.5,并且在最后 30 个 epoch 关掉,让模型回到真实分布上微调。
3. 跑通 YOLOv10 环境与配置:模型选型与 yaml 文件创建
3.1 为什么是 YOLOv10:端到端设计对麦穗这类小目标的意义
YOLOv10 论文里最核心的改动是端到端检测:去掉了传统 NMS 后处理,通过一致性双标签分配让模型直接输出唯一检测结果。对麦穗这种密集、重叠、弱边界的场景,NMS 阈值就像一个黑匣子,调小了出现重复框,调大了把两个紧挨的麦穗合并成一个;YOLOv10 把这层后处理删掉,训练和推理链路都更干净。从实际效果看,在同等模型尺寸下,它的 mAP 曲线不会比 v8 差,推理速度还更快。但端到端设计对标签质量更敏感,训练数据里漏标和错标比 v5/v8 时代代价更大,所以上一章的数据准备工作是这章的地基。
3.2 从零搭建环境:ultralytics 安装与版本注意
ultralytics 仓库目前已经原生支持 YOLOv10,不需要额外装单独的 wheel 包。我通常是新建一个 conda 环境,先装 PyTorch,再装 ultralytics,避免和老项目里的 torchvision 版本打架。
conda create -n wheat python=3.10 -y conda activate wheat pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics如果你的机器只有 CPU,把--index-url去掉,直接pip install torch torchvision装 CPU 版,训练能跑但速度慢很多,建议先用它熟悉流程,正式训练还是得上 GPU。装完之后验证一下:python -c "from ultralytics import YOLO; print(YOLO.__name__)",能输出就说明环境没问题。权重文件第一次用到时会自动下载,如果下载比较慢,也可以手动把对应的yolov10n.pt、yolov10s.pt放到当前工作目录,再通过本地路径加载。
3.3 yolov10 yaml 文件怎么创建:单类数据集的完整配置
“yolov10 yaml 文件怎么创建”是很多初学者卡住的第一道坎。其实它就是一个纯文本文件,不需要特殊工具,新建文件后写入下面内容,把扩展名改成.yaml即可。
# 麦穗单类训练配置 path: /home/user/dataset/mc # 项目根目录,建议写绝对路径 train: images/train # 相对于 path val: images/val # 相对于 path # nc 和 names 是关键字段,顺序不要乱 nc: 1 names: 0: wheat_ear这里path是数据集根目录,train和val是相对于path的图片目录路径。注意不要把path写到images/train这一层,否则路径拼接会错。nc是类别数,麦穗只有一类就写 1。names从 0 开始索引,类别索引和上一章转换脚本里的cls_id一一对应。如果训练时报AssertionError: labels are required,多半是names下标和 txt 里的类别号对不上,或者 labels 目录放错了位置。
4. 训练与推理:让麦穗从图片落到坐标
4.1 训练命令与关键超参数:epochs、imgsz、batch 怎么定
训练麦穗检测模型,我习惯用yolov10s.pt作为起点。它有 7.2M 左右的参数量,在中等密度麦田图像上精度和速度平衡得最好。n版本虽然更快,但密集场景下小穗漏检偏多;m和l精度有提升,但推理延迟翻倍,对于计数系统来说性价比不高。以下是常用的一组超参数配置:
| 参数 | 建议值 | 说明 |
|---|---|---|
| imgsz | 640 | 麦穗常见像素宽度 20~50,640 足够 |
| batch | 16 | 按显存调整,8G 显存建议 8 |
| epochs | 120 | 配合早停,一般 80 轮后开始收敛 |
| patience | 20 | 连续 20 轮无提升就停 |
| lr0 | 0.01 | 预训练权重稳定,不需要太高 |
| mosaic | 0.5 | 密集小目标场景建议降低 |
| fliplr | 0.5 | 水平翻转安全增强 |
imgsz是最值得试的一个参数。麦穗属于中小目标,6000×4000 的原图直接缩到 640 会损失细节;如果你的验证集里大量麦穗宽度小于 15 像素,可以考虑imgsz=768,但显存占用和训练时间都会上涨。batch大小和workers配合时,出现过数据加载瓶颈,先把workers调到 4~8 再说。
4.2 训练脚本与日志监控:从权重文件到 best.pt
用 Python 脚本启动训练最灵活,也方便后来写自动化实验。
from ultralytics import YOLO # 加载预训练权重,自动下载 yolov10s.pt model = YOLO('yolov10s.pt') results = model.train( data='mc.yaml', epochs=120, imgsz=640, batch=16, device=0, workers=4, patience=20, lr0=0.01, fliplr=0.5, mosaic=0.5, val=True, )训练过程里,results对象会在后台写入runs/detect/train/目录,里面有weights/best.pt、weights/last.pt、results.png、混淆矩阵等文件。best.pt保存的不是 loss 最低的节点,而是验证集 mAP 最高的节点,这一点对后续推理非常重要。训练日志里重点看三个信号:训练 loss 是否持续下降、验证 loss 是否同步、mAP50曲线是否在最后 20 轮还在上升。如果 val loss 先降后升,说明过拟合,可以把epochs砍到 80,或者加强hsv增强。
4.3 推理输出:单图、批量目录和结果文件说明
训练完用best.pt做推理。我常用的命令是把整个测试目录一次性跑完,并同时保存可视化结果和 txt 检测文件。
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model.predict( source='test_images', conf=0.35, imgsz=640, save=True, save_txt=True, save_conf=True, )conf=0.35是我对麦穗场景的默认起始阈值,这个值要根据后续计数误差再校准,不是一成不变。save_txt=True会在runs/detect/predict/labels/下生成同名 txt,每一行格式为class x_center y_center width height conf,这就是后面计数程序的原始输入。save_conf=True把置信度也写进 txt,便于在计数时按不同阈值做对比。这里最容易犯的错误是推理时imgsz和训练时不一致。麦穗模型在 640 训练,你图省事用 320 推理,速度上去了但小穗漏检会明显增多;反过来用 1280 推理,会把很多背景噪声放大成误检。
5. 检测结果到麦穗计数:5 个高频翻车点与排查方法
检测模型输出的是一个个框,计数系统要的是最终数量。这一步看着简单,实际翻车的地方比训练还多,我按自己踩过的坑整理了五条。
5.1 计数策略:直接数框的适用边界
最简单的计数实现是统计超过置信度阈值的检测框数量:
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') res = model.predict('test/field_01.jpg', conf=0.3, verbose=False)[0] detections = res.boxes count = 0 if detections is None else int((detections.conf > 0.3).sum()) print('麦穗数量:', count)这种直接数框的方式在单张完整图上基本可用,前提是置信度阈值调得合适。但如果你的计数流程里有大图切片、多尺度推理、TTA 等操作,同一个麦穗可能出现多个检测框,直接数框就会虚高。更稳的做法是先用中心点聚类去重,再统计唯一目标数量。下面的函数按置信度从高到低遍历,只保留中心位置距离足够远的框:
import numpy as np def dedup_centers(centers, scores, min_dist=12): """按中心距离合并重复检测,保留高置信度框""" order = np.argsort(scores)[::-1] keep = [] for idx in order: c = centers[idx] if not keep: keep.append(c) continue if np.min(np.linalg.norm(np.array(keep) - c, axis=1)) > min_dist: keep.append(c) return len(keep)min_dist 的取值跟图像尺寸有关,麦穗的边界框宽度一般在 20~50 像素,推理分辨率 640 时我把 min_dist 设为 12~15 像素。这个函数对单张图也可以用,它能消除模型在密集区域输出的重复框,代价是可能把紧紧挨着的两个真麦穗合并掉,需要结合置信度阈值一起调。
5.2 翻车点一:类别数配错,导致 mAP 一直为 0
现象:训练过程中 loss 明明在下降,但验证集 mAP50 始终是 0,预测结果里一个目标都画不出来。
原因:最常见的是 yaml 里nc和标注 txt 的类别索引对不上。比如转换脚本里写的cls_id = 0,但 yaml 配成nc: 2且names: [wheat_ear, background]。YOLOv10 会把第二类当成你要检测的类别,而你的数据里根本没有第二类样本,验证集自然全错。
解决:训练前抽样检查 labels 目录里的 txt,确认最大类别号小于nc。用下面的命令快速看一个文件的内容:cat labels/train/000001.txt,如果类号是 0,yaml 里nc: 1才对。同时可以用model.val()之前先跑model.predict画几张图,人工确认检测框位置正确,再回头看指标。
5.3 翻车点二:小目标全漏检,imgsz 和增强参数在打架
现象:模型在训练集上 mAP50 很高,但一到实际照片,较远背景里的麦穗一个都没检出来,只有近景大穗被正确识别。
原因:两个因素叠加。一是推理分辨率不够,原图 6000×4000 缩到 640 时,远处麦穗可能只占 3~5 个像素,超出检测头的特征分辨率上限;二是训练时mosaic=1.0把大量小目标进一步缩小,让网络根本没学过这种尺度。
解决:先检查训练配置里mosaic是不是 1.0,如果是,降到 0.5 甚至 0.3;然后把imgsz提到 768 或 1024 重新训练。如果显存不够,可以保持 640 训练,推理时用切片(把大图切成 640 的重叠小块,逐块检测后合并坐标)。切片推理能显著提升小目标召回,代价是推理时间变长。
5.4 翻车点三:多尺度预测出现重复框,计数虚高
现象:人工数一块 100 株的样方是 100 个麦穗,程序输出 130 个。把检测结果画出来一看,部分麦穗被两个高度重叠的框同时框住,而且置信度都大于 0.4。
原因:虽然 YOLOv10 去掉了 NMS,但在密集场景下模型仍可能对同一个目标输出多个置信度接近的候选。特别是穗头分叉、芒刺遮挡时,模型容易把同一个穗头拆成两个局部响应。
解决:计数前必须做中心点聚类去重,就是我前面写的dedup_centers函数。还有一种常见做法是直接按检测框的 IoU 做二次 NMS,但 YOLOv10 的输出已经经过端到端稀疏化,IoU 阈值很难选;我更推荐只对conf相对接近的两个框做距离合并。经验值是:两个框中心距离小于较小框宽度的 1/3,就认为是重复检测。
5.5 翻车点四:拼接大图计数重复,重叠区没去重
现象:你用切片推理处理大图,每个切片设了 64 像素重叠,最后把所有结果拼回原图坐标时,重叠区域里的麦穗被重复计数,整体数量比实际多 10%~20%。
原因:切片推理时,同一个麦穗可能同时出现在两个相邻切片的重叠区域里,模型在两边都检测出它。如果没有按原图坐标做全局去重,它就会在最终的计数集合里出现两次。
解决:对每个切片推理时记录该切片在原图中的偏移量,把检测框中心坐标换算成原图坐标,再统一跑一次dedup_centers。重叠区域的大小要大于麦穗平均直径,我一般设 64 像素,既能保证边界处的麦穗完整出现在至少一个切片里,又不会让重叠区过大导致去重压力上升。
5.6 翻车点五:验证集指标高、实际计数不准的“黑匣子”现象
现象:验证集 mAP50 达到 0.92,计数误差只有 5%,但换到另一天、另一块地拍摄的数据上,误差直接飙到 30% 以上。
原因:这属于数据分布偏移。训练照片大多是晴天上午、顺光拍摄,测试场景是傍晚侧光或者雨后湿度大的环境,麦穗颜色、背景泥土湿度都变了。模型看起来学的是“麦穗”这个类别,实际上把很多光照和颜色特征一并学了进去。
解决:数据层面,训练时把hsv_h、hsv_s、hsv_v增强强度提高,让模型不依赖特定色调;工程层面,保留一块“风格差异集”,专门放不同时间、不同天气拍的照片,每次验证除了看 mAP,还要单独报告这块数据上的计数误差。部署时如果条件允许,先在新场景拍 20~30 张人工数一遍,用它校准置信度阈值,再跑全量推理。
6. 进阶:评估指标、TTA 与 ONNX 部署的最后一公里
6.1 用 mAP 和计数误差共同评价模型
检测模型的 mAP 不能直接等价于计数准确率,我通常两个指标一起看。mAP50反映框定位的宽松准确度,mAP50-95反映框贴合度,而计数系统最终关心的是数量偏差。用验证脚本输出 mAP:
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') metrics = model.val(data='mc.yaml') print('mAP50:', metrics.box.map50) print('mAP50-95:', metrics.box.map)与此同时,统计计数误差 MAE 和 RMSE,建议以 30~50 张人工计数的图片作为基准,分别计算abs(pred - human)的均值和均方根。如果一个模型 mAP50 很高但计数 MAE 也高,多半是重复框或漏检共存,这时候要回到第 5 章的排查看问题,而不是继续堆训练时长。
6.2 三个提升精度的进阶技巧
第一个技巧是 TTA。推理时给model.predict加tta=True,模型会对同一张图做多尺度缩放和翻转,再综合输出,能提升小穗召回,但推理时间翻倍,适合离线统计不适合实时。第二个技巧是分辨率分级:对密度高的区域用 768 甚至 1024 推理,对稀疏区域用 640,可以用检测数量作为密度指标,自动决定下一块采用哪个 imgsz。第三个技巧是阈值校准:在验证集上画置信度与计数误差的关系曲线,取 MAE 最低点对应的阈值,而不是默认的 0.25 或 0.35。麦穗数据集上我常常发现最优置信度在 0.3 左右,低于 0.25 则误检增多,高于 0.4 则漏检增多。
6.3 导出 ONNX 做批量推理:部署时少踩的坑
训练完的 PyTorch 模型部署到生产环境,最常见做法是导出 ONNX,再配合 onnxruntime 做批量推理。导出命令很简单:
yolo export model=runs/detect/train/weights/best.pt format=onnx imgsz=640 dynamic=True但这里有个容易踩的坑:YOLOv10 的 ONNX 输出结构和 YOLOv8 不完全一样,不要沿用 v8 的(1, 84, 8400)解析脚本。导出后先用下面的代码打印输入输出张量形状,再写后处理逻辑:
import onnxruntime as ort session = ort.InferenceSession( 'best.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'] ) inp = session.get_inputs()[0] print('输入形状:', inp.shape) for out in session.get_outputs(): print('输出名称:', out.name, '形状:', out.shape)动态 shape 开启后,输入可以按实际推理尺寸变化,但输出坐标的解码逻辑也要跟着变。我之前踩过一版坑:直接用 v8 的解析代码去读 v10 的输出,结果坐标全乱了,后来才发现输出头结构不同。从那以后我养成了一个习惯,每次导完 ONNX 先拿五张训练集图片对拍,确保 PyTorch 结果和 ONNX 结果一致再交付。这个习惯帮我挡掉了不少部署现场的尴尬,也希望帮到你。
本文还有配套的精品资源,点击获取