概述
OCR(Optical Character Recognition,中文叫做光学字符识别),是利用光学、数学以及计算机技术把图像上的文字(打印体或手写体)识别出来。OCR作为计算机视觉的核心课题之一并且经过这么多年的发展已经是比较成熟了,而且已经渗透到我们生活的方方面面,比如身份证识别、车牌号识别、票据单证识别、手机拍照搜题等等。
OCR要识别的内容将是人类的所有语言(汉语、英语、德语、法语等)。如果仅按照我们国人的需求,那识别的内容就包括:汉字、英文字母、阿拉伯数字、常用标点符号。识别数字最简单,毕竟要识别的字符只有0~9,英文字母识别相对容易,有26个(如果区分大小写那就是52个),而中文文字识别,要识别的字符高达数千个(二级汉字一共6763个),因为汉字的字形各不相同,结构非常复杂(比如带偏旁的汉字)如果要将这些字符都比较准确地识别出来,还是一件相当具有挑战性的事情。
在一些简单场景下OCR的准确度已经比较高了(比如电子文档、身份证、票据单证等),现在越来越多的研究人员把精力放在如何准确的把文字从复杂场景中识别出来,也就是所谓的场景文本识别(文字检测+文字识别)。 从上图可以看出,自然场景下的文字识别比简单场景的文字识别实在困难太多了。
方法
那么对于一个单纯的OCR任务,我们有哪些方法可以选择呢?主要有以下几种:
- 使用谷歌开源OCR引擎Tesseract。它是一个比较著名的OCR引擎,对汉字识别的精度不高,但是在阿拉伯数字和英文字母上的识别效果还是可以的。
- 使用大公司的OCR开放平台(比如百度、阿里等)。经过亲测,效果已经非常让人满意了(绝对不是看在老东家的份上打广告),而且简单易用,方便快捷,且小量调用是不收费的,当然缺点就是自己的控制程度不足,因为模型是别人的,没办法做迭代更新以及个性化训练,能做的只是预处理和后期矫正。
- 暴力的字符模板匹配法。比如在对电表数字进行识别时,考虑到电表数字的种类有限(可能就只有阿拉伯数字),而且字体很统一,清晰度也很高,对于类似这种情况,我们首先定义出数字模板(0~9),然后利用这些数字模板依次滑动匹配电表上的数字,这种策略虽然也能达到一定效果,但是仅适用于简单场景。
- 传统机器学习方法以及深度学习方法训练OCR模型。传统方法即特征设计、特征提取、分类。第一步是特征设计和提取,即通过分析待识别的目标字符,提取字符特征,越全效果越好,包括字符的端点、交叉点、圈的个数、横线竖线条数等等,比如“品”字,它的特征就是它有3个圈,6条横线,6条竖线。第二步是将这些特征送入分类器(如SVM)训练分类模型。这种方式的缺点显而易见,需要花费大量时间做特征的设计,并且在字体变化、模糊或背景干扰时泛化能力迅速下降。近年来随着深度学习的大放异彩,传统方法逐渐退出历史舞台,通过CNN自动提取字符特征,进而训练分类模型,一气呵成,并且泛化性强。
实现一个OCR系统的基本流程包括几个方面:预处理-> 行列切割 -> 字符识别 -> 后处理识别矫正。
首先要对图像进行预处理,比如角度矫正、去噪、图像还原等操作;然后对每一行文字按行进行分割,再对每一行文本进行列分割,切割出每个字符;第三步是将每个字符送入训练好的OCR识别模型进行字符识别;但是模型识别结果往往是不太准确的,我们还需要对识别结果进行矫正和优化,比如我们可以设计一个语法检测器,去检测字符的组合逻辑是否合理等(考虑单词Because,我们设计的识别模型有可能把它识别为8ecause,那么我们就可以用语法检测器去纠正这种拼写错误)。这样整个OCR流程就走完了。
实战
下面我将一步步带领大家手动实现一个自己的OCR系统。假设待识别的图像如下:
- 首先需要对图像进行预处理,比如上一讲谈到的水平矫正、透视矫正、去噪、二值化等操作。
- 紧接着是进行文字切割。切割算法分为两步,首先对图片进行水平投影,找到每一行的上界限和下界限,进行行切割;然后对切割出来的每一行,进行垂直投影,找到每一个字符的左右边界,进行单个字符的切割。文字切割的代码详见:text_split.py 注意:对英文字符的切割能够达到比较好的效果,这是因为英文字符大部分是连通体。而对于汉字字符的切割,其实很难做得很好,如上所示,会存在过切割(切割后一个字被拆成两个)和欠切割(多个字粘连在一起)的问题。对于欠切割问题,可以统计字符集合的大多数字符的尺寸,作为标准尺寸,根据标准尺寸对切割结果进行裁剪;对于过切割问题,通常在OCR识别阶段再把它修正,比如“刺”字被分为两部分了,那么我们就直接将这两个“字”送去识别,结果当然是得到一个置信度很低的一个反馈,那么我们就将这两个部分往他们身边最近的、而且没被成功识别的部分进行合并,再将这个合并后的字送进OCR识别。
- 训练OCR字符识别模型。这一步跟上一步没有依赖关系,可以并行进行。
训练模型之前,首先需要准备训练数据,需要哪些数据呢?52个英文字母(区分大小写)+ 14个汉字标点符号 + 常用的3500个汉字 + GB2312收录的3755个一级汉字(不在上面3500中的部分),先网上收集不同的字体文件,然后根据不同的字体分别生成这三千多个字符图片,最后还需要对生成的字符图片做一些图像增强工作,比如:文字扭曲、背景噪声(椒盐)、文字位置(设置文字的中心点)、笔画粘连(膨胀来模拟)、笔画断裂(腐蚀来模拟)、文字倾斜(文字旋转),以生成更多的训练数据,增强模型的泛化能力。最终生成的训练数据集如下所示: 生成数据集需要花几个小时的时间,可以直接去这里下载。
训练数据准备好了,下一步要做的就是利用这些数据集设计一个CNN网络做文字识别了。这里我们选用LeNet模型,模型图结构:conv2d->max_pool2d->conv2d->max_pool2d->conv2d->max_pool2d->conv2d->conv2d->max_pool2d->fully_connected->fully_connected。keep_prob = tf.placeholder(dtype=tf.float32, shape=[], name='keep_prob') images = tf.placeholder(dtype=tf.float32, shape=[None, 64, 64, 1], name='image_batch') labels = tf.placeholder(dtype=tf.int64, shape=[None], name='label_batch') is_training = tf.placeholder(dtype=tf.bool, shape=[], name='train_flag') conv3_1 = slim.conv2d(images, 64, [3, 3], 1, padding='SAME', scope='conv3_1') max_pool_1 = slim.max_pool2d(conv3_1, [2, 2], [2, 2], padding='SAME', scope='pool1') conv3_2 = slim.conv2d(max_pool_1, 128, [3, 3], padding='SAME', scope='conv3_2') max_pool_2 = slim.max_pool2d(conv3_2, [2, 2], [2, 2], padding='SAME', scope='pool2') conv3_3 = slim.conv2d(max_pool_2, 256, [3, 3], padding='SAME', scope='conv3_3') max_pool_3 = slim.max_pool2d(conv3_3, [2, 2], [2, 2], padding='SAME', scope='pool3') conv3_4 = slim.conv2d(max_pool_3, 512, [3, 3], padding='SAME', scope='conv3_4') conv3_5 = slim.conv2d(conv3_4, 512, [3, 3], padding='SAME', scope='conv3_5') max_pool_4 = slim.max_pool2d(conv3_5, [2, 2], [2, 2], padding='SAME', scope='pool4') flatten = slim.flatten(max_pool_4) fc1 = slim.fully_connected(slim.dropout(flatten, keep_prob), 1024, activation_fn=tf.nn.relu, scope='fc1') logits = slim.fully_connected(slim.dropout(fc1, keep_prob), FLAGS.charset_size, activation_fn=None, scope='fc2') loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels)) accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.argmax(logits, 1), labels), tf.float32)) optimizer = tf.train.AdamOptimizer(learning_rate=0.001) train_op = slim.learning.create_train_op(loss, optimizer) probabilities = tf.nn.softmax(logits)
至此,我们的OCR系统已经开发完毕,但是这仅仅只是一个理想环境下的OCR系统,对于一些复杂场景或是一些干扰比较大的文字图像,效果可能不会太理想,这就需要针对特定场景做进一步优化。
社群
- 微信公众号