基于YOLOv10的苹果腐烂检测系统:从数据集训练到UI部署全流程
1. 为什么仓库里那堆烂苹果值得一个检测系统前阵子帮一个做果蔬供应链的朋友整理需求他提了个很实际的问题每天经手的苹果按吨算从产地冷库到批发市场一路颠簸下来总有一批苹果开始腐烂。人工分拣呢工人盯着传送带挑烂果一小时下来眼都花了小腐烂斑被阴影盖住就漏过去到了终端客户手里就成了投诉。他问能不能搞一个自动检测系统——苹果从传送带过去机器直接标出哪些有腐烂区域省掉一半人工。这就是标题里那个基于YOLOv10的苹果腐烂检测系统的起点YOLOv10深度学习模型负责看懂图像YOLO数据集负责喂给模型学什么是腐烂UI界面负责让人能上手用最后导出一个训练好的模型文件整套东西才算是真正落地。这篇文章我打算把整个过程完整拆开讲为什么选YOLOv10、数据集是怎么凑出来和清洗的、训练参数怎么调、UI界面怎么封装推理逻辑以及最后真实上机测的时候踩了哪些坑。适合正在做农业视觉项目的开发者、做毕设或课程项目的学生还有想把手头模型快速变成能给人用的小工具的工程师。系统边界先说清楚它做的是检测腐烂区域不是判断这个苹果能不能吃。苹果表面碰伤、黑斑、腐烂在图像上经常长得差不多这件事我后面会专门讲也是整个项目里最容易翻车的地方。2. YOLOv10选型不是追新是这几个改动刚好打在我痛点上2.1 YOLO家族横向对比v10到底改了什么YOLO系列走到现在已经迭代了很多版本v5、v8在工业界用得最多v10是2024年提出的新版本。做项目选型我一般不太在乎是不是最新更在乎改动是否正好解决我的问题。v10这版有几个关键变化跟苹果腐烂检测的需求刚好能对上。第一是去掉了NMS非极大值抑制。老版本的YOLO推理完后同一个目标上会叠很多个候选框需要用NMS按置信度挑一个最优框。NMS本身有计算量而且对参数敏感IoU阈值设高了漏检、设低了误检。v10提出了一种一致匹配consistent matching的标签分配策略把类别预测和框回归对齐起来训练阶段就不依赖NMS的调参。这意味着推理代码更简单、逻辑更干净部署到嵌入式设备上少一层处理。第二是分类头和回归头解耦。老架构里分类和回归共享一个检测头v10把两者拆开设计每个任务用独立的轻量级模块。直观感受是框的定位更稳尤其是在目标物边缘模糊的场景——而腐烂苹果的边缘恰恰是非常模糊的腐烂区域和健康果肉之间的过渡带很宽这个改动对边界框稳定性的提升是实打实的。第三是模型结构上的轻量化改造。v10用了一些新的结构单元整体计算量比同规格的v8有下降。我做实测的时候同样的工作站环境下YOLOv10s的推理帧率比v8s大概快了一个档次。2.2 v10s还是v10n算力约束下的选择做这种落地项目选什么规格的模型完全取决于部署环境。如果只追求极致精度直接上YOLOv10x但你要先想清楚冷库车间里用的是工控机还是带显卡的服务器如果是几年前的CPU工控机跑大模型基本不现实。我的选择是主线用YOLOv10s备选YOLOv10n。两个都训练了对比情况大概是这样规格参数量单帧推理耗时GPUmAP50验证集适合场景v10n较小约5毫秒较高实时摄像头、低功耗盒子v10s中等约9毫秒最高离线检测、批量图片、质检工位说句实话v10n和v10s在腐烂检测上的精度差距没有想象中大但v10s对边缘模糊的小块腐烂区域明显更敏感。所以如果设备跑得动优先选s。3. 数据集准备腐烂苹果长什么样模型就得学什么样3.1 数据来源与自采细节深度学习这事模型结构是发动机数据是汽油。这个项目的数据主要由两部分构成公开的果蔬检测数据集里挑出苹果腐烂相关的图片加上自己用手机拍的实拍图。自采这部分很重要因为公开数据的场景干净但真实仓库的光线、果筐纹理、传送带阴影都没覆盖到。自采的时候我换了几个地方拍冷库里灯光偏白、有雾气、分拣线上方日光灯直射、普通室内自然窗光。三个场景混在一起训练模型才能学到腐烂在什么光线下都有特征而不是把某个光照环境背下来。采集数量方面我最终攒了大概两千多张图每张图上腐烂苹果数量不等。这个规模不算大但配合标注和增强策略已经能跑出一个可用的模型。3.2 标注规范腐烂、碰伤和阴影到底怎么画框这是整个项目里最脏最累、也最影响模型上限的环节。腐烂的苹果图像特征通常表现为果皮凹陷、深褐色或灰褐色区域、表面可能发软发皱。但问题在于碰伤也是深褐色黑斑也是深褐色甚至一个苹果被另一个苹果的影子挡住半边颜色也会发暗。我第一批标注的时候没定好规则A同学把碰伤全标成了腐烂结果训练完模型对碰伤疯狂输出检测框——这显然是错的。后来我们定了统一规范核心腐烂有明显凹陷、皱缩、变色的区域标为目标。碰伤但未腐烂颜色有局部变化但果皮没有明显凹陷/皱缩的不标。腐烂区域被遮挡只标注能被清楚辨认的部分不强行标全。一个苹果多个腐烂点分开标多个框不要并成一个。这个规范听起来简单但标注协力人员不盯着就很容易变形。我的经验是每个标出来的框都过一遍二次审核特别是边界模糊的图。前期花一小时定标准胜过后期返工三天。3.3 VOC格式怎么转成YOLO格式标注工具我用的环境是LabelImg标完导出的是Pascal VOC格式的XML——每个目标一个框坐标是左上角和右下角。YOLO训练需要的是txt格式每行是类别id cx cy w h其中cx、cy是中心点坐标w、h是宽高四个值全部归一化到0-1之间。转换脚本如下import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) box obj.find(bndbox) x_min float(box.find(xmin).text) y_min float(box.find(ymin).text) x_max float(box.find(xmax).text) y_max float(box.find(ymax).text) cx (x_min x_max) / 2 / img_w cy (y_min y_max) / 2 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_name os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines))注意这里归一化的w和h是相对于原图尺寸的不是相对于中心点的偏移这个点新手经常搞混。还有坐标检查cx ± w/2 应该在0-1之间越界了说明标注框有问题训练前要清洗掉。3.4 数据增强和划分比例模型要做的一个核心事是泛化就是没见过的场景也能检测出来。数据增强相当于给模型出变形题把图片旋转、翻转、裁剪、调亮度、调色温逼它学本质特征而不是死记场景。我用的增强策略主要靠ultralytics框架里内置的增强参数# 训练时部分增强参数 mosaic: 1.0 # 四图拼接增加复杂性 mixup: 0.2 # 图像混合增加多样性 fliplr: 0.5 # 水平翻转 hsv_h: 0.015 # 色相微调 hsv_s: 0.5 # 饱和度变化 hsv_v: 0.4 # 亮度变化数据集划分上面我按大约7:2:1分成训练集、验证集和测试集。值得提醒的是划分之前先把来自同一批拍摄的照片打散避免同场景的图片同时出现在训练集和测试集造成测试分数虚高。说直白点就是让测试集考没见过的题而不是背过的题换个数字。4. 训练全流程从命令行到模型文件中间有哪些关键决策4.1 环境搭建与版本锁定训练环境我建议直接PyTorch CUDA ultralytics。版本这块非常坑先说个原则先装ultralytics包让它自动帮你配好对应依赖而不是自己凭感觉装一堆库。# 创建独立的虚拟环境 conda create -n apple_detect python3.10 -y conda activate apple_detect # 安装CUDA版PyTorch根据你的显卡驱动版本选择对应CUDA版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装ultralytics pip install ultralytics安装完可以验证一下import torch import ultralytics print(torch.cuda.is_available()) # True 才说明GPU可用 print(ultralytics.__version__)4.2 训练命令的每个参数为什么这么设训练入口很简单yolo detect train一行命令就起来了但参数选择影响很大。我用的训练命令如下yolo detect train \ dataapple.yaml \ modelyolov10s.pt \ epochs120 \ imgsz640 \ batch16 \ patience15 \ optimizerAdamW \ lr00.001 \ cos_lrTrue \ rectTrue \ device0每个参数背后都有决策逻辑挑几个重点讲imgsz640YOLO系列的默认输入太高了对显存压力大太低了小腐烂区域可能被压缩没掉。640是个平衡点。batch16取决于显卡显存。16G显存跑v10s、640分辨率大概这个批次能跑不够就降到8或4。patience15早停机制连续15个epoch验证集指标没有提升就自动停。不是省时间的问题是防止训练到最后把训练集背下来验证集反而变差过拟合。optimizerAdamW lr00.001默认的SGD偏慢AdamW收敛稳定对苹果这种类别区分度不算高、样本量又不大的任务比较友好。cos_lrTrue学习率余弦衰减前期学得快后期逐步收敛到精细化比固定学习率平滑很多。4.3 从训练曲线里读信息而不是干瞪眼训练跑起来后ultralytics会在runs/detect/train/目录下生成曲线图。我做项目的习惯是obsessively盯着三张图看第一是loss曲线。box_loss框回归损失和cls_loss分类损失都在下降说明模型在学。如果loss曲线在某个epoch后反而掉头往上走多半是学习率太大或者数据里有脏标注。第二是PR曲线precision-recall curve横轴召回率、纵轴精确率曲线越往右上包模型越好。第三是混淆矩阵这个能直接看出模型把哪类目标搞混了。我训练到大概60个epoch的时候验证集mAP50就已经到了挺可用的水平之后主要靠早停机制自动判断收尾时机。不建议无脑跑满120个epoch那多出来的训练时间大部分是在瞎忙。4.4 训练完成后的评估指标怎么看训练完会输出一串指标最重要的四个precision检出的框里多少比例是正确的。recall实际的腐烂区域里多少比例被检出来了。mAP50IoU阈值0.5下的平均精度衡量框大概对就算对。mAP50-95多个IoU阈值0.5到0.95的平均精度要求严格得多。对苹果腐烂检测来说recall比precision更优先。宁可多标几个疑似腐烂让工人看一眼也不能漏掉真腐烂的苹果——漏一箱损失的是整批货的信誉。实际操作里我会把置信度阈值调低一点比如0.25-0.35让模型倾向于多报再由人工复核这个策略在落地阶段非常有效。5. UI界面模型再准得让不会写代码的人也能用才算完5.1 界面功能拆解检测系统不只是能跑训练好的模型只是一个黑盒文件真正的检测系统必须有界面给人操作。我把界面拆成三个功能模块单张图片检测选择本地图片显示检测结果并保存。摄像头实时检测调摄像头预览画面实时画框。批量检测目录选择文件夹跑完生成标注过的图片和统计报表。界面框架我用的PySide6。选它不是因为花哨而是跨平台、打包方便、控件成熟。如果你完全不熟悉Qt可以用gradio快速搭Web界面但作为本地工具桌面界面更稳妥。5.2 推理逻辑封装模型加载和预测界面里调用模型的代码要封装在独立模块里不要在界面代码里堆逻辑。核心代码如下from ultralytics import YOLO class AppleDetector: def __init__(self, model_path, conf_thres0.30): self.model YOLO(model_path) self.conf_thres conf_thres def detect_image(self, image_path): results self.model.predict( sourceimage_path, confself.conf_thres, device0, # 有GPU用GPU没有就用cpu verboseFalse ) boxes [] for r in results: for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].tolist() conf float(box.conf[0]) cls_id int(box.cls[0]) boxes.append({ bbox: (int(x1), int(y1), int(x2), int(y2)), confidence: conf, class_id: cls_id }) return boxes注意这里device0是GPU加速如果部署机器没有NVIDIA显卡改成devicecpu代价是实时检测帧率会明显下降。折中方案是用yolov10n.pt在CPU上跑680x480分辨率的视频帧虽然谈不上丝滑但也能用。5.3 多线程不卡界面的最小实现实时检测涉及摄像头视频流一直在更新如果直接在UI线程里做推理界面会卡到未响应。标准的做法是用QThread把摄像头采集和模型推理放到后台线程主线程只负责刷新画面。核心思路是这样from PySide6.QtCore import QThread, Signal class DetectThread(QThread): frame_ready Signal(object) # 每帧结果信号 def __init__(self, detector, source): super().__init__() self.detector detector self.source source self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running: ret, frame cap.read() if not ret: break boxes self.detector.detect_image(frame) # 推理 for b in boxes: cv2.rectangle(frame, b[bbox][:2], b[bbox][2:], (0, 0, 255), 2) self.frame_ready.emit(frame) cap.release()界面那边槽函数只管frame_ready信号把拿到的画面显示到QLabel上就行。这个结构是很多桌面视觉工具的通用骨架日常自己接其他检测任务也完全能复用。5.4 打包成exe给不会配环境的人用到这一步我还做了一件事用PyInstaller把整套界面和模型文件打包成exe朋友那边的人双击就能运行。打包有几个注意事项模型文件.pt要作为资源文件一起打包路径最好用相对路径或动态获取否则exe换个目录就找不到模型。PySide6打包体积偏大正常都在300MB以上这是Qt的代价。如果目标机器CPU不支持某些指令集可以换用CPU版PyTorch重新打包体积能小很多。第一次打包建议做成单个文件夹不要做成单文件排错方便。6. 实测与避坑白天没问题换到冷库就翻车这些坑我都踩过6.1 光照敏感模型的近视眼问题模型训练完在测试集上效果不错放到真实冷库里一测误报率一下子涨了不少。冷库顶灯是白冷光苹果表面有雾气和水珠反光和训练集里干净的室内照片完全两回事。后来我做了两件事解决一是往训练集里补图冷库环境实拍加入数据增强的训练robustness是靠数据喂出来的不是靠调参数调出来的。二是推理前做图像预处理读帧后先做一次简单的亮度归一化减少光照差异带来的分布偏移def preprocess(frame): lab cv2.cvtColor(frame, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) l clahe.apply(l) lab cv2.merge((l, a, b)) return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)CLAHE自适应直方图均衡就像是给图片祛一层灰在光照不均匀的环境下有奇效。但要注意训练和推理的预处理流程要保持一致。如果你训练时没有加这个预处理那推理时加了反而会让图像分布偏移更严重。正确做法是把预处理逻辑写进推理模块训练阶段用同样的处理再抽出增强图。6.2 腐烂初期vs后期模型到底在学什么苹果腐烂是个渐进过程最开始是一个针尖大小的水渍斑慢慢扩大、变褐、凹陷、皱缩到最后表面发黑发软。模型对早期斑点和中后期腐烂的检测能力差距很大。早期斑点太小imgsz640的情况下可能只占几个像素特征极弱基本会被模型忽略。中后期腐烂区域大、纹理明显检测框很稳。这个问题的本质是目标尺度和分辨率之间的权衡——想检测早期斑点要么用更高分辨率输入imgsz1280要么走分块检测路线把大图切成小块分别推理。我试下来后者更划算但速度会翻几倍。做质检项目要认清边界你搭建的系统到底要检测什么阶段的腐烂不要指望一个模型通吃所有细节。和需求方确认最小可接受腐烂面积再反推标注分辨率和输入尺度这样才不会把系统做到一半才发现方向偏了。6.3 果框里的重叠苹果怎么避免漏检真实场景里苹果是堆在果筐里的不是一个个整齐排开。两个苹果叠在一起被遮挡的腐烂区域模型压根看不见。检测系统能看到的只是露出表面的腐烂这部分没法靠算法解决只能靠产线设计比如让苹果单层排列过检或者接受漏检率。给UI增加一个重叠堆叠预警的功能也很不现实因为检测框没有深度信息。我的做法是分级报告检测到腐烂的框按置信度分成高置信和需人工复核在界面里用不同颜色框展示。这样虽然不能消除遮挡漏检但至少把工人的复核精力聚焦到风险区。6.4 实时检测的帧率优化思路摄像头实时检测对帧率有要求。经过一系列优化我的实测数据是这样的方案帧率结论v10s GPU (RTX级别)30FPS左右流畅可用v10s CPU2-5FPS基本不能实时v10n CPU8-12FPS勉强能用v10n GPU30FPS以上非常流畅CPU上想提高帧率还可以把推理分辨率从640降到480通过降分辨率压缩计算量代价是早期腐烂斑更可能被漏掉。如果必须CPU跑且对召回有要求另一个思路是跳帧处理每3帧设1帧检测中间两帧直接沿用上一次结果画面看起来仍然连贯但目标快速移动时框会拖影。6.5 数据闭环上线之后才是数据积累的开始模型真正上线之后最有价值的环节其实是收集漏检和误检样本。我在UI里加了一个错误样本保存按钮工人看到检测结果明显不对漏检的烂果、误检的好果就点一下图片自动存到回收站文件夹每周做一次增量标注加入训练集里重新微调。这个流程跑了一个多月模型在真实场景的recall稳定提升了一截。深度学习落地不是一锤子买卖而是数据—训练—部署—反馈的持续循环。很多项目死在模型训练完就撒手上后面的战场在脏活累活里——清洗数据、校准标注、处理长尾场景。最后的实操体会整个项目做下来最真实的感受是YOLOv10让能跑通一个检测系统变得很容易但让系统在真实环境里稳定干活功夫全在数据细节和工程边界上。选模型、调参数、搭界面每一步都有官方文档可抄但腐烂的标注边界怎么定冷库光线下怎么不翻车遇到遮挡怎么设计预警这种问题没有现成答案只能自己在现场一遍遍试。如果你也要做类似的农业检测项目我的建议是先花一周时间把数据整理到标注规范清晰、分布覆盖真实场景的程度再开始训练。模型结构换起来很快数据质量换不来。另外从一开始就留出数据反馈通道哪怕只是一个简单的保存错误样本按钮也会在后续迭代中帮你省下大量时间。