资讯动态

YOLOv10麦穗计数系统:从训练到ONNX部署的完整实践

发布时间:2026/9/30 2:58:36 来源:尧图企业网站定制
简介面向熟悉Python且具备深度学习基础的科研工作者与工程开发人员基于YOLOv10的麦穗计数系统以1个docx技术文档交付包体仅46KB。文档从零梳理完整落地流程先完成PyTorch等环境配置再加载预训练YOLOv10模型并导出ONNX格式随后逐步实现图像预处理、模型推理、结果后处理及GUI展示并通过数据示例评估识别效果。最后还给出代码逐段解释和完整代码整合便于读者对照调试与二次开发。针对实际农田中光照变化、麦穗遮挡等泛化性问题文档列出了日间多条件采集数据、环境适应性测试等注意事项并展望了超参数优化、引入EfficientDet的多模型集成、边缘计算部署及融合天气土壤数据的智能管控方向。目前已有143人浏览学习适合希望通过GUI方式把YOLOv10落地到农业麦穗计数与产量估算场景的开发者也可作为相关课程设计的参考。1. 把地里的麦穗数清楚一个能跑通的 YOLOv10 计数系统长什么样基于 YOLOv10 的麦穗计数系统解决的是农业场景里一个很实际的痛点靠人工蹲在田边数穗子又慢又容易数错。这套资源把深度学习目标检测、实时视频流和 GUI 三件事串成了一条完整链路——摄像头或者一张图片进去画面上每个麦穗被框出来窗口右上角实时刷新数量训练阶段记录下来的 precision、recall 曲线也能一起展示。它适合两类人一类是想把 YOLOv10 落地到真实项目的开发者另一类是正在做农业自动化或计算机视觉课设的学生想在短时间内搭起一个能演示、能改、能复现的检测系统。拿到手是一份能直接运行的代码骨架配套有说明和数据基本不用从零摸索环境依赖和推理细节。2. 环境与模型管线为什么从 .pt 到 .onnx 这条路最省心2.1 YOLOv10 的端到端设计天生适合导出推理YOLOv10 和之前的 YOLO 系列一个关键区别按照论文里的说法是它把 NMS 从推理流程里拿掉了。之前用 v5/v8 导出的模型后处理还得在代码里写一遍非极大值抑制不然同一个目标会被一堆框重复命中而 YOLOv10 用 one-to-one 的头部设计在训练阶段就解决了框的归属问题推理输出直接是一组已经去重后的候选框。这个细节直接决定了系统代码可以写得这么简单——不用维护一堆后处理逻辑拿到输出遍历一遍就能画框、计数。那为什么不直接在 PyTorch 里加载 .pt 权重做推理我个人的习惯是训练和调参用 PyTorch部署和写 GUI 一律导出 ONNX再交给 onnxruntime 跑。原因很实际第一PyTorch 的推理结果依赖你的 Python 环境和 torch 版本换台机器很容易因为缺库或版本不一致跑不起来第二onnxruntime 安装包小CPU 上也能跑得动跟 PyQt5 集成很少出幺蛾子第三导出成 ONNX 之后你可以留着它做 INT8 量化、转 TensorRT不需要回头去动训练代码。对一个最终要落到农业现场甚至边缘设备上的系统来说这条路性价比最高。需要提醒的是训练出来的 .pt 权重如果是拿 COCO 预训练模型微调的那预训练能帮你少走弯路但前提是你的数据集得让模型真正见过麦穗的样子。麦穗这类密集小目标和 person、car 差异很大网上随便下个 COCO 权重直接拿去数麦穗出来的框基本不能用。这个系统用的是你训练完成后导出的 ONNX不是开箱即用的通用权重。2.2 环境安装一条 pip 命令把依赖配齐项目里给的环境依赖比较精简照着装就行pip install torch torchvision torchaudio opencv-python PyQt5 matplotlib onnx onnxruntime拆开看每一件是干什么的。torch、torchvision、torchaudio 是 PyTorch 全家桶训练和导出模型用到opencv-python 负责摄像头读取、颜色转换和画框PyQt5 是 GUI 框架承载窗口、按钮和图像显示matplotlib 用来画 precision/recall 评估曲线onnx 和 onnxruntime 一个管导出、一个管加载推理。如果只想跑 GUI 推理不需要额外装标注工具但后面准备数据时一定会用到 LabelImg 这类工具建议顺手装一个。版本上踩过一个小坑PyQt5 尽量不要用 conda 装优先 pip。conda 的 PyQt5 有时候会携带 Qt 插件和 OpenCV 自带的 Qt 后端抢资源表现就是一打开窗口就黑屏闪退。另外在服务器或者 Docker 里跑OpenCV 还会报缺少 libGL.so.1那是没装系统图形库Ubuntu 执行 apt install libgl1 libglib2.0-0 就解决跟 Python 代码没关系。这种环境问题不提前处理好后面每一步都会显得莫名其妙。2.3 模型导出固定输入尺寸把输出结构摸清楚拿到训练好的 .pt 后导出 ONNX 是关键一步。项目里的导出脚本骨架如下我加了几个参数统一了输入输出名import torch # 加载训练好的模型文件custom 表示加载自定义权重 model torch.hub.load(your_channel/yolov10, custom, pathyour_model.pt) model.eval() # YOLOv10 默认以 640x640 为输入dummy input 只用来走一遍图结构 dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, yolov10_wheat_counting.onnx, opset_version11, # onnxruntime 兼容性最好 input_names[images], # 手动指定输入名后面 GUI 代码要用 output_names[output0], # 输出名固定方便排查 dynamic_axesNone # 固定 batch1简单场景够用 )这里input_names[images]不是可选项。如果不指定torch.onnx.export 会给输入节点自动生成一个类似onnx::Conv_0的名字而你后面代码里写的是self.model.run(None, {images: blob})交付到别人手里时名字对不上session 会直接报 Invalid Key。固定dynamic_axesNone意味着模型只接受 1×3×640×640 这一个形状换来的是导出简单、部署更稳适合摄像头实时推理如果以后想对任意分辨率图片批量检测就得把 dynamic_axes 配给 height 和 width但边框坐标的还原逻辑也要跟着改。导出之后不要急着写 GUI先花一分钟验证模型有没有问题。常见输出结构是类似 [1, 300, 6] 的候选框张量6 对应 x1、y1、x2、y2、置信度、类别。这个输出格式不是固定死的取决于你的训练配置所以验证这一步不能省。推荐的做法是在代码里加一行import onnxruntime as ort session ort.InferenceSession(yolov10_wheat_counting.onnx) for inp in session.get_inputs(): print(inp.name, inp.shape) for out in session.get_outputs(): print(out.name, out.shape)看到输入名是 images、输出形状符合预期再接 GUI 才不会在推理阶段摸黑。很多拿到这套系统的人第一个翻车点就出在这里后边避坑章节还会单独讲。3. 核心代码拆解GUI、推理循环和计数逻辑怎么串起来3.1 GUI 骨架三个控件加一个定时器就能跑起来这部分代码量不大一个类把界面和逻辑全部装下。窗口分两块上方是开始/停止按钮和一个计数标签下方是一块大的 QLabel 用来显示实时画面。控制摄像头取流的是一个 QTimer每 20ms 触发一次定时事件触发后从摄像头读一帧、送入模型、画框、刷新界面。整个流程用伪代码描述是这样的def update_frame(self): ret, frame self.capture.read() if ret: detections self.detect_wheat(frame) # 模型推理 self.render_detections(frame, detections) # 画检测框 self.imageLabel.setPixmap(self.convert_to_pixmap(frame)) self.update_count(detections) # 刷新计数界面布局用的是setGeometry绝对定位800×600 的窗口里放三个控件代码直观不用查布局管理器。这个写法对固定窗口够用但如果你要多分辨率适配建议换成 QGridLayout省得一个个调坐标。startButton 和 stopButton 的 click 信号分别绑到 start_counting 和 stop_counting一个 startTimer 一个 stopTimer逻辑很直接。3.2 推理管线一帧数据从 OpenCV 到 ONNX 的完整路径detect_wheat 函数是整套系统里最核心的一段它做的事情可以拆成四步resize 到 640×640、BGR 转 RGB、归一化到 0~1、调整通道顺序成 NCHW然后交给 onnxruntime 推理。项目给出的代码里有两处值得注意。第一blob 构造时用了blob cv2.resize(frame, input_size)直接拉伸。640×640 是模型的输入尺寸如果摄像头出来的画面是 16:9直接 resize 会把麦穗压扁导致长宽比失真检测框和真实麦穗对不齐。更好的做法是用 letterbox也就是等比缩放后补灰边YOLO 系列训练时就是这么处理图的推理时保持一致效果最稳。第二self.model.run(None, {images: blob})里的 None 表示返回模型所有输出。如果导出时只有一个输出preds 就是一个元素的列表preds[0] 就能拿到 [300, 6] 的候选框如果模型还有其他附加输出preds 会多几个元素取下标要小心。稳妥的写法是读输出的 shape 判断一下或者直接用输出名取结果def detect_wheat(self, frame): input_size (640, 640) blob cv2.resize(frame, input_size) blob cv2.cvtColor(blob, cv2.COLOR_BGR2RGB) # OpenCV 默认 BGR模型要 RGB blob blob.transpose(2, 0, 1).astype(np.float32) / 255.0 blob np.expand_dims(blob, axis0) # 变成 [1,3,640,640] preds self.model.run([output0], {images: blob}) # 按输出名取避免下标混乱 dets preds[0][0] if len(preds[0].shape) 3 else preds[0] return dets加的这个 dets 判断就是为了应对导出输出带 batch 维度的情况shape 是 [1, 300, 6] 就取第一个 batch已经是 [300, 6] 就直接用。这是写推理代码时最容易被忽略的维度问题后面避坑一节会把它列入典例。3.3 计数与绘制置信度阈值决定你数的是麦穗还是噪点检测结果画到帧上用的是简单的遍历画矩形。每个候选框解包成 x1、y1、x2、y2、conf、cls只有置信度大于 0.5 的才会被画出来并计入总数。这个 0.5 的阈值很关键设得太高被遮挡的麦穗、远处的小麦穗会被漏掉计数偏低设得太低误检的框也会被数进去出现把背景当麦穗、数出几千个的离谱结果。我的建议是把阈值做成可调参数不要写死在代码里。常见做法是在 GUI 上加一个 QDoubleSpinBox 或者 QSlider让使用者根据画面自行调整。对于麦穗这种密集目标0.35~0.6 都是实际项目中常见的范围具体值取决于数据质量和训练情况。每次现场演示前我都习惯先跑一段固定的视频把阈值过一遍调到一个不漏不重的位置再开始正式计数这是经验也是这套系统从“能跑”到“能用”的分水岭。计数逻辑本身可以更严谨。项目原代码里unique_wheat len([det for det in detections if det[4] 0.5])是直接统计置信度超过阈值的框数量在遮挡严重的麦田里相邻麦穗的框经常会交叠到 IoU 很高但实际确实是两个目标的情况。如果模型输出的框本身就干净YOLOv10 端到端的优势就在这里这个简单统计就够用担心重复计数的话可以在这个结果上再按框中心点做一个聚类去重把中心距离小于麦穗半径的框合并这个后面展开讲。4. 避坑排查麦穗计数系统最常见的 5 个翻车点4.1 同一个麦穗被重复计数画面上一个穗子被圈了两三个框计数器蹭蹭往上翻实际数明显对不上。原因有两类一是模型后处理没做干净输出里还残留重叠候选框二是画框时只用置信度过滤没有做非极大值抑制 NMS。虽然 YOLOv10 宣传端到端不需要 NMS但自己导出 ONNX 时如果训练配置和官方不完全一致输出里仍可能有重复框。解决方法是人工补一道 NMS 兜底代码量不大import numpy as np def nms(boxes, scores, iou_threshold0.5): idxs np.argsort(scores)[::-1] # 置信度从高到低 keep [] while len(idxs) 0: i idxs[0] keep.append(i) x1 np.maximum(boxes[i, 0], boxes[idxs[1:], 0]) y1 np.maximum(boxes[i, 1], boxes[idxs[1:], 1]) x2 np.minimum(boxes[i, 2], boxes[idxs[1:], 2]) y2 np.minimum(boxes[i, 3], boxes[idxs[1:], 3]) inter np.clip(x2 - x1, 0, None) * np.clip(y2 - y1, 0, None) area_i (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1]) area_j (boxes[idxs[1:], 2] - boxes[idxs[1:], 0]) * (boxes[idxs[1:], 3] - boxes[idxs[1:], 1]) iou inter / np.maximum(area_i area_j - inter, 1e-6) idxs idxs[1:][iou iou_threshold] # 把高 IoU 的框剔除 return keep调用时传入所有候选框坐标和对应置信度返回的索引数组就是最终保留的框。这个 NMS 版本是往常用法在推理线程里跑 300 个框耗时几乎可以忽略。4.2 摄像头读不到画面一启动就黑屏或报错现象是self.capture.read()返回 False界面上一片黑。刚拿到代码的人最容易怀疑模型坏了其实多数是摄像头源的问题。cv2.VideoCapture(0) 里的 0 是设备索引笔记本自带的摄像头一般是 0外接 USB 摄像头可能是 1 或其他数字在无图形界面的远程环境或者 WSL 里摄像头设备可能压根不可见。排查顺序推荐这样先写三行代码cap cv2.VideoCapture(0)然后cap.isOpened()检查索引能不能打开再把摄像头权限确认一遍。如果是为了验证算法本身最省心的做法是把数据源临时换成图片或视频文件项目里摄像头和推理逻辑是解耦的把cv2.VideoCapture(0)换成cv2.VideoCapture(test.mp4)就能先跑通整个检测流程回头再治摄像头的问题。4.3 ONNX 推理报错 Invalid Key模型名对不上现象是session.run(None, {images: blob})直接抛异常提示输入键无效。原因基本就是 2.3 节说的导出时没有手动指定input_names[images]onnx 里的输入节点名叫别的。有时候是导出后又被别的工具重新处理过名字被改写。解决办法是先打印再跑session ort.InferenceSession(yolov10_wheat_counting.onnx) print(session.get_inputs()[0].name) # 实际输入名 print([o.name for o in session.get_outputs()])看到实际名字后改代码里 dict 的键就行。这个习惯我养成了很久任何模型接到手里先确认输入输出再进推理循环能省下大量排查时间。4.4 GUI 卡顿画面像幻灯片现象是窗口能开、按钮能点但画面刷新率低有时候拖动窗口界面直接冻住。原因是 QTimer 每 20ms 触发一次读帧和推理这些耗时操作全在 GUI 主线程里执行模型一次推理可能就要 30~80ms界面自然没有富余时间去响应重绘事件。解决思路有两个看硬件条件选。一是把 QTimer 间隔调大到 50~100ms牺牲刷新率换流畅度二是把推理丢到 QThread 里做主线程只负责显示结果。对跑在普通笔记本 CPU 上的系统来说先调大 timer 间隔是最快的止血方式等进了部署阶段再考虑用 QThread 或者换更轻量的模型。4.5 远距离、小尺寸的麦穗直接漏检现象是近处的大穗子检出很好镜头拉远或者用无人机拍的图一盘里的麦穗大多检不出来。原因很直接640×640 的输入下一个在 4000×3000 原图里只有 40×20 像素的麦穗缩到 640 之后不到 7 个像素模型根本看不清。这不是阈值能调回来的问题是信息被 resize 丢掉了。惯用解法是把大图切成若干 640×640 的 tile带一定的重叠度分别推理再把结果按坐标映射回原图。代价是推理次数变多、耗时上升但小目标的召回率提升非常明显。对于麦田这种背景单一的场景tile 推理基本是必经之路。5. 数据与训练调优让 YOLOv10 在麦田里更准的必备配置5.1 数据集目录与 YOLO 标注格式这套系统里的推理代码只负责用训练好的模型做检测模型泛化能力其实取决于你喂给它的数据。YOLO 系列的数据集格式是统一约定图片放在 images 目录下同名 txt 标注放在 labels 目录下一张图对应一个 txt每行一个目标。常见的目录结构长这样datasets/wheat/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 └── labels/ ├── train/ # 对应的标注文件 └── val/每个 txt 文件的内容是几行数字比如麦穗的例子0 0.5234 0.4210 0.0431 0.0722 0 0.6112 0.3854 0.0398 0.0689第一列是类别 ID这套系统只有一类麦穗所以全是 0后面四列分别是目标中心点的 x、y 坐标和框的宽、高全部除以图片原始宽高做了归一化取值在 0~1 之间。标注工具一般直接选 LabelImg导出的就是这种格式。手动标注这种批量小目标比较累但数据质量直接决定模型的漏检率这一关绕不过去。标注时建议把挤在一起的穗子也一个个标清楚别为了省事把重叠的略过否则模型学到的边界会很糊。5.2 yolov10.yaml 怎么创建数据配置这一步别跳过训练之前要先写一个数据集配置文件也就是常说的 yolov10.yaml。它的作用是把数据路径、类别数量和类别名称告诉训练框架有了它训练命令才能找到数据。创建方法很简单在项目目录下新建一个文本文件命名随意内容如下# 数据集根目录train/val 路径是相对这个根目录的 path: ./datasets/wheat train: images/train val: images/val # 类别只有麦穗一种id 0 就对应 names 里的第一个 nc: 1 names: [wheat]几个容易踩的点yaml 文件不认 tab缩进只能用空格否则训练脚本直接报错path 建议写绝对路径因为不同机器的当前工作目录不一样相对路径容易找不到数据。训练时加载这个 yaml 即可。第一次创建的人最容易犯的错是把 train 和 val 写成图片的真实路径而这里要求的是相对 path 的子目录多一个斜杠少一个斜杠都可能白跑一次训练。注意yaml 文件里不能用 tab 缩进统一用两个空格。另外要确认 labels 目录下没有与图片不一致的空标注文件空 txt 不算问题但文件缺失会导致训练中断。5.3 训练超参与评估曲线解读麦穗数据集的规模一般不会太大几百到几千张图是常态这时候从零训练不现实通常是基于预训练权重做迁移学习。常见做法是先加载 yolov10n.pt 这类公开权重再把自己的数据集路径传进去训练。核心超参里imgsz 建议保持 640 和导出一致epochs 设在 150~300 之间batch 根据显存来8~16 在多数单卡上都能跑。数据增强方面YOLO 自带 mosaic、翻转、色调扰动这些对麦田这种光照变化大的场景很有帮助尽量开着别关。训练日志里最有参考价值的两个指标是 precision 和 recall。precision 高意味着检出来的框大多是准的适合要求“数出来的数别虚高”的场景recall 高意味着真正存在的麦穗大多被找出来了适合“漏检最致命”的场景。麦穗计数更看重 recall宁可多画几个误检框也不能漏了真实穗子所以阈值取舍上会偏保守。项目里的 evaluate_model 函数就是把这两个曲线画在同一张图上def evaluate_model(metrics): plt.figure(figsize(10, 5)) plt.plot(metrics[epochs], metrics[precision], labelPrecision, colorblue) plt.plot(metrics[epochs], metrics[recall], labelRecall, colorgreen) plt.xlabel(Epochs) plt.ylabel(Scores) plt.legend() plt.show()注意这里 metrics 的键名是假设的。实际训练日志一般会生成 results.csv用 pandas 读进去之后列名可能是 metrics/precision(B) 这种带斜杠的形式取数据前先print(df.columns)看一下真实键名别直接套字典下标。数据集多样性这个事多说一句。如果训练图全是晴天中午拍的模型一遇到阴天或者傍晚就会明显拉胯。收集数据时要刻意覆盖不同光照、不同角度、不同密度这是提升鲁棒性的性价比最高的手段比调任何超参数都管用。6. 进阶技巧从单机演示到批量统计与轻量部署纯摄像头演示跑通之后再往前走两步这套系统就能用在更实际的环节里。批量图片推理是最容易接上的功能。把cv2.VideoCapture(0)换成图片目录遍历对每张图做一次检测、存一次结果再把计数写进 CSV就能得到一块麦田的穗数分布表。这个输出格式对农学上估算产量很实用代码改动量不大核心就是把 detect_wheat 复用到循环里顺带用 csv.writer 写行。部署侧的优化方向是模型瘦身。ONNX 可以直接用 onnxruntime 的量化工具做 INT8 动态量化模型体积能压到原来的四分之一CPU 推理延迟显著下降精度损失在可接受范围内。如果目标是边缘设备转成 TensorRT 或者 NCNN 是更彻底的方案但前提是先把 ONNX 这一步的输入输出名和动态轴理清楚后面所有转换工具都要基于这个中间格式。大图小目标的场景走 tile 推理。把 4000×3000 的无人机图切成 640×640 带 10% overlap 的小块分别推理后再把坐标按偏移量映射回原图最后整合计数。多算一次交界处的重叠目标是比漏掉远处整片穗子划算得多的代价。最后说一个我自己的习惯。以前吃过亏模型在标称分辨率下一切正常换一张尺寸不同的图就出现越界框从那以后我每次接到一个检测模型都会先打印 get_inputs/get_outputs 的形状再用 320 和 640 两种尺寸各测一张图确认框坐标没有跑出图像边界才敢把它接进 GUI。这套麦穗计数系统不难跑通但想让你自己的数据、你自己的机器上稳定出数上面这些排查习惯比多调几个超参数都重要希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑