前言
在机器学习领域,手写数字数据集MNIST之于机器学习几乎相当于HelloWorld之于编程语言,其重要地位不言而已。但是,然后呢?给你一张如下所示的图片,你的模型能否也预测出结果?(其实下面这个应用就是OCR领域的内容了,另详细的代码内容和注释可以参考我的github https://github.com/Wangzg123/HandwrittenDigitRecognition ) 这篇博客我想从一个工程的角度谈谈手写数字识别的应用,期间将涉及到
① CV (computer vision)方面的知识② 用Keras编写及导出预测手写数字的模型③ 手写字符的分割(提供两个解决思路)④ 特征工程(将自己的手写数字转换为MNIST数字集的模式)⑤ 用我们编写的模型预测出结果并输出(如上所示的效果)
一、MNIST手写数字预测模型
为了要识别我们自己手写的数字,那么我们就要用到手写数字数据集MNIST。MNIST在网上有很多介绍和下载方式,详细我就不过多说明了,在Keras(一个基于TensorFlow的高级api,在今年谷歌大会上TensorFlow 2.0已经将keras作为官方高级api了)下已经内置了MNIST,我们可以通过下面的代码很轻松的导入
from keras.datasets import mnist
(train_data, train_labels), (test_data, test_labels) = mnist.load_data()
print('train_shape {} {}'.format(train_data.shape,train_labels.shape))
print('test_shape {} {}'.format(test_data.shape,test_labels.shape))
'''
output:
train_shape (60000, 28, 28) (60000,)
test_shape (10000, 28, 28) (10000,)
'''
我们挑选一下其中的一个字符来看下,他是一个28*28的灰度图,但是要注意和现实中我们拍出来的照片不一样的是字体是白色的背景是黑色的,那么就意味着在预测我们自己的模型时也必须转换为 黑色背景、白色数字和相对居中的图 不然的话和数据集相差太大会有很大的误差,详细说明见 第三部分——特征工程。 接下来做的事情就很简单了,定义一个keras的model,然后将0-255的灰度值转为0-1之间的值(均一化处理),标签数据转为onehot形式,然后通过fit就可以训练我们的模型了
from keras import models
from keras import layers
import numpy as np
from keras.utils.np_utils import to_categorical
def model_conv():
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu',input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])
return model
# 数据预处理
x_train = train_data.reshape((60000, 28, 28, 1))
x_train = x_train.astype('float32')/255
x_test = test_data.reshape((10000, 28, 28, 1))
x_test = x_test.astype('float32')/255
y_train = to_categorical(train_labels)
y_test = to_categorical(test_labels)
print(x_train.shape, y_train.shape)
# 定义模型
model = model_conv()
model.summary()
his = model.fit(x_train, y_train, epochs=5, batch_size=64, validation_split=0.1)
# 看下测试集的损失值和准确率
loss, acc = model.evaluate(x_test, y_test)
print('loss {}, acc {}'.format(loss, acc))
model.save("my_mnist_model.h5")
'''
output:
(60000, 28, 28, 1) (60000, 10)
loss 0.02437469101352144, acc 0.9927
测试集结果是99.27%,非常不错的模型
'''
二、字符分割
我们知道我们预测的模型是一个一个的数字,那么给下面的图示我们怎么转为MNIST那种图像的表示方式。这就要用到我们的字符分割的方法了,这里面我提供两种方法,一种是行列扫描分割,一种是opencv里面的findContours模式。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190408102811669.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxODk5MzE3NA==,size_16,color_FFFFFF,t_70)
1、行列扫描分割
平时我们写字,通常都是一行一行的,不会出现那种很不起的情况(不齐也可以,见第二种方法)。那么这个时候就可以用到我们的行列扫描法(我自己命名的,我也不知道叫什么),首先我们把上面的图片通过以下代码 反相处理
# 反相灰度图,将黑白阈值颠倒
def accessPiexl(img):
height = img.shape[0]
width = img.shape[1]
for i in range(height):
for j in range(width):
img[i][j] = 255 - img[i][j]
return img
# 反相二值化图像
def accessBinary(img, threshold=128):
img = accessPiexl(img)
# 边缘膨胀,不加也可以
kernel = np.ones((3, 3), np.uint8)
img = cv2.dilate(img, kernel, iterations=1)
_, img = cv2.threshold(img, threshold, 0, cv2.THRESH_TOZERO)
return img
path = 'test1.png'
img = cv2.imread(path, 0)
img = accessBinary(img)
cv2.imshow('accessBinary', img)
cv2.waitKey(0)
接下来我们就要进行行列扫描了,首先了解一个概念,黑色背景的像素是0,白色(其实是灰度图是1-255的)是非0,那么从行开始,我们计算将每一行的像素值加起来,如果都是黑色的那么和为0(当然可能有噪点,我们可以设置个阈值将噪点过滤),有字体的行就非0,依次类推,我们再根据这个图来筛选边界就可以得出行边界值,直观的绘制成图像就是如下所示 有了上面的概念,我们就可以通过行列扫描,根据0 , 非0,非0 … 非0,0这样的规律来确定行列所在的点来找出数字的边框了
# 根据长向量找出顶点
def extractPeek(array_vals, min_vals=10, min_rect=20):
extrackPoints = []
startPoint = None
endPoint = None
for i, point in enumerate(array_vals):
if point > min_vals and startPoint == None:
startPoint = i
elif point |