摘要

手写数字识别是模式识别中一个非常重要和活跃的研究领域,数字识别也不是一项孤立的技术,他涉及的问题是模式识别的其他领域都无法回避的;应用上,作为一种信息处理手段,字符识别有广阔的应用背景和巨大的市场需求。因此,对数字识别的研究具有理论和应用的双重意义。

人工神经网络识别方法是近年该研究领域的一种新方法,该方法具有一些传统技术所没有的优点:良好的容错能力、分类能力强、并行处理和自学习能力,并且是离线训练和在线识别的。这些优点使它在手写体字符的识别中能对大量数据进行快速实时处理,并达到良好的识别效果。

由于手写数字识别难于建立精确的数学模型,所以本文使用Python基于TensorFlow 卷积神经网络设计手写数字识别算法,并编程实现GUI 界面,构建手写数字识别系统。本系统界面设计友好,功能完善。通过测试,本识别系统对于较规范的手写体数字的识别达到了很好的识别效果。

下载mnist数据集

http://yann.lecun.com/exdb/mnist/

安装必要的库

pip install PyQt5
pip install PyQt5 -sip
pip install PyQt5 -tools
pip install numpy
pip install tensorflow

训练模型

keras训练模型,并保存模型,该模型识别正确率99%

# -*- coding: utf-8 -*-
"""
Created on Thu Dec  5 10:39:19 2019

@author: dell
"""

import tensorflow as tf
try:
    import tensorflow.python.keras as keras
except:
    import tensorflow.keras as keras
from tensorflow.python.keras import layers

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

mnist = keras.datasets.mnist
(x_train,y_train),(x_test,y_test) = mnist.load_data()
#x_train, x_test = x_train/255.0, x_test/255.0  # 除以 255 是为了归一化。

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.2 的失活率。
# 再接一个全连接层,激活函数使用 softmax,得到对各个类别预测的概率。
#model = keras.Sequential()
#model.add(layers.Flatten(input_shape=(28,28)))
#model.add(layers.Dense(128,activation="relu"))
#model.add(layers.Dropout(0.2))
#model.add(layers.Dense(10,activation="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 优化器。
# 损失函数使用 sparse_categorical_crossentropy,
# 还有一个损失函数是 categorical_crossentropy,两者的区别在于输入的真实标签的形式,
# sparse_categorical 输入的是整形的标签,例如 [1, 2, 3, 4],categorical 输入的是 one-hot 编码的标签。
#model.compile(optimizer="adam",
 #             loss="sparse_categorical_crossentropy",
  #            metrics=['accuracy'])

# 训练模型
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=10,
                          verbose=2)


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

训练模型后续将直接上传此博客,直接供大家使用!

手写数字识别

from PyQt5.Qt import QWidget, QColor, QPixmap, QIcon, QSize, QCheckBox
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QPushButton, QSplitter,\
    QComboBox, QLabel, QSpinBox, QFileDialog
from PaintBoard import PaintBoard

import tensorflow as tf
import numpy as np
try:
    import tensorflow.python.keras as keras
except:
    import tensorflow.keras as keras
    
class MainWidget(QWidget):


    def __init__(self, Parent=None):
        '''
        Constructor
        '''
        super().__init__(Parent)
        
        self.__InitData() #先初始化数据,再初始化界面
        self.__InitView()
    
    def __InitData(self):
        '''
                  初始化成员变量
        '''
        self.__paintBoard = PaintBoard(self)
        #获取颜色列表(字符串类型)
        self.__colorList = QColor.colorNames() 
        
    def __InitView(self):
        '''
                  初始化界面
        '''
        self.setFixedSize(640,480)
        self.setWindowTitle("手写数字识别")
        
        self.label_name = QLabel('XXXX大学', self)
        self.label_name.setGeometry(500,5,120,35)
        
        self.label_name = QLabel('电信院', self)
        self.label_name.setGeometry(500,35,100,35)

        self.label_name = QLabel('班级', self)
        self.label_name.setGeometry(500,65,100,35)

        self.label_name = QLabel('姓名', self)
        self.label_name.setGeometry(500,95,100,35)
        
        #新建一个水平布局作为本窗体的主布局
        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_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 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):
        
        savePath = "E:/wu/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('E:/wu/my_model.h5')
        prediction = new_model.predict(x)
        output = np.argmax(prediction, axis=1)
        print("手写数字识别为:" + str(output[0]))   
        
    def Quit(self):
        self.close()

主函数

from MainWidget import MainWidget
from PyQt5.QtWidgets import QApplication

import sys

def main():
    app = QApplication(sys.argv) 
    
    mainWidget = MainWidget() #新建一个主界面
    mainWidget.show()    #显示主界面
    
    exit(app.exec_()) #进入消息循环
    
    
if __name__ == '__main__':
    main()

运行界面

在这里插入图片描述

改变画笔颜色

在这里插入图片描述

保存手写数字图片

在这里插入图片描述
手写板程序参考 https://blog.csdn.net/creatorgg/article/details/81542837
感谢前辈的分享

后续将继续剖析程序细节,分章节讲解程序。

源程序也将上传至此博客

完整工程文件link

大家对程序有什么疑问或建议均可留言,感谢大家支持!!!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐