第一次跑通樱桃成熟度检测这个项目的时候,我心里其实挺感慨的。以前采摘期得靠老师傅一颗颗看颜色、捏硬度,一个人一天累死累活也就能分拣几十斤,而且标准还不统一——同一个筐里经常混着七八成熟的果子和已经软了的过熟果。现在用基于YOLO26深度学习的目标检测方案,一套Python源码加PyQt5界面,连上摄像头或者导入照片,就能实时把樱桃按成熟度分级并统计数量,误判率比肉眼稳定得多。这套系统很适合做智慧农业、果蔬分拣、果园产量预估的朋友参考,也适合刚入门深度学习目标检测的人拿来练手——它把数据集标注、模型训练、推理部署和一整套桌面端交互界面都串起来了,是一个完整闭环的落地项目。
1. 项目定位与总体架构
1.1 这个项目到底解决了什么问题
先说说痛点。樱桃这东西成熟窗口期特别短,从转色到完全熟透往往只有三五天,而且同一棵树上不同枝杈的成熟度差异很大。传统的人工判断靠两个办法:一是看颜色,绿果、黄果、红果、深红果各有各的收购价;二是捏硬度,但捏多了容易伤果皮,损耗率下不来。更麻烦的是产量预估——树上的果子密密麻麻,靠人数根本数不清,只能靠经验“毛估估”,误差经常超过三成。
这个项目把两个任务同时解决了:检测(Detection)+ 分类(Classification)。它用YOLO26模型在图像中框出每一颗樱桃的位置,同时给出这颗樱桃属于哪个成熟度等级。有了检测框的位置和类别,计数变成了很简单的事——数框就行。这样一来,种植端可以拿它做果园产量预估,分拣端可以拿它辅助分级筛选,甚至配合机械臂还能做自动化采摘的视觉引导。
这套系统不是只给你一个孤零零的模型文件,而是把完整链路都配齐了:标注好的樱桃数据集、训练脚本、能让你自己调参重训练的代码,以及一个基于PyQt5的可视化操作界面。也就是说你拿到手不是“半成品算法”,而是一个可演示、可二次开发的完整成品。
1.2 系统架构与技术选型
这个项目的整体架构用三条线来理解最清楚:
数据线:图像采集 → 数据标注 → 数据集划分 → 数据增强。标注环节用的是YOLO通用的txt格式,每个目标一行记录,包含类别ID和归一化后的中心点坐标、宽高。这套格式的好处是无论后续换成哪个YOLO版本,数据都不用重新标。
模型线:YOLO26作为检测主干,输入图像被缩放到统一尺寸后送入网络,经过特征提取和特征融合,输出每个目标的类别概率和回归框坐标。训练过程采用迁移学习策略,即加载官方在通用数据集上的预训练权重,再用樱桃数据集微调。这样即便你的樱桃样本只有几千张,模型也能快速收敛,达到可用的精度。
交互线:PyQt5构建桌面客户端,包含图像加载区、检测结果展示区、参数配置区、统计信息面板。推理过程放在后台线程执行,避免界面卡死。检测结果实时绘制锚框、类别标签和置信度分数,统计面板会同步更新各成熟度等级的数量和占比。
选型上,YOLO26这套框架最核心的优点是检测速度快+小目标友好。樱桃在果园场景里往往体积小、密度高、相互遮挡严重,老版本YOLO在小目标召回率上会吃力一些,而这一代在特征融合和注意力机制上做了不少优化,实测对密集小果实的检测效果有明显改善。
2. 环境搭建与数据集准备
2.1 开发环境与依赖清单
先列一套我实际跑通的环境配置,你可以参考,不一定完全照抄,但版本兼容性验证过,能少踩不少坑。
| 依赖项 | 推荐版本 | 说明 |
|---|---|---|
| 操作系统 | Windows 10/11 或 Ubuntu 20.04+ | 两边都能跑,Windows下配PyQt5更方便 |
| Python | 3.9 ~ 3.11 | 太高的版本有些旧版CUDA算子会编译报错 |
| 显卡驱动 | NVIDIA 驱动 470+ | 建议更新到当前最新稳定版 |
| CUDA Toolkit | CUDA 11.8 | |
| cuDNN | 8.6+ | |
| PyTorch | 2.0+ | 带CUDA版本的,比如 torch-2.0.1+cu118 |
| PyQt5 | 5.15.x | 用pip安装即可 |
| OpenCV | 4.8+ | 图像读取、预处理、绘制检测框 |
| ultralytics | 对应YOLO26版本 | 训练和推理的核心库 |
| numpy / pandas | 最新稳定版 | 计算和统计 |
安装的时候建议直接创建一个独立的conda环境,不要往系统Python里乱塞依赖,不然以后项目多了互相打架会让人非常崩溃。命令很简单:
conda create -n cherry_yolo python=3.10 conda activate cherry_yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics pyqt5 opencv-python pandas numpy注意:如果机器没有NVIDIA显卡,可以用CPU训练,但训练速度会慢很多倍。这种情况更建议用现成的训练好的模型权重做推理,或者租用云端GPU实例跑训练。
2.2 数据集构成与标注规范
这个项目配套的樱桃数据集,按我实际经验来看,合理的规模应该在2000~5000张之间,覆盖不同光照、不同拍摄距离、不同生长密度的情况。图像里既要有一棵树的远景,也要有单枝果簇的特写,否则模型学到的特征会偏科——比如只在远处绿色背景下训练,换成近处红绿交错的果簇就抓瞎。
标注类别设计上,建议按成熟度分档,参考这样的标准:
| 类别标签 | 名称 | 判定标准 |
|---|---|---|
| 0 | immature | 未成熟,果皮以绿色为主,果型偏硬 |
| 1 | midripe | 半成熟,黄绿色或淡红色,开始转色 |
| 2 | ripe | 成熟,鲜红色至深红色,光泽明显 |
| 3 | overripe | 过熟,暗红色甚至紫黑色,果皮皱缩 |
这四档分类方式很关键。有些项目只分“生”和“熟”两类,看着简单,但实际应用时会发现收购商根本不买账——半熟和全熟的收购价差好几倍,你不把中间档分开,分拣线就没办法执行不同等级的价格策略。所以宁可标注工作量多一些,也要把类别分细。
标注工具我用的是X-AnyLabeling,它比老牌LabelImg好用不少,支持自动标注辅助,配合一个预标注模型能省一半精力。标注完保存为YOLO格式,每张图对应一个同名txt文件,内容长这样:
0 0.452377 0.348833 0.058931 0.063188 2 0.381215 0.444642 0.048933 0.051230五个数字依次是:类别ID、归一化的中心点x坐标、中心点y坐标、归一化框宽度、归一化框高度。注意坐标必须除以图片宽高,范围在0到1之间,否则训练直接报错。
2.3 数据增强策略
樱桃数据集的增强策略比一般项目要更有讲究。常规的翻转、旋转、缩放肯定要做,但针对樱桃场景还有两个特别有效的增强手段:
色彩抖动(HSV变换)。樱桃的成熟度判断本质上是颜色任务,但如果你在训练时不做色彩增强,模型会过度依赖固定的光照条件。比如在树荫下拍的图片偏暗偏绿,正午拍的偏亮偏红,模型容易学成“针对某一种光照的检测器”。加入适当的饱和度、明度、色调抖动,相当于模拟了一天中不同时段的光照变化,泛化能力提升非常明显。
Mosaic拼接增强。YOLO系列的Mosaic增强是把四张图随机裁剪后拼成一张大图,等于变相增加了batch内部的图像多样性。对于樱桃这种小目标密集的场景,Mosaic能有效增加模型对小果实的感受野覆盖,我在训练中实测下来,mAP能提升2到4个百分点。
注意:数据增强不是越猛越好。尤其是颜色类增强强度如果调太高,会把绿果抖成红果、红果抖成暗果,反而把类别边界搞混。建议饱和度调整幅度限制在±30%以内,明度限制在±25%以内。
3. 模型训练与调参实战
3.1 训练流程与关键参数
数据准备完毕后,训练流程分成五步走:
第一步,划分数据集。按8:1:1分成训练集、验证集、测试集。划分的时候注意先随机打乱再分,否则同一棵树上的连续帧图像可能都进了训练集,验证集就失去意义了。
第二步,准备预训练权重。下载YOLO26官方提供的COCO预训练权重。用预训练权重的道理很简单:模型已经学会了通用的边缘、纹理、形状特征,我们只需要让它“复习”一下樱桃的长相,而不是从零开始认识世界。这让训练收敛速度快了不止一个量级。
第三步,写配置文件。YOLO系列用yaml文件描述数据路径和类别名:
path: ./datasets/cherry train: images/train val: images/val test: images/test nc: 4 names: ['immature', 'midripe', 'ripe', 'overripe']第四步,启动训练。用命令行或脚本训练,核心参数参考:
from ultralytics import YOLO model = YOLO('yolo26n.pt') results = model.train( data='cherry.yaml', epochs=150, imgsz=640, batch=16, lr0=0.01, lrf=0.01, optimizer='SGD', device=0, workers=4, cache=True )参数含义拆解一下:
imgsz=640:输入图像统一缩放到640x640。樱桃是小目标,如果你发现小果实检测不足,可以试着提升到896甚至1280,代价是训练和推理速度变慢。batch=16:根据自己的显存调整,12GB显存跑16是安全的,显存小就降一半。lr0=0.01:初始学习率。迁移学习场景下这个值比较稳妥,太高容易把预训练权重冲坏,太低收敛慢。epochs=150:樱桃数据集规模不算大,150轮足够,再多就会过拟合。
第五步,监控训练曲线。训练时会输出两个关键指标:train/loss和val/mAP@0.5。正常情况是train loss稳步下降,mAP节节升高。如果你看到train loss下降但val mAP不升反降,就是过拟合了,这时候需要早停或者加大数据增强。
3.2 模型评估指标怎么看
训练结束后不要急着部署,先跑一段验证代码看看模型真实水平:
model = YOLO('runs/train/exp/weights/best.pt') metrics = model.val(data='cherry.yaml', split='test') print(metrics.box.map50) print(metrics.box.map)两个关键指标的含义:
mAP@0.5:预测框和真实框的重叠度(IoU)超过0.5就算正确检出,这个指标主要衡量“大概框到了没有”。对樱桃分拣来说,如果框的位置偏差稍微大一点,比如框到了旁边的叶片上,影响也不是很大,因为后期还要根据类别做剔除,所以mAP@0.5在0.9以上就算很棒了。
mAP@0.5:0.95:IoU从0.5到0.95按0.05间隔取十个阈值求平均,这个指标对框的精准度要求更高。樱桃果实挨得近,框偏了就可能把两颗果子框成一颗,所以这个值最好能上0.7以上。
此外,要重点看各类别的precision和recall——不要只看总平均。我的经验是樱桃数据里问题往往出现在**midripe(半成熟)**这类过渡样本上,因为它颜色介于绿和红之间,模型容易误判成immature或ripe。如果某类别的recall明显低,优先考虑是标注边界不清晰导致模型学混了,应该回去清理标签,而不是盲目加训练轮数。
4. 推理与计数逻辑实现
4.1 成熟度分级与置信度过滤
训练好的模型拿到推理阶段,输出的是一个形如[x1, y1, x2, y2, conf, class_id]的检测结果。其中x1,y1是框左上角坐标,x2,y2是右下角坐标,conf是置信度,class_id对应当前这个框预测的成熟度类别。
推理阶段不能把所有框都当成有效结果,必须先过两道滤网:
置信度过滤。模型预测每个框都会带一个0到1的置信度,这个值代表模型对自己判断的把握程度。把它当作判断可靠性的开关,过滤掉过低的预测结果。樱桃检测场景里confidence阈值一般设在0.25到0.5之间。设低了,噪声框很多,把叶子褶皱误检成樱桃;设高了,一些光线较暗或遮挡严重的果实会被漏检。根据我多次实验的经验,0.35左右是比较均衡的选择。
NMS去重过滤。同一颗樱桃可能被模型输出两个重叠的框,这时候用非极大值抑制把重叠度高的低置信度框去掉,保留最佳框。这个过程YOLO内部在推理时已经默认执行了,不需要额外手写,但理解NMS的意义对排查问题很有帮助——如果你发现界面上一个果子被画了两个框,大概率是NMS的IoU阈值设得太大,把它调低即可。
4.2 计数逻辑与统计面板
计数本身不复杂,统计不同成熟度等级的目标数量即可:
from collections import Counter counts = Counter(results[0].boxes.cls.int().tolist()) name_map = {0: 'immature', 1: 'midripe', 2: 'ripe', 3: 'overripe'} total = 0 for cls_id, cnt in counts.items(): label = name_map[cls_id] print(f'{label}: {cnt} 颗') total += cnt print(f'总计: {total} 颗')但要注意,这种计数方式在“果实相互遮挡”的场景会有明显误差。樱桃是簇生的,经常五六颗挤在一起,模型只能看到外层的几颗,被挡住的会漏检。改进思路有两个:
- 基于时序的多视角统计。在果园里用摄像头缓慢扫过果树,逐帧检测后对不同帧之间做目标追踪匹配,把跨帧出现的同一颗樱桃合并为一次计数。这种方式能显著减少漏检,但工程复杂度高不少,需要引入ByteTrack之类的追踪算法。
- 基于密度的合理外推。在固定点位检测时,用当前可见果实数量乘以一个根据冠层密度统计得到的修正系数,比如实测遮挡率通常在20%到30%,那就把最终计数除以0.75。这种方式简单粗暴,但胜在稳定,适合产量预估用途。
5. PyQt5桌面界面系统设计
5.1 界面功能布局与交互流程
PyQt5界面是整个项目的门面,也是让非技术用户能够真正用起来的关键环节。界面设计遵循“少就是多”的原则,核心就三个区域:操作区、显示区、统计区。
操作区放在界面左侧,包含四个核心按钮和一个下拉选择框:
- 选择图片:点击后弹出文件对话框,支持jpg、png、bmp格式;
- 启动检测:对当前图片执行模型推理;
- 选择模型文件:加载你自己训练的或者别人训练的.pt权重文件,方便随时切换不同精度的模型;
- 保存结果:把检测后的图片保存到指定路径,同时可以输出一份CSV格式的统计报告。
显示区是界面的核心,用于展示原始图像以及叠加了检测结果的图像。实现上使用QLabel组件配合QPixmap来显示图像,检测框的绘制通过OpenCV完成后再转换显示,这样效率比用Qt自带的绘图组件手动画框要高不少。缩放显示时注意保持长宽比,否则图像会被拉伸变形。
统计区是一个QTableWidget表格,实时刷新各类别数量和占比,同时显示检测总帧数和总目标数。
5.2 后台线程与界面防卡顿
这个部分是新手最容易踩坑的地方。有人图省事,把模型推理直接写在按钮的clicked信号处理函数里,结果一点按钮界面就白屏好几秒,体验非常差。原因就是推理任务阻塞了Qt的事件循环,界面刷新和鼠标响应全部被暂停。
正确做法是把推理放到QThread子线程里,通过信号与槽机制把结果传回主线程更新界面。核心逻辑如下:
class DetectThread(QThread): result_ready = pyqtSignal(object) def __init__(self, model_path, image_path): super().__init__() self.model = YOLO(model_path) self.image_path = image_path def run(self): results = self.model.predict(source=self.image_path, conf=0.35, imgsz=640) self.result_ready.emit(results)主线程这边连接信号:
self.detect_thread = DetectThread(self.model_path, self.image_path) self.detect_thread.result_ready.connect(self.update_result_view) self.detect_thread.start()这样做的好处有两个:界面始终流畅响应,进度提示可以正常刷新;同时当图像特别大或者模型推理速度较慢时,用户可以取消操作,不至于卡死在那里。
注意:子线程里不要直接更新任何UI组件,Qt要求所有UI操作必须在主线程执行。你不遵守这个规则的话,程序偶尔会直接崩溃,不是每次都会崩,是“概率性”崩——这类bug极难排查。
6. 常见问题与避坑指南
6.1 训练与推理阶段问题速查表
把我跑这个项目时踩过的坑和同行遇到最多的问题整理成了一张速查表,建议收藏:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 训练时CUDA out of memory | batch太大或图片分辨率太高 | 调小batch为8/4,或改用更低分辨率训练 |
| loss下降慢或震荡不收敛 | 学习率过高/过低,或数据格式错误 | 检查yaml路径是否正确,把lr0降到0.005再跑 |
| 所有检测框都偏向图像角落 | 标注坐标没有归一化 | 检查txt中的坐标值是否在0~1之间,不正确则重新标注导出 |
| 漏检严重,尤其小果实 | 输入分辨率太低了 | 把imgsz从640提到896,或单独用大分辨率图微调 |
| 熟果和半熟果互相混判 | 这两类在颜色上确实连续过渡 | 增加过渡颜色样本,重新整理标签的分界规则 |
| 界面上检测框“漂移”偏移 | 图像显示缩放时没有同步坐标 | 需要把模型输出的原始坐标按显示比例换算后再绘图 |
| PyQt界面点按钮后转圈卡死 | 推理没有放子线程 | 参照上面的QThread方案改造 |
| 标记了4个类别但训练时只识别出2个 | 某一类别的样本数量太少 | 给该类别的图片做针对性增强,至少保证每类样本数不低于300张 |
6.2 独家经验与提升技巧
说几个常规文档里不会写、但我实际验证过很有用的经验。
第一,数据清洗比调参重要。我一开始项目精度一直卡在0.83左右上不去,后来把所有misclassified的图像全导出来逐一检查,发现大约15%的标注框有“位移+紧贴问题”——比如把两颗紧挨着但不同成熟度的樱桃标在了同一个框里。把这些错误标注修正后精度直接跳到了0.91。模型的毛病往往不是模型不行,是数据的问题让模型学不到正确规律。
第二,用难例挖掘收尾。第一轮训练完,用模型去预测那些它漏检或误判的图片,把错误样本挑出来手动修正后再混入训练集重训。两三轮迭代下来,模型对密集遮挡场景的表现会有质的提升。这个过程在工程上叫hard negative mining,但实际实现并不复杂——推理一遍,筛置信度低于阈值的框,人工确认,补充标签即可。
第三,部署到边缘设备时做模型转换。如果将来要脱离电脑跑在Jetson Nano或者树莓派这种设备上,可以用ultralytics自带的模型导出功能把模型转成TensorRT或ONNX格式,推理速度可以快一倍以上,尤其配合FP16半精度后显存占用几乎砍半。转换命令一条就够了:model.export(format='onnx', half=True)。注意转完要重新做一遍精度验证,避免半精度带来的损失超出可接受范围。
我自己在跑这个项目的过程中,最深的一次体会是:视觉检测系统的难点往往不在模型本身,而在数据和工程衔接。模型训练只花了半天,调界面交互和整理数据花了两个礼拜。但正是这些细节完善的功夫,决定了系统最后能不能真正用起来。如果你也打算做类似的项目,我建议在动手之前先花足够的时间把数据标准定清楚、把界面流转梳理顺,后面会省心非常多。
最后再分享一个小技巧:如果是在果园实地采集数据,尽量挑一个光线稳定的时间段连续拍摄,并且配合白板做一次白平衡校正。模型对颜色的敏感度远高于人的直觉,数据采集环节的颜色一致性,直接决定了成熟度分类的准确率上限。