简介:这是一套面向计算机相关专业在校学生与教师的Python水果识别系统源码,采用PyQt5搭建GUI界面、OpenCV完成图像处理与识别,适合作为毕业设计、课程设计或大作业的完整参考方案。压缩包共55个文件,约4.63MB,包含16个py源码文件、20个png与2个jpg水果样本图、2个ui界面文件、2个qrc资源文件及若干pyc缓存与md说明文档,源码带详细注释,目录涵盖图像处理、百度识别接口调用、界面切换与识别主逻辑等模块。已有439人学习下载,可帮助读者快速理解图像预处理、去模糊、目标裁剪与识别流程,并支持在此基础上二次开发或DIY其他识别功能。运行前建议将项目路径重命名为英文,避免中文路径导致解析异常。
1. 水果识别系统:从一张货架照片到可点击的 GUI 到底要几步
超市结账台前,收银员把一堆苹果、橙子、香蕉往摄像头底下一推,屏幕自动框出每个水果并标上名字——这个场景听起来像大厂方案,其实用 PyQt5 + OpenCV + Python 就能在本地跑通。标题里的「水果识别系统源码(带 GUI 界面 + 详细注释)」本质是一套桌面端图像分类应用:OpenCV 负责读图、预处理、轮廓分割,Python 做特征提取或调用轻量分类逻辑,PyQt5 把整个流程包成有按钮、有预览区、有结果输出的窗口程序。它适合三类人:想拿一个完整项目练手的 Python 入门者、需要给课程设计交作业的学生、以及想理解「图像处理 + 桌面 GUI」怎么串起来的初中级开发者。热词里 pyqt5 安装、opencv 图像处理、python 安装教程反复出现,说明卡点从来不在算法本身,而在环境能不能跑起来、界面能不能显示、识别结果能不能稳定输出。这篇笔记就按「先跑通、再调参、最后避坑」的顺序,把这条链路拆开讲清楚。
2. 环境搭建与最小可运行骨架:先让窗口弹出来,再谈识别
2.1 为什么选 PyQt5 + OpenCV 而不是 Web 或命令行
很多人第一反应是用 Flask 做个网页上传图片,或者干脆写个命令行脚本。但水果识别这个场景有个硬需求:实时预览。收银台或分拣线上,操作员需要看到摄像头画面里被框出的水果,而不是等三秒后返回一个 JSON。PyQt5 的 QLabel 配合 QTimer 可以做到毫秒级刷新,OpenCV 的 VideoCapture 直接读摄像头帧,两者通过 numpy 数组无缝衔接——OpenCV 读出来是 BGR ndarray,转成 QImage 只需要改一下通道顺序。命令行方案调试算法方便,但交付给非技术用户就是灾难;Web 方案要处理上传延迟、跨域、前端渲染,对「识别」这个核心逻辑反而是干扰。所以标题里 GUI 界面不是装饰,它是这个项目能落地的关键组件。
另一个选型理由是依赖轻。PyQt5 是纯 Python 绑定,pip 就能装;OpenCV 的 Python 包 opencv-python 自带常用图像处理函数,不需要编译整个 OpenCV 源码。热词里有人搜「opencv 3.4.1 mingw64 下载」「编译 opencv」,那是 C++ 开发者的路径,做 Python 水果识别完全用不上。你只需要确认 Python 版本在 3.7 到 3.10 之间,太新或太旧都可能遇到 PyQt5 轮子缺失的问题。
2.2 三条命令装完依赖,验证 cv2 和 PyQt5 都能导入
先建虚拟环境,避免和系统里其他包打架。Windows 下用 python -m venv venv,Linux 或 macOS 用 python3 -m venv venv,然后激活。激活后依次执行:
pip install opencv-python==4.8.1.78 pip install PyQt5==5.15.9 pip install numpy==1.24.3版本号不是随便写的。opencv-python 4.8.x 对 numpy 1.24 兼容稳定,PyQt5 5.15.9 是最后一个支持 Python 3.10 的成熟版本。装完后用下面这段代码验证,不要跳过:
import cv2 import numpy as np from PyQt5.QtWidgets import QApplication, QLabel import sys print("OpenCV version:", cv2.__version__) print("NumPy version:", np.__version__) app = QApplication(sys.argv) label = QLabel("环境 OK") label.show() # 不进入事件循环,只验证能否创建窗口对象 print("PyQt5 window created")如果报ModuleNotFoundError: No module named 'cv2',说明 opencv-python 没装进当前虚拟环境,检查 pip 路径是否指向 venv。如果报DLL load failed,通常是 Visual C++ Redistributable 缺失,装一下 2015-2022 的 x64 版本即可。Linux 下如果报cannot open shared object file: libGL.so.1,执行sudo apt install libgl1-mesa-glx。这些报错在热词里对应「modulenotfounderror: no module named 'opencv'」和「ubuntu 配置 opencv」,本质都是环境隔离没做好。
2.3 最小 GUI 骨架:一个按钮、一个预览区、一个结果标签
不要一上来就写完整识别逻辑,先把界面骨架搭出来,确认信号槽能连通。下面这段代码创建一个 800x600 的窗口,左边是图像预览 QLabel,右边是按钮和结果文本:
import sys import cv2 import numpy as np from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog) from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt class FruitGUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("水果识别系统") self.resize(900, 600) central = QWidget() self.setCentralWidget(central) main_layout = QHBoxLayout(central) # 左侧预览区 self.image_label = QLabel("等待加载图片...") self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet("border: 2px solid gray;") self.image_label.setFixedSize(600, 500) main_layout.addWidget(self.image_label) # 右侧控制区 right_layout = QVBoxLayout() self.btn_load = QPushButton("加载图片") self.btn_load.clicked.connect(self.load_image) self.result_label = QLabel("识别结果:--") self.result_label.setWordWrap(True) right_layout.addWidget(self.btn_load) right_layout.addWidget(self.result_label) right_layout.addStretch() main_layout.addLayout(right_layout) self.cv_image = None # 保存原始 OpenCV 图像 def load_image(self): path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.jpg *.png *.jpeg)") if not path: return # OpenCV 读取,注意中文路径要用 np.fromfile self.cv_image = cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR) self.show_image(self.cv_image) self.result_label.setText("图片已加载,等待识别...") def show_image(self, img): # BGR 转 RGB rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape bytes_per_line = ch * w qimg = QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) pixmap = QPixmap.fromImage(qimg).scaled( self.image_label.width(), self.image_label.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.image_label.setPixmap(pixmap) if __name__ == "__main__": app = QApplication(sys.argv) window = FruitGUI() window.show() sys.exit(app.exec_())这段代码的关键点有三个。第一,cv2.imdecode配合np.fromfile是为了解决 OpenCV 在 Windows 下读中文路径失败的问题,直接cv2.imread遇到中文目录会返回 None。第二,show_image里做了 BGR 到 RGB 的转换,因为 Qt 的 QImage 期望 RGB 顺序,不转的话红色和蓝色会互换,苹果看起来像蓝莓。第三,scaled时用Qt.KeepAspectRatio保持比例,否则水果会被拉扁。跑通这一步,你已经有了一个能加载并显示图片的 GUI,接下来才是识别逻辑。
3. 水果识别核心链路:从颜色阈值到轮廓筛选的完整实现
3.1 为什么先用 HSV 颜色空间做分割,而不是直接上深度学习
标题里没有指定识别算法,但「水果识别系统源码」这个描述通常对应两类实现:一类是传统图像处理,靠颜色、形状、纹理特征做分类;另一类是轻量 CNN 推理。对于带 GUI 的桌面项目,我一般推荐先用 HSV 颜色阈值 + 轮廓分析跑通全流程,原因很实际:不需要训练数据、不需要 GPU、代码可读性强、调试直观。你打开一张苹果照片,调一调 H 通道范围就能看到掩膜变化,这种即时反馈对理解图像处理帮助极大。深度学习方案虽然精度高,但模型文件动辄几十兆,推理还要装 onnxruntime 或 torch,对只想跑通 GUI 的人门槛太高。
HSV 相比 RGB 的优势在于颜色分离更稳定。RGB 三个通道都受光照影响,而 HSV 把色调(H)、饱和度(S)、亮度(V)拆开,水果的颜色主要由 H 决定,光照变化主要影响 V。比如红色苹果的 H 值在 0 到 10 和 170 到 180 两个区间(OpenCV 里 H 范围是 0-179),你只需要框定 H 范围,再对 S 和 V 设一个最低阈值过滤掉灰白背景,就能得到比较干净的水果区域掩膜。
3.2 颜色阈值分割的代码实现与参数调节
下面这段代码对加载的图片做 HSV 转换、阈值分割、形态学去噪,最终输出二值掩膜:
def segment_fruit(self, img): # 高斯模糊降噪,kernel 必须是奇数 blurred = cv2.GaussianBlur(img, (7, 7), 0) hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) # 红色范围(苹果、草莓等),H 跨 0 需要两段 lower_red1 = np.array([0, 80, 80]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([170, 80, 80]) upper_red2 = np.array([180, 255, 255]) mask_red = cv2.inRange(hsv, lower_red1, upper_red1) | \ cv2.inRange(hsv, lower_red2, upper_red2) # 橙色范围(橙子、橘子) lower_orange = np.array([11, 100, 100]) upper_orange = np.array([25, 255, 255]) mask_orange = cv2.inRange(hsv, lower_orange, upper_orange) # 黄色范围(香蕉、柠檬) lower_yellow = np.array([26, 100, 100]) upper_yellow = np.array([35, 255, 255]) mask_yellow = cv2.inRange(hsv, lower_yellow, upper_yellow) # 合并掩膜 mask = mask_red | mask_orange | mask_yellow # 形态学开运算去噪,闭运算填洞 kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=2) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2) return mask参数说明:GaussianBlur的 (7,7) 是模糊核大小,越大越模糊,对噪声多的图片可以加到 (9,9) 或 (11,11),但太大会把水果边缘也糊掉。lower_red1里的 80 是饱和度下限,如果水果颜色偏淡(比如粉红苹果),可以降到 50;如果背景有红色干扰,就升到 100 以上。morphologyEx的 iterations=2 表示做两次开运算,对细小噪点很有效,但如果水果本身很小(比如樱桃),iterations 要降到 1,否则整个水果会被腐蚀掉。这些参数没有万能值,必须对着你的实际图片调,调参时把 mask 用cv2.imshow单独显示出来看效果,比盲猜快得多。
3.3 轮廓提取与水果计数:findContours 的三个关键过滤条件
拿到掩膜后,用cv2.findContours找轮廓,再按面积、宽高比、圆形度过滤掉噪声和背景碎片:
def count_fruits(self, mask, original): contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) result_img = original.copy() count = 0 for cnt in contours: area = cv2.contourArea(cnt) if area < 800: # 过滤小噪点 continue x, y, w, h = cv2.boundingRect(cnt) aspect_ratio = w / float(h) if aspect_ratio < 0.5 or aspect_ratio > 2.0: # 过滤细长形状 continue # 圆形度:4π*面积 / 周长^2,越接近 1 越圆 perimeter = cv2.arcLength(cnt, True) if perimeter == 0: continue circularity = 4 * np.pi * area / (perimeter * perimeter) if circularity < 0.4: # 太不规则的不要 continue count += 1 cv2.rectangle(result_img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(result_img, f"Fruit {count}", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return result_img, countRETR_EXTERNAL只取最外层轮廓,避免水果内部纹理产生嵌套轮廓。面积阈值 800 是针对 640x480 以上分辨率图片的经验值,如果你的图片很小,按比例降到 200 左右。宽高比过滤能去掉长条形的背景边缘,圆形度过滤能去掉不规则碎片。这三个条件叠加后,误检率会明显下降。但要注意,香蕉的宽高比可能达到 3:1 甚至更高,如果你要识别香蕉,宽高比上限要放宽到 3.5,或者干脆对香蕉单独走一套形状判断逻辑。
3.4 把识别结果嵌回 PyQt5 界面:信号槽与图像刷新
识别函数写好后,在 GUI 里加一个「开始识别」按钮,点击后调用 segment 和 count,再把画了框的结果图显示到 QLabel 上:
def recognize(self): if self.cv_image is None: self.result_label.setText("请先加载图片") return mask = self.segment_fruit(self.cv_image) result_img, count = self.count_fruits(mask, self.cv_image) self.show_image(result_img) self.result_label.setText(f"识别结果:共检测到 {count} 个水果")这里有个容易翻车的点:show_image里用了self.image_label.width()和height(),如果 QLabel 还没完成布局,拿到的可能是默认值。稳妥做法是在__init__里给 QLabel 设固定尺寸,或者用setScaledContents(True)让 QLabel 自己缩放。另外,OpenCV 的putText不支持中文,如果想把「苹果」「橙子」显示在图上,需要用 PIL 的 ImageDraw 或者 Qt 的 QPainter 在 QPixmap 上画字,这是很多源码里没写清楚的地方。
4. 避坑与排查:水果识别系统最常见的 5 个翻车现场
4.1 现象:窗口一闪而过,报 Process finished with exit code -1073740791
原因:PyQt5 和 OpenCV 的 Qt 库冲突。opencv-python 自带一套 Qt 运行时,PyQt5 也带一套,两者版本不一致时 Windows 下会直接崩溃。解决:在 import cv2 之前先 import PyQt5,或者设置环境变量os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"]指向 PyQt5 的 plugins 目录。更彻底的办法是装opencv-python-headless,它不带 GUI 模块,不会和 PyQt5 抢 Qt 库,但代价是不能用cv2.imshow调试,只能靠保存图片到磁盘看中间结果。
4.2 现象:识别结果框出一堆小方块,水果没框住
原因:HSV 阈值范围太窄,或者形态学开运算 iterations 太多,把水果区域腐蚀碎了。解决:先把 mask 单独显示出来,如果 mask 里水果区域是碎的,降低开运算 iterations 到 1,或者把 kernel 从 (5,5) 改成 (3,3)。如果 mask 里水果区域根本不亮,说明 H 范围没覆盖到,用取色工具读一下水果像素的 HSV 值,把范围往两边各扩 5 到 10。
4.3 现象:加载图片后 QLabel 显示全黑或全白
原因:QImage 构造时数据指针指向了临时变量。cv2.cvtColor返回的 numpy 数组在函数结束后可能被回收,而 QImage 只保存指针不复制数据。解决:在show_image里加一句rgb = rgb.copy(),确保数据在 QImage 使用期间有效。另一个可能是 bytes_per_line 算错了,对于连续存储的 numpy 数组,ch * w是对的,但如果数组不是 C 连续(比如切片过的),要先np.ascontiguousarray。
4.4 现象:pip install PyQt5 卡在下载,或者装完 import 报 DLL 错误
原因:网络问题或 Python 版本不匹配。PyQt5 5.15.x 对 Python 3.11 以上的轮子支持不完整,如果你用的是 Python 3.12,pip 会尝试从源码编译,大概率失败。解决:确认 Python 版本是 3.8 到 3.10,用python --version看。如果已经是 3.12,要么降级 Python,要么改用 PyQt6 或 PySide6,但 PySide6 的 API 和 PyQt5 有差异,源码里的 import 语句要改。热词里「python 3.8」出现频率高,不是没有道理。
4.5 现象:识别速度慢,点一下按钮要等好几秒
原因:图片分辨率太高,或者形态学操作 kernel 太大。一张 4000x3000 的手机照片,做高斯模糊和两次形态学运算,CPU 要跑好几秒。解决:在segment_fruit开头把图片缩放到宽度 800 以内,scale = 800 / img.shape[1],然后cv2.resize。识别完再把框的坐标按比例映射回原图。这样速度能提升 10 倍以上,精度损失肉眼几乎看不出来。另一个优化是把GaussianBlur的 kernel 从 (7,7) 降到 (5,5),对结果影响很小。
5. 进阶技巧:用轮廓面积排序 + 颜色投票提升多水果识别准确率
前面讲的流程对单张图片里颜色分明的水果已经够用,但实际场景里经常遇到苹果和橙子挨在一起、香蕉被部分遮挡、背景里有类似颜色的物体。这时候单纯靠颜色阈值会翻车。我自己的习惯是加两层后处理:第一层按轮廓面积从大到小排序,只保留前 N 个(N 根据画面里水果数量预估),避免小噪点干扰;第二层对每个轮廓区域内的像素做颜色投票,统计 H 通道落在红、橙、黄三个区间的比例,哪个比例最高就判为哪类水果。
def classify_by_color(self, hsv, cnt): mask = np.zeros(hsv.shape[:2], dtype=np.uint8) cv2.drawContours(mask, [cnt], -1, 255, -1) pixels = hsv[mask == 255] if len(pixels) == 0: return "unknown" h_values = pixels[:, 0] red_count = np.sum((h_values <= 10) | (h_values >= 170)) orange_count = np.sum((h_values >= 11) & (h_values <= 25)) yellow_count = np.sum((h_values >= 26) & (h_values <= 35)) total = len(h_values) ratios = {"red": red_count / total, "orange": orange_count / total, "yellow": yellow_count / total} best = max(ratios, key=ratios.get) if ratios[best] < 0.5: return "unknown" return best这段代码的关键是drawContours填充轮廓生成掩膜,然后只取掩膜内的 HSV 像素做统计。阈值 0.5 表示超过一半像素落在某个颜色区间才判定,否则返回 unknown。这个逻辑对光照变化有一定鲁棒性,因为 H 通道对亮度不敏感。但要注意,如果水果表面有高光反射,高光区域饱和度低,H 值可能不稳定,可以在统计前过滤掉 S < 50 的像素。
验证方法很简单:找 10 张包含不同水果组合的图片,跑一遍看分类结果,把判错的图片单独拿出来分析 H 值分布。如果某个水果总是被判成 unknown,说明它的 H 范围不在你预设的三个区间里,比如绿苹果、紫葡萄、火龙果,需要额外加区间。这也是传统颜色方法的边界——它只能识别你预先定义好颜色的水果,遇到新品类就要改代码。想突破这个限制,就得引入形状特征(比如香蕉的细长、苹果的圆)或者上轻量分类模型,但那是另一个话题了。
最后说个我踩过的坑:调参时不要同时改多个参数。先把 mask 调好,确认水果区域完整;再调轮廓过滤,确认框的位置对;最后调分类投票,确认标签正确。一次只动一个变量,用同一张测试图对比效果。我见过太多人把 HSV 范围、kernel 大小、面积阈值一起改,结果好了不知道为啥好,坏了不知道为啥坏。希望帮到你。
本文还有配套的精品资源,点击获取