基于YOLOv8的水果新鲜度检测:数据集构建与模型训练全流程
2026/9/8 11:33:04 网站建设 项目流程

简介:面向YOLO系列算法学习者和目标检测开发者,这份水果新鲜程度检测数据集提供约1000张真实场景图像,共覆盖苹果、香蕉、番石榴、橙子等12个类别,并区分新鲜、腐烂、成熟等状态,适合用于农产品新鲜度识别、智能分拣以及相关视觉算法的验证与对比。该数据集已按YOLO格式完成标注,包含984张jpg图片与1015个txt标签文件,标签采用归一化目标框坐标;同时附有data.yaml配置文件,train、val、test目录划分完整,YOLOv5、YOLOv7、YOLOv8等常见框架修改数据路径后即可直接开始训练。资源包共2000个文件,压缩后约422.25MB,目录结构清晰,便于使用者按训练、验证、测试集分别取用。目前已有1022人浏览学习,适合需要快速获得高质量标注数据、开展水果新鲜程度检测实验或模型迭代的开发者与研究人员。 做水果新鲜度检测,听起来是个挺垂直的方向,但真正动手之后你会发现,yolo算法、数据集、标注质量,每一环都能把人折腾得够呛。我这个项目的目标很朴素:做一套能用于yolo算法训练的水果新鲜程度检测数据集,把苹果、香蕉、橙子三种常见水果的“新鲜、表面损伤、明显腐烂”三种状态,用目标检测的方式自动框出来。这套东西做完之后能干什么?摄像头对着水果筐一扫,模型在零点几秒内给出每个果子的位置和质量状态,识别结果可以接分拣线做自动化分级,也可以给商超理货做损耗预警。文章里记录的是我从数据采集、标注,到用YOLOv8训练调优的完整过程,中间踩了不少坑,解决办法都写出来了,适合准备用目标检测做农产品分级的开发者,也适合拿来做毕业设计或者比赛项目的同学参考。

1. 项目定位与整体思路

1.1 为什么选YOLO做新鲜度检测

先回答一个基本问题:为什么是YOLO,而不是Faster R-CNN、分类网络或者语义分割?

这个项目最核心的约束是“又要有位置,又要有状态”。直接上分类网络只能告诉你“这张图里水果整体是什么状态”,但实际场景中一筐苹果往往好坏掺杂,你需要的是“哪个果在哪个位置上,它新鲜还是不新鲜”,这要求模型输出目标框加类别,属于目标检测的任务范畴。两阶段的Faster R-CNN精度确实高,但推理速度和对部署环境的要求,在小分拣线、移动端这类资源受限的场景里不占优势;语义分割能细化到像素级判断腐烂面积,但标注成本高一个量级,而且实际业务并不需要精确到腐烂边缘的轮廓,一个紧致的矩形框就足够支撑后续机械臂抓取和分级判断。

再看YOLO本身。YOLOv8是目前工程落地最稳的版本,官方生态完整,训练命令、导出ONNX、TensorRT部署都是全套的,社区资料也充足,出了问题好排查。更重要的一点是,它把数据增强、训练策略这些细节都做了比较好的默认处理,对新手友好,对老手也省事。我这次直接选择了YOLOv8作为主力模型,实测下来在本文这个规模的数据集上,训练速度和精度平衡得相当好。

1.2 新鲜度等级怎么定,直接决定数据集质量

刚开始我犯过一个典型的错误:把新鲜度等级拍脑袋定成了五档。结果标注员和我在“轻微皱皮”和“中度失水”这种分级上反复纠结,标注一致性非常差,模型训练出来也是懵的。

后来我把标准砍成三档,并且为每一档写了明确的判断依据:

类别名称判断标准覆盖场景
fresh果面光滑、色泽正常、无肉眼可见损伤斑块完好的苹果、香蕉、橙子
blemish有小范围压伤、碰伤、褐变斑点或表皮发皱,可食用但卖相受影响运输磕碰果、初期的褐变果
rotten有大面积腐烂、霉变、流水或明显异味特征的区域变质果、霉变果

这三个状态本质上是“可卖、降价卖、不能卖”,和实际业务直接挂钩,标注员也好执行。这里我特别要提醒一句:标注标准里一定要写清楚,一个水果上同时存在斑点和腐烂区域时,按腐烂类别标。不然不同的人看同一张图会给出不同的标签,后面模型学到的边界就是乱的。

另外,所有检测目标是“单个水果个体”,类别取该果子上最严重的状态。如果一个框里同时出现了两个挨得很近的果子,要分别框出来,不要合并。这个细节直接影响了后面目标数量统计的准确性。

2. 数据集构建:拍摄、标注与格式转换

2.1 图像采集与样本覆盖策略

数据集的采集我分了三个渠道:自己用手机拍、找供应商要实拍图、以及从公开水果图像数据集中筛选补充。自己拍的时候我特别注意了场景的多样性——自然光下的早市摊位、超市冷柜的灯光环境、仓库里的暗光条件、甚至包括办公室LED灯下的场景都拍了一部分。水果有单果入镜的,也有一堆果子堆叠的,因为真实业务场景里果子很少会整整齐齐排成一排等你识别。

样本规模上,我最终保留的原始图像是6500张左右,其中部分图像包含多个目标,总的标注框数量在7200个以上。三个类别在框级别上的分布大概保持在fresh占38%、blemish占33%、rotten占29%这样一个近似均衡的状态。为什么刻意控制均衡?因为目标检测模型对类别数量天然敏感,某一类样本过少时,模型会倾向把所有不确定目标都预测成高频类别。你可以不做到完全均等,但不要任性地让某一类占比超过50%。

这里还有一个经验:光线条件一定要在采集阶段就覆盖全,不要指望后期靠数据增强硬补。我第一批数据因为全是在采光很好的室内拍的,模型一到光线偏暗的仓库实测就明显掉点,后来补拍了暗光样本才拉回来。场景多样性必须在源头解决,增强只能锦上添花。

2.2 标注工具选型与YOLO格式转换

标注工具我用的是labelImg,轻量、支持Pascal VOC格式,对类别树不复杂的项目完全够用。如果团队需要在线协作标注,也可以考虑Roboflow,但要注意免费版对数据量和私有化有一些限制。

标注规范我写在便签上贴在显示器旁边,核心三条:框要紧贴果实边缘,不要留太多背景;遮挡严重的果实可以不标,但绝对不能框进去半个身子;腐烂区域即使很小,只要肉眼可辨就必须标出来。

标注完成后,一个容易踩的坑是格式转换。labelImg默认保存的是xml,而YOLO训练需要的是txt格式,且每个txt文件内容和图片同名,一行代表一个目标。YOLO格式的坐标是归一化后的中心点x、中心点y、框宽w、框高h,全部是0到1之间的小数。转换脚本我放在后面,直接改一下路径就能用:

import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_file, class_names, out_dir): tree = ET.parse(xml_file) 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') x1 = float(box.find('xmin').text) y1 = float(box.find('ymin').text) x2 = float(box.find('xmax').text) y2 = float(box.find('ymax').text) cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") if lines: base = os.path.splitext(os.path.basename(xml_file))[0] with open(os.path.join(out_dir, base + '.txt'), 'w') as f: f.write('\n'.join(lines)) class_names = ['fresh', 'blemish', 'rotten'] xml_dir = 'annotations' out_dir = 'labels' os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if xml_file.endswith('.xml'): convert_voc_to_yolo(os.path.join(xml_dir, xml_file), class_names, out_dir)

转换完之后一定要抽查验证,我最开始就因为某个子目录的图片尺寸读取失败,导致坐标全部错位,训练出来mAP惨不忍睹。比较好的检查方式是把YOLO格式的txt重新画回图片上,肉眼确认框的位置是否正确。

2.3 数据划分与目录组织

完成标注和格式转换之后,我按照80%训练、10%验证、10%测试的比例做了随机划分。这里有个细节值得注意:同一批水果的多张连续拍摄图要放进同一个集合里,否则模型会在验证阶段“作弊”,因为训练集里已经见过几乎一样的画面,验证分数会虚高。

目录结构统一这样组织,后续训练配置直接引用即可:

fruit_freshness/ ├── data.yaml ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/

划分完成后统计一下每个集合里的各类目标数量。我做完之后发现测试集里rotten的样本偏少,又手动从原始图像中抽了一批补进去。这个小动作对最终评估结果的可信度影响挺大,建议不要跳过。

3. YOLO训练:环境、配置与参数实测

3.1 显卡与训练环境配置

先聊一个被问得最多的问题:到底需要什么样的显卡,以及AMD显卡能不能跑。训练环境我用的是NVIDIA RTX 4060,8GB显存跑YOLOv8s配合batch=16完全没有压力。至于AMD的RX 580这类卡,结论是能跑,但要注意不再依赖NVIDIA CUDA,而是通过PyTorch的ROCm版本来支持,安装时要选择对应的ROCm版本。只是实测下来训练速度比同价位N卡要慢一些,如果你只是做推理验证,CPU也能扛得住,就是训练周期会比较长。总的来说,如果预算有限,优先买一块显存不低于6GB的N卡,能省很多折腾的时间。

安装YOLOv8的依赖我建议直接用官方套件,命令很简单:

pip install ultralytics

它会把PyTorch、torchvision、opencv这些核心依赖一并处理好。装完之后可以先跑一条官方预训练模型的检测命令验证环境,不要一上来就训练自己的数据。

3.2 data.yaml配置文件的写法

YOLO训练配置入口是data.yaml,它定义了数据路径和类别信息。我的写法如下:

path: /home/user/datasets/fruit_freshness train: images/train val: images/val test: images/test nc: 3 names: 0: fresh 1: blemish 2: rotten

这里最需要注意的是path字段要写绝对路径,或者确保运行训练命令时当前工作目录相对路径能正确解析。我见过不少人在Windows上写路径时不注意反斜杠导致读取不到数据,建议统一使用正斜杠。

如果你不想单独写yaml,也可以直接用命令行参数传:

yolo detect train data=/home/user/datasets/fruit_freshness/data.yaml ...

但把配置文件单独放一份的好处是方便复现,训练参数、数据集描述都留档,后面回归测试或者换机器训练时不容易漏配置。

3.3 模型选型与训练参数逐个说

YOLOv8根据网络深度和宽度分成n、s、m、l、x几个档位。这个项目我建议从yolov8s起步,因为检测对象是水果这类中等尺寸目标,不是特别小的物体,s档的参数量足够,训练速度也快;如果部署设备的算力很弱再降级到n档。盲目上x档只会让训练时间成倍增加,精度提升却很有限。

我的实际训练命令如下:

yolo detect train \ model=yolov8s.pt \ data=/home/user/datasets/fruit_freshness/data.yaml \ epochs=150 \ imgsz=640 \ batch=16 \ patience=20 \ optimizer=auto \ project=runs/detect \ name=fruit_fresh_v1

几个关键参数我解释一下。epochs设为150,配合patience=20的早停策略,意思是连续20轮验证集指标没有提升就自动停止,这样可以防止无效训练浪费时间。imgsz=640是默认分辨率,如果你发现图像里腐烂斑块很小、检测不到,可以尝试提升到768,代价是训练和推理速度下降。batch=16在8GB显存下跑yolov8s没有问题,如果报显存不足就降到8或4。

这里还得说说YOLOv8的损失函数结构,它由分类损失、框回归损失和DFL损失三部分组成。DFL这项对水果检测很关键,它的作用是让模型更精细地回归边界框位置,腐烂区域往往只占水果表面的很小一部分,DFL能让框更准地贴合这个区域,而不是松松垮垮地包进去一大圈背景。这也是YOLOv8在目标边缘模糊的场景下框得比老版本更稳的原因之一。

4. 训练结果分析与调优记录

4.1 评估指标怎么看

训练完成后,优先看验证集上的mAP50和mAP50-95。mAP50是IoU阈值取0.5时的平均精度,比较宽松,适合判断“有没有检出”;mAP50-95是在0.5到0.95之间每隔0.05计算一次再取平均,更严格,能反映框的贴合质量。对水果新鲜度检测来说,mAP50主要决定该检的是否都检出来了,mAP50-95则决定机械臂抓取时定位准不准。两个都要看,不能只盯着其中一个。

我最终跑出来的结果大致是mAP50在0.94左右,mAP50-95在0.87左右。相比之下,fresh类别的精度最高,blemish次之,rotten略低一些。这个分布符合预期,因为rotton的特征更复杂,早期霉变可能只是一小块白毛或黑斑,视觉差异没有新鲜和腐烂之间那么大。

再看混淆矩阵,核心是分析fresh和blemish之间是否互相误判。第一次训练完我发现有不少fresh被误判成blemish,原因是我标注时对“轻微褐变”的尺度把握不统一,有人把自然果绣也算成了blemish,导致模型学到了错误边界。后来我统一了标注口径:只有带磕碰或病斑的才算blemish,正常的果面纹理不算,重新标注后混淆明显改善。

4.2 实测中的三个大坑和对应调整

第一个坑是模型对颜色背景过度敏感。初期训练集里大量图片是木纹桌面背景,结果一到蓝色塑料筐背景上,fresh的置信度明显下降。解决方法是训练时给数据增强加上背景干扰项,以及补拍不同颜色的背景图,让模型把注意力放在水果本身的纹理而不是环境颜色上。

第二个坑是小目标漏检。橙子的早期腐烂区域可能只有十几个像素,imgsz=640训练时这些细节很容易在降采样过程中丢信息。我对比测试后把imgsz提高到768,rotten类别的召回率提升了三个多点。如果你也遇到类似问题,优先试这个参数。

第三个坑不是模型问题,而是标注标签错位。有一次训练完发现某几张图的检测框偏到图片边缘,查下来是转换脚本在读取xml时,把某几个文件的图片尺寸写错了。这类问题排查起来很费时间,所以再次强调:训练前务必把txt重画回图片逐一检查,这一步省不得。

补充一个数据增强的心得。YOLOv8自带mosaic、mixup、HSV变换、随机翻转等增强策略,默认开启的配置已经不错。但mosaic在这个任务里偶尔会制造出半张图拼接,导致水果形态特别怪异,模型训练初期容易被带偏。我个人的做法是保留mosaic,但把剪切比例调小一点,让拼接后的目标尺寸更接近真实场景。

5. 常见问题排查与部署补充

5.1 新手最容易遇到的四个问题

我把这个项目里遇到过的典型问题整理成一张速查表,训练出问题先对着查一遍,能省不少时间:

问题现象大概率原因排查与解决
训练直接报CUDA out of memorybatch太大或imgsz太大调小batch到4/8,imgsz降到640;还是不行就换显存更大的卡
loss降不下去,mAP一直在低水平标签文件错位或类别数给错打开labels目录检查txt数量;随机挑几张图把框画回原图确认
训练正常但推理全预测成同一类别类别严重不均衡统计各集合类别数,补样本或做类别加权
验证集mAP高,实测泛化差同一批场景图片被错误分到训练和验证重新划分数据,确保同场景不同帧归入同一集合

5.2 模型导出与边缘设备部署要点

训练完成后的部署环节,我用得最多的是导出ONNX格式:

yolo export model=runs/detect/fruit_fresh_v1/weights/best.pt format=onnx dynamic=True

导出的onnx可以用onnxruntime或者OpenCV的DNN模块加载推理,不依赖PyTorch环境,在NVIDIA Jetson、树莓派、工控机上都能跑。实际推理时,检测置信度阈值的选择有讲究。对新鲜果分级,可以把置信度阈值放到0.5左右,减少误检;但如果你更关心“腐烂果绝不能漏掉”,阈值可以降到0.25,代价是会多出一些把blemish误判为rotten的情况。这种取舍要结合业务定,没有统一答案。

另一个部署小技巧:如果边缘设备算力吃紧,不要一上来就推整张图。可以先用一个快速检测器把所有水果框出来,再对每个框内的区域做细粒度分类,这种级联方式在设备侧的表现通常比单模型硬扛要好。

5.3 后续还能怎么扩展

这套数据集和模型跑通之后,扩展方向也不少。比如给腐烂区域标成多边形做实例分割,用YOLOv8-seg训练,对腐烂面积的量算会更准,适合需要按腐烂比例定价的场景。再比如接入主动学习流程,把模型置信度低的样本人工复核后作为增量数据回填训练集,数据利用率会明显提升。

最后分享一个很实在的心得:从零复刻这个项目的话,不要一上来就拍几千张图,先拍三五百张,把采集、标注、转换、训练、部署全流程跑通,再回头补数据。我第一次就是直接猛拍,结果标注规范不统一,后面返工了将近一半,时间成本非常痛。小步快跑,先把链路打通,再慢慢把数据做厚,这条路走起来会顺很多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询