手把手运行视频链接
视频链接: 目标深度学习
整个项目下载在文章末尾处,运行效果:
文章目录
前言
手写数字识别是经典的 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模型结构搭建
- 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')) # 激活函数
2.4 cnn模型训练并保存模型
- 优化器选择 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)
最后整个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
配置环境也很简单看我发的视频就行,有问题请留言,创作不易,对你有帮助点个赞赞。