果农的一句话让我重新思考了“成熟度检测”这件事“红了就该摘但红得发黑反而太熟了。”颜色阈值不是简单能定义的——光照、阴影、遮挡都在干扰判断。所以我把目光放在了实时目标检测上基于YOLOv11搭建了一套苹果成熟度识别检测系统用深度学习同时解决“苹果在哪”和“什么成熟度”两个问题配套YOLO数据集、UI界面、登录注册和完整的Python项目源码。这篇文章会把从数据准备、模型训练到界面开发和部署排障的完整过程记录下来适合正在做农业视觉项目或者想快速做一个可演示、可验收系统的朋友参考。1. 为什么选YOLOv11选型逻辑与系统架构1.1 成熟度检测到底是个什么任务苹果成熟度检测从算法角度看本质是一个目标检测加细粒度分类的复合任务。它比“找出苹果”要难一点因为同一颗树的苹果颜色变化是渐进的相邻两级之间的边界很模糊。我把它定义为三类未熟、适熟、过熟视觉上对应绿色、黄绿色与红色偏深。这三类之间确实存在大量中间态所以数据标注的一致性直接决定模型上限。既然要定位又要分类一类方案是先做分割再做分类一类方案是用单阶段检测器直接回归边界框和类别概率。实测下来在果园这种背景杂乱、目标密集且重叠严重的场景里两阶段方法精度虽然不错但推理速度不太够尤其将来要接到实时视频流或者采摘机器人上时帧率就是硬指标。1.2 选YOLOv11而不是更早版本的几点考虑用过YOLOv5和YOLOv8的人应该都有体会官方仓库的可复现性和部署友好度确实好。YOLOv11在速度和精度之间做了新的权衡尤其是模型结构里加强了浅层特征融合对小目标的召回有明显提升。对苹果这种中等大小目标可能感知不明显但密集果园场景下一堆果子叠在一起时小目标能力的提升会直接体现在mAP上。我也对比过直接在YOLOv8上训练精度差距其实很小最终选择YOLOv11更多是因为预训练权重、官方配套工具链和后续支持都比较新。另外在NVIDIA显卡上训练时YOLOv11对显存的占用相对友好batch可以开得更大这对个人开发者来说很实际。如果用的是企业级两阶段模型同样的任务可能需要一次推理跑两次模型部署成本差很多。方案推理速度密集场景小目标部署复杂度我的结论两阶段检测器较慢中高不适合实时分级YOLOv8快较好低可用但生态略旧YOLOv11很快好低最终选择1.3 系统整体架构和模块划分整个项目不只是一个模型还包括可交互界面和账号系统。我按四层来组织代码界面层登录注册窗口、主操作窗口负责用户交互。业务逻辑层图像选择、推理调度、结果格式化、历史记录管理。推理层加载YOLOv11模型权重预处理图像执行检测输出边界框和类别。数据层SQLite保存用户信息与识别记录YOLO标注文件负责训练数据组织。为什么需要登录注册因为实际使用场景不是一个人在电脑上玩而是多人轮班使用比如合作社里不同小组分别上传各自果园的照片。如果不做账号隔离记录会混在一起时间久了根本没法追溯哪批照片是谁传的。做一个简化的登录注册是让系统真正可用起来的第一步。2. 苹果成熟度YOLO数据集从标注到增强的完整链路2.1 先把“成熟度”定义成可标注的规则这是整个项目里最容易被低估的环节。找标注工时如果只给人家看“未熟、适熟、过熟”三个词标注标准肯定会乱。我做了张标准参照图把每类对应的典型颜色、果柄状态和光泽度可视化让每位标注者对同一个苹果的判断保持一致。我用的三类标准仅供参考不同品种需调整未熟果皮主色调为绿或黄绿果面光泽较弱果柄偏绿。适熟果皮出现明显红色区域或整体黄底红晕果肉硬度适中光泽增强。过熟果皮全红或暗红局部可见软斑或果皮皱缩。这套标准最大的价值是让“边界争议”尽量少。遇到实在拿不准的我宁可删掉也不硬标因为模糊样本会直接把模型训练过程带偏。标注完成后有必要抽检一部分数据统计三类样本数量是否均衡。2.2 YOLO数据集格式一张图片对应一个txtYOLO格式的标注文件是纯文本每行一个目标格式为class_id center_x center_y width height所有坐标都做了归一化范围在0到1之间。比如一张宽1280像素的图片里某个苹果的中心点在x320、y240宽高为160x160那么对应内容是0 0.25 0.1875 0.125 0.125这里类索引0对应未熟。建议标注前先和标注人员确认类索引顺序因为模型训练时会根据class id读取类别名标注中途换顺序等于全部返工。标注工具我用的是常规的图形化标注工具支持导出YOLO格式。唯一要注意的是标注框不要贴得太紧或留太多背景通常建议框略紧贴苹果轮廓再外扩3%左右。遮挡严重的苹果要不要标我选择标但只标遮挡面积不超过40%的目标。遮挡太狠的框会让模型学习到错误特征特别是“过熟”类别本来就容易被叶片阴影遮盖错误样本更麻烦。2.3 数据采集、增强与划分的实操经验采集阶段尽量覆盖不同天气和时段因为果园里的光照变化非常大。我在晴天、阴天和早晚弱光环境分别拍了一部分加上手机和相机混拍让图片分辨率有差异。整个数据集大约1300张每张平均12个标注目标总计约1.5万个框。数据增强方面我关闭了YOLOv8默认的自动优化里的一些过强增强单独设置了饱和度、亮度、轻微旋转和水平翻转。成熟度判断很大程度上依赖颜色饱和度增强幅度要控制不能把绿色苹果增强成红色。阴影对比度也做了部分增强因为实际场景里苹果总有一半在树荫下。数据划分有个很容易忽略的坑如果同一个苹果出现在多张连续拍摄的照片里按单张图片随机划分会导致同一目标同时出现在训练集和验证集验证指标虚高。我改成按拍摄批次划分同一批拍摄的照片只进入一个集合最终按约8:1:1分成训练、验证和测试集。3. 训练实战参数配置、训练命令与调优过程3.1 环境准备与数据配置我用的环境是Python 3.9、PyTorch 2.x、官方ultralytics包。训练前先写数据配置yamlpath: apple_maturity_dataset train: images/train val: images/val test: images/test names: 0: unripe 1: ripe 2: overripe注意path字段建议用绝对路径避免在不同机器上运行时因为相对路径问题加载不到图片。我第一次训练时报错“Dataset not found”排查了很久才发现是yaml里相对路径的基准目录不对。这一点在给别人分享项目源码时特别重要建议在代码里动态拼接绝对路径。3.2 训练命令与参数选择我用yolo11n作为基础权重因为目标是快速迭代很多次小模型单次训练时间短先在n级上跑通流程再根据结果决定是否升级到s级或m级。训练命令如下yolo detect train dataapple_maturity.yaml modelyolo11n.pt epochs150 imgsz640 batch16 patience20 optimizerAdamW lr00.001几个关键参数我的理解是imgsz640是性价比最高的默认尺寸苹果不是极小目标不需要强制拉大分辨率。batch受显存限制如果显存不够优先降到8而不是缩小imgsz否则小目标特征丢失明显。patience20做早停当验证集mAP连续20轮不涨训练自动结束省时间。optimizer我习惯用AdamW微调时收敛稳定。3.3 训练日志里到底要看什么指标训练过程中重点看三类指标precision、recall和mAP0.5:0.95。对成熟度检测我更关心recall而不是precision。漏检一个苹果比误检一个重要得多因为后面做产量统计时漏检直接导致计数偏低。我这轮训练最终在验证集上达到了mAP0.5 0.93mAP0.5:0.95 0.71。单看均值还不错但看类别精度的混淆矩阵时暴露了一个问题过熟类别的召回率显著低于另外两类。原因很直观过熟苹果颜色暗红在树荫下和枯叶背景容易混淆。针对这个问题我做了两件事。第一是提高过熟类别的损失权重让模型更关注分类错误。第二是收集更多阴影遮挡下的过熟苹果照片做针对性补充。第二件事效果最明显数据补齐后过熟类别的召回率提升了约6个百分点。指标未熟适熟过熟总体precision0.950.900.880.91recall0.960.890.810.89mAP0.50.950.920.900.933.4 导出模型onnx与engine的选择训练完导出onnx是部署前的一步标准操作。onnx格式的好处是不依赖PyTorch运行时系统可以直接调用onnxruntime推理体积也更小。如果目标机器有NVIDIA显卡再进一步导出engineTensorRT的推理速度可以提升一倍以上。导出命令很简单yolo export modelbest.pt formatonnx imgsz640 yolo export modelbest.pt formatengine device0导出的onnx文件大约在几十MB量级放到UI项目的models目录下即可。这里提醒一句onnx推理时输入层的归一化方式要和训练时保持一致否则检测结果可能全是空或全是错误框。我的做法是在推理代码里手动将图像缩放到640x640再归一化到0-1避免不同库的预处理差异。4. UI界面与登录注册把系统做成能交付的样子4.1 界面方案取舍与页面结构设计界面层我选了PySide6而不是Tkinter。虽然Tkinter更轻但做这种“检测结果实时显示历史记录表格”的功能PySide6的控件成熟度更高拖拽布局和样式表能让我很快搭建出像样的交互界面。页面结构分两大部分登录注册页账号密码输入、注册入口。主操作页左侧图像预览区右侧检测结果区下方历史记录表格。登录页和主页面通过窗口切换实现登录成功后关闭登录窗口打开主窗口。这个流程逻辑清晰也方便后期把系统扩展成带权限的管理后台。4.2 登录注册的实现要点密码绝不能明文存储登录注册系统看起来简单但密码存储方式是个红线。我使用加盐哈希来处理密码而不是把明文写进数据库。具体做法是注册时生成随机盐值把盐和密码拼接后做哈希运算存进SQLite时记录盐和哈希值。登录时用输入密码加同一个盐再做哈希比对结果。import hashlib import os def hash_password(password: str, salt: str None) - tuple: if salt is None: salt os.urandom(16).hex() digest hashlib.sha256((salt password).encode()).hexdigest() return salt, digest用户表结构包含用户ID、用户名、加盐哈希、注册时间和角色字段。角色字段目前只有普通用户和管理员两级管理员可以查看所有历史记录普通用户只能查看自己的。这个设计在多次演示中都很加分评委或客户会认为系统考虑了实际使用权限。4.3 把推理放到QThread里界面卡死是最大的体验杀手界面上传图片后如果直接在主线程执行模型推理图像一多界面就会假死。用户以为程序崩了实际上还在算只是界面没法刷新。这是新手最容易犯的错误。解决办法是把推理放到QThread子线程中通过信号把结果传回主线程更新界面。核心代码如下class InferenceThread(QThread): result_ready Signal(list, str) def __init__(self, model, image_path): super().__init__() self.model model self.image_path image_path def run(self): boxes self.model.predict(self.image_path)[0].boxes parsed [] for box in boxes: cls int(box.cls[0]) xyxy box.xyxy[0].tolist() conf float(box.conf[0]) parsed.append((cls, xyxy, conf)) self.result_ready.emit(parsed, self.image_path)推理过程中主线程可以继续响应用户操作比如显示进度提示或让用户取消当前任务。实测中单张640x640图片的推理时间在CPU上约200到400毫秒在显卡上约20到50毫秒UI线程分离后体验是质的提升。4.4 检测结果的展示与历史记录导出每个检测框按照类别映射到不同颜色未熟为绿色适熟为橙色过熟为红色。这个映射在UI中用颜色条直观说明用户不需要看日志就能理解。识别结果区还会统计三类数量占比并计算“适熟占比”这个指标便于用户判断整批果子的采摘优先级。历史记录按用户名和时间戳存储每次识别的图片路径、各类别数量和导出时间都会写入SQLite。再提供一个导出到CSV的功能这样用户可以把批次数据导入表格软件做后续统计。CSV导出代码很简单但一定注意用utf-8-sig编码否则用表格软件打开时中文会乱码。5. 部署排障从“能跑”到“稳定运行”的距离5.1 打包成可执行文件时最容易踩的坑把Python项目交付给不懂Python的用户打包是必要的一步。用PyInstaller打包PySide6程序时最容易出现两个问题一是模型文件没有被自动收集二是动态库缺失。我的做法是打包时显式指定模型路径并加上--collect-all参数收集PySide6相关依赖pyinstaller -w -F main.py --add-data models/best.onnx;models --collect-all PySide6注意-f和-w的组合-w表示不弹出控制台窗口-F表示打包成单个exe文件。生成的exe体积可能接近300MB因为PySide6和onnxruntime都比较大。如果对体积敏感可以考虑用更轻量的界面方案或者把onnxruntime替换成更小的推理引擎。但为了稳定和功能完整我通常接受这个体积。5.2 运行中最常见的四类问题第一类是中文路径问题。模型文件和图片路径里如果带中文某些推理环境会读不到文件。我的解决方案是代码里统一用Path对象处理路径优先使用英文目录同时在文件选择对话框里做路径检查路径不合规直接提示。第二类是模型文件缺失或版本不匹配。onnx模型和推理库版本之间可能存在算子兼容问题。现场报错往往很怪比如“Unknown model type”或“Failed to load onnx”。排查时先确认模型文件是否完整再看onnxruntime版本是否过旧升级到最新版本能解决大部分兼容问题。第三类是SQLite数据库初始化异常。首次启动时需要自动创建用户表和记录表如果程序目录没有写权限整个流程会卡在登录页。我后来把数据库路径改到了用户主目录下彻底绕开了权限问题。第四类是摄像头或连续图片流场景下的显存泄漏。如果系统加上摄像头实时识别频繁创建推理会话会导致显存持续增长。要注意复用同一个推理处理器而不是每一帧都new一个实例。这个坑在演示长时间运行后特别明显界面响应变慢最终直接卡死。5.3 边缘扩展把模型接到真实业务里去这套系统目前是单机版本但模型推理本身是通用的。下一步如果扩展到移动端导出成NCNN或TFLite格式就行如果要接到采摘机器人只需要把检测结果通过Socket接口实时传出给机械臂端提供坐标和类别信息。另外前面提到按面积估算产量的思路也很实用检测框的面积可以粗略换算成苹果的投影面积再做按批次汇总。这样从“数出多少个”升级成“估出大概多少斤”对果农更友好。虽然精度受拍摄距离影响但作为分级辅助工具已经足够。这套系统的核心价值说到底不在模型多新、界面多炫而在于把“成熟度”这样一个模糊的农业概念转化成了可量化的工程产品。我在跑完整流程后最大的感受是数据标准决定了项目的天花板模型和界面只是把数据里的信息挖掘出来。如果你准备复现这套方案建议先把标注标准想清楚再动手写训练代码——这一步省下来的返工时间足够你多训好几轮模型。