☰
YOLOv10麦穗检测与计数实战:从数据集准备到部署避坑全指南
2026/9/30 10:25:12 网站建设 项目流程

简介:基于YOLOv10的麦穗计数系统完整方案文档,面向熟悉Python与深度学习基础的科研工作者和工程开发人员,解决农业生产中麦穗人工计数效率低、误差大的问题,实现自动化识别、计数与GUI可视化。压缩包内含1个docx文档,大小仅46KB,却完整覆盖环境配置、模型准备、检测代码、数据示例与项目总结等关键环节。已有143人学习下载。文档以具体项目为线索,先给出torch、opencv-python、PyQt5等依赖的安装命令,再说明YOLOv10模型准备与ONNX导出示例;核心检测代码支持摄像头视频流实时处理,通过GUI直观展示计数结果与精度、召回率等评估指标。数据示例部分特意选取不同光照、角度和密度的麦穗图像,便于验证模型的泛化能力。此外,文档还总结了项目特点、注意事项,并展望了超参数优化、模型集成、边缘计算等未来改进方向,适合作为快速复现和二次开发的实用参考。

1. 麦穗计数为什么不是“检测完数一下”这么简单

做小麦测产、育种材料评估或者田间表型分析的人,大概率都被“数麦穗”这件事折磨过。弯腰在田里一穗一穗数,一天下来脖子酸、眼睛花,数据还不一定准。于是很多人第一反应是:拿目标检测模型跑一遍,检测出麦穗框,数一下框的数量不就行了?想法没错,但真把 YOLOv10 用在麦穗计数上,你会发现坑比想象中多——麦穗密集、互相遮挡、小目标占比高,再加上 YOLOv10 和 v5/v8 的推理逻辑不太一样,直接把老代码搬过来经常“翻车”。

这篇笔记要解决的就是一件事:用 YOLOv10 把麦穗检测和计数做成一套能落地、可复现的流程。从数据集怎么准备、yaml 文件怎么写,到训练参数怎么调、计数结果怎么验证,再到实际部署时踩过的坑,一条线讲完。适合正在做农业视觉项目、想把目标检测用在小麦表型数据上的工程师和研究生,新手能跟着步骤跑通,熟手能直接拿走参数和避坑经验。

2. YOLOv10 在麦穗检测上的选型逻辑:无 NMS 和 C2fCIB 到底改了什么

2.1 YOLOv10 的架构变化和麦穗场景的匹配度

YOLOv10 相比 v8 最大的改动是去掉了 NMS(非极大值抑制)。传统检测模型在推理阶段要靠 NMS 把重叠的候选框合并,而 YOLOv10 通过 one-to-many 和 one-to-one 双头结构,在训练时用 one-to-many 提供丰富监督,推理时用 one-to-one 直接输出最终结果,省掉了 NMS 这一步。这对麦穗计数的意义很直接:去 NMS 后推理延迟降低,且不会因为 NMS 参数设置不当把密集排列的麦穗框合并掉。

另一个关键改动是 C2fCIB 模块和 PSA(部分自注意力)结构。C2fCIB 在 C2f 的基础上引入了上下文信息融合,对小目标的特征表达更充分;PSA 模块则让模型在保持较低计算量的同时,能捕捉更大范围的上下文关系。麦穗检测恰恰是小目标密集场景——一株小麦上的麦穗可能只有几十像素宽,且背景纹理(叶片、土壤)非常杂乱,这两个结构比 v5 的 C3 模块更合适。

选型时还有人会纠结要不要用 YOLOv8。我的看法是:v8 的生态更成熟、文档更多,如果项目周期紧、团队没接触过新架构,先用 v8 出基线没问题。但麦穗计数这种“小目标 + 密集 + 要求计数精度”的场景,v10 的 one-to-one 输出让计数逻辑更干净——不需要在检测后额外调 NMS 阈值,代码少一层。缺点是 v10 的 ONNX 导出偶尔有算子兼容问题,后面避坑章节会具体讲。

2.2 项目目录结构和最小复现命令

假设你已经准备好了数据集(没有的话先用公开麦穗数据集跑通流程),项目结构我习惯这样组织:

wheat-counting/ ├── data/ │ ├── images/ # 图片,按 train/val/test 分子目录 │ ├── labels/ # YOLO 格式标签,和 images 子目录一一对应 │ └── wheat.yaml # 数据集配置文件 ├── runs/ │ └── detect/ # 训练输出 ├── train.py # 训练入口 ├── detect.py # 推理计数脚本 └── requirements.txt

注意 images 和 labels 的子目录结构必须完全一致,训练集和验证集分开。YOLOv10 官方仓库里已经带了全套训练代码,直接基于它改数据路径就行。把 wheat.yaml 放在 data 目录下,内容如下:

path: /absolute/path/to/wheat-counting/data train: images/train val: images/val test: images/test nc: 1 names: ['wheat_spike']

这里nc: 1表示单类别——只检测麦穗。如果你做的项目还要区分不同品种或者病穗,再相应调大 nc 并在 names 里列出全部类别名。最容易犯的错就是把 nc 写成类别数加 1,YOLO 系列不需要背景类,写多了模型训练时直接报 shape 不匹配。

目录建好、yaml 写完后,训练命令是最常见的写法:

python train.py --model yolov10s.pt --data data/wheat.yaml --epochs 100 --batch-size 16 --imgsz 640 --device 0

--model用预训练权重做迁移学习,我这里选 yolov10s.pt,因为麦穗检测不需要超大模型,s 版本在精度和推理速度之间最平衡;--imgsz 640是训练输入尺寸,麦穗如果普遍偏小可以试 736 或 832,但显存占用会明显上涨;--batch-size按显存调,16G 显卡跑 s 模型 16 没问题,8G 就降到 8。

训练跑起来后,每过几个 epoch 看一次runs/detect/下的验证集图片。不要只盯 loss 曲线,直接看验证集上框和麦穗的贴合程度——重叠严重的区域是不是漏检了、背景叶片是不是被误检,这些在 loss 曲线上看不出来。

2.3 yaml 文件怎么创建:手写还是让代码生成

热搜词里很多人问“yolov10 yaml 文件怎么创建”,这里分开说明一下。训练前需要两种 yaml:一种是上面写的数据集描述文件(wheat.yaml),另一种是模型结构文件(yolov10.yaml 之类的网络定义)。

数据集描述文件不用纠结,手写就行,就四五行内容。模型结构文件不需要你从零写,官方仓库里有现成的 yolov10n.yaml、yolov10s.yaml、yolov10m.yaml、yolov10l.yaml、yolov10x.yaml,直接用预训练权重对应的那个。只有两种情况需要动结构文件:一是要改类别数以外的结构(比如自定义 neck 层),二是加载预训练权重时遇到 shape mismatch 想排查,否则别碰。

有同学会问:能不能用脚本自动生成 yaml?能,但没必要。yaml 文件是给人读的,内容就几个字段,自动生成反而容易把路径写错。把时间省下来去核对数据集的标签质量,收益大得多。训练前用下面这个小脚本快速检查数据集配置:

import yaml with open('data/wheat.yaml', 'r', encoding='utf-8') as f: cfg = yaml.safe_load(f) print('类别数:', cfg['nc']) print('类别名:', cfg['names']) print('训练集路径:', cfg['train']) print('验证集路径:', cfg['val']) # 检查 train/val 路径是否存在 import os for key in ['train', 'val']: p = os.path.join(cfg['path'], cfg[key]) if not os.path.exists(p): print(f'警告: {key} 路径不存在 -> {p}')

这段脚本的作用是跑训练前做一轮静态检查。yaml.safe_load把配置读成字典,然后逐项打印;路径检查用os.path.exists判断目录是否真的存在。经常有人把 path 字段写成相对路径,然后在别的目录下执行训练命令,结果数据集怎么都加载不出来——把 path 写成绝对路径就没这个问题。

3. 麦穗数据集的整理:从标注格式转换到训练验证分布

3.1 公开麦穗数据集和自采数据怎么统一成 YOLO 格式

麦穗数据集常见的有两类来源:一类是公开的田间麦穗检测数据集(比如全球小麦检测竞赛的数据),一类是自采的无人机或手机拍摄图像。公开数据集通常是 Pascal VOC 格式(XML 标注)或者 COCO 格式(JSON 标注),YOLOv10 训练需要的是 YOLO 格式的 txt 文件,因此第一步是转换。

VOC 转 YOLO 的转换脚本比较固定,核心是把 XML 里的 bndbox 坐标换算成归一化的 cx, cy, w, h:

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, output_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in class_names: continue class_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) # VOC坐标转YOLO归一化坐标:中心点 + 宽高 cx = ((x1 + x2) / 2) / img_w cy = ((y1 + y2) / 2) / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f'{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') xml_name = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(output_dir, xml_name + '.txt'), 'w') as f: f.write('\n'.join(lines)) # 使用示例 class_names = ['wheat_spike'] voc_to_yolo('data/annotations/001.xml', 'data/labels/train', class_names)

这段脚本的关键在于坐标换算。VOC 的xmin/ymin/xmax/ymax是像素绝对值,YOLO 格式要的是相对图片宽高的比例,而且是中心点坐标加宽高。容易踩的坑是忘记归一化或者把 xmax 当成宽直接除——必须先用x2 - x1算出宽再归一化。

自采数据如果没人帮标,先用 LabelImg 或 X-AnyLabeling 这类工具标注,导出格式选 YOLO,省去转换那一步。自采数据有个额外问题:图片可能带 EXIF 旋转信息,手机竖拍的照片如果没做预处理,标注框和训练时读入的图像对不上。建议先把所有图片转成统一方向(比如全部水平)再标注。

3.2 标签文件命名匹配和边界框越界检查

转换完标签后,先检查两件事:文件的命名是否和图片一一对应,以及标签里的坐标是否合法。前者出错的情况是 XML 文件名和图片名不一致(比如 IMG_001.xml 对应 IMG_001.JPG,大小写不同),后者会出现 cx、cy 超出 [0,1] 范围或者 w、h 为负值。

批量检查脚本如下:

import os import glob def check_labels(img_dir, label_dir): imgs = set(os.path.splitext(os.path.basename(p))[0] for p in glob.glob(os.path.join(img_dir, '*.*'))) labels = set(os.path.splitext(os.path.basename(p))[0] for p in glob.glob(os.path.join(label_dir, '*.txt'))) missing = imgs - labels extra = labels - imgs if missing: print('有图片但没有标签:', list(missing)[:10]) if extra: print('有标签但没有图片:', list(extra)[:10]) # 检查标签内容合法性 for label_file in glob.glob(os.path.join(label_dir, '*.txt')): with open(label_file, 'r') as f: for line in f: parts = line.strip().split() if len(parts) != 5: print(f'格式错误: {label_file} -> {line.strip()}') continue _, cx, cy, w, h = parts cx, cy, w, h = map(float, [cx, cy, w, h]) if not (0 < cx < 1 and 0 < cy < 1 and 0 < w < 1 and 0 < h < 1): print(f'坐标越界: {label_file} -> {line.strip()}') check_labels('data/images/train', 'data/labels/train')

用集合差集检查命名匹配是效率最高的方法。imgs - labels得到的是缺标签的图片集合,labels - imgs得到的是没有对应图片的孤儿标签。坐标合法性检查需要逐行解析,每个标注行必须是“类别 + 四个浮点数”五个字段,数值范围都必须在 0 到 1 之间。这个检查一定要在训练前做,不然训练时你会看到 loss 突然变成 NaN,或者 mAP 奇低无比,排查起来非常折腾。

3.3 训练验证划分:别让同一块地的麦穗同时出现在两边

数据划分是个容易被忽视但非常影响结果的问题。很多人直接用train_test_split随机划分,但田间采集的图片往往是按地块拍的,同一块地的照片高度相似——如果同一地块的照片同时进了训练集和验证集,验证指标会虚高,模型一换地块就“见光死”。

正确做法是按采集来源分组划分。比如你有 10 块地的数据,按地块分组后,用 8 块地做训练、1 块做验证、1 块做测试。实现上可以先给每张图片打上地块标签,再按组划分:

from sklearn.model_selection import GroupShuffleSplit # 假设 images 是图片路径列表,groups 是对应的地块编号 gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(images, groups=groups)) train_imgs = [images[i] for i in train_idx] val_imgs = [images[i] for i in val_idx]

GroupShuffleSplit和普通的train_test_split区别就在groups参数——它保证同一个组的样本不会被同时分到两边。这块看着不起眼,但对最终模型在未知地块上的表现影响很大。我见过不止一个项目,随机划分时 mAP 有 0.85,换地块一测直接掉到 0.6 以下,问题就出在这里。

4. 麦穗计数的实现:从检测框到数量统计的两种方案

4.1 直接按类别统计:简单直接但有个背景误差

模型训练收敛后,写推理脚本做计数。最直接的方式是加载模型、跑推理、统计检测到的目标数量:

from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model('data/images/test/001.jpg', conf=0.25, imgsz=640) # 统计检测到的目标数量(单类别场景) count = 0 for result in results: boxes = result.boxes count += len(boxes.cls) # 也可以用 boxes.shape[0],但 cls 能同时确认类别 print('检测框数量:', len(boxes)) print('类别分布:', {int(c): (boxes.cls == c).sum() for c in set(boxes.cls.tolist())}) print('麦穗总数:', count)

这里model()返回的是一个 Results 对象列表,每个元素对应一张输入图片。boxes.cls保存所有检测框的类别索引,len(boxes.cls)就是目标总数。为什么要强调类别过滤?因为如果模型误检了背景或其他物体,这些也会被统计进去,所以后面要加置信度阈值过滤:

# 只保留置信度大于 0.3 的目标,过滤背景误检 mask = boxes.conf > 0.3 filtered_boxes = boxes[mask] count = len(filtered_boxes.cls)

具体置信度阈值要根据验证集上的 precision/recall 曲线来定。麦穗密集场景下,阈值设太严会漏检严重遮挡的麦穗,设太松会把叶片影子当麦穗。我一般先在验证集上跑 0.2、0.25、0.3、0.4 四档阈值,对比人工计数结果,选误差最小的一档做固定配置。

4.2 中心点密度法:处理边缘遮挡时的更稳选择

直接统计检测框数量遇到一个问题:一张图片里麦穗密集到互相遮挡时,模型只能框住露出来的部分,一个麦穗可能被切成两个框,或者两个紧挨的麦穗被框成一个。这时候更稳的计数方法是中心点密度估计——不数框,数目标中心点。

实现思路是把检测框的 bbox 中心点提取出来,叠加聚类或做核密度估计,然后按峰值数量计数:

import numpy as np from scipy.ndimage import gaussian_filter # 取所有检测框的中心点 centers_x = boxes.xyxy[:, 0] + (boxes.xyxy[:, 2] - boxes.xyxy[:, 0]) / 2 centers_y = boxes.xyxy[:, 1] + (boxes.xyxy[:, 3] - boxes.xyxy[:, 1]) / 2 # 构建二维直方图,高斯平滑后找局部峰值 H, xedges, yedges = np.histogram2d(centers_x, centers_y, bins=[100, 100]) H_smooth = gaussian_filter(H, sigma=2) # 找局部极大值数量作为麦穗数 from scipy.ndimage import maximum_filter local_max = H_smooth == maximum_filter(H_smooth, size=5) count = local_max.sum()

这段代码不适合直接生产使用,但思路值得参考:先用直方图把离散的中心点栅格化,再用高斯滤波平滑,最后用邻域极大值检测找“真正”的麦穗位置。好处是对检测框的尺寸不一致不敏感——哪怕一个麦穗框大一个框小,中心点位置基本稳定。缺点是对检测结果的依赖一点没减少,检测漏了中心点就漏了。

生产环境我一般直接用框计数,中心点法用于验证数据的偏差分析。做法是:同一批验证集图片,分别用“框计数”和“人工计数”算出误差,如果误差稳定在某个固定偏移量上,说明模型对遮挡麦穗存在稳定的误检模式,可以在最终计数时做线性校正。

4.3 大图切片推理:提高小目标召回率的实用手段

无人机拍的田间正射影像动辄 4000×3000 像素,麦穗在整图里可能只有 20×30 像素。直接缩放到 640×640 推理,小目标信息基本丢光,召回率惨不忍睹。常规做法是先切片再推理,把大图切成 640×640 的 patch,每个 patch 重叠 20% 左右,推理完再合并结果。

from ultralytics import YOLO import cv2 model = YOLO('best.pt') def slice_inference(image, slice_size=640, overlap=0.2): h, w = image.shape[:2] step = int(slice_size * (1 - overlap)) detections = [] for y in range(0, h, step): for x in range(0, w, step): # 处理边界:保证切片不超过原图范围 x_end = min(x + slice_size, w) y_end = min(y + slice_size, h) patch = image[y:y_end, x:x_end] results = model(patch, conf=0.25, imgsz=640) for r in results: if r.boxes is None: continue for box in r.boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = box.conf[0].item() # 坐标映射回原图 detections.append([x + x1, y + y1, x + x2, y + y2, conf]) return detections

切片推理的代价是计算量成倍上涨——一张 4000×3000 的图要切 40 多个 patch,推理耗时可能是整图推理的 10 倍以上。所以这个方案我只推荐用在离线处理(比如出测产报告),在线实时计数不适用。切片重叠率 20% 是经验值:太小,目标正好卡在切片边界时容易被切半导致漏检;太大,重复检测变多,合并去重逻辑又要额外处理。

5. 踩坑合集:麦穗计数项目里最耗时的四个问题

5.1 现象:训练 loss 不降、mAP 一直是 0

原因排查过很多次,占比最高的是数据集 yaml 写错,其次是标签文件格式错误。有一次团队同学把自己的数据集 yaml 里nc: 1写成了nc: 2,names 只给了一个类别,模型初始化时类别数对不上,训练直接崩。

解决:训练前先把 yaml 里的 nc、names、路径打印出来核对一遍,再用上面“检查标签”的脚本过一遍全部 txt 文件。别嫌这一步啰嗦,80% 的“训练异常”问题都出在这。

5.2 现象:模型在验证集上表现不错,换到新地块上计数偏差很大

原因:训练验证划分时随机打散,没有按地块分组——同一地块高度相似的图片出现在训练集和验证集,模型“背”了场景而不是学麦穗特征。换到新地块就露馅。

解决:数据划分用 3.3 节提到的GroupShuffleSplit,按采集来源分组。如果数据量不够分组,宁可减少训练集比例,也要保证验证集来自模型没见过的地块。

5.3 现象:YOLOv10 在推理时置信度阈值调不动,设了 0.5 和 0.25 结果一模一样

原因:YOLOv10 去掉了 NMS,one-to-one 头在训练时已经做了正负样本分配,推理阶段输出的目标数量对 conf 阈值不敏感——这是新架构的特性,不是 bug。

解决:不要用 v8 的调参思路去调 v10 的 conf。v10 中真正影响精度的是训练阶段的数据增强和 loss 权重,推理时 conf 只负责过滤低质量输出。如果发现误检框太多,优先检查训练数据里是不是有大量背景相似样本,而不是在推理脚本里反复调 conf。

5.4 现象:训练时显存不足,batch-size 调到 4 还是不行

原因:--imgsz设得过大。很多人为了小目标检测把输入分辨率调到 832 甚至 1024,显存消耗指数上升。

解决:优先降--imgsz到 640 试,再考虑--batch-size。或者用梯度累积:

python train.py --model yolov10s.pt --data data/wheat.yaml --epochs 100 --batch-size 8 --imgsz 640 --device 0 --accumulate 2

--accumulate 2表示每 2 个 batch 梯度累加一次再更新权重,等效 batch-size 16,显存占用只有一半。代价是训练速度慢一些,但精度基本不降。

5.5 现象:导出 ONNX 部署时直接报错,算子不支持

原因:YOLOv10 的 PSA 模块在导出 ONNX 时可能产生模型结构不兼容的算子,常见报错是缺少torch.onnx支持的某个自定义算子。

解决:导出时加--opset 12稳定兼容性,或者用官方推荐的导出命令:

python export.py --model runs/detect/train/weights/best.pt --format onnx --opset 12

如果还是报错,检查 torch 版本和 onnx 版本是否匹配——torch 2.x 配 onnx 1.14+ 是常用组合。还有一步后悔药:导出的 ONNX 用 onnxsim 简化一下,很多奇怪的算子报错在简化后就消失了。

6. 精度再提升一截的调参细节:mosaic、训练轮次和在线增强

训练主流程跑通后,再动三个参数,计数精度还能往上提一截。

**第一,mosaic 增强的后 10 个 epoch 关闭。**YOLOv10 默认把 mosaic 开满全程,但 mosaic 拼接出的图像和真实麦穗分布差别很大,最后几个 epoch 模型本该收敛到真实分布,却还在看拼接图。常见做法是训练到总 epoch 的 80% 时,把--mosaic 0关掉再训剩余轮次:

python train.py --model yolov10s.pt --data data/wheat.yaml --epochs 100 --batch-size 16 --imgsz 640 --device 0 --mosaic 0 --epochs 100

注意上面命令是示意,实际控制 mosaic 的开关在不同训练脚本里不一样。如果用的官方 train.py,要看代码里 mosaic 是按 epoch 动态控制的还是在 data loader 里写死的——先确认你的框架支持训练中途关 mosaic 再执行,否则改了参数没效果。

**第二,验证指标别只看 mAP50,麦穗计数场景要盯 mAP50-95 和 F1。**mAP50 对框的位置误差不敏感,框偏移 20% 一样算对;但计数任务里,框偏移过大说明定位不准,后续按中心点计数会引入误差。mAP50-95 对定位精度更严格,F1 则能反映漏检误检的平衡点。训练完把三个指标一起看,才有把握判断模型能不能用于计数。

**第三,数据增强的 scale 参数要控制。**YOLOv10 默认增强参数里包含随机缩放,但对麦穗这种尺度相对固定的目标,随机缩放范围过大反而让模型学到错误的尺度关系。我一般在训练配置里把 scale 从默认的 0.9 改成 0.5:

# 写在训练超参配置里 scale: 0.5

这样图片最大只缩放 50%,不会出现麦穗被缩成几个像素的极端情况。类似的参数还有 fliplr(水平翻转),麦穗形状在水平翻转后仍然合理,可以打开。

最后一句经验:**任何调参都要在验证集上回测,而且验证集里一定包含模型没见过的地块图片——宁可多花时间做数据划分,也别指望调参能把泛化能力调出来。**我做完这套流程后,最快一次从数据整理到出可用模型花了三天,其中一天半在清数据和查标签。先把数据底子打干净,YOLOv10 在麦穗计数上真能省掉你弯腰数穗的时间,希望帮到你。

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

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

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

立即咨询