恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
PyQt5+Keras手写数字识别GUI实战:3分钟部署毕设级MNIST应用
首页
资讯中心
/
PyQt5+Keras手写数字识别GUI实战:3分钟部署毕设级MNIST应用
PyQt5+Keras手写数字识别GUI实战:3分钟部署毕设级MNIST应用
发布时间:2026/10/18 5:01:23
简介本资源是一份面向计算机专业本科生及毕业设计学习者的高分实战项目基于卷积神经网络CNN在MNIST数据集上实现手写数字识别并集成图形化界面GUI完整覆盖模型构建、训练、评估与交互式预测全流程。资源包共23个文件含3个核心Python脚本gui.py、recognition.py、CNN-Model.py、10张测试数字图像PNG、5个XML配置或标注文件、1个权重说明文本weights.txt及图标、项目配置等辅助文件整体压缩包仅3.53MB轻量易部署。已有130人学习下载项目经导师指导并获98分高分评价所有代码均本地实测可运行GUI界面支持图片上传与实时识别适合作为课程大作业、毕设参考或深度学习入门实践范例。1. 这不是“又一个MNIST demo”它能让你在3分钟内用鼠标画个“7”GUI立刻弹出识别结果置信度条连训练好的权重都打包进去了毕业答辩前一晚改完代码、打个包、双击运行就能过——专治计算机专业学生毕设焦虑你有没有经历过这种场景导师说“加个界面吧不然太单薄”你翻了三小时PyQt文档卡在按钮点击后怎么把画布图像喂给模型或者好不容易跑通训练脚本发现model.predict()输出的是[0.02, 0.91, 0.003, …]但不知道怎么映射回数字、怎么显示进度条、怎么把.h5权重转成轻量级格式嵌入GUI这个项目就是为这种时刻写的。它不讲理论推导不堆论文引用整个工程从数据加载、CNN建模、训练保存到手绘板交互、实时推理、结果可视化全部用纯PythonKerasPyQt5实现无外部服务依赖无云API调用所有文件都在zip里——包括那10张带编号的测试图0.png~9.png、图标icon.ico、甚至IDEA的配置文件说明作者真在本地完整跑过。它不是玩具是某高校计算机系助教团队审过三轮、98分毕设的落地版本训练脚本可复现GUI响应无卡顿识别准确率稳定在99.2%±0.1%且关键路径全有中文注释。如果你正卡在“毕设只剩两周但还不会把模型塞进界面”或想拿一个结构清晰、能讲清每行代码作用、改两行就能换自己数据集的参考项目它比网上90%的“MNIST教程”更接近真实交付。2. 项目结构与技术栈从文件清单看懂“为什么这样组织”——不是炫技是让毕设答辩时你能指着目录说清每一层职责这个项目表面是“MNISTGUI”但目录结构暴露了作者对工程落地的真实理解它没把所有代码塞进一个main.py而是按关注点分离Separation of Concerns做了四层切分。我们先看根目录下这16个实体不含隐藏文件gui.py # GUI主入口画布、按钮、事件绑定、结果展示 recognition.py # 推理核心加载权重、预处理图像、调用模型、返回数字置信度 CNN-Model.py # 模型定义Keras Sequential构建CNN含卷积/池化/全连接层 numbers_images/ # 测试图集10张png0.png~9.png用于GUI“加载图片”功能 9.png, 3.png, ... # 单独列出的测试图与numbers_images/内容重复可能是历史遗留 weights.txt # 训练好的权重参数文本格式非.h5这是关键设计点 icon.ico # 窗口图标 README.md # 基础说明但没写清楚weights.txt怎么生成 .idea/ # JetBrains IDE配置证明作者用PyCharm/IDEA开发并调试过 .gitignore # 忽略pycache、__pycache__等说明有Git协作意识提示别被.idea/和.iml文件吓到——它们只是IDE元数据不影响运行。真正要盯住的是weights.txt和recognition.py的耦合方式它没用Keras标准的.h5保存而是手动将model.get_weights()转成文本再在推理时用np.loadtxt()加载。这么做牺牲了一点通用性但换来两个硬好处一是避免.h5依赖h5py库新手常因版本冲突报错二是方便你在答辩PPT里直接截图展示权重文件内容“看这就是模型学到的特征”。2.1 四层架构拆解GUI层如何与模型层解耦项目没用“把CNN代码全塞进gui.py”的野路子而是通过明确定义接口实现松耦合GUI层gui.py只负责“用户看到什么、能做什么”。它创建QMainWindow内置QPainter画布支持鼠标拖动画线、QPushButton“识别”、“清空”、“加载图片”、QLabel显示数字和置信度条。它不碰任何模型代码只调用recognition.recognize_digit(image_array)这个函数。推理胶水层recognition.py承上启下。接收GUI传来的numpy.ndarray28×28灰度图做归一化/255.0、维度扩展np.expand_dims(img, axis(0, -1))然后调用CNN_Model.load_and_predict()。它不定义模型结构只负责“把图喂进去把结果吐出来”。模型定义层CNN-Model.py纯粹的Keras模型工厂。build_model()返回一个编译好的Sequential对象load_and_predict(weights_path)则负责从weights.txt重建权重并执行预测。这里的关键是模型结构与权重分离——你改网络结构比如加一层Dropout只需动build_model()不用碰GUI或推理逻辑。数据层numbers_images/ 内置测试图提供验证闭环。GUI的“加载图片”按钮会从该目录读取PNG走和手绘图完全一致的预处理流程确保“训练-验证-演示”链条一致。这种分层不是教科书摆设。某次我帮A同学调试时他把recognition.py里预处理逻辑复制到gui.py里结果手绘图和加载图用了不同归一化系数一个除255一个除128导致同一张图识别结果差30%置信度——分层强制你思考“谁该负责什么”避免这种低级错误。2.2 技术栈选型深意为什么用PyQt5而不是Tkinter为什么权重存txtPyQt5 vs Tkintergui.py中大量使用QPainter、QPen、QGraphicsView这决定了它需要精细控制画布渲染。Tkinter的Canvas对鼠标轨迹平滑插值支持弱手绘“7”时容易断成几截而PyQt5的QPainter.drawLine()配合setRenderHint(QPainter.Antialiasing)能生成抗锯齿线条更接近真实手写感。更重要的是PyQt5的信号槽机制self.recognize_btn.clicked.connect(self.on_recognize)比Tkinter的command回调更易管理复杂事件流比如“按下鼠标→移动→抬起”需连续触发重绘。weights.txt vs .h5查看CNN-Model.py中save_weights_to_txt()函数def save_weights_to_txt(model, filepath): weights model.get_weights() with open(filepath, w) as f: for i, w in enumerate(weights): f.write(f# Layer {i} weights shape: {w.shape}\n) np.savetxt(f, w.flatten(), fmt%.6f)它把每层权重展平后存为浮点文本。对应加载逻辑在load_and_predict()def load_and_predict(weights_path): model build_model() # 先建空模型 weights [] with open(weights_path, r) as f: lines f.readlines() # 跳过注释行按预设顺序读取权重数组... # 此处省略解析逻辑实际代码中用shape信息重建数组 model.set_weights(weights) # 关键用Keras原生方法注入 return model这种设计让权重文件可读、可调试。当你发现识别总错判“4”为“9”可以直接打开weights.txt搜索“Layer 3”全连接层权重用Excel加载查看数值分布快速判断是否过拟合——而.h5文件是二进制黑匣子。2.3 文件依赖关系图一张图看清模块间数据流向模块输入输出调用方被调用方gui.py鼠标坐标、PNG路径image_array(28×28)—recognition.recognize_digit()recognition.pyimage_arraydigit: int,confidence: floatgui.pyCNN_Model.load_and_predict()CNN-Model.pyweights.txt路径Keras模型实例recognition.py—numbers_images/PNG文件numpy.ndarraygui.py加载图片功能—注意没有反向依赖。CNN-Model.py不导入guirecognition.py不导入PyQt5。这意味着你可以轻松替换GUI层——比如把gui.py换成Web界面FlaskHTML Canvas只需重写输入接收逻辑recognition.py和CNN-Model.py一行不用改。这种可替换性正是毕设答辩时老师最想听到的“架构设计合理性”。3. 核心模块代码逐行解析从手绘板到识别结果看懂每一行代码在解决什么问题现在我们聚焦最关键的链路用户在画布上画一个“3” → 点击“识别” → GUI弹出“识别为3置信度96.2%”。这条链路横跨三个文件我们将逐行拆解告诉你为什么这么写、不这么写会怎样。3.1 GUI层手绘板如何把鼠标轨迹变成28×28图像gui.py中手绘板核心是DrawingWidget类继承QWidget。关键不在绘画而在如何把任意大小的画布区域缩放到标准MNIST尺寸# gui.py 第127行起 def mousePressEvent(self, event): if event.button() Qt.LeftButton: self.drawing True self.last_point event.pos() def mouseMoveEvent(self, event): if self.drawing: painter QPainter(self.image) painter.setPen(QPen(Qt.black, 15, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) painter.drawLine(self.last_point, event.pos()) self.last_point event.pos() self.update() # 触发重绘 def get_image_array(self): # 关键将画布缩放到28x28并转灰度 scaled self.image.scaled(28, 28, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) # 注意Qt.SmoothTransformation用双三次插值比NearestNeighbor更保边缘 img_array QImageToNumpyArray(scaled) # 自定义函数转numpy # 转灰度取R/G/B均值虽是黑白图但QImage可能存RGB gray np.dot(img_array[...,:3], [0.299, 0.587, 0.114]) # 反色MNIST是白底黑字手绘是黑底白字 gray 255 - gray return gray.astype(np.uint8)参数说明QPen(Qt.black, 15, ...)画笔宽度15像素这是玄学经验值。太细如3px手绘“0”中间留白会被误判为“8”太粗如30px相邻数字粘连。15px在28×28分辨率下刚好覆盖2-3个像素模拟真实手写粗细。Qt.KeepAspectRatioByExpanding强制填满28×28区域多余部分裁剪。若用Qt.KeepAspectRatio会留黑边导致模型看到大量0值干扰识别。255 - grayMNIST数据集是“背景白255、数字黑0”而手绘板默认黑底白字必须反色。漏掉这行识别准确率直接跌到10%以下——这是血泪经验。QImageToNumpyArray函数在文件底部第288行def QImageToNumpyArray(qimage): # 将QImage转为numpy array兼容ARGB格式 qimage qimage.convertToFormat(QImage.Format_RGBA8888) ptr qimage.bits() ptr.setsize(qimage.byteCount()) arr np.array(ptr).reshape(qimage.height(), qimage.width(), 4) return arr为什么用RGBA8888PyQt5的QImage在不同系统下格式不一Windows常为ARGB32Linux为RGB32。统一转Format_RGBA8888确保arr.shape恒为(H, W, 4)后续取arr[...,:3]安全。若直接用qimage.bits().tobytes()在Mac上可能因字节序错乱导致图像花屏。3.2 推理层recognition.py如何把图像喂给模型recognition.py的recognize_digit()函数是胶水核心# recognition.py 第42行 def recognize_digit(image_array): image_array: numpy.ndarray, shape (28, 28), dtype uint8 Returns: (digit: int, confidence: float) # 步骤1归一化到[0,1]Keras模型训练时用的 img_normalized image_array.astype(np.float32) / 255.0 # 步骤2增加batch和channel维度 - (1, 28, 28, 1) img_expanded np.expand_dims(img_normalized, axis(0, -1)) # 步骤3加载模型并预测复用CNN-Model.py逻辑 model CNN_Model.load_and_predict(weights.txt) predictions model.predict(img_expanded) # shape: (1, 10) # 步骤4取最大概率索引和值 digit int(np.argmax(predictions[0])) confidence float(np.max(predictions[0])) return digit, confidence关键细节axis(0, -1)0加batch维模型要求输入有batch-1加channel维MNIST是单通道灰度图。漏掉-1会导致predict()报错expected input shape (None, 28, 28, 1)。predictions[0]model.predict()返回(batch_size, 10)即使batch1也要取[0]。新手常写predictions[0][0]想取第一个样本第一个类但predictions是二维数组[0]才是第一行。float(np.max(...))显式转float避免PyQt5的QLabel.setText()接收numpy类型时报错Qt不认np.float32。3.3 模型层CNN-Model.py的权重加载为何要“手动重建”CNN-Model.py的load_and_predict()是项目最精妙的设计点。它不直接load_model(model.h5)而是# CNN-Model.py 第65行 def load_and_predict(weights_path): model build_model() # 创建未训练的空模型 # 读取weights.txt按层顺序解析权重 weights [] with open(weights_path, r) as f: lines [l.strip() for l in f if not l.startswith(#)] # 预设各层权重shape必须与build_model()中层定义严格一致 layer_shapes [ (32, 1, 3, 3), # Conv1: 32 filters, 1 input channel, 3x3 kernel (32,), # Conv1 bias (64, 32, 3, 3), # Conv2 (64,), # Conv2 bias (128, 64, 3, 3), # Conv3 (128,), # Conv3 bias (128 * 3 * 3, 128), # Dense1 weight matrix (128,), # Dense1 bias (128, 10), # Dense2 weight matrix (10,) # Dense2 bias ] idx 0 for shape in layer_shapes: size np.prod(shape) layer_weights np.array(lines[idx:idxsize], dtypenp.float32) weights.append(layer_weights.reshape(shape)) idx size model.set_weights(weights) # 注入权重 return model为什么敢这么干因为build_model()中层定义是确定的见下节所以layer_shapes可硬编码。这种方式绕过了Keras的load_weights()对文件格式的强约束让权重文件成为纯文本方便教学演示。但代价是你改了build_model()的层顺序或shape必须同步更新layer_shapes列表。这是典型的“用可维护性换可解释性”——毕设场景下可解释性能向老师讲清权重在哪比自动化更重要。4. 运行与调试全流程从环境搭建到识别失败排查一份能照着敲的实操指南别跳过这章。很多同学下载源码后第一反应是python gui.py然后看到ModuleNotFoundError: No module named PyQt5就放弃。下面是从零开始的完整路径包含所有坑点和绕过方案。4.1 环境准备最低可行配置不装CUDA也能跑项目依赖极简仅需三个包pip install numpy1.21.6 keras2.6.0 pyqt55.15.6版本锁定原因keras2.6.0这是TensorFlow 2.6的配套Keras对model.predict()的输入校验较宽松。新版Keras2.12要求明确指定batch_size而本项目没传会报错。pyqt55.15.65.15.x是最后一个支持Python 3.10以下的稳定版。若你用Python 3.11需降级到3.10或升pyqt56.5.0但需同步改gui.py中QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)为QApplication.setHighDpiScaleFactorRoundingPolicy(...)。numpy1.21.6与Keras 2.6兼容性最佳。新版numpy的array.astype()行为微调可能导致get_image_array()返回float64而Keras模型要求float32。验证安装python -c import numpy, keras, PyQt5; print(OK)若报ImportError: DLL load failedWindows大概率是MSVC运行库缺失去微软官网下载vc_redist.x64.exe安装即可。4.2 一键运行三步启动GUI解压zip包进入根目录确保看到gui.py,CNN-Model.py等文件确保weights.txt在当前目录它必须和gui.py同级因为recognition.py中写死路径weights.txt执行python gui.py窗口弹出即成功。此时可尝试用鼠标在画布画一个数字如“5”点击“识别”按钮 → 应显示“识别为5置信度XX.X%”点击“加载图片” → 选择numbers_images/5.png→ 结果应一致如果窗口空白或闪退在gui.py开头添加import sys sys.excepthook lambda t, v, tb: print(fError: {t.__name__}: {v})然后重跑错误会打印在终端而非静默消失。4.3 常见问题与排查那些让你怀疑人生却只需改一行的Bug现象1点击“识别”后无反应终端无报错原因recognition.py中load_and_predict()读取weights.txt失败但没加异常处理静默返回None导致model.predict()调用空对象。解决在recognition.py的recognize_digit()函数开头加if not os.path.exists(weights.txt): raise FileNotFoundError(weights.txt not found! Please check file location.)现象2识别结果总是“0”且置信度95%原因手绘图没反色255 - gray漏了模型看到的是“白字黑底”而训练数据是“黑字白底”特征完全相反。排查在gui.py的get_image_array()末尾加print(Min/Max pixel value:, gray.min(), gray.max()) # 正常应为0~255若全是255则没反色现象3加载numbers_images/0.png时报ValueError: cannot reshape array of size XXX into shape (28,28)原因PNG文件不是28×28尺寸如原始图是100×100QImageToNumpyArray后arr.shape不是(28,28,4)。解决在gui.py的load_image()函数中加载PNG后强制缩放pixmap QPixmap(filepath) scaled pixmap.scaled(28, 28, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation) self.image scaled.toImage() # 替换原逻辑现象4PyQt5窗口在高分屏如Mac Retina显示模糊原因Qt未启用高DPI适配。解决在gui.py最顶部import之后加import os os.environ[QT_SCALE_FACTOR] 1.5 # 根据屏幕调整1.5适合2K屏 # 或用自动适配推荐 from PyQt5.QtWidgets import QApplication QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)现象5训练自己的权重时CNN-Model.py的train_model()函数报ValueError: Input 0 is incompatible with layer原因train_model()中X_train形状是(60000, 28, 28)但模型输入层期望(None, 28, 28, 1)。解决在train_model()中添加维度扩展X_train X_train.reshape(-1, 28, 28, 1) # 加channel维 X_test X_test.reshape(-1, 28, 28, 1)5. 二次开发实战如何把MNIST换成你的手写汉字/签名数据集三步改造法这才是毕设真正的价值点它不是一个终点而是一个可生长的起点。我帮A同学把这套架构迁移到“手写签名验证”项目时只改了三处核心三天就跑通demo。下面是你能立刻上手的改造路径。5.1 数据集替换从MNIST到自定义图像文件夹项目默认用Keras内置MNISTfrom keras.datasets import mnist但你要用自己的数据就得砍掉这部分改用ImageDataGenerator。步骤准备数据目录结构示例my_dataset/ ├── train/ │ ├── person_a/ # 类别1 │ │ ├── sig_001.png │ │ └── sig_002.png │ └── person_b/ # 类别2 │ ├── sig_001.png │ └── sig_002.png └── test/ ├── person_a/ └── person_b/修改CNN-Model.py的train_model()函数删除原MNIST加载代码替换为from tensorflow.keras.preprocessing.image import ImageDataGenerator def train_model(): # 数据增强防过拟合 datagen ImageDataGenerator( rescale1./255, rotation_range10, # 随机旋转±10度 width_shift_range0.1, # 水平平移10% height_shift_range0.1 # 垂直平移10% ) train_gen datagen.flow_from_directory( my_dataset/train, target_size(28, 28), # 强制缩放 batch_size32, class_modecategorical, # 多分类 color_modegrayscale # 灰度图 ) # 构建模型保持原有build_model()不变 model build_model() model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) model.fit(train_gen, epochs20) model.save_weights(my_weights.txt) # 仍存txt复用现有加载逻辑 return model关键点target_size(28,28)确保输入尺寸与原MNIST一致无需改模型结构color_modegrayscale保证输出是单通道匹配Conv2D(input_shape(28,28,1))。5.2 模型结构调整当你的数据需要更深网络原build_model()是3层卷积适合MNIST的简单特征但签名有更复杂纹理。升级方法# CNN-Model.py 中 build_model() 函数 def build_model(): model Sequential([ Conv2D(32, (3,3), activationrelu, input_shape(28,28,1)), MaxPooling2D((2,2)), Conv2D(64, (3,3), activationrelu), # 原有 MaxPooling2D((2,2)), Conv2D(128, (3,3), activationrelu), # 新增捕获更高级特征 MaxPooling2D((2,2)), # 新增压缩尺寸 Flatten(), Dense(128, activationrelu), Dropout(0.5), # 新增防过拟合签名数据易过拟合 Dense(len(train_gen.class_indices), activationsoftmax) # 动态输出类别数 ]) return model注意新增层后load_and_predict()中的layer_shapes列表必须同步更新否则权重加载失败。建议把layer_shapes改为从模型自动推导def get_layer_shapes(model): return [w.shape for w in model.get_weights()]5.3 GUI适配支持多类别结果展示原GUI只显示一个数字和置信度条但签名验证需显示“相似度Top3”。修改gui.py的on_recognize()函数# gui.py 第215行 def on_recognize(self): img_array self.drawing_widget.get_image_array() digit, confidence recognition.recognize_digit(img_array) # 原逻辑显示单一结果 # self.result_label.setText(f识别为{digit}置信度{confidence:.1%}) # 新逻辑显示Top3需修改recognition.py返回top_k top3 recognition.recognize_top3(img_array) # 返回[(class_name, conf), ...] result_text 相似度Top3br for name, conf in top3: result_text f{name}: {conf:.1%}br self.result_label.setText(result_text)对应修改recognition.py的recognize_top3()def recognize_top3(image_array): img_normalized image_array.astype(np.float32) / 255.0 img_expanded np.expand_dims(img_normalized, axis(0, -1)) model CNN_Model.load_and_predict(my_weights.txt) predictions model.predict(img_expanded)[0] # 获取类别名从ImageDataGenerator获取 class_names [person_a, person_b] # 实际从train_gen.class_indices动态读 top3_idx np.argsort(predictions)[-3:][::-1] return [(class_names[i], float(predictions[i])) for i in top3_idx]教训从那以后我每次接新数据集都强制走一遍“数据目录检查→模型结构匹配→GUI结果展示适配”三步 checklist。哪怕只是换字体也先确认target_size和color_mode是否匹配因为90%的翻车都发生在数据预处理环节——模型再强喂错数据也是垃圾进垃圾出。希望帮到你。本文还有配套的精品资源点击获取