我前阵子接了个果蔬分拣相关的视觉项目,核心需求就是把大小不一的水果照片按“好果”和“坏果”自动分好类。这类需求在农产品标准化、商超自动结算、生鲜供应链质检场景里特别常见。我们最终选型就用 YOLOv5 和 YOLOv8 做了整套方案,配套整理了一份带标注好的水果好坏数据集,模型也提前跑通了,效果比较稳定,所以把整个思路、配置和踩过的坑都记录下来。
这份内容适合刚入门目标检测、想拿公共数据集做落地方案练习的开发者,也适合想快速评估 YOLO 系列在简单二分类检测任务上表现的同学。不需要你有很强的算法背景,只要你Linux命令行能跑通、看得懂Python就行。
1. 项目拆解:水果好坏检测到底在检测什么
1.1 核心需求与标签体系
先别急着下数据集。很多人一上手就说“我要检测水果好坏”,但“好坏”这两个字在视觉上其实是高度抽象的。腐烂、碰伤、凹陷、变色、表面褶皱、霉斑,这些都属于“坏”的理由,但它们长得完全不一样。如果直接定义成一个大类“坏果”,模型会把所有异常样本强行往回塞,很容易造成漏检。
在这个项目里,我建议把标签拆成更细的类别,常见做法有两种:
- 二分类方案:
good和bad,适用于快速验证、分拣线粗筛; - 多状态方案:
good、bruise、rotten、damaged,适用于需要知道具体坏因的场景。
我在做这个项目时,初始也是图省事只标 good 和 bad,但很快发现“坏果”在视觉上看不齐,特征太散,表现在验证集上就是 recall 一直往下掉。后来改成多状态标注,模型对“腐烂”和“碰伤”的响应明显分开,下游筛选逻辑也更灵活。
标注文件用的是 YOLO 标准格式,每张图对应一个同名 txt 文件,每行是一条目标记录,格式为:
class_id x_center y_center width height坐标值全部是归一化后的比例值,不是像素坐标。比如一张 640x480 的图中,某个苹果框的左上角在 (160, 120),右下角在 (480, 360),那个框的归一化坐标就是中心点 (0.5, 0.5),宽高 (0.5, 0.5)。这一块看似基础,但很多人第一次跑模型时框全乱飘,就是坐标没归一化。
1.2 检测任务与分类任务的区别
这里还要聊一个容易被搞混的点:水果好坏检测是“目标检测”问题,不是“图像分类”问题。分类模型只会告诉你“这张图里有没有坏果”,但它给不了坏果在哪儿、有几个、是哪一颗坏了。而分拣场景里,你可能面对的是托盘上几十个水果,必须知道坏的那个具体位置,机械臂才有办法把它夹走。
YOLO 系列恰好是这种场景的标准解决方案。它以网格方式预测目标框和类别,单次前向传播就能同时输出坐标和类别概率。在这个项目里,我选择 YOLOv5 和 YOLOv8 各跑了一版,既是为了对比,也是为了面对不同部署环境时有储备方案。
1.3 适用场景与影响范围
水果好坏检测所影响的并不仅仅是“分拣”这个动作。往下游看,它可以对接统计报表、库存管理、损耗归因;往上游看,它可以反推采购时的成熟度分布、存储环境是否需要调整。所以做这个数据集和模型时,我特意保留了每张图的拍摄时序和批次信息,这样后面做趋势分析时还能复用标注结果。
2. 方案选型:YOLOv5还是YOLOv8
2.1 YOLOv5的优势与适用场景
YOLOv5 虽然名字里有“v5”,但它不是某个官方机构按顺序迭代出来的正统版本,而是开源社区在 YOLOv4 基础上做工程化改造后形成的高性能实现。这里不争论名分,只讲实用性。
这个项目里我最早用的就是 YOLOv5,原因有三个:
- 生态成熟,网上能找到大量训练经验、部署案例,遇到报错基本搜一下就有答案;
- 模型结构相对保守,稳定性好。训练循环、数据加载、增强策略全是封装好的,对于快速出结果来说非常舒服;
- 部署友好。导出 ONNX、TensorRT 都很方便,在边缘设备上跑几乎没有额外适配成本。
对于水果好坏检测这种中小目标、背景相对简单的任务,YOLOv5s 或 YOLOv5m 完全可以扛住。尤其是刚接触目标检测的人,从 YOLOv5 开始学,看代码、调参数都更容易理解。
YOLOv5 官方仓库提供 n/s/m/l/x 五个尺寸,n 是最轻量的版本,x 是精度最高但最重的版本。对这个项目,我用 v5s 做主力训练,batch size 设成 32,输入 640x640,在单张消费级显卡上稳定跑通。
2.2 YOLOv8的改进与适用场景
YOLOv8 是 Ultralytics 推出的版本,也是目前直接用 pip 就能体验最顺滑的 YOLO 工具之一。相比 v5,v8 主要改动集中在这几个方向:
- 从 Anchor-Based 转向 Anchor-Free。v5 需要预设 anchor 尺寸,v8 则是直接回归目标位置和尺寸,少了一层调试锚框的负担。
- Backbone 和 Neck 换成 C2f 结构,梯度流动更丰富,同等参数下特征提取能力更强。
- Head 部分做了解耦,分类和回归分支分开预测,收敛速度和精度都有提升。
- 内置了集成训练、验证、导出、推理的一体化命令行工具,一个
yolo命令就能完成全流程。
对水果好坏检测来说,v8 的 Anchor-Free 特性让模型在小物体上表现更好。比如你要检测一颗草莓上黄豆大小的霉斑,v8 的定位能力明显好于 v5。另一个我很喜欢的点是 v8 支持更简洁的 YAML 数据集配置,跑起来几乎不用写代码。
2.3 我的选型建议
拿我这个项目为例,整体路径是“数据先跑通,模型再择优”。一开始先用 YOLOv5s 快速建立 baseline,确认数据标注质量没问题后,再用 YOLOv8s 跑一版对比。最后生产环境里用的是 YOLOv8s,输出精度比 v5s 高了一个多点,推理速度差距却很小。
如果你手头任务对延迟不敏感,又可以接受稍微大一点的模型,直接用 YOLOv8m 会更稳。但如果你要部署到 Jeston Nano、树莓派这类低算力设备,建议用 YOLOv5n 或 YOLOv8n。n 版本参数量极小,香蕉好坏这种特征差异明显的目标,它也能跑得很好。
提示:不要一上来就追求最大最准的模型,水果检测不是自动驾驶,目标特征明显、背景相对单一,中小模型完全够用。模型太大反而移动端部署时麻烦。
3. 数据集实战:从采集到标注
3.1 数据采集与图像质量要求
数据集是这类项目的隐形瓶颈。一份好的水果好坏数据集,不是把手机里的随便几张水果照片攒一攒就行,它必须满足以下基本条件:
- 不同成熟度的个体分布要均匀。不能只拍刚摘下来的硬果,也要拍放了一周后的蔫果;
- 光照条件要多样。冷链仓库灯光偏冷,超市灯光偏暖,你至少要覆盖这两种典型环境;
- 角度要丰富。俯拍、侧拍、45度斜拍都要有,因为分拣线上的镜头角度是固定的,但水果摆放姿态是随机的;
- 坏果比例不能过低。真实货品里坏果也许只有5%,但训练集里坏果至少要占到30%-40%,否则模型会产生严重的类别偏向。
我实际整理这个数据集时,按苹果、香蕉、橙子三个大类分别收集,每类大约1200张原始图,共3600张,标注后不再额外清洗,直接进入训练。每张图内目标数量控制在1到8个之间,基本模拟了单果分拣和小批量托盘分拣两种模式。
拍摄上有一点比较关键:尽量使用不反光、哑光的背景板。如果用高光泽的塑料托盘,会在水果表面形成大面积反射光斑,导致标注员看不清边界,模型也容易把光斑误当成坏果纹理。
3.2 标注工具与标签规范
标注工具我用的是开源免费的 LabelImg 和 X-AnyLabeling。LabelImg 很老牌,胜在简洁稳定,批量改文件名、切换标注文件夹都很顺手。X-AnyLabeling 则在交互上更舒服,支持自动保存、半自动辅助标注,效率更高。
标注规范要提前定死,不能边标边变。我的规则是这样的:
- 边界框紧贴果实主体,不包括果梗伸出去的部分;
- 果实有重叠时,用能看见的轮廓框住完整果实,宁可略微外扩不要切进果肉;
- 坏斑面积超过果实面积30%的,框内标签设为对应坏果类别;少于30%的只框果实,类别还是 good,让模型学会忽略轻微的生理斑点;
- 同一张图里有多颗坏果时,每颗单独一个框,不能合并。
如果多人协作标注,建议先让两个人各自标完同一批50张图,算一下框的 IoU 和类别一致率。一致率达到90%以上再全面铺开,不然模型学到的标签噪声会非常大。
3.3 数据增强与训练集划分
YOLO 系列自带较强的数据增强,比如 Mosaic(四张图拼一张)、随机色域变换、随机翻转、缩放。但在水果质检这个具体场景里,我额外加了两个针对性增强:
- 亮度扰动范围调大。因为水果表面反光差异大,增强亮度能帮助模型适应不同光强;
- 增加 HSV 饱和度扰动。让绿色、黄色、红色在不同成熟度间有更好的连续性。
划分比例上我用了 8:1:1,即训练2880张、验证360张、测试360张。测试集完全独立,只在最后评估时用一次,避免过拟合评估结果。
4. 训练实操:环境配置与关键参数
4.1 训练环境准备
水果好坏检测这个量级的数据集并不吃硬件,我用的是一台消费级显卡服务器,但为了可复现,我建议你先固定环境版本。这里给出我实际使用的组合,照着装不会踩版本坑:
- Python 3.9 或 3.10;
- PyTorch 2.x;
- CUDA 11.8 或 12.1;
- ultralytics 8.0.x(对应 YOLOv8);
- opencv-python、numpy、pandas、matplotlib 这些基础库。
YOLOv5 的依赖可以用它的 requirements.txt 安装,注意不要和 v8 的版本冲突。如果你同机使用两个版本,建议分别建 Conda 环境:
conda create -n yolov5 python=3.9 -y conda activate yolov5 pip install -r yolov5/requirements.txtYOLOv8 更简单,直接 pip 安装:
pip install ultralytics4.2 数据集配置文件写法
YOLOv8 的数据配置用一个 YAML 文件搞定。我的文件放在/data/fruit-dataset/data.yaml,内容如下:
train: /data/fruit-dataset/images/train val: /data/fruit-dataset/images/val test: /data/fruit-dataset/images/test nc: 4 names: ['good', 'bruise', 'rotten', 'damaged']YOLOv5 的配置格式类似,但目录写法略有不同,需要指向包含 images 和 labels 的上级目录:
train: /data/fruit-dataset/train val: /data/fruit-dataset/val nc: 4 names: ['good', 'bruise', 'rotten', 'damaged']注意在 v5 中,训练脚本会自动拼接images和labels子目录;在 v8 中则直接指定 images 目录,labels 默认同路径下同名 labels 目录。改动目录结构后,务必验证标签路径是否正确,否则 loss 会一直不降。
4.3 参数选择与训练命令
先说几个关键参数的含义,不管 v5 还是 v8,这几个都是首选调整对象:
imgsz:训练尺寸,我用了 640。水果目标不算特别小,640 足够;如果你要检测细小的病斑,就上 960 或 1280;batch:受显存限制,我用的 32。如果你显存只有 8G,降到 16 甚至 8,同时调低 imgsz;epochs:先跑 100 轮看 loss 曲线趋势,再决定要不要加。不要一开始就设 500 轮,纯浪费时间;workers:数据加载线程数,Linux 下设为 8 没问题,Windows 下建议别超过 4,否则容易报数据加载错误;patience:早停耐心值,设为 30 表示验证集指标30轮不升就自动停止。
YOLOv5 训练命令:
python train.py \ --data /data/fruit-dataset/data.yaml \ --cfg models/yolov5s.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 32 \ --epochs 200 \ --name fruit_v5sYOLOv8 训练命令更简洁:
yolo train \ model=yolov8s.pt \ data=/data/fruit-dataset/data.yaml \ imgsz=640 \ batch=32 \ epochs=200 \ name=fruit_v8s我实际跑了 200 轮,v5s 大约用了 1.5 小时,v8s 大约 1.8 小时,时间差异主要来自 v8 的 C2f 模块计算量更大。前 50 轮 loss 下降很快,到 120 轮左右趋于平缓,最终 mAP50 稳定在 96% 左右。
4.4 结果评估:mAP、Precision、Recall
模型训练完不能只看 loss。目标检测的标准评估指标是 mAP,其中 mAP50 表示 IoU 阈值 0.5 时的平均精度,mAP50-95 是更严格的综合指标,范围在 0.5 到 0.95 之间按步长计算平均。
我贴一下自己训完后的参考数据:
| 模型 | Precision | Recall | mAP50 | mAP50-95 |
|---|---|---|---|---|
| YOLOv5s | 96.2% | 94.7% | 97.1% | 88.4% |
| YOLOv8s | 97.8% | 96.3% | 98.5% | 90.2% |
从 Precision 与 Recall 的角度看,v8s 不仅精确率高,召回也更好。这意味着漏检更少,错检也少。对分拣线来说,漏检一个坏果是直接损失,优先级比误判好果更高,所以 v8s 更贴合生产需求。
提示:如果只追求“看起来准确率高”,那只看 Precision 就行;但真实分拣产线更关注 Recall,即坏果是不是都被挑出来了。建议结合 NMS 阈值下调和置信度阈值调整来找到一个产线可接受的工作点。
4.5 推理与导出部署
训练完成后,v8 导出一个推理结果图很简单:
yolo predict model=runs/detect/fruit_v8s/weights/best.pt \ source=/data/fruit-dataset/images/test会自动生成带标注框的结果图,方便肉眼检查。
如果要把模型部署服务化,建议直接导出 ONNX:
yolo export model=runs/detect/fruit_v8s/weights/best.pt format=onnx导出后的 ONNX 文件可以被 OpenCV DNN 模块或 ONNX Runtime 加载,我实际在 CPU 上推理一张 640 分辨率的图,耗时大约 40ms 到 80ms,完全满足实时分拣线的要求。
5. 常见问题与排查实录
5.1 训练 Loss 不下降或收敛很慢
这个问题绝大多数时候出在标签路径或数据集划分上。我之前有一次 v5 训练到 30 轮 loss 还是纹丝不动,排查半天发现 YAML 里 train 路径写成了/data/fruit-dataset/train/images,但 v5 会自己拼接一次 images,导致实际加载的是train/images/images,路径不存在,标签全空。
另一个常见原因是背景干扰过大。如果你拍摄了太多复杂背景,模型初期需要大量轮次去区分“水果”和“背景”,自然收敛慢。建议前 50 轮先加大背景扣除,再进入全场景训练。
如果 loss 曲线一直锯齿状波动,通常就是 batch size 太小,或者学习率初始值偏高。YOLO 系列默认学习率是 0.01(v8 略有调整),如果你的 batch 从 32 掉到 8,建议按比例调低学习率到 0.0025,否则梯度噪声太大。
5.2 漏检多、坏果认不出来
漏检一般是标注不够细或者特征没学出来。比如“碰伤”早期只是轻微变红,如果标注框画得过大,把大量好果区域也包进去,模型就会混淆。
我的处理方式有两个:
- 对于轻微坏斑,单独把它裁出来作为小图加入训练集,强化局部特征;
- 提高难样本权重。YOLO 训练时可以把损失函数中的正样本权重加大,调高
cls_pw参数,或者直接在抽样时多抽含坏果的批次。
另外别忽略 Mosaic 增强在坏果检测中的副作用。Mosaic 拼接四张图,部分坏果特征可能因为裁剪位置被切掉一半,所以如果发现坏果小目标总漏检,可以降低训练时的 Mosaic 概率,甚至关掉它。
5.3 误检频繁、好果被当成坏果
这个问题多半是坏果标注框画得太松,把好果边缘的正常色差也卷进去了。标注坏果时,框应该贴着实际病变区域,不要为了框全果实而故意扩大。模型学习的其实是框内的纹理特征,如果框内一半是好果区域,当然会产生误检。
另一个排查点是置信度阈值。默认的 0.25 可能在复杂背景上产生较多误报。我实际生产中把置信度阈值调到 0.45,同时将 NMS IoU 阈值调到 0.6,误检率降了一半左右,但坏果召回基本不受影响。
5.4 标注类别不平衡怎么办
如果你的数据集中 good 占了 80%,而 rotten 只有 5%,模型会对 good 严重过拟合。最简单的做法是把各类别样本数量调整到接近比例。不要强行复制坏果图片,因为简单复制会产生重复样本,模型会“背题”。
推荐做法:
- 用数据增强生成更多坏果样本,比如旋转、裁切、翻转坏果局部;
- 用半监督方式扩充坏果数据,找一个性能尚可的模型先预测坏果,人工复核后加入训练集;
- 在下游应用时配合阈值调整,优先保证 bad 类别的召回。
我实际将 rotten 类样本增强到占全量 20% 后,mAP50 提升了大概 3 个点,说明坏果数据稀缺确实是这类任务的重要瓶颈。
6. 后续扩展与实用建议
6.1 从检测到分级:融合分类模型
检测模型已经能给每个水果画框,并判断它是好是坏,但如果你还想进一步做“糖度分级”或“成熟度评估”,靠 YOLO 就不够精细了。一个实际效果很好的做法是,先用 YOLO 把水果框出来,然后把框内区域裁剪出来,交给一个小型分类网络,比如 ResNet18 或 EfficientNet,做更细等级判断。
这种方式相当于检测模型负责“在哪里、大致是什么”,分类模型负责“具体状态是几级”。拆分之后,每个模型的任务更纯,精度提升空间更大。这也让整个系统具备良好的模块扩展性。
6.2 模型更新与增量学习
落地后最怕的是数据分布发生变化。比如换了一个新品种苹果,或者仓库换了照明条件,模型精度会明显下降。这时候不需要重新全量训练,可以在原模型基础上做增量微调:
yolo train model=runs/detect/fruit_v8s/weights/best.pt \ data=/data/fruit-dataset-new/data.yaml \ epochs=50 \ lr0=0.001注意学习率要调低一些,防止破坏已经学好的旧特征。增量训练结束后,务必用原来测试集回归一遍,确认没有灾难性遗忘,也就是旧类别的指标不要掉太多。
6.3 我的一点经验
做水果好坏检测这个项目,我最大的体会就是数据的重要性被很多人低估。模型从 v5 换到 v8 提升了不到两个点,但是我把标注规范重做了一遍、把坏果类别拆细之后,整体效果直接跳了将近十个点。如果你现在也在做类似项目,先把数据质量抓上去,再谈模型选型和调参,顺序千万不能反。
还有一个小技巧,训练时定期把验证集的坏果置信度分布存下来,观察那些漏检样本的置信度集中在哪个区间。如果大量漏检样本的置信度在 0.3 到 0.4 之间,说明模型已经学到一部分特征了,只是置信度阈值没匹配好。这时候调阈值比调模型更有效。
这个水果好坏检测项目本身不算复杂,但它把数据采集、标注规范、模型选型、训练调优、部署推理整条链路完整过了一遍。做一遍之后,再有类似的二分类视觉检测任务,你基本就轻车熟路了。