手写数字的分割和识别

前言

在机器学习领域,手写数字数据集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模式。
在这里插入图片描述

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 < min_vals and startPoint != None:
            endPoint = i

        if startPoint != None and endPoint != None:
            extrackPoints.append((startPoint, endPoint))
            startPoint = None
            endPoint = None

    # 剔除一些噪点
    for point in extrackPoints:
        if point[1] - point[0] < min_rect:
            extrackPoints.remove(point)
    return extrackPoints

# 寻找边缘,返回边框的左上角和右下角(利用直方图寻找边缘算法(需行对齐))
def findBorderHistogram(path):
    borders = []
    img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    img = accessBinary(img)
    # 行扫描
    hori_vals = np.sum(img, axis=1)
    hori_points = extractPeek(hori_vals)
    # 根据每一行来扫描列
    for hori_point in hori_points:
        extractImg = img[hori_point[0]:hori_point[1], :]
        vec_vals = np.sum(extractImg, axis=0)
        vec_points = extractPeek(vec_vals, min_rect=0)
        for vect_point in vec_points:
            border = [(vect_point[0], hori_point[0]), (vect_point[1], hori_point[1])]
            borders.append(border)
    return borders
    
# 显示结果及边框
def showResults(path, borders, results=None):
    img = cv2.imread(path)
    # 绘制
    print(img.shape)
    for i, border in enumerate(borders):
        cv2.rectangle(img, border[0], border[1], (0, 0, 255))
        if results:
            cv2.putText(img, str(results[i]), border[0], cv2.FONT_HERSHEY_COMPLEX, 0.8, (0, 255, 0), 1)
        #cv2.circle(img, border[0], 1, (0, 255, 0), 0)
    cv2.imshow('test', img)
    cv2.waitKey(0)
    
path = 'test1.png'
borders = findBorderHistogram(path)
showResults(path, borders)

在这里插入图片描述
当然以上的方法不单单是数字,汉字也是可以的(同样的代码我没做修改就有以下的效果,可以尝试修改下阈值,有兴趣自己去调吧,哈哈哈哈,逃~)
在这里插入图片描述

2、findContours模式

当然你可能会有这样的疑惑,如果写得不起的话这种是不是不太行了,明确告诉你,是的。如下所知同样的手写数字效果就不是很好,针对于行列式这种扫描方式的局限性我们来介绍第二种方法,基于opencv的一个寻找轮廓的方法
在这里插入图片描述
talk is cheap,show you code,其实没什么好说的,就是利用OpenCV里面的findContours函数将边缘找出来,然后通过boundingRect将边缘拟合成一个矩形输出边框的左上角和右下角,然后执行这个代码就可以得到以下的效果了

# 寻找边缘,返回边框的左上角和右下角(利用cv2.findContours)
def findBorderContours(path, maxArea=50):
    img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    img = accessBinary(img)
    _, contours, _ = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    borders = []
    for contour in contours:
        # 将边缘拟合成一个边框
        x, y, w, h = cv2.boundingRect(contour)
        if w*h > maxArea:
            border = [(x, y), (x+w, y+h)]
            borders.append(border)
    return borders
    
path = 'test2.jpg'
borders = findBorderContours(path)
print(borders)
showResults(path, borders)

在这里插入图片描述
同样也不局限与数字,汉字也是ok的,当然代码你要去调合适的阈值才可以,以下是我一行代码没改得出的结果
在这里插入图片描述

三、特征工程及模型预测

在做模型预测前,我想先给你看下一个例子

1936年,美国进行总统选举,竞选的是民主党的罗斯福和共和党的兰登,罗斯福是在任的总统.美国权威的《文学摘要》杂志社,为了预测总统候选人谁能当选,采用了大规模的模拟选举,他们以电话簿上的地址和俱乐部成员名单上的地址发出1000万封信,收到回信200万封,在调查史上,样本容量这么大是少见的,杂志社花费了大量的人力和物力,他们相信自己的调查统计结果,即兰登将以57%对43%的比例获胜,并大力进行宣传.最后选举结果却是罗斯福以62%对38%的巨大优势获胜,连任总统.这个调查使《文学摘要》杂志社威信扫地,不久只得关门停刊.

上面的例子说明了样本通俗化的重要性,在机器学习中也是这样的,我们收集训练的数据如果和实际使用的有些偏差,那么即使你在测试集或验证集上表现越好也是白搭,所以我们要将我们的手写数字的格式无限贴近MNIST这份数据集,遗憾的是我在网上找了很多,都找不出当初收集这份数据集的人是怎么格式化数据的,所以我只能通过自己的方法来转化我们的图片了。(我看了一些网上网友也讨论过用自己模型预测自己手写的,不像测试集那样准确,这个大部分也是因为你的转化图和MNIST数据集格式有偏差导致的
好了,通过上面两种方法,我们已经可以找出图中一个个的数字了,那么我们怎么转换为MNIST那种形式呢?如下所示,我们首先将边框转为28*28的正方形,因为背景是黑色的,我们可以通过边界填充的形式,将边界扩充成黑色即可,其中值得注意的是
MNIST那种数据集的格式是字符相对于居中的,我们得出的又是比较准的边框,所以为了和数据集相对一致,我们要上下填充一点像素

# 根据边框转换为MNIST格式
def transMNIST(path, borders, size=(28, 28)):
    imgData = np.zeros((len(borders), size[0], size[0], 1), dtype='uint8')
    img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    img = accessBinary(img)
    for i, border in enumerate(borders):
        borderImg = img[border[0][1]:border[1][1], border[0][0]:border[1][0]]
        # 根据最大边缘拓展像素
        extendPiexl = (max(borderImg.shape) - min(borderImg.shape)) // 2
        targetImg = cv2.copyMakeBorder(borderImg, 7, 7, extendPiexl + 7, extendPiexl + 7, cv2.BORDER_CONSTANT)
        targetImg = cv2.resize(targetImg, size)
        targetImg = np.expand_dims(targetImg, axis=-1)
        imgData[i] = targetImg
    return imgData

path = 'test2.jpg'
borders = findBorderContours(path)
imgData = transMNIST(path, borders)
for i, img in enumerate(imgData):
    # cv2.imshow('test', img)
    # cv2.waitKey(0)
    name = 'extract/test_' + str(i) + '.jpg'
    cv2.imwrite(name, img)

在这里插入图片描述在这里插入图片描述
接下来就是导入我们第一节写好的预测模型,用它来预测模型即可。如下示,我们将结果也写在图示上来直观的表示这个预测模型(细心的你一定发现了其中一个数字预测错了,其中一个9预测1,这个你可以再慢慢微调,到底是重定义我们模型、混合一些我们自己的数据作为训练集、加一些特征转化或者更改我们的预测模型的格式,我就不做啦,哈哈哈,逃~)

# 预测手写数字
def predict(modelpath, imgData):
    from keras import models
    my_mnist_model = models.load_model(modelpath)
    print(my_mnist_model.summary())
    img = imgData.astype('float32') / 255
    results = my_mnist_model.predict(img)
    result_number = []
    for result in results:
        result_number.append(np.argmax(result))
    return result_number

path = 'test2.jpg'
model = 'my_mnist_model.h5'
borders = findBorderContours(path)
imgData = transMNIST(path, borders)
results = predict(model, imgData)
showResults(path, borders, results)

在这里插入图片描述

总结

以上就是我们这篇博客分享的内容,更加详细的代码可以查看我的github https://github.com/Wangzg123/HandwrittenDigitRecognition ,其中用到分割、介绍了两种字符分割的方法,当然现在也许你有更先进的方法,比如yolo,但是那种计算代价肯定比我这种方法高,杀鸡不用牛刀,还是要看下你的案例再用贴切的方法去解决。然后基于keras训练一个模型来预测手写数字,最后也简要谈了预测模型的特征工程,预测数据的格式一定要贴合训练的数据,不然会照成很大的误差。这就是通过机器学习来系统性的解决问题的思路或方法了。

### 回答1: 手写数字识别是机器学习中常见的一个应用场景,可以用于识别手写数字的图像。在Python中,可以使用scikit-learn和TensorFlow等工具库来实现手写数字识别。 以下是一个基于scikit-learn的手写数字识别的示例代码: ``` python from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split from sklearn.neural_network import MLPClassifier from sklearn.metrics import accuracy_score # 加载手写数字数据集 digits = load_digits() # 数据集拆分为训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, test_size=0.3) # 定义多层感知器分类器 clf = MLPClassifier(hidden_layer_sizes=(100, 50), max_iter=500) # 训练分类器 clf.fit(X_train, y_train) # 预测测试集的结果 y_pred = clf.predict(X_test) # 计算准确率 accuracy = accuracy_score(y_test, y_pred) print("Accuracy:", accuracy) ``` 这里使用了多层感知器(MLP)分类器,可以通过调整参数来优化识别效果。另外,还可以使用卷积神经网络(CNN)等深度学习模型来实现更加准确的手写数字识别。 ### 回答2: 手写数字识别是指通过机器学习算法,使用Python代码来识别手写数字。下面是一个简单的步骤: 1. 数据准备:首先,我们需要准备一个手写数字的数据集。常用的数据集是MNIST(Modified National Institute of Standards and Technology database),里面包含了大量的手写数字图像和对应的标签。我们可以使用Python中的库来下载和加载这个数据集。 2. 数据预处理:将图像数据转换为算法可以处理的向量形式。通常,我们将图像像素值进行标准化处理,将其缩放到0到1的范围内。 3. 特征提取:从图像中提取特征,用于训练模型和预测。常见的特征提取方法是将图像分割为小的图块,并计算每个图块中像素的统计特征,如平均值和方差。 4. 模型训练:选择一个合适的机器学习算法来训练模型。常用的算法包括k最近邻算法、支持向量机、决策树和神经网络等。我们可以使用Python中的机器学习库(如scikit-learn或TensorFlow)来实现这些算法。 5. 模型评估:使用测试集来评估模型的性能。常用的评估指标包括准确率、精确率、召回率和F1值等。 6. 模型优化:根据评估结果,对模型进行调优,如调整算法的参数、增加训练数据量等。 7. 模型应用:使用训练好的模型来预测新的手写数字图像。我们可以通过提取图像特征,然后输入到模型中,得到预测结果。 通过以上步骤,我们可以编写Python代码来实现手写数字识别。这是一个极为简单的示例,更复杂的手写数字识别模型会使用更高级的算法和技术来提高识别准确率。 ### 回答3: 手写数字识别是指通过计算机程序识别手写数字字符。Python是一种广泛应用于机器学习和图像处理领域的编程语言,可以利用Python来实现手写数字识别。 实现手写数字识别的一种方法是使用机器学习算法,其中最常用的是卷积神经网络(Convolutional Neural Network, CNN)。Python中有一些优秀的开源机器学习库,如TensorFlow、Keras和PyTorch,可以用来构建和训练CNN模型。 首先,我们需要一个包含了大量手写数字数据集的训练集。常用的数据集有MNIST和Fashion-MNIST,可以通过相应库来加载这些数据集。然后,我们需要对数据进行预处理,包括归一化处理和将图像转换为灰度图像等。 接着,我们可以定义并构建一个CNN模型。模型的结构包括卷积层、池化层和全连接层等。我们可以使用Keras或者PyTorch来定义和训练模型。训练过程包括将训练集输入模型进行迭代优化,使其能够准确地分类手写数字。 完成模型训练后,我们可以使用测试集来评估模型的性能。评估结果包括准确率和损失值等指标,用于衡量模型的性能。 最后,我们可以使用经过训练的模型来对新的手写数字进行识别。将手写数字输入模型,模型会输出对应的数字结果。 总结而言,通过使用Python编程语言,结合机器学习算法和相应的开源库,我们可以实现手写数字识别。这个过程涉及到数据准备、模型构建、模型训练和模型应用等步骤。希望这个简单的回答对你有所帮助!
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值