ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv10麦穗检测与计数实战:从数据集准备到部署避坑全指南

YOLOv10麦穗检测与计数实战:从数据集准备到部署避坑全指南 简介基于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 写成类别数加 1YOLO 系列不需要背景类写多了模型训练时直接报 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, encodingutf-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, himport 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_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(images, groupsgroups)) 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, conf0.25, imgsz640) # 统计检测到的目标数量单类别场景 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, sigma2) # 找局部极大值数量作为麦穗数 from scipy.ndimage import maximum_filter local_max H_smooth maximum_filter(H_smooth, size5) 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_size640, overlap0.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, conf0.25, imgsz640) 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: 2names 只给了一个类别模型初始化时类别数对不上训练直接崩。解决训练前先把 yaml 里的 nc、names、路径打印出来核对一遍再用上面“检查标签”的脚本过一遍全部 txt 文件。别嫌这一步啰嗦80% 的“训练异常”问题都出在这。5.2 现象模型在验证集上表现不错换到新地块上计数偏差很大原因训练验证划分时随机打散没有按地块分组——同一地块高度相似的图片出现在训练集和验证集模型“背”了场景而不是学麦穗特征。换到新地块就露馅。解决数据划分用 3.3 节提到的GroupShuffleSplit按采集来源分组。如果数据量不够分组宁可减少训练集比例也要保证验证集来自模型没见过的地块。5.3 现象YOLOv10 在推理时置信度阈值调不动设了 0.5 和 0.25 结果一模一样原因YOLOv10 去掉了 NMSone-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 在麦穗计数上真能省掉你弯腰数穗的时间希望帮到你。本文还有配套的精品资源点击获取
返回列表