资讯详情 YOLOv10麦穗计数系统:从PyTorch训练到ONNX部署与PyQt5实时识别
📅 2026/10/12 19:16:57
简介基于YOLOv10的麦穗计数系统完整技术文档面向熟悉Python与深度学习基础、需要落地农业视觉检测项目的科研和工程人员用于解决大田场景下麦穗自动识别与计数、产量评估等实际问题。压缩包内为单个docx文档仅46KB内容按项目介绍、环境配置、模型准备、代码编写与逐行解释、数据示例、项目总结等章节组织完整覆盖从YOLOv10模型获取与ONNX导出到GUI实时展示计数结果、精度和召回率等评估指标的完整流程。文档还给出了两段可运行的关键代码包括环境依赖安装、模型导出以及检测主程序并整理了项目特点、相关参考资料、未来改进方向以及数据集多样性、环境适应性测试等保障模型泛化能力的注意事项。目前已有143人学习下载适合农业智能化课题入门、课程设计参考也可作为工程落地前快速了解整体技术路线的指南。1. 麦穗计数这件事为什么值得用 YOLOv10 重做一遍在田里数麦穗数到怀疑人生。传统测产是人工抽几个样方蹲在地里把每个样方的穗数一根一根数完再折算成亩穗数。一个样方几百个穗数完一个腰就废了而且不同人数的结果能差出一成多。这套基于 YOLOv10 的麦穗计数系统就是把这件事交给深度学习摄像头对准麦田YOLOv10 实时框出画面里每一个麦穗GUI 界面同时显示画面和累计数量整套代码用 PyTorch ONNX Runtime PyQt5 实现从推理到展示一条链全打通。资源里包含完整可运行的 Python 程序以及配套的数据示例与标注说明适合熟悉 Python 基础、想做农业视觉落地的开发者直接拿来跑通再按自己的田块数据重新训练。自动化麦穗计数不只是省人力它让测产从「抽样估算」变成「整田统计」这正是农业管理数字化要补的那块短板。2. 环境搭建与模型导出把 YOLOv10 权重转成 ONNX 的完整步骤这套系统的推理链路是「PyTorch 训练 → 导出 ONNX → ONNX Runtime 加载推理」。为什么不直接在 PyTorch 里做推理因为 ONNX Runtime 的推理性能更稳定部署时不依赖完整 PyTorch 环境包体积小很多后续不管是换设备还是做边缘部署都省事。如果你只是想在本地跑通 Demo下面这套环境组合是目前踩过坑之后最稳的。2.1 依赖安装顺序与版本兼容性依赖分两层训练/导出层和推理/界面层。训练层需要 torch 和 torchvision推理层只需要 onnxruntime、opencv-python、PyQt5 和 matplotlib。如果你只跑这个系统的检测功能onnxruntime 用 CPU 版本就行不需要装 CUDA 版但如果你要自己重新训练 YOLOv10 模型torch 就必须按你的显卡装对应版本。pip install torch torchvision torchaudio pip install opencv-python PyQt5 matplotlib pip install onnx onnxruntime参数说明torch/torchvision 的版本要匹配建议 torch 2.x 配 torchvision 0.15onnxruntime 版本建议 1.16 以上低版本对导出模型的算子支持不全PyQt5 固定 5.15 系列最稳PyQt6 的 API 有变化直接用项目里的代码会报错。我遇到过最典型的翻车是torch 装了 2.3 但 torchvision 还是 0.14导入模型时直接报 torch 版本不匹配训练 YOLOv10 时还会出现算子不兼容的警告。2.2 从 .pt 权重到 ONNX导出脚本与输入输出张量确认YOLOv10 的官方仓库支持直接加载训练好的 .pt 权重导出 ONNX。核心是 torch.onnx.export 的几个参数dummy_input 决定模型的输入张量形状opset_version 决定 ONNX 算子集的兼容级别。这里用 opset 11 是保守选择兼容性最好如果你的 torch 版本比较新想用更多新算子也可以升到 17但低版本 onnxruntime 可能不识别。import torch # 加载训练好的 YOLOv10 权重 model torch.hub.load(your_channel/yolov10, custom, pathyour_model.pt) model.eval() # 固定 batch 为 1输入 640x640 RGB 图像 dummy_input torch.randn(1, 3, 640, 640) # 导出为 ONNX 格式 torch.onnx.export( model, dummy_input, yolov10_wheat_counting.onnx, opset_version11, input_names[images], output_names[output0] )逻辑说明torch.hub.load从指定仓库加载 YOLOv10 的实现custom表示加载本地训练好的权重dummy_input的形状(1, 3, 640, 640)分别对应 batch、通道、高度、宽度麦穗图像统一缩放到 640x640 输入这是 YOLOv10 的默认推理分辨率。input_names和output_names是给 ONNX 模型的输入输出节点起名后面在 onnxruntime 里调用时要用这两个名字做关键字匹配。这里有个关键点dynamic_axes参数默认是关闭的这意味着导出的模型 batch 固定为 1图片尺寸也固定为 640x640。如果你想把模型部署到生产环境允许不同分辨率输入就要加上dynamic_axes{images: {0: batch, 2: height, 3: width}}但动态轴会增加推理延迟本系统的摄像头实时计数场景用固定尺寸反而更快。2.3 用 ONNX Runtime 验证导出结果导出 ONNX 之后不要急着写 GUI先用 onnxruntime 跑一遍单张图片确认输出的张量形状和内容范围。这一步能提前暴露 90% 的集成问题比后面在 GUI 里黑盒调试省力得多。import onnxruntime import numpy as np import cv2 # 创建推理会话 session onnxruntime.InferenceSession( yolov10_wheat_counting.onnx, providers[CPUExecutionProvider] ) # 打印输入输出节点信息 for inp in session.get_inputs(): print(fInput: {inp.name}, shape: {inp.shape}, type: {inp.type}) for out in session.get_outputs(): print(fOutput: {out.name}, shape: {out.shape}, type: {out.type}) # 跑一次推理 img cv2.imread(test_wheat.jpg) img_resized cv2.resize(img, (640, 640)) blob cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB) blob blob.transpose(2, 0, 1).astype(np.float32) / 255.0 blob np.expand_dims(blob, axis0) result session.run(None, {images: blob}) print(fOutput tensors: {len(result)}, first shape: {result[0].shape})逻辑说明providers指定推理后端CPU 环境用CPUExecutionProvider有 NVIDIA 显卡可以加CUDAExecutionProvider。session.get_inputs()和get_outputs()是排查模型结构的关键手段能直接看到输入节点的名字是不是images输出形状是不是你预期的格式。YOLOv10 导出 ONNX 后输出通常是(1, 300, 6)或(1, 84, 8400)这类形状前者是已经过 NMS 后处理的检测结果后者是原始特征图输出需要自己解码。项目代码里直接取preds[0]并按[x1, y1, x2, y2, conf, cls]解包意味着你导出的模型必须保证输出已经是这种格式这一点在后面的避坑章节会展开讲。3. GUI 与推理管线把模型接到摄像头上并数出麦穗这一章是整个系统的核心PyQt5 负责界面和交互OpenCV 负责视频流读取ONNX Runtime 负责推理三者在QTimer的驱动下协作完成实时计数。我会把代码拆成三块讲每一块单独跑通最后再拼起来。3.1 GUI 骨架PyQt5 布局与 QTimer 刷新机制界面元素不复杂两个按钮开始/停止、一个计数标签、一个图像显示区域。关键是定时器机制QTimer每隔一个时间间隔触发一次update_frame这个函数从摄像头读一帧、送模型推理、再刷新界面显示。间隔设 20ms 对应理论 50FPS但实际推理耗时通常到不了这个速度这个矛盾会在避坑章节详细说。import sys import cv2 import numpy as np import onnxruntime from PyQt5 import QtWidgets, QtGui, QtCore class WheatCountingApp(QtWidgets.QWidget): def __init__(self): super().__init__() self.initUI() self.model self.load_model(yolov10_wheat_counting.onnx) self.capture cv2.VideoCapture(0) self.count 0 def initUI(self): self.setFixedSize(800, 600) self.setWindowTitle(麦穗计数系统) # 开始与停止按钮 self.startButton QtWidgets.QPushButton(开始计数, self) self.startButton.setGeometry(10, 10, 120, 40) self.startButton.clicked.connect(self.start_counting) self.stopButton QtWidgets.QPushButton(停止计数, self) self.stopButton.setGeometry(140, 10, 120, 40) self.stopButton.clicked.connect(self.stop_counting) # 计数显示标签 self.countLabel QtWidgets.QLabel(当前麦穗计数: 0, self) self.countLabel.setGeometry(280, 10, 300, 40) self.countLabel.setFont(QtGui.QFont(Arial, 14)) # 图像显示区域 self.imageLabel QtWidgets.QLabel(self) self.imageLabel.setGeometry(10, 60, 780, 520) # 定时器驱动视频帧刷新 self.timer QtCore.QTimer(self) self.timer.timeout.connect(self.update_frame) def start_counting(self): self.count 0 self.timer.start(20) def stop_counting(self): self.timer.stop() def load_model(self, model_path): session onnxruntime.InferenceSession(model_path) return session参数说明setGeometry(x, y, width, height)是绝对定位布局测试阶段够用但窗口尺寸固定为 800x600换高分辨率屏幕时显示区域会偏小。self.timer.start(20)的 20 是毫秒即每 20ms 请求一帧画面但实际帧率受限于推理耗时如果模型推理要 100ms实际刷新率还是 10FPS 左右定时器只是触发上限不会加速处理。3.2 图像预处理从摄像头帧到模型输入的四个步骤OpenCV 读出来的帧是 BGR 格式而 YOLOv10 训练时用的是 RGB 顺序所以必须做颜色通道转换。接着是缩放、调整通道维序、归一化。最后一个np.expand_dims是给数据加 batch 维因为 ONNX 模型要求输入是四维张量。def detect_wheat(self, frame): input_size (640, 640) # 缩放图像到模型输入尺寸 blob cv2.resize(frame, input_size) # BGR 转 RGB匹配 YOLOv10 训练时的通道顺序 blob cv2.cvtColor(blob, cv2.COLOR_BGR2RGB) # HWC 转 CHW并归一化到 [0, 1] blob blob.transpose(2, 0, 1).astype(np.float32) / 255.0 # 增加 batch 维度 blob np.expand_dims(blob, axis0) # 推理 preds self.model.run(None, {images: blob}) return preds[0]逻辑说明cv2.resize直接把任意分辨率的摄像头帧拉伸到 640x640如果原始画面不是正方形麦穗的纵横比会轻微变形但对检测影响不大。transpose(2, 0, 1)把(height, width, channels)变成(channels, height, width)这是 PyTorch 模型输入的标准格式。归一化用的是直接除以 255而不是 ImageNet 的 mean/std 归一化因为 YOLOv10 训练时就是这么处理的。self.model.run的第一个参数None表示取所有输出节点第二个参数是输入字典键名images必须和导出时input_names一致错一个字就报错。3.3 画框、计数与界面显示检测结果怎么变成画面上的绿框推理结果是一个 NumPy 数组每一行代表一个检测框。YOLOv10 的输出格式约定为[x1, y1, x2, y2, conf, cls]前四个是检测框左上角和右下角的坐标第五个是置信度第六个是类别 ID。对麦穗单类检测来说类别 ID 一直是 0。画框和计数逻辑如下。def render_detections(self, frame, detections): # 检测结果中每个框按 [x1, y1, x2, y2, conf, cls] 排列 for det in detections: x1, y1, x2, y2, conf, cls det if conf 0.5: cv2.rectangle( frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2 # 绿色边框线宽 2 像素 ) def update_count(self, detections): # 只统计置信度大于 0.5 的检测框 unique_wheat len([det for det in detections if det[4] 0.5]) self.countLabel.setText(f当前麦穗计数: {unique_wheat}) def update_frame(self): ret, frame self.capture.read() if ret: detections self.detect_wheat(frame) self.render_detections(frame, detections) # BGR 转 RGB 后才能正确显示颜色 self.imageLabel.setPixmap(self.convert_to_pixmap(frame)) self.update_count(detections) def convert_to_pixmap(self, frame): # Qt 的 QImage 只支持 RGB 或 RGBAOpenCV 帧是 BGR 需要转换 frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch frame.shape bytes_per_line ch * w qImg QtGui.QImage(frame.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888) return QtGui.QPixmap.fromImage(qImg)逻辑说明render_detections直接在原始帧上绘制矩形OpenCV 的坐标系是左上角原点所以(x1, y1)是框的左上角(x2, y2)是右下角坐标值需要转成int才能传给cv2.rectangle。置信度阈值 0.5 是项目默认值这个值直接影响计数结果——阈值越高漏检越多阈值越低误检越多实际调试时建议做成可调节的参数。update_count的写法有个隐患它用当前帧的检测数直接覆盖标签文本而不是累加历史帧的计数这在静态画面下没问题但如果摄像头在移动同一麦穗会出现在多帧里每帧都会被重复计数。convert_to_pixmap里有一个必须做的转换Qt 的QImage.Format_RGB888只接受 RGB 数据如果直接把 BGR 帧传进去画面会变成蓝红色调。bytes_per_line是一行像素占用的字节数这里直接按ch * w计算要求图像内存是连续排列的OpenCV 的帧默认满足但裁剪过的子图不一定这是经验里踩过的坑。4. 数据集准备让模型把「麦芒」和「麦穗」分清的三个步骤YOLOv10 模型能不能数准训练数据决定上限。很多人在这一步翻车拍了上百张麦田照片标注完扔给模型训练结果检测时把麦芒当成了麦穗。原因基本都是标注不规范或者数据多样性不足。这一章从标注格式讲起到工具使用最后落到目录结构一条链说清楚。4.1 YOLO 标注格式解析一行标注到底代表什么YOLO 格式的标注文件是 txt 文件每行对应一个目标包含五个数字类别 ID、归一化中心点 x、归一化中心点 y、归一化宽度 w、归一化高度 h。关键在于「归一化」三个字——所有坐标值都是相对于图片宽高的比例取值范围 0 到 1。0 0.482031 0.536719 0.132031 0.238281 0 0.620703 0.489063 0.118750 0.214844 0 0.353125 0.585938 0.125781 0.231250逻辑说明以第一行为例类别 ID 是 0麦穗。中心点坐标(0.482031, 0.536719)表示这个麦穗的中心位于图片横向 48.2%、纵向 53.7% 的位置。宽度 0.132031 表示麦穗宽度占图片宽度的 13.2%高度同理。如果把 640x640 的图片当作输入基准那么这个框在像素坐标系下的中心点是(0.482031 × 640, 0.536719 × 640) ≈ (308, 343)宽是0.132031 × 640 ≈ 84像素。标注时最容易犯的错是直接用像素坐标写入 txt而没有除以图片宽高做归一化。训练时数据加载器会把标注当比例解析结果就是检测框全部偏到左上角。我的习惯是在标注工具里导出时确认格式再随手用 Python 读一行标注检查数值是否都在 0 到 1 之间。4.2 LabelImg 标注流程与数据多样性要求标注工具用的是 LabelImg开源免费支持 YOLO 格式直接导出。安装和涉及的命令很简单pip install labelImg然后终端输入labelImg就能打开图形界面。标注流程概括为打开图片目录 → 确认输出目录 → 画矩形框 → 输入类别名 wheat → 保存。麦穗的框要贴着麦穗的实际轮廓不要框进太多背景也不要截断麦芒。数据多样性直接决定泛化能力。麦穗计数的场景复杂在四点光照同一天早中晚的光线色温差异很大正午强光下麦穗反光傍晚暗光下对比度低角度俯拍是主流但斜拍的麦穗形状差异明显密度稀疏地块每张图几十个穗密植地块几百个穗遮挡情况完全不同生育期灌浆期和成熟期的麦穗颜色、形态都有区别我一般要求项目数据至少覆盖两种光照条件、两种拍摄角度、三种密度等级总量不低于 800 张。每张图标注完要用 LabelImg 的「验证」功能过一遍确认没有漏标和错标。如果条件有限优先保光照多样性——这是实际测试中影响最大的因素。4.3 数据划分与目录结构训练前的最后一道工序训练数据要按固定目录结构摆放YOLOv10 训练脚本默认读取images和labels两个目录图片名和标注文件名必须一一对应。dataset/ ├── images/ │ ├── train/ │ │ ├── img_001.jpg │ │ ├── img_002.jpg │ └── val/ │ ├── img_101.jpg │ └── img_102.jpg └── labels/ ├── train/ │ ├── img_001.txt │ ├── img_002.txt └── val/ ├── img_101.txt └── img_102.txt参数说明images下放图片labels下放同名 txt 标注文件train 和 val 两个子目录按比例划分。推荐 9:1 划分麦穗计数场景中 val 集至少 100 张才有统计意义。划分时要注意同一块田里连拍的照片不能一部分进 train、一部分进 val因为相邻帧内容高度相似会造成验证集虚高让模型精度看着好实际换块田就翻车。按「地块」划分而不是按「图片」划分是训练集切分里最容易被忽略但影响最大的原则。5. 避坑指南五个最容易把麦穗数错的细节这套系统跑通不难跑准很难。下面五条是从实际调试中总结的踩坑记录每一条都按「现象 → 原因 → 解决」展开你在复现时大概率会遇到其中至少两条。5.1 现象ONNX 推理报错或输出结果全是 0导入模型后第一次点「开始计数」界面黑屏或者控制台报错Shape mismatch。检查输出节点时发现输出张量的形状和你代码里写的不一样。原因YOLOv10 导出 ONNX 时默认输出可能是原始特征图例如(1, 84, 8400)而不是后处理后的检测框数组例如(1, 300, 6)。项目代码里preds[0]直接按[x1, y1, x2, y2, conf, cls]解包前提是模型输出已经做了 NMS 和坐标解码。解决导出模型时确认输出格式。如果输出是(1, 84, 8400)需要手动写一个后处理函数把特征图解成检测框如果输出是(1, 300, 6)则代码可以直接用。最稳妥的办法是导出后打印输出节点信息用之前第 2.3 节的验证脚本检查。我见过有人在这个问题上卡了一整天最后发现就是导出时少加了一个nmsTrue的配置项。5.2 现象检测框偏了半格或者框的位置和麦穗明显错位画出来的绿框整体往左上角偏移或者框比麦穗的实际大小明显小一圈。看起来模型检测到了麦穗但框的位置不对。原因YOLOv10 输出的是归一化中心坐标(cx, cy, w, h)画框时需要换算成左上角(x1, y1)和右下角(x2, y2)的绝对像素坐标。如果直接拿中心坐标当左上角坐标用框就偏到右下方了。另一种常见情况是预处理里做了cv2.resize缩放但后处理时没有把检测框坐标缩回到原图尺寸。解决后处理加一个坐标还原步骤。def postprocess(preds, orig_shape, input_size(640, 640)): 将归一化检测框还原到原图坐标 boxes [] orig_h, orig_w orig_shape for det in preds: cx, cy, w, h, conf, cls det if conf 0.5: # 归一化坐标转像素坐标基于输入尺寸 x1 (cx - w / 2) * input_size[0] y1 (cy - h / 2) * input_size[1] x2 (cx w / 2) * input_size[0] y2 (cy h / 2) * input_size[1] # 缩放到原图尺寸 x1 int(x1 / input_size[0] * orig_w) y1 int(y1 / input_size[1] * orig_h) x2 int(x2 / input_size[0] * orig_w) y2 int(y2 / input_size[1] * orig_h) boxes.append((x1, y1, x2, y2, conf, cls)) return boxes逻辑说明模型在 640x640 的输入上做推理输出的坐标是归一化值。第一步先换算回 640x640 坐标系的像素值第二步再按缩放比例映射回原始画面尺寸。如果省略第二步画框就会因为缩放而错位。5.3 现象一个麦穗被数了两次甚至三次计数结果明显超出画面里的真实穗数看检测画面发现同一个麦穗被画了多个几乎重叠的绿框。原因模型没做 NMS非极大值抑制或者 NMS 的 IoU 阈值设得太高。YOLOv10 的核心卖点之一是「无 NMS」架构但导出的 ONNX 模型是否内置 NMS 取决于导出配置。如果输出包含大量重叠框计数逻辑按框数累加自然就重复计了。解决在计数前按置信度降序排列然后逐个去掉与已保留框 IoU 超过阈值的重叠框。NMS 是标准的 20 行实现不依赖任何额外库项目代码里没有这个步骤需要自己补上。阈值 IoU 一般设 0.45麦穗密集场景可降到 0.4否则相邻麦穗的框互相重叠时会被误删。5.4 现象摄像头打不开或者打开的是错误的摄像头点击开始计数后界面黑屏但有画面窗口或者直接报错Unable to capture video。原因cv2.VideoCapture(0)的索引 0 在笔记本上通常指内置摄像头台式机或外接摄像头的索引可能是 1 或 2。系统没有摄像头时这个调用会静默失败ret一直为 False界面显示黑屏。解决做成摄像头索引可配置。启动时枚举可用设备或者用环境变量传入。最简单的改法是加一个下拉框让用户选设备但快速调试时我一般直接写个循环测试索引 0 到 2哪个能读到画面就用哪个。def get_camera(): 从索引 0 到 2 中找到可用的摄像头 for idx in range(3): cap cv2.VideoCapture(idx) if cap.isOpened(): ret, _ cap.read() if ret: print(f使用摄像头索引 {idx}) return cap cap.release() return None逻辑说明cap.isOpened()只能判断摄像头对象是否创建成功不能保证真的读到了画面必须用cap.read()试读一帧返回retTrue才算真正可用。这个习惯帮我避免过多次现场演示时摄像头黑屏的尴尬。5.5 现象界面卡死点停止按钮没反应程序运行时窗口整个无响应有时候过几秒自己恢复有时候直接崩溃。原因推理耗时长于 QTimer 的刷新间隔。self.timer.start(20)表示每 20ms 触发一次update_frame如果模型推理耗时 200msQTimer 事件会排队积压GUI 主线程被推理占满按钮事件得不到响应界面就假死了。解决两个方案简单的是把 timer 间隔从 20 改成 100让刷新频率匹配实际推理速度彻底的是用QThread把推理放到子线程主线程只负责界面刷新。后者改动大但摄像头实时计数场景下是正解。我的经验是先用方案一跑通流程确认模型精度满意后再重构线程模型避免一上来就陷入线程调试的泥潭。6. 进阶把计数精度从「能用」推到「好用」系统跑通了画面能出框、数字能跳了接下来才是真正拉开差距的部分——怎么让计数结果经得起推敲。三个方向按优先级排列。6.1 置信度阈值与 NMS 参数调优阈值 0.5 是拍脑袋设的。麦穗密集场景下遮挡严重的麦穗置信度普遍在 0.3 到 0.5 之间卡 0.5 会漏掉相当一部分。把阈值下调到 0.25配合 IoU 0.4 的 NMS是麦穗场景下的推荐起点。调参的标准不是肉眼看着框准了而是把同一组测试图跑一遍对比计数结果。可以简单写一个批量评估脚本用不同阈值跑同一批图画出计数误差曲线——阈值在哪个区间误差最小就用哪个值。6.2 用评估曲线验证模型边界项目代码里保留了evaluate_model函数单独跑一次就能看到训练阶段的精度和召回率随 epoch 的变化曲线。这两个指标在计数场景下有明确含义精度低说明把背景当成了麦穗框多而杂召回率低说明漏检多框少而疏。实际田间测试和训练集指标往往有差距最典型的表现是精度还行但召回率暴跌——换光照条件后大量低置信度麦穗直接丢了。这时不要急着调模型先看曲线的拐点如果召回率在高 epoch 段不升反降说明训练集多样性不够优先补数据。6.3 测算误差的校准习惯部署到实际田块前先用静态图片做一次「系统计数 vs 人工计数」的对比。选 5 张不同密度的图人工数出真实穗数和系统输出对比记录误差率。如果误差率稳定在 5% 以内这套系统可以辅助人工测产如果超过 10%先调阈值再考虑重新训练模型。从那以后我每次评估一个检测系统的落地价值都强制走一遍「先跑通、再调阈值、最后拿真实数据验误差」的流程不再被界面效果迷惑只看数字说话。这套 YOLOv10 麦穗计数项目的代码和数据链路是完整的你照着跑一遍大概率会在坑里摔两三次但每一处卡壳都对应着一个值得记住的视觉落地细节。希望帮到你。本文还有配套的精品资源点击获取