公司动态

分享一套锋哥原创的基于PyTorch的手写数字识别系统(深度学习+PyQt6+LetNet-5)

📅 2026/7/23 21:06:18
分享一套锋哥原创的基于PyTorch的手写数字识别系统(深度学习+PyQt6+LetNet-5)
大家好我是Java1234_小锋老师分享一套锋哥原创的基于PyTorch的手写数字识别系统(深度学习PyQt6LetNet-5)项目介绍手写数字识别是模式识别与计算机视觉领域的经典问题在邮政分拣、票据录入、教育测评、表单数字化等场景中具有重要应用价值。随着深度学习技术的快速发展卷积神经网络在图像分类任务中展现出优异性能。本文围绕“基于PyTorch的手写数字识别系统”这一课题设计并实现了一套面向桌面端的可视化应用系统完成了从模型训练到实时识别的完整闭环。系统以 Python 为主要开发语言采用 PyTorch 深度学习框架构建改进版 LeNet-5 卷积神经网络并在经典 MNIST 手写数字数据集上完成训练与评估前端交互层基于 PyQt6 实现集成首页介绍、模型训练、数字识别与关于说明等功能模块。训练模块支持在图形界面中配置训练轮数、批次大小与学习率通过 QThread 在后台执行训练任务并利用 Matplotlib 实时绘制损失与准确率曲线识别模块支持鼠标手写与本地图片上传两种输入方式经过灰度化、反色、裁剪、缩放与归一化等预处理后送入模型推理输出预测数字及各类别置信度。在系统设计方面本文采用分层架构思想将用户交互、业务逻辑、模型计算与数据存储进行解耦在功能设计方面形成介绍、训练、识别、关于四大模块在实现方面重点解决了训练不阻塞界面、手写输入采集、上传图片自适应预处理、最优模型保存与结果概率可视化等问题。论文同步给出系统架构图、功能结构图、模块结构图、网络结构图、流程图、时序图、用例图等多类图示增强了设计表达的规范性与可读性。实验结果表明所设计的改进版 LeNet-5 模型在 MNIST 测试集上可取得较高识别准确率系统界面操作简便、流程清晰能够满足本科毕业设计对功能完整性、技术综合性与可演示性的要求。本文工作对理解卷积神经网络原理、掌握 PyTorch 训练流程以及运用 PyQt6 构建桌面应用具有较好的实践意义也可作为后续扩展多位数字识别、模型对比实验或 Web 化部署的基础平台。源码下载链接: https://pan.baidu.com/s/1EIy2zxFCAHBxNn89tPg8Uw?pwd1234提取码: 1234系统展示核心代码数字识别页面。 from io import BytesIO from pathlib import Path from PyQt6.QtCore import Qt, QByteArray from PyQt6.QtGui import QImage, QPixmap from PyQt6.QtWidgets import ( QFileDialog, QFrame, QGridLayout, QHBoxLayout, QLabel, QMessageBox, QPushButton, QVBoxLayout, QWidget, ) from PIL import Image from app.core.predictor import Predictor from app.widgets.draw_canvas import DrawCanvas class RecognizePage(QWidget): 数字识别页面支持手写画板与图片上传两种输入方式。 def __init__(self, project_root: Path, parentNone): 初始化识别页。 Args: project_root: 项目根目录 parent: 父控件 super().__init__(parent) self.project_root project_root self.model_path project_root / models / best_model.pth self.predictor Predictor(self.model_path) self._uploaded_image: Image.Image | None None self._init_ui() def _init_ui(self): 构建识别页界面。 layout QVBoxLayout(self) layout.setContentsMargins(32, 24, 32, 24) layout.setSpacing(16) title QLabel(数字识别) title.setObjectName(pageTitle) subtitle QLabel(在画板上手写数字或上传本地图片进行识别) subtitle.setObjectName(subtitle) layout.addWidget(title) layout.addWidget(subtitle) content_layout QHBoxLayout() content_layout.setSpacing(24) # 左侧输入区域 input_frame QFrame() input_frame.setObjectName(card) input_layout QVBoxLayout(input_frame) input_layout.setContentsMargins(20, 20, 20, 20) input_layout.setSpacing(12) input_title QLabel(输入区域) input_title.setStyleSheet(font-weight: bold; font-size: 15px;) input_layout.addWidget(input_title) # 手写画板 canvas_label QLabel(鼠标手写:) self.draw_canvas DrawCanvas(280, 280) input_layout.addWidget(canvas_label) input_layout.addWidget(self.draw_canvas, alignmentQt.AlignmentFlag.AlignCenter) canvas_btn_layout QHBoxLayout() self.clear_btn QPushButton(清空画板) self.clear_btn.setObjectName(secondaryButton) self.clear_btn.clicked.connect(self._clear_canvas) canvas_btn_layout.addWidget(self.clear_btn) input_layout.addLayout(canvas_btn_layout) # 分隔线 sep QFrame() sep.setFrameShape(QFrame.Shape.HLine) sep.setStyleSheet(color: #e2e8f0;) input_layout.addWidget(sep) # 上传图片 upload_label QLabel(或上传图片:) self.upload_preview QLabel(暂无图片) self.upload_preview.setFixedSize(140, 140) self.upload_preview.setAlignment(Qt.AlignmentFlag.AlignCenter) self.upload_preview.setStyleSheet( border: 1px dashed #cbd5e1; border-radius: 8px; background: #f8fafc; ) upload_btn_layout QHBoxLayout() self.upload_btn QPushButton(选择图片) self.upload_btn.setObjectName(secondaryButton) self.upload_btn.clicked.connect(self._upload_image) upload_btn_layout.addWidget(self.upload_btn) input_layout.addWidget(upload_label) input_layout.addWidget(self.upload_preview, alignmentQt.AlignmentFlag.AlignCenter) input_layout.addLayout(upload_btn_layout) # 识别按钮 self.recognize_btn QPushButton(开始识别) self.recognize_btn.setObjectName(primaryButton) self.recognize_btn.clicked.connect(self._recognize) input_layout.addWidget(self.recognize_btn) content_layout.addWidget(input_frame) # 右侧结果区域 result_frame QFrame() result_frame.setObjectName(card) result_layout QVBoxLayout(result_frame) result_layout.setContentsMargins(20, 20, 20, 20) result_layout.setSpacing(12) result_title QLabel(识别结果) result_title.setStyleSheet(font-weight: bold; font-size: 15px;) result_layout.addWidget(result_title) # 预测数字 self.digit_label QLabel(-) self.digit_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.digit_label.setStyleSheet( font-size: 96px; font-weight: bold; color: #2563eb; padding: 20px; ) result_layout.addWidget(self.digit_label) self.confidence_label QLabel(置信度: -) self.confidence_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.confidence_label.setStyleSheet(font-size: 16px; color: #64748b;) result_layout.addWidget(self.confidence_label) # 预处理后图像预览 pre_label QLabel(预处理后图像 (28×28):) self.preprocessed_preview QLabel() self.preprocessed_preview.setFixedSize(112, 112) self.preprocessed_preview.setAlignment(Qt.AlignmentFlag.AlignCenter) self.preprocessed_preview.setStyleSheet( border: 1px solid #e2e8f0; border-radius: 8px; background: #000000; ) result_layout.addWidget(pre_label) result_layout.addWidget( self.preprocessed_preview, alignmentQt.AlignmentFlag.AlignCenter ) # 概率条形图 prob_title QLabel(各类别概率:) prob_title.setStyleSheet(font-weight: bold; margin-top: 8px;) result_layout.addWidget(prob_title) self.prob_grid QGridLayout() self.prob_grid.setSpacing(4) self.prob_bars: list[QLabel] [] self.prob_labels: list[QLabel] [] for i in range(10): digit_lbl QLabel(str(i)) digit_lbl.setFixedWidth(20) digit_lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) bar_lbl QLabel() bar_lbl.setFixedHeight(18) bar_lbl.setStyleSheet(background: #e2e8f0; border-radius: 4px;) prob_lbl QLabel(0.0%) prob_lbl.setFixedWidth(55) prob_lbl.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.prob_grid.addWidget(digit_lbl, i, 0) self.prob_grid.addWidget(bar_lbl, i, 1) self.prob_grid.addWidget(prob_lbl, i, 2) self.prob_bars.append(bar_lbl) self.prob_labels.append(prob_lbl) result_layout.addLayout(self.prob_grid) result_layout.addStretch() content_layout.addWidget(result_frame, stretch1) layout.addLayout(content_layout) self._update_model_status() def _update_model_status(self): 更新模型加载状态提示。 if self.predictor.is_loaded: self.recognize_btn.setEnabled(True) self.recognize_btn.setText(开始识别) else: self.recognize_btn.setEnabled(False) self.recognize_btn.setText(请先训练模型) def refresh_model(self): 刷新模型训练完成后调用。 self.predictor.load_model(self.model_path) self._update_model_status() def _clear_canvas(self): 清空画板。 self.draw_canvas.clear() self._uploaded_image None self.upload_preview.setText(暂无图片) self.upload_preview.setPixmap(QPixmap()) def _upload_image(self): 上传本地图片。 file_path, _ QFileDialog.getOpenFileName( self, 选择图片, , 图片文件 (*.png *.jpg *.jpeg *.bmp *.gif), ) if file_path: self._uploaded_image Image.open(file_path) pixmap QPixmap(file_path).scaled( 130, 130, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation, ) self.upload_preview.setPixmap(pixmap) self.upload_preview.setText() def _recognize(self): 执行数字识别。 if not self.predictor.is_loaded: QMessageBox.warning(self, 提示, 模型未加载请先在「模型训练」页面完成训练。) return # 优先使用上传图片否则使用画板内容 if self._uploaded_image is not None: input_image self._uploaded_image elif self.draw_canvas.has_content(): input_image self.draw_canvas.to_pil_image() else: QMessageBox.information(self, 提示, 请先在画板上手写数字或上传一张图片。) return try: digit, probs, processed self.predictor.predict(input_image) confidence probs[digit] * 100 self.digit_label.setText(str(digit)) self.confidence_label.setText(f置信度: {confidence:.1f}%) # 显示预处理后图像 buffer BytesIO() processed.save(buffer, formatPNG) qimage QImage.fromData(QByteArray(buffer.getvalue())) preview_pixmap QPixmap.fromImage(qimage).scaled( 100, 100, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation, ) self.preprocessed_preview.setPixmap(preview_pixmap) # 更新概率条 max_prob max(probs) if probs else 1.0 for i, (bar, lbl) in enumerate(zip(self.prob_bars, self.prob_labels)): ratio probs[i] / max_prob if max_prob 0 else 0 width max(1, int(ratio * 200)) color #2563eb if i digit else #94a3b8 bar.setStyleSheet( fbackground: {color}; border-radius: 4px; min-width: {width}px; ) bar.setFixedWidth(width) lbl.setText(f{probs[i]*100:.1f}%) except Exception as exc: QMessageBox.critical(self, 识别失败, f识别过程出错: {exc})基于PyTorch的手写数字识别系统 - 程序入口。 import sys from pathlib import Path from PyQt6.QtWidgets import QApplication from app.main_window import MainWindow def main(): 启动应用程序。 project_root Path(__file__).resolve().parent (project_root / data).mkdir(exist_okTrue) (project_root / models).mkdir(exist_okTrue) (project_root / assets).mkdir(exist_okTrue) app QApplication(sys.argv) app.setApplicationName(基于PyTorch的手写数字识别系统) window MainWindow(project_root) window.show() sys.exit(app.exec()) if __name__ __main__: main()