手把手教你完成深度学习的手写数字识别系统pyqt界面

手把手运行视频链接

视频链接: 目标深度学习

整个项目下载在文章末尾处,运行效果:
请添加图片描述



前言

手写数字识别是经典的 CNN 分类应用之一,常用的数据集就是 MNIST 手写数字数据集,包含 0~9 这 10 个数字的手写图片。本次实验采用CNN进行分类识别,为了便于对不同的网络进行实验分析,选择自定义CNN模型


一、目录结构

项目目录结构如下:
在这里插入图片描述

二、训练模型步骤

2.1 数据集构建

在train.py文件中构建手写数字识别数据集,主要代码如下:

# 直接联网加载mnist数据集,也可以先下载好,然后再导入
mnist = keras.datasets.mnist
(x_train,y_train),(x_test,y_test) = mnist.load_data()

2.2 数据归一化

对训练集和测试集进行归一化,代码如下:

X_train4D = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')
X_test4D = x_test.reshape(x_test.shape[0], 28, 28, 1).astype('float32')

X_train4D_Normalize = X_train4D / 255 # 归一化
X_test4D_Normalize = X_test4D / 255

y_trainOnehot = to_categorical(y_train)
y_testOnehot = to_categorical(y_test)

2.3 cnn模型结构搭建

  1. Sequential 用于建立序列模型
  2. Flatten 层用于展开张量,input_shape 定义输入形状为 28x28
    的图像,展开后为 28*28 的张量。
  3. Dense 层为全连接层,输出有 128 个神经元,激活函数使用 relu。
  4. Dropout层使用 0.25 的失活率。
  5. 再接一个全连接层,激活函数使用 softmax,得到对各个类别预测的概率。
    代码如下:
model = Sequential()

# 一层卷积
model.add(
    Conv2D(
        filters=16,
        kernel_size=(5, 5),
        padding='same',  # 保证卷积核大小,不够补零
        input_shape=(28, 28, 1),
        activation='relu'))
# 池化层1
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

# 二层卷积
model.add(
    Conv2D(filters=32, kernel_size=(5, 5), padding='same', activation='relu'))
# 池化层2
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(
    Conv2D(filters=64, kernel_size=(5, 5), padding='same', activation='relu'))
model.add(
    Conv2D(filters=128, kernel_size=(5, 5), padding='same', activation='relu'))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())  # 平坦层
model.add(Dense(128, activation='relu'))  # 全连接层
model.add(Dropout(0.25))
model.add(Dense(10, activation='softmax')) # 激活函数

2.4 cnn模型训练并保存模型

  1. 优化器选择 Adam 优化器。
  2. 损失函数使用 categorical_crossentropy,
  3. sparse_categorical 输入的是整形的标签,例如 [1, 2, 3, 4],categorical 输入的是 one-hot 编码的标签。
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
train_history = model.fit(x=X_train4D_Normalize,
                          y=y_trainOnehot,
                          validation_split=0.2,
                          batch_size=300,
                          epochs=11,
                          verbose=2)


# fit 用于训练模型,对训练数据遍历一次为一个 epoch,这里遍历 10 次。
# evaluate 用于评估模型,返回的数值分别是损失和指标。
# model.fit(x_train,y_train,epochs=10)
# 将整个模型保存为HDF5文件
model.save('./model/model.h5')
model.evaluate(X_test4D_Normalize, y_testOnehot)

最后整个train.py代码如下,配置完环境直接运行以下代码就可以训练模型了,操作简单:

import tensorflow.python.keras as keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPool2D
from keras.utils import to_categorical


# 直接联网加载mnist数据集,也可以先下载好,然后再导入
mnist = keras.datasets.mnist
(x_train,y_train),(x_test,y_test) = mnist.load_data()


X_train4D = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')
X_test4D = x_test.reshape(x_test.shape[0], 28, 28, 1).astype('float32')

X_train4D_Normalize = X_train4D / 255 # 归一化
X_test4D_Normalize = X_test4D / 255

y_trainOnehot = to_categorical(y_train)
y_testOnehot = to_categorical(y_test)
# Sequential 用于建立序列模型
# Flatten 层用于展开张量,input_shape 定义输入形状为 28x28 的图像,展开后为 28*28 的张量。
# Dense 层为全连接层,输出有 128 个神经元,激活函数使用 relu。
# Dropout 层使用 0.25 的失活率。
# 再接一个全连接层,激活函数使用 softmax,得到对各个类别预测的概率。

model = Sequential()

# 一层卷积
model.add(
    Conv2D(
        filters=16,
        kernel_size=(5, 5),
        padding='same',  # 保证卷积核大小,不够补零
        input_shape=(28, 28, 1),
        activation='relu'))
# 池化层1
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

# 二层卷积
model.add(
    Conv2D(filters=32, kernel_size=(5, 5), padding='same', activation='relu'))
# 池化层2
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(
    Conv2D(filters=64, kernel_size=(5, 5), padding='same', activation='relu'))
model.add(
    Conv2D(filters=128, kernel_size=(5, 5), padding='same', activation='relu'))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())  # 平坦层
model.add(Dense(128, activation='relu'))  # 全连接层
model.add(Dropout(0.25))
model.add(Dense(10, activation='softmax')) # 激活函数

# 优化器选择 Adam 优化器。
# 损失函数使用 categorical_crossentropy,
# sparse_categorical 输入的是整形的标签,例如 [1, 2, 3, 4],categorical 输入的是 one-hot 编码的标签。

# 训练模型
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
train_history = model.fit(x=X_train4D_Normalize,
                          y=y_trainOnehot,
                          validation_split=0.2,
                          batch_size=300,
                          epochs=11,
                          verbose=2)


# fit 用于训练模型,对训练数据遍历一次为一个 epoch,这里遍历 10 次。
# evaluate 用于评估模型,返回的数值分别是损失和指标。
# model.fit(x_train,y_train,epochs=10)
# 将整个模型保存为HDF5文件
model.save('./model/model.h5')
model.evaluate(X_test4D_Normalize, y_testOnehot)

三、界面可视化

3.1 画笔功能实现

画图参考链接: 画笔参考链接

Paintboard.py代码如下:

import tensorflow.python.keras as keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPool2D
from keras.utils import to_categorical


# 直接联网加载mnist数据集,也可以先下载好,然后再导入
mnist = keras.datasets.mnist
(x_train,y_train),(x_test,y_test) = mnist.load_data()


X_train4D = x_train.reshape(x_train.shape[0], 28, 28, 1).astype('float32')
X_test4D = x_test.reshape(x_test.shape[0], 28, 28, 1).astype('float32')

X_train4D_Normalize = X_train4D / 255 # 归一化
X_test4D_Normalize = X_test4D / 255

y_trainOnehot = to_categorical(y_train)
y_testOnehot = to_categorical(y_test)
# Sequential 用于建立序列模型
# Flatten 层用于展开张量,input_shape 定义输入形状为 28x28 的图像,展开后为 28*28 的张量。
# Dense 层为全连接层,输出有 128 个神经元,激活函数使用 relu。
# Dropout 层使用 0.25 的失活率。
# 再接一个全连接层,激活函数使用 softmax,得到对各个类别预测的概率。

model = Sequential()

# 一层卷积
model.add(
    Conv2D(
        filters=16,
        kernel_size=(5, 5),
        padding='same',  # 保证卷积核大小,不够补零
        input_shape=(28, 28, 1),
        activation='relu'))
# 池化层1
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

# 二层卷积
model.add(
    Conv2D(filters=32, kernel_size=(5, 5), padding='same', activation='relu'))
# 池化层2
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(
    Conv2D(filters=64, kernel_size=(5, 5), padding='same', activation='relu'))
model.add(
    Conv2D(filters=128, kernel_size=(5, 5), padding='same', activation='relu'))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

model.add(Flatten())  # 平坦层
model.add(Dense(128, activation='relu'))  # 全连接层
model.add(Dropout(0.25))
model.add(Dense(10, activation='softmax')) # 激活函数

# 优化器选择 Adam 优化器。
# 损失函数使用 categorical_crossentropy,
# sparse_categorical 输入的是整形的标签,例如 [1, 2, 3, 4],categorical 输入的是 one-hot 编码的标签。

# 训练模型
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
train_history = model.fit(x=X_train4D_Normalize,
                          y=y_trainOnehot,
                          validation_split=0.2,
                          batch_size=300,
                          epochs=11,
                          verbose=2)


# fit 用于训练模型,对训练数据遍历一次为一个 epoch,这里遍历 10 次。
# evaluate 用于评估模型,返回的数值分别是损失和指标。
# model.fit(x_train,y_train,epochs=10)
# 将整个模型保存为HDF5文件
model.save('./model/model.h5')
model.evaluate(X_test4D_Normalize, y_testOnehot)

3.2 识别功能实现

设计界面就不详细讲解了,需要学习pyqt界面搭建,实现信号与曹功能就行,通过调用训练好的模型进行预测,MainUI.py全部代码如下:

# -*- coding: utf-8 -*-

import sys  # 导入系统
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QPushButton,QLabel,QTextEdit,QFileDialog,QHBoxLayout,QVBoxLayout,QSplitter,QComboBox,QSpinBox
from PyQt5.Qt import QWidget, QColor,QPixmap,QIcon,QSize,QCheckBox
from PyQt5 import QtCore, QtGui
from Paintboard import PaintBoard
import tensorflow as tf
import numpy as np
import tensorflow.python.keras as keras

class FirstUi(QMainWindow):  # 第一个窗口类
    def __init__(self):
        super(FirstUi, self).__init__()
        self.init_ui()

    def init_ui(self):
        self.setFixedSize(self.width(), self.height())
        self.setWindowIcon(QtGui.QIcon('./UI/1.jpg'))
        self.resize(640, 500)  # 设置窗口大小
        self.setWindowTitle('基于深度学习的数字识别')  # 设置窗口标题
        self.btn = QPushButton('手写数字识别', self)  # 设置按钮和按钮名称
        self.btn.setGeometry(245, 100, 150, 50)  # 前面是按钮左上角坐标,后面是窗口大小
        self.btn.clicked.connect(self.slot_btn_function)  # 将信号连接到槽
        self.btn2 = QPushButton('数字图片识别', self)
        self.btn2.setGeometry(245, 200,150,50)
        self.btn2.clicked.connect(self.slot_btn2_function)
        self.btn_exit = QPushButton('退出', self)
        self.btn_exit.setGeometry(245, 300, 150, 50)
        self.btn_exit.clicked.connect(self.Quit)


    def Quit(self):
        self.close()

    def slot_btn_function(self):
        self.hide()  # 隐藏此窗口
        self.s = write_num()  # 将第二个窗口换个名字
        self.s.show()  # 经第二个窗口显示出来

    def slot_btn2_function(self):
        self.hide()  # 隐藏此窗口
        self.s = picture_num()
        self.s.show()


class write_num(QWidget):
    def __init__(self):
        super(write_num, self).__init__()
        self.__InitData()  # 先初始化数据,再初始化界面
        self.__InitView()

    def __InitData(self):
        '''
                  初始化成员变量
        '''
        self.__paintBoard = PaintBoard(self)
        # 获取颜色列表(字符串类型)
        self.__colorList = QColor.colorNames()

    def __InitView(self):
        '''
                  初始化界面
        '''
        self.setWindowIcon(QtGui.QIcon('./UI/lf.jpg'))
        self.resize(640, 600)
        self.setFixedSize(self.width(), self.height())
        self.setWindowTitle("手写数字识别")

        self.label_name = QLabel('结果显示:', self)
        self.label_name.setGeometry(500, 65, 100, 30)

        self.edit = QTextEdit(self)
        self.edit.setGeometry(510, 100, 110, 110)

        # 新建一个水平布局作为本窗体的主布局
        main_layout = QHBoxLayout(self)
        # 设置主布局内边距以及控件间距为10px
        main_layout.setSpacing(10)

        # 在主界面左侧放置画板
        main_layout.addWidget(self.__paintBoard)

        # 新建垂直子布局用于放置按键
        sub_layout = QVBoxLayout()

        # 设置此子布局和内部控件的间距为5px
        sub_layout.setContentsMargins(5, 5, 5, 5)

        splitter = QSplitter(self)  # 占位符
        sub_layout.addWidget(splitter)

        self.__btn_Recognize = QPushButton("开始识别")
        self.__btn_Recognize.setParent(self)
        self.__btn_Recognize.clicked.connect(self.on_btn_Recognize_Clicked)
        sub_layout.addWidget(self.__btn_Recognize)

        self.__btn_Clear = QPushButton("清空画板")
        self.__btn_Clear.setParent(self)  # 设置父对象为本界面
        # 将按键按下信号与画板清空函数相关联
        self.__btn_Clear.clicked.connect(self.__paintBoard.Clear)
        sub_layout.addWidget(self.__btn_Clear)

        self.__btn_return = QPushButton("返回")
        self.__btn_return.setParent(self)  # 设置父对象为本界面
        self.__btn_return.clicked.connect(self.slot_btn_function)
        sub_layout.addWidget(self.__btn_return)

        self.__btn_Quit = QPushButton("退出")
        self.__btn_Quit.setParent(self)  # 设置父对象为本界面
        self.__btn_Quit.clicked.connect(self.Quit)
        sub_layout.addWidget(self.__btn_Quit)

        self.__btn_Save = QPushButton("保存作品")
        self.__btn_Save.setParent(self)
        self.__btn_Save.clicked.connect(self.on_btn_Save_Clicked)
        sub_layout.addWidget(self.__btn_Save)

        self.__cbtn_Eraser = QCheckBox("使用橡皮擦")
        self.__cbtn_Eraser.setParent(self)
        self.__cbtn_Eraser.clicked.connect(self.on_cbtn_Eraser_clicked)
        sub_layout.addWidget(self.__cbtn_Eraser)

        self.__label_penThickness = QLabel(self)
        self.__label_penThickness.setText("画笔粗细")
        self.__label_penThickness.setFixedHeight(20)
        sub_layout.addWidget(self.__label_penThickness)

        self.__spinBox_penThickness = QSpinBox(self)
        self.__spinBox_penThickness.setMaximum(20)
        self.__spinBox_penThickness.setMinimum(2)
        self.__spinBox_penThickness.setValue(10)  # 默认粗细为10
        self.__spinBox_penThickness.setSingleStep(2)  # 最小变化值为2
        self.__spinBox_penThickness.valueChanged.connect(self.on_PenThicknessChange)  # 关联spinBox值变化信号和函数on_PenThicknessChange
        sub_layout.addWidget(self.__spinBox_penThickness)

        self.__label_penColor = QLabel(self)
        self.__label_penColor.setText("画笔颜色")
        self.__label_penColor.setFixedHeight(20)
        sub_layout.addWidget(self.__label_penColor)

        self.__comboBox_penColor = QComboBox(self)
        self.__fillColorList(self.__comboBox_penColor)  # 用各种颜色填充下拉列表
        self.__comboBox_penColor.currentIndexChanged.connect(self.on_PenColorChange)  # 关联下拉列表的当前索引变更信号与函数on_PenColorChange
        sub_layout.addWidget(self.__comboBox_penColor)

        main_layout.addLayout(sub_layout)  # 将子布局加入主布局

    def __fillColorList(self, comboBox):

        index_black = 0
        index = 0
        for color in self.__colorList:
            if color == "black":
                index_black = index
            index += 1
            pix = QPixmap(70, 20)
            pix.fill(QColor(color))
            comboBox.addItem(QIcon(pix), None)
            comboBox.setIconSize(QSize(70, 20))
            comboBox.setSizeAdjustPolicy(QComboBox.AdjustToContents)

        comboBox.setCurrentIndex(index_black)

    def on_PenColorChange(self):
        color_index = self.__comboBox_penColor.currentIndex()
        color_str = self.__colorList[color_index]
        self.__paintBoard.ChangePenColor(color_str)

    def on_PenThicknessChange(self):
        penThickness = self.__spinBox_penThickness.value()
        self.__paintBoard.ChangePenThickness(penThickness)

    def on_btn_Save_Clicked(self):
        savePath = QFileDialog.getSaveFileName(self, 'Save Your Paint', '.\\', '*.png')
        print(savePath)
        if savePath[0] == "":
            print("Save cancel")
            return
        image = self.__paintBoard.GetContentAsQImage()
        image.save(savePath[0])
        print(savePath[0])

    def Quit(self):
        self.close()

    def on_cbtn_Eraser_clicked(self):
        if self.__cbtn_Eraser.isChecked():
            self.__paintBoard.EraserMode = True  # 进入橡皮擦模式
        else:
            self.__paintBoard.EraserMode = False  # 退出橡皮擦模式

    def on_btn_Recognize_Clicked(self):
        config = tf.compat.v1.ConfigProto(allow_soft_placement=True)
        config.gpu_options.per_process_gpu_memory_fraction = 0.3
        tf.compat.v1.keras.backend.set_session(tf.compat.v1.Session(config=config))
        savePath = "./text/text.png"
        image = self.__paintBoard.GetContentAsQImage()
        image.save(savePath)
        print(savePath)
        # 加载图像
        img = keras.preprocessing.image.load_img(savePath, target_size=(28, 28))
        img = img.convert('L')
        x = keras.preprocessing.image.img_to_array(img)
        x = abs(255-x)
        #x = x.reshape(28,28)
        x = np.expand_dims(x, axis=0)
        x=x/255.0
        new_model = keras.models.load_model('./model/my_model.h5')
        prediction = new_model.predict(x)
        output = np.argmax(prediction, axis=1)
        #print("手写数字识别为:" + str(output[0]))
        self.edit.setText('识别的手写数字为:' + str(output[0]))

    def slot_btn_function(self):
        self.hide()  # 隐藏此窗口
        self.f = FirstUi()  # 将第一个窗口换个名字
        self.f.show()  # 将第一个窗口显示出来



class picture_num(QWidget):
    def __init__(self):
        super(picture_num, self).__init__()
        self.init_ui()

    def init_ui(self):
        self.setWindowIcon(QtGui.QIcon('./UI/lf.jpg'))
        self.resize(640,520)
        self.setFixedSize(self.width(), self.height())
        self.setWindowTitle('图片数字识别')

        self.label_name4 = QLabel('识别结果:', self)
        self.label_name4.setGeometry(510, 40, 100, 35)

        self.label_name5 = QLabel('待载入图片', self)
        self.label_name5.setGeometry(10, 20, 480, 480)
        self.label_name5.setStyleSheet("QLabel{background:gray;}"
                                 "QLabel{color:rgb(0,0,0,120);font-size:15px;font-weight:bold;font-family:宋体;}"
                                 )
        self.label_name5.setAlignment(QtCore.Qt.AlignCenter)
        
        self.edit = QTextEdit(self)
        self.edit.setGeometry(510, 80, 100, 100)

        self.btn_select = QPushButton('选择图片',self)
        self.btn_select.setGeometry(510, 220, 100, 30)
        self.btn_select.clicked.connect(self.select_image)

        self.btn_dis = QPushButton('识别图片',self)
        self.btn_dis.setGeometry(510, 285, 100, 30)
        self.btn_dis.clicked.connect(self.on_btn_Recognize_Clicked)

        self.btn = QPushButton('返回',self)
        self.btn.setGeometry(510, 350, 100, 30)
        self.btn.clicked.connect(self.slot_btn_function)

        self.btn_exit = QPushButton('退出',self)
        self.btn_exit.setGeometry(510, 420, 100, 30)
        self.btn_exit.clicked.connect(self.Quit)

    def select_image(self):
        global fname
        imgName, imgType = QFileDialog.getOpenFileName(self, "打开图片", "", "*.png;;*.jpg;;All Files(*)")
        jpg = QtGui.QPixmap(imgName).scaled(self.label_name5.width(), self.label_name5.height())
        self.label_name5.setPixmap(jpg)
        fname = imgName
        print(fname)

    def on_btn_Recognize_Clicked(self):
        global fname
        config = tf.compat.v1.ConfigProto(allow_soft_placement=True)
        config.gpu_options.per_process_gpu_memory_fraction = 0.3
        tf.compat.v1.keras.backend.set_session(tf.compat.v1.Session(config=config))
        savePath = fname
        # 加载图像
        img = keras.preprocessing.image.load_img(savePath, target_size=(28, 28))
        img = img.convert('L')
        x = keras.preprocessing.image.img_to_array(img)
        x = abs(255-x)
        #x = x.reshape(28,28)
        x = np.expand_dims(x, axis=0)
        x=x/255.0
        new_model = keras.models.load_model('./model/my_model.h5')
        prediction = new_model.predict(x)
        output = np.argmax(prediction, axis=1)
        self.edit.setText('识别的手写数字为:' + str(output[0]))
        
    def Quit(self):
        self.close()

    def slot_btn_function(self):
        self.hide()
        self.f = FirstUi()
        self.f.show()


def main():
    app = QApplication(sys.argv)
    w = FirstUi()  # 将第一和窗口换个名字
    w.show()  # 将第一和窗口换个名字显示出来
    sys.exit(app.exec_())  # app.exet_()是指程序一直循环运行直到主窗口被关闭终止进程(如果没有这句话,程序运行时会一闪而过)


if __name__ == '__main__':  # 只有在本py文件中才能用,被调用就不执行
    main()

总结

整个项目下载链接: 百度网盘链接
提取码是:8fq8

配置环境也很简单看我发的视频就行,有问题请留言,创作不易,对你有帮助点个赞赞。

  • 20
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挂科边缘(毕业版)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值