简介:一个基于YOLOv5的果蔬识别系统完整项目包,面向计算机相关专业正在准备课程设计、毕业设计或进行深度学习实战练习的学生。内含可直接运行的源代码、配套果蔬数据集、训练好的模型文件及详细教程说明,项目经导师指导并调试通过,评审得分98分,难度适中,适合快速上手完成考核任务。
资源共56个文件,打包后约94.07MB,涵盖Python脚本(数据预处理、模型训练、界面交互等)、h5模型权重、xml与txt配置标注文件、jpg/png/jpeg图像样本,以及readme教学文档,目录结构清晰,便于按需取用。
目前已有123人学习,参考价值经过验证。借助该资源可系统了解YOLOv5在果蔬识别场景的完整落地流程,包括数据划分、模型训练、实时检测与结果可视化,能大幅节省自行摸索的时间。
1. 为什么我把 YOLOv5 果蔬识别做成了“训练—评估—部署”三件套
很多人以为果蔬识别就是把 YOLOv5 的权重下载下来,对着图片跑一次 predict。实际动手才发现,真正的体力活是数据集清洗和类别分布。拿这份项目来说,zip 里除了训练好的 mobilenet_fv.h5、cnn_fv.h5,还带了一整套 data_split.py、jpeg2jpg.py、remove_wrong_image.py 这类处理脚本,以及 window_realtime.py、window_up_camera.py 两个可直接运行的 PyQt 界面。这意味着学习重点不是“调包”,而是把一份带噪声的果蔬图片整理成训练集,再把模型塞进带界面的系统里。评审分 98 分,从代码组织来看,作者确实把“能运行”和“好演示”这两件事都考虑进去了。适合准备毕业设计、期末大作业,以及想用 YOLOv5 做垂直场景识别但还没跑通全流程的开发者。
2. 数据集工程:目录划分、格式转换与坏图过滤
项目压缩包解开后,第一眼不是模型文件,而是一堆 jpg、jpeg、png 混放的 images 目录,以及 data_split.py、get_data.py、jpeg2jpg.py、remove_wrong_image.py、remove_func.py 几个看起来不起眼的脚本。经验是,这些脚本才是决定最终训练集质量的关键。果蔬图片很多是从网上抓的或手机拍的,命名混乱、存在半张图、带 EXIF 旋转角度,直接丢给 YOLOv5 会浪费大量时间在无效样本上。
2.1 用 data_split.py 把数据集切成 train/val/test
data_split.py 最基础的任务是维护类别均衡的三段划分。常见做法是读取 images 下每一个类别子目录,按 8∶1∶1 划分到 train、val、test,并保持原来的子目录结构。下面是一个简化版的可运行逻辑:
# data_split.py 的常见做法:带固定随机种子 import os import random from shutil import copy2 random.seed(42) def split_dataset(src_dir, dst_dir, ratios=(0.8, 0.1, 0.1)): for label in os.listdir(src_dir): label_dir = os.path.join(src_dir, label) if not os.path.isdir(label_dir): continue files = [f for f in os.listdir(label_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp'))] random.shuffle(files) train_n = int(len(files) * ratios[0]) val_n = int(len(files) * ratios[1]) for i, f in enumerate(files): part = 'train' if i < train_n else ( 'val' if i < train_n + val_n else 'test') dst = os.path.join(dst_dir, part, label) os.makedirs(dst, exist_ok=True) copy2(os.path.join(label_dir, f), os.path.join(dst, f)) if __name__ == '__main__': split_dataset('images', 'yolo_dataset')参数上,src_dir 是原始图片根目录,dst_dir 是 YOLOv5 需要的训练数据根目录。ratios 默认三个比例,类别样本特别少时,可以改成 ratios=(0.6, 0.2, 0.2),但 val 和 test 至少每类保留 5 张,否则评估会波动很大。注意固定 random.seed,方便复现与后续对比,不然每次跑出来的结果都对不上。
| 脚本 | 主要职责 | 输出 |
|---|---|---|
| data_split.py | 按比例划分训练/验证/测试集 | train、val、test 目录树 |
| get_data.py | 生成数据列表或解析标注索引 | train.txt / valid.txt |
| jpeg2jpg.py | 统一格式并修正 EXIF | 转换后的 jpg 文件 |
| remove_wrong_image.py | 删除损坏图片 | 清理后的目录 |
| remove_func.py | 封装修复/删除函数 | 供上面脚本调用 |
2.2 jpeg2jpg.py:别让扩展名和 EXIF 方向坑了训练
YOLOv5 对图片格式本身不挑,但对内存里的解码数据要求一致。手机拍摄的 JPEG 常常带 EXIF 旋转信息,直接读取会得到旋转前的像素,而标注框是在旋转后的视觉上画的,这会让 GT 框和图像内容错位。jpeg2jpg.py 的职责就是把这个隐患一次性清理掉。
# jpeg2jpg.py 的常见做法:EXIF 转正后强制写为 jpg from PIL import Image, ImageOps from pathlib import Path def convert(input_path: str, output_path: str, quality: int = 95): im = Image.open(input_path).convert('RGB') im = ImageOps.exif_transpose(im) # 根据 EXIF 自动旋转 im.save(output_path, 'JPEG', quality=quality)im.convert('RGB') 是为了丢掉透明通道或灰度模式,深度学习训练时统一三通道输入。exif_transpose 会读取 EXIF 里的 Orientation 字段,自动把手机竖拍图转正,这一步不做,后期画标注框和热力图时方向经常是反的。quality 参数建议 90~95,太高会增大图片体积,太低会丢失纹理,果蔬识别对边缘细节比较敏感。
2.3 坏图过滤:remove_wrong_image.py 与 remove_func.py
爬取来的图片最常见的问题不是格式错误,而是“半张图”“截断文件”“全黑图”和“纯色背景图”。文件大小不能作为唯一标准,一张 200KB 的图可能只有左上角有内容。remove_wrong_image.py 里更可靠的做法是让 Pillow 真正解码一次。
# remove_wrong_image.py —— 解码失败就删除 from PIL import Image from pathlib import Path def is_valid_image(path: Path) -> bool: try: with Image.open(path) as img: img.load() # 触发完整解码 except (IOError, OSError, SyntaxError, Image.DecompressionBombError): return False return Trueimg.load() 这一步非常关键,只做 Image.open 只读取头部,文件截断不会报错。另外发现 remove_func.py 里还做了类似“删除高度小于阈值的窄长图”的动作。果蔬图片如果被裁剪成细条,YOLOv5 的 anchor 很难匹配,训练时会变成永远不参与梯度更新的样本。清理后建议输出每个类别的样本数:低于 50 张的类要么补数据,要么在配置里降低该类权重。
到这里数据集就可靠了。下一步才进入模型选择。
3. 模型选择与训练:YOLOv5 检测 + MobileNet/CNN 分类
3.1 为什么一个果蔬识别系统需要两套模型
项目里同时出现 YOLOv5 的痕迹和 mobilenet_fv.h5、cnn_fv.h5,很多人会困惑:到底用哪个?按我的理解,作者把识别拆成了两条路线。YOLOv5 负责目标检测:在一整张桌面上把苹果、香蕉、番茄都框出来,解决“哪里有什么”的问题。MobileNet 和 CNN 则作为分类网络,解决“这个框里到底是什么”的问题,尤其在两个类别外观接近(番茄和圣女果、土豆和生姜)时,单独跑 YOLOv5 的分类头容易在低分辨率小目标上犯错,用 224×224 的裁切图再分类一次能显著提高稳定度。
两个模型配合的常见流程是:YOLOv5 以 0.25 的置信度输出候选框,NMS 后把每个框裁剪成正方形,再送入 mobilenet_fv.h5 得到细分类结果,最后把分类得分乘以检测置信度作为最终分数。这样哪怕是 jetson nano 这类边缘设备,也可以把 YOLOv5 换成更轻的版本,分类头保持不变。基于 YOLOv5 的水果识别项目,很多团队最终都会走到这个“检测+细分类”的组合上。
3.2 YOLOv5 训练自己的数据集:fruit.yaml 与训练命令
要复现这个项目,首先得把第 2 章切好的 train、val 路径写进 YOLOv5 的数据配置。一份典型的 fruit.yaml 长这样:
# fruit.yaml —— 放在 yolo_v5/data/ 下 train: ./yolo_dataset/train val: ./yolo_dataset/val nc: 8 names: ['apple', 'banana', 'orange', 'tomato', 'potato', 'carrot', 'cucumber', 'grape']nc 是类别数,names 必须和标注 id 一一对应。这里写的是常见果蔬清单,实际以项目 readme.md 里记录的类别顺序为准,顺序错一位模型就会张冠李戴。启动训练推荐用 yolov5s 作为预训练权重,性价比适合学位论文和期末大作业。
python train.py --data fruit.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0 \ --project runs/train--batch 根据显存调整,6GB 显存跑 16 比较安全,8GB 可以上 32;数据集少于 500 张图时,epochs 不建议超过 100,否则 val 的 mAP 会先升后降。如果在 CPU 上跑,把 --device 换成 cpu,同时加 --cache ram 把图片缓存到内存,能少一半 IO 等待。YOLOv5 的默认超参数一般不用大改,真正影响果蔬这种小样本场景的是 mosaic=1.0 和 mixup=0.1,前者在数据少时可以自动合成新样本,后者在样本充足时会稀释纹理特征,可以在训练时临时关掉。
| 模型 | 优点 | 适用场景 |
|---|---|---|
| YOLOv5s | 检测速度快、通用性强 | 整图定位、候选框生成 |
| MobileNetV2 | 参数量小、移动端友好 | 边缘设备上的细分类 |
| 自定义 CNN | 结构简单、容易可视化 | 教学演示、小类别数任务 |
3.3 用 train_mobilenet.py 训练细分类模型
train_mobilenet.py 看起来更像是主训练脚本,因为它输出 mobilenet_fv.h5 这种 Keras 权重,可以直接被 UI 代码加载。train_cnn.py 对应 cnn_fv.h5,两者结构不同但训练套路一致。下面是我据此整理的常见调用方式:
python train_mobilenet.py --data ./yolo_dataset/train \ --val ./yolo_dataset/val \ --img_size 224 \ --epochs 60 \ --lr 0.0005逻辑上该脚本会以 MobileNet 系列结构为基础,加载 ImageNet 预训练权重,并把最后的全连接层换成新的类别数。训练侧重点是数据增强:随机水平翻转、亮度扰动、轻微噪声,因为果蔬拍摄环境的光线变化很大。如果路径是中文目录,TensorFlow 在 Windows 下偶尔会拒绝读取,最好把数据集统一放在英文路径下。训练结束后会产出一个 h5,注意保存时是否带 optimizer 状态,推理时只需要权重,带 optimizer 会让文件体积变大几 MB。
3.4 环境一致性与超参数检查点
压缩包里有个细节:trash_tf2.3_cpu.iml 和 requirements.txt。前者说明作者在 IntelliJ 系 IDE 里用 TensorFlow 2.3 CPU 调试过,后者的关键依赖需要按版本对齐,尤其是 tensorflow、PyQt5、opencv-python、numpy。TF2.3 搭配 numpy 1.18/1.19 是常见组合;numpy 1.19 之后的二进制兼容性问题会导致 h5 模型加载时报错。建议用 conda 建独立虚拟环境:
conda create -n fruit_yolov5 python=3.7 conda activate fruit_yolov5 pip install -r requirements.txt因为项目里没有给出完整 requirements 细节,上述组合是复现时的最小可行方案。Python 3.7 是 TF2.3 支持得最好的版本,PyQt5 跑窗口也不容易出现 QPixmap 字体渲染异常。如果自己从头装环境,记住四个组件:tensorflow-gpu 或 tensorflow-cpu、PyQt5、opencv-python、Pillow。
4. 实时识别与桌面 UI:window_realtime.py 和 window_up_camera.py
4.1 三条入口分别对应什么使用场景
window_realtime.py、window_up_camera.py、window.py 三个文件看起来都是界面,但使用场景完全不同。window.py 更像主窗体和功能模块的容器,window_realtime.py 是摄像头实时检测,window_up_camera.py 是上传一张图片再识别。这样的拆分很实用,答辩演示时用“上传图片”可以精确复现某个案例,平时测试用摄像头能直观展示稳定度。
| 入口文件 | 使用场景 | 数据来源 |
|---|---|---|
| window_realtime.py | 实时摄像头检测 | cv2.VideoCapture(0) |
| window_up_camera.py | 单张图片识别 | QFileDialog 选择文件 |
| window.py | 主界面框架 | 承载上面两个页面 |
4.2 推理线程与 UI 主线程的解耦
window_realtime.py 并没有把所有逻辑堆在 Qt 的回调里,而是把读帧、推理、画框、刷新 QLabel 拆开。一个可运行的骨架长这样:
# window_realtime.py 的核心循环(简化) class DetectThread(QtCore.QThread): frameReady = QtCore.pyqtSignal(object) def __init__(self, camera_id=0): super().__init__() self.cap = cv2.VideoCapture(camera_id) self.running = True def run(self): while self.running: ret, frame = self.cap.read() if not ret: continue boxes, labels, scores = self.infer(frame) self.frameReady.emit((frame, boxes, labels, scores)) self.msleep(30) def stop(self): self.running = False self.cap.release()这里 self.infer(frame) 就是模型推理入口,可以换成 YOLOv5 的 detect API,也可以换成加载 mobilenet_fv.h5 后的 predict 调用。QThread 的 frameReady 信号会把结果带回主线程,再由主线程更新界面,避免 Qt 控件跨线程直接操作导致的崩溃。QTimer 每 30ms 触发读取一帧,实际帧率取决于模型推理时间,若在 CPU 上跑 YOLOv5s,可能需要把间隔调大到 60ms 或者把输入尺寸降到 320。
4.3 中文路径与图片解码问题
window_up_camera.py 走的是文件路径:用户点击“选择图片”,QFileDialog 返回路径,再调用绘图函数。中文路径在 Windows 下很容易让 OpenCV 的 imread 返回 None,一般会用字节流解码绕过去:
# window_up_camera.py 中处理中文路径的常见写法 import numpy as np import cv2 data = np.fromfile(file_path, dtype=np.uint8) img = cv2.imdecode(data, cv2.IMREAD_COLOR)cv2.imdecode 可以直接从内存解码图片,避开文件系统编码问题。解码后如果需要喂给 TF 模型,别忘记用 cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 转通道顺序。UI 缩略图可以用 QPixmap.fromImage 显示,并在 show 之前把 QImage 的格式转成 Format_RGB888,否则颜色会偏蓝红互换。界面布局上,视频区占左侧 stretch=3,右侧放识别结果列表和操作按钮,调高 stretch 差能让演示时主画面足够大。
5. 模型评估:热力图、训练曲线与误检定位
5.1 从 results_cnn.png 和训练日志看收敛状态
项目的 results 目录里有 cnn 训练过程.txt、mobilenet 训练过程.txt、results_cnn.png、results_mobilenet.png,以及 heatmap_cnn.png、heatmap_mobilenet.png。这些文件是评估闭环的关键。在 Windows 下可以直接用 findstr 提取日志中的关键字:
findstr 准确率 cnn训练过程.txt如果 train accuracy 快速到达 0.9 但 val 抖动,说明过拟合;如果 loss 一直降而 accuracy 平,多半是类别不均衡,需要去检查较小类别在每张图里占的像素比例。建议把每个 epoch 的 loss、val_loss、accuracy 记成表格,重点关注 loss 是否在某个 epoch 后出现反弹。
| 日志指标 | 观察方法 | 常见问题 |
|---|---|---|
| train loss | 是否持续下降 | 不降说明学习率过大或数据未归一化 |
| val loss | 是否跟随下降 | 反弹说明过拟合,需要加大增强 |
| train accuracy | 是否快速饱和 | 饱和但 val 低,说明泛化差 |
| val accuracy | 是否稳定 | 抖动大说明验证集太小或分布不均 |
5.2 用类激活热力图定位模型“看什么”
heatmap_mobilenet.png 这类文件是类激活热力图的可视化结果。它的作用是回答一个问题:模型到底是靠水果本身的纹理在判断,还是靠盘子边缘、桌面背景这些无关信息。简化版的 Grad-CAM 可以这样写:
# 用中间层卷积特征近似 Grad-CAM import tensorflow as tf from tensorflow.keras import Model def grad_cam(model, img_tensor, class_idx): grad_model = Model( inputs=model.input, outputs=[model.get_layer('last_conv').output, model.output] ) with tf.GradientTape() as tape: conv_out, pred = grad_model(img_tensor) loss = pred[:, class_idx] grads = tape.gradient(loss, conv_out)[0] weights = tf.reduce_mean(grads, axis=(0, 1)) cam = tf.reduce_sum(tf.multiply(weights, conv_out[0]), axis=-1) return tf.maximum(cam, 0)这里的 last_conv 层名要根据实际模型调整,可以先打印 model.summary() 看网络结构。热力值高的区域如果集中在水果表面,说明模型是靠谱的;如果集中在盘子反光或手指上,说明训练集里混入了太多带类似背景的图片,此时要增加颜色抖动和背景替换增强。
5.3 根据测试记录调整阈值
项目的 readme.txt 里提到“测试记录.txt”,这种文件通常记录了每个类别的 precision、recall 和具体样本的置信度。调参时可以把默认置信度从 0.25 提到 0.35 再验证:果蔬场景里错检比漏检更影响演示效果,尤其是一个番茄被框成苹果,答辩时会被当场指出来。如果某类样本全部集中在低置信度区间,可以单独对该类降低阈值,而不是全局降。实现时可以在 UI 层加一个阈值滑块,把结果实时反馈出来,这是高分项目里很实用的亮点。
6. 排错与调参:高分项目复现的最后几个细节
6.1 三个高频报错与对应解法
第一个是AttributeError: 'NoneType' object has no attribute 'shape',这通常出现在摄像头占用的场景,排查时先单独跑cv2.VideoCapture(0).read(),返回 False 就说明摄像头被任务管理器或其他程序占用。第二个是tensorflow.python.framework.errors_impl.UnknownError,常见于中文路径,解决办法是把整个项目复制到C:\project\fruit这类纯英文目录下再跑。第三个是窗口黑屏但程序不退出,问题多半在frameReady.emit传出的图像格式不对,统一转成RGB888再交给 Qt。
6.2 把模型导出成 ONNX 后部署
如果需要在无 TensorFlow 环境或边缘设备上跑,可以把训练好的 YOLOv5 best.pt 导出成 ONNX,再把分类 h5 转成 tflite 或 ONNX,推理延迟能下降不少。YOLOv5 自带的导出命令是:
python export.py --weights runs/train/exp/weights/best.pt \ --include onnx \ --img 640导出后记得用 onnxruntime 验证一遍输出,确认输出层的 shape 没有丢失。最后把 best.pt、mobilenet_fv.h5、cnn_fv.h5 复制到项目根目录,运行 window_realtime.py 验证整个识别链路。
本文还有配套的精品资源,点击获取