参考:
1、http://blog.csdn.net/xingchenbingbuyu/article/details/70208199
2、https://github.com/LiuXiaolong19920720/opencv-soduko
首先需要说明,这里所说的数字识别不是手写数字识别!
但凡对机器学习有所了解的人,相信看到数字识别的第一反应就是MNIST。MNIST是可以进行数字识别,但是那是手写数字。我们现在要做的是要识别从九宫格图片中提取出来的印刷体的数字。手写数字集训练出来的模型用来识别印刷体数字,显然不太专业。而且手写体跟印刷体相差不小,我们最看重的正确率问题不能保证。
本文从零开始做一遍数字识别,展示了数字识别的完整流程。从收集数据开始,到数据预处理,再到训练KNN,最后进行数字识别。
我们一步一步来说。
数据收集
为了便于处理,我百度找到了10张下面这样按照1-9-0顺序排列的图片,作为我们的初始数据集。
有的图片可能本来除数字区域外,周围空白部分比较多。为了便于处理,首先用windows自带的画图软件把图片裁剪成上面这样只包含数字区域的样子。
这十张数据集基本涵盖了印刷数字体的不同样式、字体,而且颜色、背景甚至渐变方式都各不相同。
数据处理
第一步要做的就是上一节的内容,那就是把图片中的数字分别提取出来。
训练knn,还有其他任何有监督的机器学习模型,不光要有样本数据,还要有知道每一个样本对应的标签。这也是为什么我要选择上面这样按顺序排列的数字图片。
提取数字之后,我们可以对每一个数字的位置进行排序,然后根据位置信息可以知道每一个数字是几。标签也就由此生成了。
这一部分的内容可以分两部分来说,第一部分就是提取数字,第二部分是提取数字之后的数据预处理。
1.提取数字
1.遍历文件夹下的原始数字图片;
2.对每一张图片进行轮廓提取操作,只提取外围轮廓(参考上一节讲解);
img_path = gb.glob("numbers\\*")
k = 0
labels = []
samples = []
for path in img_path:
img = cv2.imread(path)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray,(5,5),0)
thresh = cv2.adaptiveThreshold(blur,255,1,1,11,2)
image, contours, hierarchy = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) # 只检测上下左右关系的轮廓
3.求轮廓外包矩形,并根据矩形大小信息筛选出所有的数字轮廓;
4.然后根据位置信息对数字框排序,显然第一排依次是12345,第二排依次是67890;
height,width = img.shape[:2]
w = width/5
rect_list = []
list1 = []
list2 = []
for cnt in contours:
#if cv2.contourArea(cnt)>100:
[x,y,w,h] = cv2.boundingRect(cnt)
if w>30 and h > (height/4):
if y < (height/2):
list1.append([x,y,w,h])
else:
list2.append([x,y,w,h])
list1_sorted = sorted(list1,key = lambda t : t[0])
list2_sorted = sorted(list2,key = lambda t : t[0])
5.提取出每一个数字所在的矩形框,作为ROI取出。
for i in range(5):
[x1,y1,w1,h1] = list1_sorted[i]
[x2,y2,w2,h2] = list2_sorted[i]
number_roi1 = gray[y1:y1+h1, x1:x1+w1] #Cut the frame to size
number_roi2 = gray[y2:y2+h2, x2:x2+w2] #Cut the frame to size
数据预处理
为了加快训练速度,我们不用原图作为输入,而是对每一个数字原图做一定的处理。此处可选方案很多,提取特征有很多经典特征可选,也可以是自己设计的特征。
这里我用比较简单的方法,把每一张数字图片ROI转换为二值图像。大致流程是这样的:
1.把每一张ROI大小统一变换为40 x 20。
2.阈值分割。
resized_roi1=cv2.resize(number_roi1,(20,40))
thresh1 = cv2.adaptiveThreshold(resized_roi1,255,1,1,11,2)
resized_roi2=cv2.resize(number_roi2,(20,40))
thresh2 = cv2.adaptiveThreshold(resized_roi2,255,1,1,11,2)
3.把二值图像转换为0-1二值图像。
4.把处理完的数字图片保存到对应数字的文件夹中。(此为中间过程,可注释掉)
number_path1 = "number\\%s\\%d" % (str(i+1),k) + '.jpg'
j = i+6
if j ==10:
j = 0
number_path2 = "number\\%s\\%d" % (str(j),k) + '.jpg'
k+=1
normalized_roi1 = thresh1/255.
normalized_roi2 = thresh2/255.
cv2.imwrite(number_path1,thresh1)
cv2.imwrite(number_path2,thresh2)
处理完之后保存的文件夹如下:
每一个文件夹里面类似这样,可以看到背景有黑有白,数字也是有黑有白:
5.把处理完的二值图像展开成一行。
6.最后把展开成的一行行样本保存起来作为训练用的数据。
7.对应的,把数字标签按照数字的保存顺序对应保存成训练用的数据。
sample1 = normalized_roi1.reshape((1,800))
samples.append(sample1[0])
labels.append(float(i+1))
sample2 = normalized_roi2.reshape((1,800))
samples.append(sample2[0])
labels.append(float(j))
import numpy as np
samples = np.array(samples,np.float32)
labels = np.array(labels,np.float32)
labels = labels.reshape((labels.size,1))
np.save('samples.npy',samples)
np.save('label.npy',labels)
训练kNN识别数字
这里用opencv自带的knn算法实现。我同时尝试了opencv自带的神经网络和SVM,发现还是kNN的效果最好。有兴趣的可以自己去尝试一下。也可能是我参数没调好。
这里的流程是:
1.加载上面保存的样本和标签数据;
2.分别用80个作为训练数据,20个作为测试数据;
3.用opencv自带的knn训练模型;
4.用训练好的模型识别测试数据中的数字;
5.输出预测值和实际标签值。
import numpy as np
import cv2
samples = np.load('samples.npy')
labels = np.load('label.npy')
k = 80
train_label = labels[:k]
train_input = samples[:k]
test_input = samples[k:]
test_label = labels[k:]
model = cv2.ml.KNearest_create()
model.train(train_input,cv2.ml.ROW_SAMPLE,train_label)
retval, results, neigh_resp, dists = model.findNearest(test_input, 1)
string = results.ravel()
print(test_label.reshape(1,len(test_label))[0])
print(string)
下面是输出结果:
可以看到,预测值和实际值简直一模一样!
注意
1.opencv中的knn只能训练模型,不能保存和加载模型。所以只能用的时候训练,训练好直接用。
2.此次训练样本只有不到一百,暂时只能保证对于本系列文章自带的九宫格图片进行完美的数字识别。其他图片的数字识别准确率不敢保证。如果想要得到更好的效果,请按照机器学习的方法进行优化,或进行更好的数据与处理,或加大数据集等。
代码
# -*- coding: UTF-8 -*-
import glob as gb
import cv2
import os
## 获取numbers文件夹下所有文件路径
img_path = gb.glob("numbers\\*")
k = 0
labels = []
samples = []
## 对每一张图片进行处理
for path in img_path:
img = cv2.imread(path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
thresh = cv2.adaptiveThreshold(blur, 255, 1, 1, 11, 2)
image, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# cv2.drawContours(img,contours,-1,(0,0,255),3)
height, width = img.shape[:2]
# w = width/5
## 图片第一行和第二行数字
list1 = []
list2 = []
for cnt in contours:
# if cv2.contourArea(cnt)>100:
[x, y, w, h] = cv2.boundingRect(cnt)
if w > 30 and h > (height / 4):
## 按y坐标分行
if y < (height / 2):
list1.append([x, y, w, h]) ## 第一行
else:
list2.append([x, y, w, h]) ## 第二行
# rect_list.append([x,y,w,h])
# cv2.rectangle(img,(x,y),(x+w,y+h),(0,0,255),2)
# cv2.imshow("number",img)
# cv2.waitKey(0)
## 按x坐标排序,上面已经按y坐标分行
list1_sorted = sorted(list1, key=lambda t: t[0])
list2_sorted = sorted(list2, key=lambda t: t[0])
# print list1
# print list1_sorted
# print len(list1)
for i in range(5):
[x1, y1, w1, h1] = list1_sorted[i]
[x2, y2, w2, h2] = list2_sorted[i]
## 切割出每一个数字
number_roi1 = gray[y1:y1 + h1, x1:x1 + w1] # Cut the frame to size
number_roi2 = gray[y2:y2 + h2, x2:x2 + w2] # Cut the frame to size
# number_roi1 = thresh[y1:y1+h1, x1:x1+w1] #Cut the frame to size
# number_roi2 = thresh[y2:y2+h2, x2:x2+w2] #Cut the frame to size
## 对图片进行大小统一和预处理
resized_roi1 = cv2.resize(number_roi1, (20, 40))
thresh1 = cv2.adaptiveThreshold(resized_roi1, 255, 1, 1, 11, 2)
resized_roi2 = cv2.resize(number_roi2, (20, 40))
thresh2 = cv2.adaptiveThreshold(resized_roi2, 255, 1, 1, 11, 2)
## 每一个数字存入对应数字的文件夹
number_path1 = "number\\%s\\%d" % (str(i + 1), k) + '.jpg'
j = i + 6
if j == 10:
j = 0
number_path2 = "number\\%s\\%d" % (str(j), k) + '.jpg'
k += 1
## 归一化
normalized_roi1 = thresh1 / 255.
normalized_roi2 = thresh2 / 255.
# cv2.imwrite(number_path1,number_roi1)
# cv2.imwrite(number_path2,number_roi2)
## 把图片展开成一行,然后保存到samples
## 保存一个图片信息,保存一个对应的标签
sample1 = normalized_roi1.reshape((1, 800))
samples.append(sample1[0])
labels.append(float(i + 1))
sample2 = normalized_roi2.reshape((1, 800))
samples.append(sample2[0])
labels.append(float(j))
if not os.path.exists(os.path.join(number_path1.split("\\")[0],number_path1.split("\\")[1])):
os.makedirs(os.path.join(number_path1.split("\\")[0],number_path1.split("\\")[1]))
if not os.path.exists(os.path.join(number_path2.split("\\")[0],number_path2.split("\\")[1])):
os.makedirs(os.path.join(number_path2.split("\\")[0],number_path2.split("\\")[1]))
cv2.imwrite(number_path1, thresh1)
cv2.imwrite(number_path2, thresh2)
cv2.imshow("number", normalized_roi1)
cv2.waitKey(5)
# print sample1
## 这里才引入numpy是因为前面引入的话会自动把所有的list编程np.array
## 感觉array的append没有list的好用...
import numpy as np
## 这里还是把它们保存成了np.array...
samples = np.array(samples, np.float32)
# samples = samples.reshape((samples.size,1))
labels = np.array(labels, np.float32)
labels = labels.reshape((labels.size, 1))
np.save('samples.npy', samples)
np.save('label.npy', labels)
## 保存完加载一下试试
test = np.load('samples.npy')
label = np.load('label.npy')
print(test[0])
print(test[0].shape)
print('label: ', label[0])