2015年,我首次踏入在线教育行业时,技术总监给我安排了一个任务:研究一下OCR识别。
记得他是一个博士,给我推荐了谷歌的开源项目Tesseract。当时,我试了一下,感觉效果不好。他微微一笑,并没有说什么。
十年后,我对OCR稍微有了些经验。用过商业的,用过开源的,甚至自己也用基础的神经网络,手打过特定场景的数字、字母识别。
我想到,曾对Tesseract的质疑应当是误会。Tesseract是在1985年由惠普公司开发的收费OCR,当时是基于规则的字符识别。2006年,由谷歌接手。到目前,谷歌已经又维护了20年。它见证了OCR的发展史,支持100多种语言,率先引入了LSTM神经网络,96%的代码是底层和高效的C++语言,Github上有62.5k Star,是全球最受欢迎的开源OCR引擎之一,也是众多商业OCR服务的基石。
我居然感觉它是垃圾。忏悔,面壁。
我忐忑不安地又试了试。
语言的设置
我怀着朝圣的心,沐浴更衣后,拍下了这张图片。
我自认为拍得清晰、高质量。针对这张图片,让Tesseract去识别下。
我安装的是Windows版本的Tesseract。然后又安装了pytesseract,可实现Python代码调用Tesseract功能。
python
from PIL import Image
import pytesseract
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
image = Image.open('test.jpg')
text = pytesseract.image_to_string(image)
print(f"识别结果:\n{text}")
很快,结果出来啦。
python
识别结果:
/) BA IE FEF I --- TRAE, PRA "Data Science Basics" . wPREIEA 12 4%
HH, PRAAAS 30K AA. thitReKRAA 1 Dt, FWREB 5 Hl.
ARETE LOR AASEATA BA, (HALE Ftth BT AER ABE SKN
AEséA "Data Science Basics" J
SOAR TAR IX FE AE, A) A BLA ABE
我看看图,看看它,看看它,又看看图,心有点凉。
正当我转身要走,忽然在凌乱的结果里,发现了"Data Science Basics"和"Data Science Basics"。再对照原图,发现这两句英文的识别,非常正确。
那是不是因为要设置语言呢?
我又试了试,新增了语言设置。
python
text = pytesseract.image_to_string(image, lang='chi_sim')
结果让我出乎意料。
python
识别结果:
小明正在学习一门新课程,名称为"DataScienceBasics"。课程共有12个章
节,每章大约包含30页内容。他计划每天学习1小时,平均能看5页。
小明想在10天内学完所有章节,但有些日子他可能只能学习半小时。
如果按照这样的速度,请问小明需要几天才能完成"DataScienceBasics"的
学习?
幸好,我心存敬畏,回头看了一眼。否则,错失良机。
lang='chi_sim'是指定语言,这里指定的是简体中文。如果一张纸上同时有简体中文、繁体中文、英文、意大利语。那么,参数可以如此配置:
python
pytesseract.image_to_string(image, lang='chi_sim+chi_tra+eng+ita')
分割模式
我又找到了这张图片,想让Tesseract给认认。
执行代码:
python
text = pytesseract.image_to_string(image, lang='eng')
结果什么也没有识别出来,控制台打印如下:
python
识别结果:
我转身要走,心想鼎鼎大名的Tesseract,也不过如此耳。不求你把骑缝处的小字认出来,最起码图上有个巨大"Water",你得能看到吧!
那是不是因为需要设置页面分割模式呢?
python
text = pytesseract.image_to_string(image, lang='eng', config=r'--psm 11')
我增加了--psm 11
,也就是指定页面分割模式为稀疏文本。结果控制台打印如下:
python
识别结果:
sir Water | Eaten
C.
......
虽然,还有很多奇奇怪怪的字符,但是很明显出现了"Water"字符。说明这个配置优化了识别。
Tesseract提供14种页面分割模式,适用于不同的页面布局。
模式 | 描述 |
---|---|
0 |
仅检测方向和脚本,不进行 OCR |
1 |
自动页面分割,假设图像为单列文本 |
2 |
自动页面分割,假设图像为单块垂直对齐文本 |
3 |
自动页面分割(不做方向检测),适合单一文本块 |
4 |
将页面视为单个文本块的行 |
5 |
将页面视为单个文本块的竖排文本 |
6 |
将页面视为单个文本块的混排文本 |
7 |
将页面视为单行文本 |
8 |
将页面视为单个单词 |
9 |
将页面视为单个圈出的单词 |
10 |
将页面视为单个字符 |
11 |
稀疏文本 |
12 |
稀疏文本(带 OSD) |
13 |
原始图像的行检测,不进行 OCR |
调用方法就是通过--psm 编号
传给config参数。
恕小弟无理了,幸好多看了一眼。
在此基础上,本来这张图识别不出来的。
通过设置--psm 5
,也就是竖排文本,识别出来了。
python
识别结果:
UpSample
黑白名单
来吧,继续展示。
这张图是用户在网页手写板涂鸦的图。软件厂家用在小孩学写数字上,他们只想识别数字,别的都忽略掉。因为3岁的孩子不大可能写出超纲的字。那么,我们可以设置白名单。
python
text = pytesseract.image_to_string(image, lang='eng', config=r'-c tessedit_char_whitelist=0123456789')
print(f"识别结果:\n{text}")
结果是只考虑数字。
python
识别结果:
668
参数配置-c tessedit_char_whitelist
表示结果只能是白名单里的内容。如果出现大写字母O会被识别为0,字母Z会被识别为2,小写l会被识别为1。因为,白名单优先。
有时候,从视觉上,不只是人工智能,人类也很难识别。比如L的小写是l,i的大写是I,可能都是一条竖线。
因此,我们不能期望任何一个OCR能100%识别出符合预期的内容。我们要做的就是尽量去做规则上的鼓励和限制。Tesseract也提供了黑名单的功能,就是限制不可能出现的字符。
我们这个识别,其实也能用到。这张图不加任何限制,识别结果是668 (BMW
。因为某些原因,字母i
被识别成了符号(
。可能i
的下部分写得有些弧度了,更像是(
。如果这是一个数字+字母场景,那么不会出现符号,因此将符号(
列入黑名单。
python
image = Image.open('test.png')
text = pytesseract.image_to_string(image, lang='eng', config=r'-c tessedit_char_blacklist=(')
print(f"识别结果:\n{text}")
控制台说话了。
python
识别结果:
668
iBMW
OCR识别是一个分类问题,其实i
有很多备选项,比如1
、l
、(
。当我们把(
通过-c tessedit_char_blacklist=(
列入黑名单之后,排在第二位的i
就因为第一位被过滤掉,成功上位。
这类需求,尤其体现在车牌号识别上。
模型的选择
谈这个很多余,因为有好用的,没有人会用不好用的。但是,也不一定。可能有人要解决兼容问题呢?
看这张图。
这张图比较模糊,我们人类甚至需要费点儿精力才能连看带猜地识别完全。
如果你这么调用,Tesseract的识别效果很不好。
python
pytesseract.image_to_string(image, lang='eng', config=r'--oem 0')
--oem
(OCR Engine Mode)是Tesseract OCR的引擎模式。我们前文说过,Tesseract在40年前用的是规则匹配,后来才引入的神经网络。因此,它也是一个模式参数。
Tesseract支持4种模式:
--oem 0
使用传统的Tesseract OCR 引擎,旧版本的模型,传统的OCR任务。--oem 1
使用基于神经网络的LSTM(一种神经网络)进行识别,适合低质量图像。--oem 2
将传统引擎和LSTM引擎结合起来使用,两种都走,然后对结果整合。--oem 3
使用LSTM,在准确度和性能之间取得良好平衡。
如果你不设置,它默认是--oem 3
。
至于前几种,算是历史遗留问题。它一路走来,你不能说以前的不支持了。而且,并不是所有设备都可以运行神经网络。对于比如打印机设备,规则匹配算法兼容性更好。
选择模型3,走一个代码运行。
python
pytesseract.image_to_string(image, lang='eng', config=r'--oem 3')
模糊图也是可以识别的。
行检测增强
上面的图像都比较规范,清晰无污染,是理想的识别素材。但是在实际场景中,情况不一定。
比如下面这张图。
图像有些部分被划花了。一般的识别结果是这样的。
python
识别结果:
小奚正在学习一门新课程、吊称为"BataScienceBasics"......
小明
被识别成了小奚
,名称
被识别成了吊称
。因为,那部分的字体模糊了。没有关系,有一个参数-c textord_heavy_nr=1
。它的应用场景是含有很多噪声的文档。
走一个代码:
python
pytesseract.image_to_string(image, lang='chi_sim', config=r'-c textord_heavy_nr=1')
控制台打印。
python
识别结果:
小明正在学习一门新课程。名称为"TataScienceBasics"。......
OK了。
图像预处理
有这么一张图。
我怎么调整参数,它都识别不出来。
这张图命运悲惨。它经过多次加工、转发,早就千疮百孔了。因此,我们需要对它进行一些预处理。
首先,把这图片变成灰度图。
python
import cv2
mg = cv2.imread('test.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
还是不能识别?进行二值化,让它黑白分明。
python
_, thresh = cv2.threshold(gray, 125, 225, cv2.THRESH_BINARY)
还是不能识别?进行腐蚀一下,清场,瘦身。
python
kernel = np.ones((4, 4), np.uint8)
eroded_image = cv2.erode(thresh, kernel, iterations=1)
还是不能识别?改成白纸黑字!
python
thresh_not = cv2.bitwise_not(eroded_image)
这下总行了吧,识别一下:
python
text = pytesseract.image_to_string(thresh_not, lang='chi_sim')
print(f"识别结果:\n{text}")
看看识别结果是什么。
python
识别结果:
胖晚回到家,女朋友已经睡着了,
胡计是做坤梦了,尖叫着坐了起来,
说东十角有不干浑的东西,
我提着棍子走过去
发现是砌没有洗。
大体识别出来了,还有四五个字没有识别准。比如昨
识别成了胖
,碗
识别成了砌
。但是大家记住,之前它是一个字也识别不出来的。这说明,经过我们的预处理,它进步了。我们还可以继续优化。
更详细的分析
我们可以看看Tesseract在识别过程中,哪些字被选中了,又被识别成了什么。
我的思路是调用image_to_boxes
,它返回的是识别出来的字符以及它们的坐标。我们将这些坐标进行编号然后画出来,看看结果和位置是否对应,以便确定优化方向。
python
pil_image = Image.fromarray(thresh_not)
detection_boxes = pytesseract.image_to_boxes(pil_image, lang='chi_sim')
h, w, _ = img.shape
img_copy = img.copy()
counter = 1
for box in detection_boxes.splitlines():
b = box.split()
char = b[0]
x, y, x2, y2 = int(b[1]), int(b[2]), int(b[3]), int(b[4])
cv2.rectangle(img_copy, (x, h - y), (x2, h - y2), (0, 0, 255), 3)
cv2.putText(img_copy, str(counter), (x, h - y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
print(f"区域编号 {counter}: {char}, 坐标: ({x}, {y}) 到 ({x2}, {y2})")
counter += 1
cv2.imwrite('result.jpg', img_copy)
最后结果绘制在result.jpg
上。
然后控制台打印结果为:
python
区域编号 1: ~, 坐标: (212, 1484) 到 (983, 1526)
区域编号 2: 胖, 坐标: (164, 1287) 到 (547, 1389)
区域编号 3: 晚, 坐标: (269, 1258) 到 (355, 1393)
区域编号 4: 回, 坐标: (354, 1258) 到 (422, 1393)
区域编号 5: 到, 坐标: (421, 1258) 到 (506, 1393)
区域编号 6: 家, 坐标: (505, 1258) 到 (574, 1393)
......
区域编号 55: 是, 坐标: (595, 57) 到 (715, 269)
区域编号 56: 砌, 坐标: (715, 57) 到 (835, 269)
区域编号 57: 没, 坐标: (834, 57) 到 (954, 269)
区域编号 58: 有, 坐标: (1011, 113) 到 (1037, 254)
区域编号 59: 洗, 坐标: (1059, 114) 到 (1115, 265)
区域编号 60: 。, 坐标: (1136, 125) 到 (1164, 163)
区域编号 61: ~, 坐标: (1166, 1482) 到 (1194, 1526)
这样,我们可以对应一下位置和内容,便于分析。
总结
最后,我相信大家会有一个感觉、一个疑问。
一个感觉就是Tesseract的配置太复杂了。它甚至还整出14个页面分割模式。还有那么多配置都要手动添加。对它不了解的人,或者稍微没有耐心的人,很容易对它做出误判断。
一个疑问就是你全部改成自动的不好吗?为什么增强要单独自己加参数?为什么不把所有优化都用上?管他图像什么质量,把最好的增强,最强的模型都用上。因为,很多国内的OCR就是这样,一句话调用,从不废话。
我作为开发过AI模型的人,我想做下解释。你认为通用的是最好的吗?
当然不是。世界上根本就没有通用的解决方案。
举个例子。有没有一种药,可以治疗所有的感染?把这些药都混在一起?有没有一种胶水,什么材料都能粘合?把各种胶水混在一起就可以?
实际上,我们发现在很多领域,都会有参数和配置的概念。比如一个简单的家用打孔钻头,有钻混凝土的,有钻木头的,有钻瓷砖的。这几种我都买过,所以我清楚。后来研究发现,哦,原来他们的纹路各不相同,都是根据目标材质来设计的。甚至旋转方式还有平钻和冲击钻的区别。这都是参数。能混用吗?或者设计成一个通用的,可以吗?可以,我曾经用钻木头的钻了墙,不是说不能用,你用安迪的锤子也能掏洞,但是效率极低。
我做AI模型对外提供能力时,一般会提供两种模式。一种叫高精度模式,一种叫高容错模式。这都是被需求逼出来的。有人对准确性要求高,他说有一个字识别不对,用户下次就不用了。有人对速度要求高,他说让我的用户超过5秒没有识别结果,他就走了。有人对两者都要求高,我对他说我对钱要求高,他也走了。
其实,Tesseract的所有参数,都是一种取舍。它没有平衡,就是取舍。
你的图很清晰,那么就这么调用。你就是个图多字少的广告牌,你就用稀疏文本分割模式。你的识别区域只有一行文本,那就用单行模式好了。选对了,结果是完美。至于,你怎么去分辨,那不是Tesseract考虑的范围。那将会是个无底洞的工作,世间的文本类型情况太多了。你自己走图片分类也好,图片预处理也好,自己做裁剪矫正也好,那是你的事情。反正它就做一件事情,那就是清晰文字的高质量识别。这一点,就是Tesseract的强项。
经典就是经典。读开源项目如同读人生,初识不懂,再识感慨万千。
我是TF男孩,一个平时爱喝75ml 50度白酒的老程序员。