ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyQt5+OpenCV实现简易水果识别系统:从界面到识别全流程解析

PyQt5+OpenCV实现简易水果识别系统:从界面到识别全流程解析 简介基于PyQt5框架设计并实现的一套简易版水果识别系统是适合毕业设计、课程设计以及图像处理与机器学习入门学习的Python源码。系统利用PyQt5构建了简洁易用的图形用户界面包含菜单栏、工具栏和状态栏等常见组件同时完整覆盖图像灰度化、二值化、边缘检测等预处理环节结合颜色直方图、形状描述符等特征提取方法并可采用支持向量机或K近邻等分类器完成训练与水果名称输出。压缩包内文件共27个压缩后约1.54MB以Python脚本、PyQt界面文件、测试图片及说明文档为主.py文件对应图像处理、特征提取、模型训练与识别等模块.ui和.qrc用于界面布局与资源引用.txt和.md提供运行说明整体目录结构清晰方便对照学习。目前已有80人学习或下载这套源码。整份资源不仅能让读者理解PyQt5界面开发与图像处理、分类模型训练结合的具体写法还提供了可运行、可二次修改的完整项目框架适合作为课程设计或毕业设计的起点参考。1. 一个 Python 课设/实战项目为什么我用 PyQt5 而不是网页拿到这个标题时我最先想确认的是一个“水果识别系统”到底是谁在什么场景下用。如果只是做一个命令行脚本识别完打印一行“苹果”那十分钟就能跑通但带 PyQt5 的桌面界面意味着你要把识别结果可视化、要让不懂编程的人也能点按钮完成识别。这正是课程设计、毕业设计里最常见的形态后端用图片分类或检测前端用 PyQt5 做桌面壳打包成一个 zip 发给老师解压即演示。顺着这个思路往下拆一个合格的简易版水果识别系统应当包含四块能选图/拍照的 PyQt5 界面、负责推理的识别引擎、串起两者的业务逻辑层、以及让同学或老师能一键运行的环境说明。这里有个反直觉的点很多人以为难点在 PyQt5 界面实际上界面部分半天就能搭完真正决定这个项目能不能交差、能不能演示的是识别准确率和换图后的稳定性。本篇文章就把这套方案完整讲透从界面到识别、从串流程到排坑全部给出可以抄作业的代码。2. PyQt5 界面层先让界面能跑能选图再谈识别2.1 为什么选用 PyQt5 而不是 Tkinter 或 Web 前端如果你只交一个大作业Tkinter 其实够用但 PyQt5 的用户体感明显更好控件更现代、支持样式表QSS、自带信号槽机制而且文件对话框、缩略图、进度条这些细节都是现成的。网页方案还需要起一个 Flask/Django 服务演示时浏览器、端口、静态文件一堆麻烦事反而把“识别水果”这个核心目标冲淡了。所以我一般会建议离线单机演示优先 PyQt5尤其是那种需要老师走到你电脑前看效果的场景。安装 PyQt5 本身没什么门槛常规操作是先用 pip 装包再顺手验证一下能不能正常拉起界面。很多人的翻车现场不在“安装”而在“装完之后运行报错找不到 Qt 平台插件”这一条我在第 5 章避坑里会重点展开先继续往下搭界面。2.2 搭一个能选图并显示预览的主窗口常见做法是先用 QMainWindow 充当壳用一个 QLabel 当图片展示区再放两个按钮一个选图、一个开始识别。先不接任何识别算法只做一件事把本地图片选出来原样铺到控件里。这一步跑通后面所有的识别结果展示就都挂在这个链路上。先看最小可运行代码文件命名 main_window.pyimport sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog) from PyQt5.QtGui import QPixmap from PyQt5.QtCore import Qt class FruitWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(水果识别系统 - PyQt5) self.resize(800, 600) # 图片展示区开启 setScaledContents 让图片自适应缩放 self.image_label QLabel(请选择一张水果图片) self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setMinimumSize(480, 360) self.image_label.setStyleSheet(border: 1px solid #aaa;) # 两个按钮 self.btn_open QPushButton(选择图片) self.btn_predict QPushButton(开始识别) self.btn_predict.setEnabled(False) # 布局 central QWidget() self.setCentralWidget(central) layout QVBoxLayout(central) layout.addWidget(self.image_label) btn_layout QHBoxLayout() btn_layout.addWidget(self.btn_open) btn_layout.addWidget(self.btn_predict) layout.addLayout(btn_layout) # 信号槽 self.btn_open.clicked.connect(self.open_image) self.btn_predict.clicked.connect(self.do_predict) self.current_path None def open_image(self): # 返回 (fileName, filter)只取第一个 path, _ QFileDialog.getOpenFileName( self, 打开图片, , Images (*.png *.jpg *.jpeg *.bmp) ) if not path: return self.current_path path pixmap QPixmap(path) if pixmap.isNull(): self.image_label.setText(图片加载失败请检查路径) return self.image_label.setPixmap(pixmap) self.image_label.setScaledContents(True) self.btn_predict.setEnabled(True) def do_predict(self): # 识别逻辑后续接入这里先做占位 self.image_label.setText(f待识别: {self.current_path}) if __name__ __main__: app QApplication(sys.argv) win FruitWindow() win.show() sys.exit(app.exec_())这段代码的逻辑很简单open_image 用 QFileDialog 弹出系统文件选择框拿到路径后通过 QPixmap 读取并交给 QLabel 显示do_predict 目前只是一个占位槽函数后面我们要把水果识别引擎挂进来。有一个细节值得留意图片能不能撑满界面靠的是 setScaledContents(True)否则大图只显示左上角一部。参数说明setAlignment(Qt.AlignCenter) 控制文字和图片居中setStyleSheet 里 border 属性只是方便调试时看清控件边界真正做漂亮界面时改用 QSS 文件统一管理。MinimumSize 设成 480x360 是为了避免窗口拖小时图片消失具体数值根据你的分辨率习惯调整。2.3 用 QThread 保住界面不卡死的框架识别一张图片如果耗时几十毫秒还好但如果加载模型后首次推理达到一两秒放在主线程里会导致窗口“无响应”Windows 会直接弹白色蒙层提示用户强制关闭。正确姿势是提前把识别任务塞进 QThread识别完成后通过信号把结果传回界面线程。这里先把线程骨架搭出来import sys from PyQt5.QtCore import QThread, pyqtSignal class RecognizeThread(QThread): # 第一个参数传类别名称第二个参数传置信度 result_ready pyqtSignal(str, float) def __init__(self, image_path): super().__init__() self.image_path image_path def run(self): # 真正调用识别接口后续在第 4 章替换为实际实现 category, confidence apple, 0.95 self.result_ready.emit(category, confidence) if __name__ __main__: # 单独运行此文件只会退出属于正常现象 t RecognizeThread(test.jpg) t.start()QThread 的 run 会在独立线程中执行emit 发出信号后主线程里连接的槽函数会自动回到 GUI 线程处理不需要手动加锁。这个“界面与推理分离”的框架是后期不返工的关键现在看不出来等接入摄像头连续识别时没有 QThread 基本每三秒崩一次。3. 水果识别引擎从经典图像处理到轻量模型3.1 先分清三种实现路线的适用边界简易版水果识别系统的核心词是“简易”实现方式通常有三条路线选择决定了代码量和演示效果的上限。第一条是颜色空间 轮廓分析。用 HSV 阈值框出水果区域再统计颜色占比来区分苹果、橙子、香蕉。优点是零训练、无需样本、代码量极低缺点是对光照极敏感稍微偏色就翻车。第二条是传统机器学习加特征比如 HOG/SIFT 特征 SVM。这一类需要在固定背景下对每种水果采集几十张图训练一次效果比纯颜色好但对“简易版”来说采集样本本身就是一个隐形工作量如果你手里没有现成数据集不建议从这条路入手。第三条是深度卷积网络比如对 ImageNet 预训练的分类模型做迁移学习只训练水果类别。这是目前识别效果上限最高、但环境配置最折腾的路线需要装 PyTorch 或 TensorFlow运行时还得把模型文件一起打包进 zip。对于一台老师用的普通 Windows 电脑CPU 推理一张图大约耗时 0.5 到 2 秒。我见过很多课设选手一上来就冲深度学习最后卡在“模型下载不了”“版本冲突跑不起来”上。这个标题里带“简易版”你自己也要有取舍意识识别三类水果且背景简单用 HSV 阈值优先识别类别多或画面复杂再换卷积模型。建议做两手准备核心逻辑写成接口底层实现可以随时替换。3.2 用 HSV 与掩膜识别苹果、橙子、香蕉的最短实现先给一个只依赖 OpenCV 的识别函数不涉及任何训练。常见做法是定义好三类水果的 HSV 上下界掩膜提取区域后统计颜色占比import cv2 import numpy as np # HSV 范围依据 OpenCV 的 H: [0, 180], S: [0, 255], V: [0, 255] FRUIT_HSV { apple: (np.array([156, 43, 46]), np.array([180, 255, 255])), # 红苹果 banana: (np.array([11, 43, 46]), np.array([34, 255, 255])), # 黄香蕉 orange: (np.array([11, 100, 100]), np.array([24, 255, 255])) # 橙子与香蕉区分靠饱和度 } def recognize(image_path): bgr cv2.imread(image_path) if bgr is None: return unknown, 0.0 hsv cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) results {} for fruit, (lower, upper) in FRUIT_HSV.items(): mask cv2.inRange(hsv, lower, upper) # 去掉细小噪点再统计像素占比 mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8)) ratio float((mask 0).sum() / (hsv.shape[0] * hsv.shape[1])) results[fruit] ratio # 取占比最高的类别如果最高占比过低判定为未知 best_fruit max(results, keyresults.get) best_ratio results[best_fruit] if best_ratio 0.05: return unknown, 0.0 return best_fruit, best_ratio这段代码的识别依据是“画面中某个颜色像素的比例”。香蕉与橙子用同一色相带单独靠 S饱和度区分橙子偏橙红、饱和度更高香蕉偏黄。逻辑说明里最值得关注的一个参数是滤波器核大小 5x5核太大容易把分离的色块连成一片导致误判核太小则去不掉背景噪点建议在 3x3 到 7x7 之间调。另一个关键阈值是 best_ratio 的 0.05也就是颜色区域占全图至少 5% 才认定识别成功否则直接返回 unknown。如果你要识别的是绿苹果只需多加一组 HSV 范围如果你要识别梨子方法一模一样。这也是我觉得简易版系统最好的一个切入点识别逻辑是可插拔的扩展新水果只需要扩充字典。3.3 换用轻量模型把 MobileNetV2 转成 ONNX 再推理如果你手里的样本背景比较乱HSV 的稳定性撑不住演示压力下一步一定是上小型分类网络。移动端或桌面 CPU 环境里我一般会选 MobileNetV2它比大模型小很多在 i5 CPU 上跑一张图大致在几百毫秒量级。转到 ONNX 后推理就不需要安装 PyTorch/TensorFlow 了只需 onnxruntime 这个轻量库。这里不给完整的训练代码默认你已经有训练好的 PyTorch 模型权重转换和部署的步骤是这样import cv2 import numpy as np import onnxruntime as ort class OnnxFruitClassifier: def __init__(self, onnx_path, labels): # 创建推理会话Providers 可以按机器情况调整 self.session ort.InferenceSession( onnx_path, providers[CPUExecutionProvider] ) self.labels labels # 从模型输入节点拿尺寸常见的 MobileNet 系列是 [1,3,224,224] input_info self.session.get_inputs()[0] _, _, self.h, self.w input_info.shape def predict(self, image_path): img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (self.w, self.h)) # 归一化到 [-1, 1]这是大多数移动分类网络的输入约定 img img.astype(np.float32) / 127.5 - 1.0 # 扩展维度 HWC - NCHW tensor np.expand_dims(np.transpose(img, (2, 0, 1)), axis0) logits self.session.run(None, {self.session.get_inputs()[0].name: tensor})[0] idx int(np.argmax(logits)) conf float(np.max(logits)) return self.labels[idx], confONNX 推理的核心就三点输入张量的布局是 NCHW输入像素归一化要与模型训练时一致输出层拿到的是 logits 而非 softmax 概率。如果你在部署时发现置信度普遍是负数就是这个 logits 没有过 softmax只需要加一次 np.exp(logits) / np.sum(np.exp(logits))。Labels 的顺序必须和训练时一致否则会出现“模型识别很准但结果全错”的怪现象。4. 把界面和识别引擎串起来跑通一次完整识别流程4.1 双击即用的项目目录结构要交付一个 zip先想清楚别人解压后会看到什么。常见做法是把我上面写的两类文件合并成下面的结构fruit_recognition/ ├── main_window.py # PyQt5 主界面与线程调度 ├── recognizer.py # 识别逻辑HSV 或 ONNX 二选一 ├── models/ │ └── fruit_mobilenet.onnx ├── test_images/ │ ├── apple.jpg │ ├── banana.jpg │ └── orange.jpg └── requirements.txtrequirements.txt 里固定好依赖保证用 pip 能一次性装齐pyqt5 一个版本、opencv-python 一个版本用 ONNX 方案再加 onnxruntime。很多人会在演示时临时发现缺包提前写好这个文件能少很多尴尬PyQt55.15.9 opencv-python4.8.0.74 onnxruntime1.16.0 numpy1.24.04.2 用信号槽连接“按钮 - 线程 - 界面标签”上一节里主界面和识别线程互不相通现在把它们焊死。核心思路是点击开始识别后创建 RecognizeThread将它的 result_ready 信号连到界面的槽函数上线程结束后自动更新 QLabel 文本。from PyQt5.QtWidgets import QMessageBox import recognizer from main_window import FruitWindow from recognize_thread import RecognizeThread class FruitApp(FruitWindow): def do_predict(self): if not self.current_path: return # 先把按钮锁住避免重复触发多个线程 self.btn_predict.setEnabled(False) self.image_label.setText(识别中请稍候...) # 创建线程并绑定信号 self.thread RecognizeThread(self.current_path, modehsv) self.thread.result_ready.connect(self.on_result) self.thread.start() def on_result(self, category, confidence): self.image_label.setText(f识别结果{category}置信度{confidence:.2f}) self.btn_predict.setEnabled(True)对应的 RecognizeThread 需要改成真正执行识别逻辑from PyQt5.QtCore import QThread, pyqtSignal import recognizer class RecognizeThread(QThread): result_ready pyqtSignal(str, float) def __init__(self, image_path, modehsv): super().__init__() self.image_path image_path self.mode mode def run(self): # 模型加载放在 run 里面避免阻塞界面线程 if self.mode hsv: category, confidence recognizer.recognize_hsv(self.image_path) else: model recognizer.OnnxFruitClassifier(models/fruit_mobilenet.onnx, [apple, banana, orange]) category, confidence model.predict(self.image_path) self.result_ready.emit(category, confidence)这里有一个值得注意的设计选择模型初始化不要放在init里否则加载 ONNX 的耗时依然会卡住主界面。放在 run 方法里每次预测前重新加载尽管速度慢一点但对简易版系统来说换一个“线程安全”和“界面流畅”的安心值得。课程设计阶段每次耗时增加一两百毫秒完全可以接受。4.3 批量识别一个目录为验证准备素材验收场景往往不是只识别一张图而是老师会自己丢几张图片进系统。为了给自己留后路最好在界面里加一个“目录识别”按钮把整个文件夹跑一遍结果存到 CSV 里方便事后复盘错图。import os import csv from PyQt5.QtWidgets import QFileDialog, QMessageBox def batch_recognize(self): folder QFileDialog.getExistingDirectory(self, 选择包含图片的文件夹) if not folder: return ext (.jpg, .jpeg, .png, .bmp) images [f for f in os.listdir(folder) if f.lower().endswith(ext)] if not images: QMessageBox.warning(self, 提示, 该目录下没有图片文件) return results [] for name in images: path os.path.join(folder, name) category, confidence recognizer.recognize_hsv(path) results.append([name, category, round(confidence, 4)]) print(f已识别{name} - {category}) out_path os.path.join(folder, results.csv) with open(out_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([filename, prediction, confidence]) writer.writerows(results) QMessageBox.information(self, 完成, f共识别 {len(results)} 张图片结果已保存到 results.csv)批量识别的两个细节CSV 用 utf-8-sig 编码而不是 utf-8否则 Excel 打开中文列名会乱码识别前先收集扩展名列表过滤掉目录里可能存在的 .txt 或 .db 文件避免 OpenCV 读取报错。这个功能在验收时非常加分至少向老师证明了你有系统化验证的意识。5. PyQt5 与识别系统的高频坑现象、原因、解决5.1 运行提示 “could not load the Qt platform plugin xcb / windows”现象程序启动瞬间报错This application failed to start because no Qt platform plugin could be initialized然后闪退。很多同学就在这一步放弃了。原因PyQt5 的 wheels 把 Qt 动态库拆成了主包和平台插件包pip 安装时版本没对齐或者系统 PYTHONPATH 里混入了另一套 Qt 安装路径。最常见的是 PyQt5 与 PyQt5-Qt5 版本不一致平台插件加载失败。解决先把 PyQt5 相关包全部卸载干净然后一次性装齐指定版本pip uninstall PyQt5 PyQt5-Qt5 PyQt5-sip -y pip install PyQt55.15.9 PyQt5-Qt55.15.2 PyQt5-sip12.13.0如果还在报错查一下系统环境变量里是否有旧的 QT_QPA_PLATFORM_PLUGIN_PATH这是最隐蔽的原因之一。还有个更简单的处理换 conda 建一个干净环境再装conda 的 Qt 依赖管理比 pip 可靠很多。5.2 open_image 里显示黑屏或 “图片加载失败”现象文件对话框正常弹出但选择一张 jpg 后 QLabel 显示“图片加载失败”或者显示一片黑。原因一般不是 PyQt5 的问题而是 OpenCV 与 PyQt5 在读取图片时的像素格式冲突。很多人习惯先用 cv2.imread 读取再做其他处理但 cv2 读进来是 BGR 顺序直接喂给 QPixmap 会把红蓝通道调换更常见的是 cv2.imread 对中文路径返回 None导致后续流程直接拿不到图片对象。解决统一用 QPixmap 负责界面显示cv2 只在识别函数内部重新读取并按 BGR 处理两者各管一段。如果确实要让 cv2 读取中文路径先把路径转成 bytes 再传import cv2 import numpy as np from PyQt5.QtGui import QPixmap, QImage # 中文路径下的可靠读取方式 data np.fromfile(self.current_path, dtypenp.uint8) bgr cv2.imdecode(data, cv2.IMREAD_COLOR) # BGR - RGB - QImage rgb cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimage QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(qimage))注意 QImage 构造时 bytesPerLine 必须等于 width * channels这是一条最容易忽略的规则。如果你看到图片上下颠倒或者斜条纹基本是这一项没有对齐。5.3 摄像头显示闪烁但识别结果不变现象接入摄像头后界面能预览但按下识别按钮后结果卡在上一帧换角度、换水果都不更新。原因摄像头线程和识别线程共用同一个 QPixmap 引用但识别线程拿到的是预览时生成的句柄信号回传的时机与摄像头更新循环不同步导致识别结果回填后又立刻被新预览帧覆盖。解决给预览单独开一个 QThreadqimage 用 deep copy 深拷贝识别时暂停预览刷新。一个相对省事的处理办法是预览线程只负责把帧绘到 QLabel 上识别按钮按下时发一个capture_signal把当前帧复制一份传给识别线程而不是在预览循环里直接调用识别函数。5.4 HSV 阈值在白天正常、晚上偏色现象同一张苹果图片办公室自然光下识别准确换成偏暖色的灯光后识别成“unknown”。原因HSV 的 V明度和 S饱和度受光照影响极大采集阈值时是在一种光源下标定的换个环境后整幅图偏移。解决在进入 HSV 判断前先做一次白平衡矫正最简单的是用灰世界假设或者同时准备两套 HSV 阈值识别时用两份掩膜做与运算哪个占比高取哪个。更实用的方案是放弃纯色块识别换上一节提到的 ONNX 模型光照鲁棒性会明显上一个台阶。5.5 代码直接运行报 “No module named ‘numpy’ ”但 README 里说装过现象在 A 机器上跑得好好的copy 到老师电脑上后提示各种依赖缺失。原因PyCharm 默认用的是项目虚拟环境命令行 python 用的是全局环境两个环境里的包互不相通。老师不会先激活你的 conda 环境再运行他只会在终端敲 python main.py。解决交付 zip 时不要只给源码附上 requirements.txt并在 README 里写清楚安装命令更省心的做法是用 PyInstaller 把整个程序打成 exePyQt5 的 Warning 可以忽略只要程序能起来就说明 dll 和插件都打包完整了。打出 exe 前记得切到 Release 模式避免一打出来就报毒或者被杀软拦掉。6. 从“能跑”到“能演示”验证方法、批量评估与一个小技巧这一章的核心是帮你回答两个最具体的问题识别系统到底对不对以及老师看演示时怎么稳定不出错。先说验证方法。不要只在本地挑几张苹果图片眼看效果好就收工至少准备一个 30 张左右的小评估集每个类别各 10 张覆盖不同角度、不同光照、不同大小。跑一次批量识别用脚本快速算准确率和各类别的混淆情况。批处理 CSV 的功能前面已经有把它作为测试工具来用看文件里每一行的预測结果找出问题图片集中分析。如果你发现苹果和橙子互相打错的图特别多优先检查 HSV 阈值里 S 通道的范围因为这两个类别颜色接近时最容易混淆。再说一个很实用的排序技巧。程序识别结果默认按文件名排序输出比如 apple_1.jpg、apple_2.jpg 这些但如果你的测试图片是从网页或手机导入的命名可能是 img_20240315_01.jpg 这样的时间戳。为了让演示时 30 张图按类别一条条稳定跑下来建议在 batch 读取文件后用文件名解析图片类别与预测结果做自动核对。这一步不是科研必需但能让你在答辩前快速发现“有一整类水果识别全挂了”而不是等到老师发问才意识到。最后分享一个我常用的参数设置习惯窗口打开时默认加载一张测试图片并且打开图片后立刻执行一次识别这样老师点开程序看到的不是一片空白而是已经出了一次结果。界面交互上有一个小技巧先用 setToolTip 把识别置信度附加到图片标签上鼠标悬停就能看到数值这样不影响界面整洁又保留证据链。再有就是窗口标题不用“水果识别系统”这六个字解决一切把当前用的识别模式写进去比如改成“水果识别系统HSV 模式”这样老师一眼知道实现方式你也不用口头解释半天。我自己做这类系统踩过最深的一个坑是把所有代码写在一个文件里窗口逻辑、识别逻辑、CSV 导出逻辑全部堆在一起界面一复杂就再也改不动。后来强制自己按“界面层 - 线程层 - 识别层”拆成三个文件再改起来效率高很多。这个文件结构在第四章已经完整列出建议开局就照着写别等代码两千行之后再后悔。希望这篇完整的 PyQt5 水果识别系统实战拆解能帮你少走弯路照这个方案把目录结构、识别接口和线程框架搭好剩下贴图和调参只是时间问题。本文还有配套的精品资源点击获取
返回列表