
做农业视觉的朋友应该都有同感模型跑通容易真正让人头疼的是把检测结果变成用户能直接看懂的东西。最近我把一套基于YOLO26的樱桃成熟度检测与计数系统完整地从源码到界面跑通了一遍Python源码、PyQt5界面、数据集、训练代码全都齐了。这个项目的核心任务其实很聚焦把图片或者视频里的樱桃一颗颗框出来判断每一颗处于未熟、成熟还是过熟状态顺带统计出画面里到底有多少颗果子。整个过程做下来我觉得它特别适合三类人一是想快速搭建农业目标检测Demo的开发者二是做目标检测相关毕业设计的学生三是需要一个能现场演示、能打包交付的桌面工具的工程人员。我这篇就按实际动手的顺序来写先把项目整体结构和功能拆开再讲YOLO26在这个场景里为什么好用然后重点说训练过程和PyQt5界面开发时那些文档里不会写的细节最后整理一份常见问题排查表。内容不搞花架子尽量做到你照着也能复现。1. 项目概述这套系统到底解决了什么问题先说清楚这套系统能干什么。它不是那种只出一个训练精度数字的算法实验而是把“检测计数界面展示”串成了一个完整闭环。你打开程序后可以加载单张图片也可以拖入一段视频甚至直接调用USB摄像头对着樱桃树实时检测。每一颗樱桃都会被画上矩形框框的颜色对应成熟阶段右侧的面板同步更新统计结果未熟多少颗、成熟多少颗、过熟多少颗总共多少颗。这个交互方式很直观放在果园分拣流水线、温室产量预估、采购定级这些场景里基本不需要额外培训工人也能看懂。我最早接触这个项目时第一反应是樱桃检测的难点到底在哪后来标注了一批图片才明白樱桃属于典型的小目标密集场景。一棵树上的樱桃动辄几十个果子之间挨得近有的还被枝叶遮挡成熟度颜色过渡又很连续——从浅黄、粉红到深红边界并不清晰。早期版本的检测模型在这种场景下很容易漏检、误检尤其是把遮住的果子漏掉或者把两颗紧挨着的樱桃识别成一个框。所以项目选择了YOLO26理由后面细说简单讲就是它在小目标检测和密集场景下的表现比旧版本更好模型体积和控制逻辑也适合做成桌面软件。从定位上看这个项目不是纯研究型代码它把数据集、训练脚本、推理脚本、GUI界面四部分都打包好了。对于想快速验证“深度学习能否解决农业计数问题”的人来说这套结构可以直接当脚手架用——换成果实、叶片、害虫数据集改一改类别数和标签路径就能迁移到别的农业视觉任务上去。2. 整体设计思路与核心功能拆解2.1 六个功能模块逐一说整个程序从用户视角看入口是主界面。启动后先看到登录页输入账号密码才能进主窗口——这个设计在学校展示或公司内部分发时挺重要避免了随便一个人打开就能改配置的尴尬。主界面里最醒目的是图像显示区域占左侧大部分空间检测结果实时渲染在上面。图片检测是最基础的功能支持jpg、png、bmp这三种常见格式。点击“打开图片”按钮后文件对话框弹出选择图片后立即执行推理静态图大概几百毫秒出结果。视频检测支持mp4和avi这里注意一下很多网上找的演示视频编码格式特殊OpenCV的VideoCapture不一定能直接解码遇到打不开的情况可以先用格式工厂或FFmpeg转成H.264的mp4。摄像头检测调用本机默认摄像头在温室或果园现场的演示效果最好但光线变化大后面我会讲怎么调整置信度阈值来适应环境。数据集管理模块是我觉得比较容易被忽略的部分。程序里做了一个图片浏览和标签分布统计的功能能直接看到训练集里每个类别的图片数量、标签数量以及各类别占比的柱状图。这个功能非常实用因为类别不平衡是农业检测任务里的常态。比如我整理的数据里成熟樱桃占60%未熟占25%过熟只有15%如果不做任何处理直接训练模型对过熟樱桃的召回率会很难看。2.2 为什么用PyQt5而不是别的框架界面层没有用Web方案而是选择PyQt5桌面框架原因有三个。第一PyQt5生态成熟Qlabel、QPushButton、QTableWidget这些控件组合起来两天就能搭出一个像样的工具界面不需要额外起服务、配前端。第二它和OpenCV、PyTorch的配合非常顺numpy数组转QImage只差一个构造函数的事数据不用经网络传输。第三打包成exe后可以脱离Python环境独立运行这对现场部署很友好。架构上需要注意一点推理耗时虽然只有几十毫秒到几百毫秒但如果直接放在主线程里执行界面照样会卡死。这里我强烈建议把检测逻辑放到QThread子线程里通过信号槽把检测结果传回主线程更新界面。后面我会给出具体代码段这是整个GUI工程里最核心的坑十个人里至少有八个人会在这里栽一次。2.3 检测与计数的数据流从摄像头或者视频流拿到一帧图像后先转成RGB格式送入模型模型输出的是检测框列表每个框包含坐标、类别、置信度。之后程序会做几步后处理过滤掉置信度低于阈值的框做NMS去重然后把框画到原图上。计数就更直接了按类别累加框数量再算一下每类的平均置信度填到表格里。这里有个容易被忽略的细节如果画面里出现同一颗樱桃被重复计数不要慌很可能是NMS的IoU阈值设得太低。IoU阈值一般在0.4到0.5之间设太大会放过重复框设太小又会把相邻的樱桃错误合并。我实测下来密集樱桃场景用0.45比较稳既能去掉同一目标的重复框又不会把两颗紧挨的果子糊成一个。3. YOLO26模型原理与训练数据处理3.1 YOLO26在樱桃检测上的优势虽然YOLO26这个名字听起来像YOLOv8的简单升级但它的结构变化其实不小。最明显的几个点骨干网络做了轻量化改造在相同精度下参数量更少推理速度更快这对桌面端软件很重要GPU不太好的电脑也能跑得动特征融合部分在C2f模块基础上做了改进让浅层特征和深层特征的信息交互更充分小目标漏检率明显下降检测头换成了无锚框设计省去了预设anchor的麻烦训练时不用根据数据集手动调anchor尺寸省了一大步。樱桃检测正好吃到了这些改进的红利。回到数据本身成熟樱桃的直径在图片里通常只有几十个像素和整张640×640的输入图相比属于小目标。旧模型对这类目标容易在特征图下采样过程中丢失信息YOLO26通过更精细的多尺度融合把高分辨率特征保留得更完整所以召回率更高。另外樱桃密集排列导致相互遮挡解耦检测头把“分类”和“回归”分支分开处理也降低了相邻果实的互相干扰。3.2 成熟度标签体系怎么定这个项目的标签分了三类未熟、成熟、过熟。有人会问为什么不用五级分级实际原因很简单五级标注的主观性太强两个标注员对“八成熟”和“九成熟”的理解很难一致标注质量没法保证。三级分类的边界相对清晰未熟对应果皮偏绿或浅黄成熟对应粉红到鲜红过熟对应深红甚至发紫。从工程落地的角度三级已经能满足采摘机器人的“可采/不可采/快烂了”判断。标注工具选支持YOLO格式的就行导出的txt文件里每一行是“类别id 中心点x 中心点y 宽度 高度”坐标都是归一化到0到1的数值。类别id按顺序来0代表未熟1代表成熟2代表过熟。注意一个容易出错的地方标注框尽量贴合果实边缘不必包含太多枝叶背景否则模型会学到错误特征。我最初标注时偷懒把带叶子的区域也框了进去训练出来的模型会把叶子误判成樱桃。3.3 数据集规模与训练环境参考数据量没有统一标准但樱桃这种小目标密集场景图片太少模型根本记不住细节。我这里按常规做法准备了两千多张图片训练集、验证集按82划分并且保证同一棵树的图片不会同时出现在两个集合里避免数据泄漏导致验证指标虚高。如果觉得标注工作量太大可以先用公开的果实检测数据集做预训练再用自己的樱桃数据微调这样几百张图片也能出效果。训练环境方面我这套配置是Python 3.8、PyTorch 2.x、CUDA 11.8显卡使用一张中端卡batch size和输入尺寸控制好之后100个epoch大概花两三个小时。显存不够的同学把batch size从16降到8输入尺寸从640降到512速度能快一半精度损失不算大。不需要服务器级别的算力普通游戏本也能跑。4. 源码结构、训练流程与关键参数实测4.1 拿到手先看这几个文件一个标准的训练工程目录结构一般是这样的project/ ├── train.py # 训练入口 ├── detect.py # 命令行推理脚本 ├── main.py # PyQt5界面入口 ├── models/ # 模型定义 ├── datasets/ │ └── cherry/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ ├── labels/ │ │ ├── train/ │ │ └── val/ │ └── data.yaml └── weights/ ├── yolov26n.pt # 预训练权重 └── best.pt # 训练产出权重拿到代码后先不要急着跑train.py先打开data.yaml看一眼。里面配置了数据集路径、类别数和类别名称。我最开始复现时有段糟糕经历data.yaml里写的是相对路径换了一台电脑之后路径全失效训练直接报错找不到图片。后来我的习惯是改成绝对路径或者确保每次都在项目根目录下启动命令。这个坑很小但能卡住很多人。4.2 训练命令和参数怎么看训练命令比较直观python train.py --data datasets/cherry/data.yaml --weights weights/yolov26n.pt --epochs 100 --batch 16 --imgsz 640参数里epochs是训练轮数我实测100轮足够收敛超过这个数收益很小还容易过拟合。batch size受显存限制卡不够就调小。imgsz是输入图片尺寸640是速度和精度的平衡点樱桃这种小目标不建议低于512否则小果子细节丢失严重。训练时日志里重点看几个指标mAP50、mAP50-95、precision、recall。mAP50表示IoU阈值0.5时的平均精度樱桃检测场景一般能达到0.85以上就算可用mAP50-95更严格对框的贴合度要求高农业场景不用太执着0.6以上已经能看出模型学到东西了。训练过程可以在results.csv里逐项查看也可以用TensorBoard可视化损失曲线。我遇到过一种假象训练损失降得很漂亮但验证集mAP一直上不去这种情况多半是过拟合了解决方法是加大数据增强或者降低epoch数。4.3 数据增强和类别不均衡的处理训练代码里默认开了Mosaic增强把四张图拼成一张喂给模型这对小目标检测特别管用等于变相增加了每个batch里樱桃目标的数量。还有一个常用技巧是复制粘贴增强把标注过的樱桃抠出来随机贴到其他图片上对密集场景效果显著。如果你用自定义数据训练时发现过熟樱桃总是漏检先别急着调参看看是不是标签太少。我的做法是给每类设置一个权重损失函数里对少样本类别加大惩罚比单纯复制图片更稳。跑完训练后会得到best.pt和last.ptbest是验证集上表现最好的权重部署时直接用best。这里有个经验训练结束后不要马上删除中间权重有时best.pt在验证集上指标高但实际视频流里表现不如某个中间epoch的权重因为验证集和真实场景多少有差异。建议多留几个阶段的权重在界面上切换着看效果。5. PyQt5界面开发与交互逻辑详解5.1 窗口布局和控件安排界面整体用的是左右分栏布局。左侧是图像显示区占主窗口的七成宽度用一个QLabel显示缩放后的图像。右侧是一个垂直面板上半部分放检测信息表格QTableWidget三列类别、数量、平均置信度下半部分放操作按钮包括打开图片、打开视频、摄像头识别、保存结果、退出。底部再放一个状态栏显示当前推理耗时和实时帧率。很多人做界面时会忽略图像显示比例问题。摄像头画面是1920×1080直接塞进QLabel会被拉伸变形检测框位置和实际位置对不上。我的做法是在显示前先按QLabel的尺寸等比例缩放图片记录缩放系数画框时先除回去。这个细节直接决定了界面好不好用必须处理。5.2 多线程是界面不卡的关键先看一段推理线程的骨架代码class DetectThread(QThread): frame_ready pyqtSignal(object) result_ready pyqtSignal(dict) def __init__(self, model, source_typeimage): super().__init__() self.model model self.running True def run(self): while self.running: # 从摄像头或视频取帧 ret, frame self.capture.read() if not ret: break # 推理 results self.model(frame) # 绘制框并统计 plotted draw_boxes(frame, results) self.frame_ready.emit(plotted) self.result_ready.emit(build_stats(results)) self.msleep(10)关键点是frame_ready和result_ready这两个信号一个把绘制好的图像发回主线程刷新QLabel另一个把统计结果发回主线程刷新表格。任何耗时的读取和处理都在run方法里做主线程只负责接收信号。如果直接在按钮的clicked信号里写循环读视频界面一定会卡到点击关闭都没反应。关闭程序时还有个隐患如果线程还在读取摄像头直接关窗可能报错。比较稳妥的做法是重写主窗口的closeEvent在里面先把线程的running标志设为False再调用wait()等方法等线程安全退出最后再销毁窗口。5.3 图像格式转换不能想当然OpenCV读进来的是BGR格式的numpy数组QLabel显示需要QImage。很多人直接转结果颜色全乱了。正确的转换顺序是先用cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)把BGR转成RGB再构造QImage最后转成QPixmap放到QLabel上。代码就三行rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img QImage(rgb.data, rgb.shape[1], rgb.shape[0], rgb.shape[2] * rgb.shape[1], QImage.Format_RGB888) pixmap QPixmap.fromImage(img)这步不出错画框显示的坐标才不会有颜色失真以外的偏差。5.4 计数逻辑与结果保存计数在检测结果的DataFrame上做每个检测框都有自己的类别id。遍历一遍后按类别累加就能得到每类数量总数就是所有框的数量。表格里还可以加一行置信度均值这样能直观看出模型对哪一类更自信。保存结果这个功能也别小看。做实验时经常需要把检测后的图片批量导出方便在论文里做对比图。界面里我加了一个“保存标注结果”按钮点击后把当前检测框的坐标、类别、置信度写进txt或csv文件格式和YOLO标签类似。这个功能对后期统计果园产量很有用按帧输出的计数累加起来可以估算总产量。6. 部署打包、常见问题与避坑记录6.1 用PyInstaller打包成独立程序开发环境里跑得好好的打包给别人用的时候容易出各种幺蛾子。我这边用PyInstaller打包基本命令是这样的pyinstaller -F -w main.py --hidden-import ultralytics --hidden-import PyQt5-F表示打包成单个exe文件-w表示不显示控制台窗口。注意--hidden-import要显式加上ultralytics和相关库PyInstaller有时候分析不到动态导入的模块不加的话打包出的程序一运行就报ModuleNotFoundError。还有三件事必须处理。第一模型权重文件best.pt不会自动打进包要在打包命令里用--add-data加上或者把权重放在exe同目录下程序运行时用sys.path或相对当前目录的方式找到它。第二程序里如果用了中文路径或中文界面文字打包后可能因为编码问题显示乱码我建议所有资源文件用英文命名界面文字用UTF-8编码。第三PyQt5在某些Windows电脑上运行会缺VC运行库现场部署时最好把对应运行库一起拷过去或者用--onefile模式减少依赖项。6.2 常见问题排查速查表现象可能原因解决方案图片检测正常但视频识别特别卡推理没有放子线程主线程被阻塞把所有检测逻辑迁移到QThread摄像头打不开或黑屏摄像头索引不对或权限被占用换索引0/1试试检查其他软件是否占用摄像头模型加载失败报FileNotFoundError权重文件路径不对将best.pt放到项目根目录或绝对路径指定界面中文乱码编码问题或系统字体问题统一UTF-8编码添加中文字体文件打包后运行时崩溃PyInstaller缺少动态导入模块添加--hidden-import逐个排查缺失模块训练时显存不足输入尺寸或batch太大batch从16降到8imgsz从640降到512成熟和过熟樱桃经常混淆两类颜色相近或过熟样本太少增加过熟样本数量使用类别加权损失函数界面关闭后进程还在后台跑子线程没有正确退出重写closeEvent设置退出标志并wait线程6.3 那些不写进文档的实操心得有几个经验是这次跑完整项目之后才真正理解的。第一数据标注阶段花的时间一定比训练长。两千多张樱桃图片每张平均几十个框标完一遍眼睛都花了。建议至少两个人标注每张图由另一个人抽检类别边界争议提前统一标准。标注质量的好坏直接决定模型上限这个投入值得。第二界面开发时不要一上来就追求功能完整。我最初把摄像头识别、视频检测、数据集统计全部集成好才开始联调结果到处都是bug找不到因果。正确做法是先跑通一张图片的检测流程确认模型推理和画框没问题再加视频最后加摄像头。每一步都验证过再继续排查问题会快很多。第三关于置信度阈值。静态图片用默认的0.25很合适但摄像头实时画面里光线变化大樱桃颜色有时会失真误检率会上升。我建议程序里提供一个滑动条让人动态调整阈值从0.15到0.5连续可调比写死一个值实用很多。第四如果打算在论文里用这套系统光是界面截图还不够。建议把检测结果以表格形式导出统计不同光照条件下的AP值对比YOLO26和旧模型的精确率召回率差异。农业场景的数据集很少公开能把数据标注规范写清楚本身就是一个很有价值的贡献点。最后分享一个小技巧如果训练时发现模型对某一类樱桃几乎检测不出来可以先把那类图片单独拎出来看看是不是标签框的坐标或类别id标错了。我遇到过一次过熟樱桃全部漏检最后发现是标注工具的类别列表顺序和data.yaml不一致id对不上模型等于从来没学过这一类。这类低级错误排查起来最费时间但检查顺序一般从数据本身开始最靠谱。这套“YOLO26PyQt5”的架构我觉得后续还可以往两个方向扩展。一是加上轻量级目标跟踪算法实现视频流里的按果计数而非逐帧独立计数更接近真实果园产量统计需求。二是把成熟度三级改成连续成熟度回归配合近红外光谱数据能更精细地判断含糖量和采摘窗口。当然这些都属于进阶玩法先把眼前这套系统跑通把数据闭环建立起来后面的可能性就都在自己手里了。