OCR的全称是Optical Character Recognition,光学字符识别技术。目前应用于各个领域方向,甚至这些应用就在我们的身边,比如身份证的识别,交通路牌的识别,车牌的自动识别等等。本文就学习一下基于开源软件和大厂服务的文字识别效果。
关于ocr的简介,请参考博客:
刚入门不久,而且还是自己摸着石头过河,所以学的知识深一点,浅一点的,博客里面记录的是自己学习的过程,希望记录自己的学习之路,回头看也好回顾。
在去年八九月份的时候,学习了好多关于OCR文字识别的文章,并做了笔记,如下。那现在对一些简单快速识别的接口做下笔记。
方案1:基于开源软件Tesseract实现
有些时候使用开源软件比较方便,当然目前最有名的就是tesseract,目前由谷歌在进行维护,貌似使用的还是传统机器学习的方式。
那这里想了解,可以参考我写过的文章:
1,深入学习使用OCR算法识别图片中文字的方法
2,深入学习Tesseract-ocr识别中文并训练字库的方法
3,深入学习python解析并读取PDF文件内容的方法
4,深入学习python解析并解密PDF文件内容的方法
方案2:百度文字识别接口服务
下面主要针对Python开发者,描述百度文字识别接口服务的相关技术内容。OCR接口提供了自然场景下整图文字检测、定位、识别等功能。文字识别的结果可以用于翻译、搜索、验证码等代替用户输入的场景。
首先我们可以找一个产品服务的定价单:
可以看到,倘若你的服务只有偶尔用一次,完全可以使用这种体验型的免费服务。如果一天需要调用一万次,那么一个月基本的花费在5w左右——成本还是很高的,所以很多商用的场景大多数都采用自主研发的方法来做。
下面学习一下百度API的SDK软件开发工具包(Software Development Kit)的使用。
1,百度AipOcr接口能力
2,安装接口模型
pip install baidu-aip
3,新建AipOcr
AipOcr是OCR的Python SDK客户端,为使用OCR的开发人员提供了一系列的交互方法。
参考如下代码新建一个AipOcr:
from aip import AipOcr
""" 你的 APPID AK SK """
APP_ID = '你的 App ID'
API_KEY = '你的 Api Key'
SECRET_KEY = '你的 Secret Key'
client = AipOcr(APP_ID, API_KEY, SECRET_KEY)
在上面代码中,常量APP_ID
在百度云控制台中创建,常量API_KEY
与SECRET_KEY
是在创建完毕应用后,系统分配给用户的,均为字符串,用于标识用户,为访问做签名验证,可在AI服务控制台中的应用列表中查看。
上面代码块里APP_ID 、API_KEY、SECRET_KEY 三个值对应在http://console.bce.baidu.com/ai/#/ai/ocr/app/list 这里找到,需要用百度账号登录,然后创建一个应用,如下图:
4,配置AipOcr
如果用户需要配置AipOcr的网络请求参数(一般不需要配置),可以在构造AipOcr之后调用接口设置参数,目前只支持以下参数:
5,通用文字识别接口说明
我们向服务请求识别某张图中的所有文字
""" 读取图片 """
def get_file_content(filePath):
with open(filePath, 'rb') as fp:
return fp.read()
image = get_file_content('example.jpg')
""" 调用通用文字识别, 图片参数为本地图片 """
client.basicGeneral(image);
""" 如果有可选参数 """
options = {}
options["language_type"] = "CHN_ENG"
options["detect_direction"] = "true"
options["detect_language"] = "true"
options["probability"] = "true"
""" 带参数调用通用文字识别, 图片参数为本地图片 """
client.basicGeneral(image, options)
url = "https//www.x.com/sample.jpg"
""" 调用通用文字识别, 图片参数为远程url图片 """
client.basicGeneralUrl(url);
""" 如果有可选参数 """
options = {}
options["language_type"] = "CHN_ENG"
options["detect_direction"] = "true"
options["detect_language"] = "true"
options["probability"] = "true"
""" 带参数调用通用文字识别, 图片参数为远程url图片 """
client.basicGeneralUrl(url, options)
6,通用文字识别 请求参数详情
7,通用文字识别 返回数据参数详情
8, 通用文字识别返回示例
{
"log_id": 2471272194,
"words_result_num": 2,
"words_result":
[
{"words": " TSINGTAO"},
{"words": "青島睥酒"}
]
}
9,最终代码:
# -*- coding: UTF-8 -*-
from aip import AipOcr
# 定义常量
APP_ID = '14544448'
API_KEY = 'yRZGUXAlCd0c9vQj1kAjBEfY'
SECRET_KEY = 'sc0DKGy7wZ9MeWFGZnbscbRyoDB2IQlj'
# 初始化AipFace对象
client = AipOcr(APP_ID, API_KEY, SECRET_KEY)
# 读取图片
def get_file_content(filePath):
with open(filePath, 'rb') as fp:
return fp.read()
image = get_file_content('binary_best.jpg')
# 调用通用文字识别, 图片为本地图片
res=client.general(image)
print(res)
for item in res['words_result']:
print(item['words'])
10,识别结果
识别图片1为:
识别结果1为:
{'log_id': 599780865793529561, 'words_result_num': 7, 'words_result':
[{'location': {'width': 321, 'top': 6, 'left': 2, 'height': 23}, 'words': '山有
木兮木有枝,心悦君兮君不知。'}, {'location': {'width': 143, 'top': 46, 'left': 2,
'height': 19}, 'words': '出自先秦的《越人歌》'}, {'location': {'width': 146,
'top': 84, 'left': 1, 'height': 18}, 'words': '今夕何夕兮,拳舟中流'}, {'location':
{'width': 181, 'top': 112, 'left': 1, 'height': 19}, 'words': '今日何日兮,得与王
子同舟。'}, {'location': {'width': 152, 'top': 141, 'left': 1, 'height': 17}, 'wo
rds': '蒙羞被好兮,不訾诟耻。'}, {'location': {'width': 181, 'top': 168, 'left': 1,
'height': 19}, 'words': '心几烦而不绝兮,得知王子。'}, {'location': {'width': 222,
'top': 195, 'left': 2, 'height': 20}, 'words': '山有木兮木有枝,心悦君兮君不知。'}]}
山有木兮木有枝,心悦君兮君不知。
出自先秦的《越人歌》
今夕何夕兮,拳舟中流
今日何日兮,得与王子同舟。
蒙羞被好兮,不訾诟耻。
心几烦而不绝兮,得知王子。
山有木兮木有枝,心悦君兮君不知。
Process finished with exit code 0
识别图片2为:
识别结果2为:
{'log_id': 7769540578562875641, 'words_result_num': 7, 'words_result':
[{'location': {'width': 44, 'top': 20, 'left': 39, 'height': 21}, 'words': 'GTIN'},
{'location': {'width': 54, 'top': 18, 'left': 329, 'height': 20}, 'words': 'Count'},
{'location': {'width': 139, 'top': 45, 'left': 37, 'height': 21}, 'words': '001
90198720566'}, {'location': {'width': 73, 'top': 76, 'left': 36, 'height': 24},
'words': 'SSCC18:'}, {'location': {'width': 69, 'top': 78, 'left': 329, 'height':
21}, 'words': 'Product'}, {'location': {'width': 178, 'top': 104, 'left': 35,
'height': 21}, 'words': '008859090495625554'}, {'location': {'width': 108,
'top': 104, 'left': 328, 'height': 22}, 'words': 'MRJP2CH/A'}]}
GTIN
Count
00190198720566
SSCC18:
Product
008859090495625554
MRJP2CH/A
参考:http://ai.baidu.com/docs#/OCR-Python-SDK/e4ddca43
方案3:KNN实现简单的OCR
这次在网上看到KNN在ocr的识别,非常想尝试一下,原文作者说KNN在ocr的识别过程中能发挥作用的地方就是将图像中的文字转化为文本格式,而OCR的其他部分,比如图像预处理,二值化等操作将丢给OpenCV去操作。
代码地址:github:https://github.com/hahahaha1997/OCR
1,训练集简介
由于我们采用的是KNN来转换图像中的文字为文本格式,需要一个庞大的手写字符训练集来支撑我们的算法。这里我使用的是《机器学习实战》2.3实例:手写识别系统中使用的数据集,其下载地址为:https://www.manning.com/books/machine-learning-in-action,在Source Code\Ch02\digits\trainingDigits中的两千多个手写字符既是我所使用的训练集。
这个训练集配合上它所提供的测试集,提供了一个准确度非常高的分类器:
训练集是由0~9十个数字组成的,每个数字有两百个左右的训练样本。所有的训练样本统一被处理为一个32*32的0/1矩阵,其中所有值为1的连通区域构成了形象上的数字,如下所示:
所以,在构造我们的测试集的时候,所有的手写数字图片必须被处理为这样的格式才能够使得分类算法正确地进行,这也是KNN的局限所在。
2、构建测试集
上面已经提到,要想算法正确地进行,测试集的样式应该和训练集相同,也就是说我们要把一张包含有手写数字的图像,转换为一个32*32的0/1点阵。
测试集使用我自己手写的10个数字:
这里存在一个非常大的问题:这个数据集的作者是土耳其人,他们书写数字的习惯和我们有诸多不同,比如上面的数字4和数字8,下面这样子的数字就无法识别:4/8。哈哈,也就是说它连印刷体都无法识别,这是这个训练集的一大缺陷之一。
1)图像预处理
图像预处理的过程是一个数字图像处理(DIP)的过程,观察上面的10个数字,可以发现每张图像的大小/对比度的差距都非常大,所以图像预处理应该消除这些差距。
第一步是进行图像的放大/缩小。由于我们很难产生一个小于32*32像素的手写数字图像,所以这里主要是缩小图像:
import cv2
def readImage(imagePath):
image = cv2.imread(imagePath,cv2.IMREAD_GRAYSCALE)
image = cv2.resize(image,(32,32),interpolation = cv2.INTER_AREA)
return image
这里我没有去实现图像重采样的方法,而是采用的OpenCV,通过area来确定取样点的灰度值(推荐用bicubic interpolation,对应的插入函数应该是INTER_CUBIC),在读入图像的时候读入方式位IMRAD_GRAYSCALE,因为我们需要的是识别手写字符,灰度图对比彩色图能更好的突出重点。
进行图像的缩放是不够的,因为观察上面的图片可以发现:拍摄环境对于对比度的影响非常大,所以我们应该突出深色区域(数字部分),来保证后面的工作顺利进行,这里采用的是伽马变换(也可以采用对数变换):
def imageGamma(image):
for i in range(32):
for j in range(32):
image[i][j]=3*pow(image[i][j],0.8)
return image
2)图像二值化
缩小/放大后的图像已经是一个32*32的图像了,下一步则是将非数字区域填充0,数字区域填充1,这里我采用的是阈值二值化处理:
def imageThreshold(image):
ret,image = cv2.threshold(image,150,255,cv2.THRESH_BINARY)
return image
经过二值化处理,数字部分的灰度值应该为0,而非数字部分的连通区域的灰度值应该为255,如下所示:
3)去噪
图像去噪的方式有很多种,这里建立使用自适应中值滤波器进行降噪,因为我们的图像在传输过程中可能出现若干的椒盐噪声,这个噪声在上述的二值化处理中有时候是非常棘手的。
到目前为止,一副手机摄像的手写数字图像就可以转换为一个32*32的二值图像。
4)生成训练样本
如何将这个32*32的二值图像转换为0/1图像,这个处理非常简单:
def imageProcess(image):
with open(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits\6_0.txt','w+') as file:
for i in range(32):
for j in range(32):
if image[i][j] == 255:
file.write('0')
else:
file.write('1')
file.writelines('\n')
这里我的代码在扫描这个图像的同时,将其保存为一个训练样本,命名和训练集的明明要求一样为N_M.txt,其中N代表这个训练样本的实际分类是什么数字,M代表这是这个数字的第几个样本。这里对图像进行灰度变换已经是多此一举了,我所需要的是0/1矩阵而非一个0/1图像,所以在扫描过程中一并生成训练样本更加省时直观。
5)形成训练集
上面的示例只是生成一个图像的训练样本的,而实际上我们往往需要一次性生成一个训练集,这就要求这个图像预处理、二值化并且生成0/1矩阵的过程是自动的:
from os import listdir
def imProcess(imagePath):
testDigits = listdir(imagePath)
for i in range(len(testDigits)):
imageName = testDigits[i]#图像命名格式为N_M.png,NM含义见4)生成训练样本
#imageClass = int((imageName.split('.')[0]).split('_')[0])#这个图像的数字是多少
image = cv2.imread(imageName,cv2.IMREAD_GRAYSCALE)
image = cv2.resize(image, (32, 32), interpolation=cv2.INTER_AREA)
ret, image = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
with open(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits\\'+imageName.split('.')[0]+'.txt','w+') as file:
for i in range(32):
for j in range(32):
if image[i][j] == 255:
file.write('0')
else:
file.write('1')
file.writelines('\n')
这个函数将imagePath文件夹中所有的N_M命名的手写数字图像读取并经过预处理、二值化、最后保存为对应的0/1矩阵,命名为N_M.txt,这就构成一个训练集了。
3、构建分类器
分类器如下:
def classify(vector,dataSet,labels,k):
distance = sqrt(abs(((tile(vec,(dataSet.shape[0],1)) - dataSet) ** 2).sum(axis = 1))); #计算距离
sortedDistance = distance.argsort()
dict={}
for i in range(k):
label = labels[sortedDistance[i]]
if not label in dict:
dict[label] = 1
else:
dict[label]+=1
sortedDict = sorted(dict,key = operator.itemgetter(1),reverse = True)
return sortedDict[0][0]
def dict2list(dic:dict):#将字典转换为list类型
keys=dic.keys()
values=dic.values()
lst=[(key, value)for key,value in zip(keys,values)]
return lst
distance的计算和dict2list函数的详解在上一节,戳上面的classify既可以跳转过去。
分类器已经构建完成,下一步是提取每一个测试样本,提取训练集,提取label的过程:(这个过程大部分用的是《机器学习实战》中的代码,对于难以理解的代码在下文中做了解释:)
1)读取0/1矩阵文件:
def img2vector(filename):
returnvec = numpy.zeros((1,1024))
file = open(filename)
for i in range(32):
line = file.readline()
for j in range(32):
returnvec[0,32*i+j] = int(line[j])
return returnvec
这里要注意:构造一个32*32的全零矩阵的时候,应该是numpy.zeros((1,1024)),双层括号!双层括号!双层括号!代表构造的是一个二维矩阵!
2)读取训练集和测试集并求解准确率:
def handWritingClassifyTest():
labels=[]
trainingFile = listdir(r'F:\Users\yang\PycharmProjects\OCR_KNN\trainingDigits')
m = len(trainingFile)
trainingMat = numpy.zeros((m,1024))
for i in range(m):
file = trainingFile[i]
filestr = file.strip('.')[0]
classnum = int(filestr.strip('_')[0])
labels.append(classnum)
trainingMat[i,:] = img2vector('trainingDigits/%s' % file)
testFileList = listdir(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits')
error = 0.0
testnum = len(testFileList)
for i in range(testnum):
file_test = testFileList[i]
filestr_test = file_test.strip('.')[0]
classnum_test = int(filestr_test.strip('_')[0])
vector_test = img2vector('testDigits/%s'%file_test)
result = classify(vector_test,trainingMat,labels,1)
if(result!=classnum_test):error+=1.0
print("准确率:%f"%(1.0-(error/float(testnum))))
代码其实没有很难懂的地方,主要任务就是读取文件,通过img2vctor函数转换为矩阵,还有切割文件名获取该测试样本的类别和该训练样本的类别,通过对比获得准确率。
3、使用分类器
现在为止,我们的分类器已经构建完成,下面就是测试和使用阶段:
1)测试《机器学习实战》中给出的训练集:
2)测试手写训练集:
果然学不出来大佬写字,附上几张无法识别的0/1数字矩阵:(0,4,6无法识别的原因是比划太细哈哈,8无法识别的原因……太端正了吧)
4、完整代码:
from os import listdir
import numpy
import operator
import cv2
def imProcess(imagePath):
testDigits = listdir(imagePath)
for i in range(len(testDigits)):
imageName = testDigits[i]#图像命名格式为N_M.png,NM含义见4)生成训练样本
#imageClass = int((imageName.split('.')[0]).split('_')[0])#这个图像的数字是多少
image = cv2.imread(imageName,cv2.IMREAD_GRAYSCALE)
image = cv2.resize(image, (32, 32), interpolation=cv2.INTER_AREA)
ret, image = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
with open(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits\\'+imageName.split('.')[0]+'.txt','w+') as file:
for i in range(32):
for j in range(32):
if image[i][j] == 255:
file.write('0')
else:
file.write('1')
file.writelines('\n')
def img2vector(filename):
returnvec = numpy.zeros((1,1024))
file = open(filename)
for i in range(32):
line = file.readline()
for j in range(32):
returnvec[0,32*i+j] = int(line[j])
return returnvec
def handWritingClassifyTest():
labels=[]
trainingFile = listdir(r'F:\Users\yang\PycharmProjects\OCR_KNN\trainingDigits')
m = len(trainingFile)
trainingMat = numpy.zeros((m,1024))
for i in range(m):
file = trainingFile[i]
filestr = file.strip('.')[0]
classnum = int(filestr.strip('_')[0])
labels.append(classnum)
trainingMat[i,:] = img2vector('trainingDigits/%s' % file)
testFileList = listdir(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits')
error = 0.0
testnum = len(testFileList)
for i in range(testnum):
file_test = testFileList[i]
filestr_test = file_test.strip('.')[0]
classnum_test = int(filestr_test.strip('_')[0])
vector_test = img2vector('testDigits/%s'%file_test)
result = classify(vector_test,trainingMat,labels,1)
if(result!=classnum_test):error+=1.0
print("准确率:%f"%(1.0-(error/float(testnum))))
def classify(inX,dataSet,labels,k):
size = dataSet.shape[0]
distance = (((numpy.tile(inX,(size,1))-dataSet)**2).sum(axis=1))**0.5
sortedDistance = distance.argsort()
count = {}
for i in range(k):
label = labels[sortedDistance[i]]
count[label]=count.get(label,0)+1
sortedcount = sorted(dict2list(count),key=operator.itemgetter(1),reverse=True)
return sortedcount[0][0]
def dict2list(dic:dict):#将字典转换为list类型
keys=dic.keys()
values=dic.values()
lst=[(key, value)for key,value in zip(keys,values)]
return lst
# def imProcess(image):
# image = cv2.resize(image, (32, 32), interpolation=cv2.INTER_AREA)
# ret, image = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY)
# cv2.imshow('result',image)
# cv2.waitKey(0)
# with open(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits\6_0.txt','w+') as file:
# for i in range(32):
# for j in range(32):
# if image[i][j] == 255:
# file.write('0')
# else:
# file.write('1')
# file.writelines('\n')
# iamge = cv2.imread(r'C:\Users\yang\Desktop\6.png',cv2.IMREAD_GRAYSCALE)
# image = imProcess(iamge)
imProcess(r'F:\Users\yang\PycharmProjects\OCR_KNN\testDigits')
handWritingClassifyTest()
5、总结
KNN还是不适合用来做OCR的识别过程的,虽然《机器学习实战》的作者提到这个系统是美国的邮件分拣系统实际运行的一个系统,但是它肯定无法高准确率地识别中国人写的手写文字就对了,毕竟中国有些地方的“9”还会写成“p”的样子的。这一节主要是将KNN拓展到实际运用中的,结合上一节的理论,KNN的执行效率还是太低了,比如这个系统,要识别一个手写数字,它需要和所有的训练样本做距离计算,每个距离计算又有1024个(a-b)²,还有运行效率特别低下的sqrt(),如果是一个非常大的测试集,需要的时间就更加庞大,如果训练集非常庞大,在将0/1矩阵读入内存中的时候,内存开销是非常巨大的,所以整个程序可能会非常耗时费力。不过KNN仍旧是一个精度非常高的算法,并且也是机器学习分类算法中最简单的算法之一。
KNN实现OCR的转载地址:
知识储备:使用Python写入数据到Excel文件中
1 安装XlsxWriter
python XlsxWriter模块创建aexcel表格,生成的文件后缀名为.xlsx,最大能够支持1048576行数据,16384列数据
pip install XlsxWriter
2 在Excel写数据
# 建立文件
workbook = xlsxwriter.Workbook("text.xlsx")
# 可以制定表的名字
# worksheet = workbook.add_worksheet('text')
worksheet = workbook.add_worksheet()
# 设置列宽
# worksheet.set_column('A:A',10)
# 设置祖体
bold = workbook.add_format({'bold':True})
# 定义数字格式
# money = workbook.add_format({'num_format':'$#,##0'})
# 写入带粗体的数据
worksheet.write('A1','data',bold)
worksheet.write('B1','work')
'''
worksheet.write(0, 0, 'Hello') # write_string()
worksheet.write(1, 0, 'World') # write_string()
worksheet.write(2, 0, 2) # write_number()
worksheet.write(3, 0, 3.00001) # write_number()
worksheet.write(4, 0, '=SIN(PI()/4)') # write_formula()
worksheet.write(5, 0, '') # write_blank()
worksheet.write(6, 0, None) # write_blank()
'''
worksheet.write('A3',15)
worksheet.write('B3',20)
worksheet.write('C3',44)
worksheet.write('D3',36)
# xlsx计算数据
worksheet.write('E3','=SUM(A3:D3)')
'''
建立Chart对象: chart = workbook.add_chart({type, 'column'})
Chart: Area, Bar, Column, Doughnut, Line, Pie, Scatter, Stock, Radar
将图插入到sheet中: worksheet.insert_chart('A7', chart)
'''
# 定义插入的图标样式
chart = workbook.add_chart({"type":'column'})
headings = ['a','b','c']
data = [
[1,2,3,4,5],
[2,4,6,8,10],
[3,6,9,12,15],
]
# 按行插入数据
worksheet.write_row('A4',headings)
# 按列插入数据
worksheet.write_column('A5',data[0])
worksheet.write_column('B5',data[1])
worksheet.write_column('C5',data[2])
# 图行的数据区
# name:代表图例名称;
# categories:是x轴项,也就是类别;
# values:是y轴项,也就是值;
chart.add_series({
'name':'=Sheet1!$B$4',
'categories':'=Sheet1!$A$5:$A$9',
'values':'=Sheet1!$B$5:$B$9',
})
chart.add_series({
'name':['Sheet1', 3, 2],
'categories':['Sheet1', 4, 0, 8, 0],
'values':['Sheet1', 4, 2, 8, 2],
})
# 图形的标题
chart.set_title ({'name': 'Percent Stacked Chart'})
# 图形X轴的说明
chart.set_x_axis({'name': 'Test number'})
# 图形Y轴的说明
chart.set_y_axis({'name': 'Sample length (mm)'})
# 设置图表风格
chart.set_style(11)
# 插入图形,带偏移
worksheet.insert_chart('D12',chart,{'x_offset': 25, 'y_offset': 10})
workbook.close()