1.引言
文本识别即对一张文本图像进行识别,将其中的文字转化为文本信息,这样才能变成计算机可以理解的语言。前面我们介绍了两种文本检测方法,请参见《CTPN文本检测与tensorflow实现》、《EAST文本检测与Keras实现》,在文本检测之后,我们可以获得了一张图像中各个文本的位置,这时,我们可以将各个文本片段剪切出来,进行仿射变换,得到类似图1这样的文本图像,但是,这时计算机还是没法理解图像中具体是什么文字,因此,需要进行文本识别,即将图像中的文本转化为纯文本,我们平时见到的验证码识别其实也是文字识别的一种场景。

在以往的文本识别模型中,习惯是采用一种滑动窗口的方式,逐步检测每个窗口下的文本,这种做法对于不同的字体、字体检测效果就特别差,特别对于中文文字的识别。然后也有一些模型采用对齐的方式,对图像的每一帧都进行文本标注,然后采用类似encoder-decoder这样的结构来进行文本识别,但是这样的做法需要耗费大量的人力进行对齐标注,特别是当文本前后带有空白字符时,标注起来就特别繁琐。因此,文本将介绍一个在文本识别中效果相对比较好的模型——CRNN,该模型不需要对图像进行对齐标注 ,直接输入文本图像,然后就可以输出对应的识别结果,而且准确率非常高!
2.模型介绍
2.1 模型结构介绍
CRNN的模型结构总共包含三部分,分别是卷积层、RNN层和转录层,如图2所示。

在卷积层部分,首先将每一张图像的高度固定在某一个值,然后对图像进行卷积操作,接着,对于卷积后得到的feature maps构建RNN层的输入特征序列,具体的操作就是,将这些feature maps从左到右每次取出一列,然后将每个feature map对应该列的向量进行拼接,拼接后的向量就作为RNN该时间步对应的特征输入。由于卷积后得到的feature maps每一列都对应原图的一个矩形区域,因此,按照这种操作得到的feature Sequence中每一个向量其实也是与原图的某个矩形区域相对应,并且这些矩形区域也是按照从左到右顺序排列的,因此,每个特征向量之间其实是带有时序关系的。如图3所示。

接着,是模型的RNN层部分,由前面我们知道,卷积层结束后得到的feature Sequence中,每个向量之间是具有时序关系的,不是独立的,因此,很自然就会想到用RNN来操作,作者在论文中采用的是深层双向递归神经网络,其中RNN单元采用的是LSTM单元,如图4所示。引入RNN主要有三个好处:①有些比较大的字符同时横跨多列,采用RNN可以记住前面序列的信息,另外,有些字符放在一起时,可以进行高度对比,更容易识别出其标签,比如‘i’和‘l’。②RNN可以将误差传递给CNN层,从而使得模型可以同时训练RNN和CNN的参数。③RNN可以解决文本序列变长的问题。

假设在卷积层得到的feature Sequence为,则对于每个时间步的输入
,RNN将输出该时间步对应的类别分布
,其中
的长度即为所有字符类别的长度。记RNN层得到的输出序列为
,其中T为序列的长度,其中,
表示第t个时间步的字符类别概率分布,
表示所有字符类别和空字符的集合。这里可能有人会觉得,既然已经输出了各个时间步的输出,那么可不可以像机器翻译那样,直接对输出序列的前后标记start和end字符,然后从输出里面进行截取,获得预测的标签序列,这么想是可以的,不过呢,就需要人为对整个图像每个时间步对应的感受野事先标记好其标签,会产生很繁琐的手工标注工作,因此,作者并没有这样操作,而是采用了一种转录方法,即模型中的转录层。
在转录层,作者引入了一个变换,即对于一个字符序列
,
变换会将其中的重复字符、空字符移除,得到最后的字符序列
,比如对于预测序列“--hh-e-l-ll-oo--”,其中“-”表示空字符,则经过
变换后得到的输出为“hello”,这里需要注意的是,当两个字符相同,并且中间隔着“-”时,则去重时不移除,因此,
的条件概率即为那些经过
变换后得到
的字符序列
的概率加总,具体表达式如下:
其中,为每个字符序列中每个字符概率的乘积,
表示第t个时间步为字符
的概率,但是,这种算法将非常耗时,因此,作者借鉴了CTC中的forward-backward的算法使其更有效率。
关于CTC中forward-backward的算法原理介绍可以参见我另一篇博文《CTC原理介绍》,这里不再具体展开。
转录的时候有两种方式,一种是无词典的转录方式,一种是基于词典的转录方式。
对于无词典的转录方式,其计算公式如下:
其实就是对每个时间步选择概率最大的字符,最后将该字符序列用变换得到对应的
。
对于基于词典的转录方式,其思想是构建一个词典集,然后计算词典中每个字符序列的概率,从中选择概率最大的作为最终的转录文本,其计算公式如下:
其中,即为构建的词典集,基于这种计算方法有个缺点,就是当词典集比较大时,计算复杂度比较大,因此,作者提出了一种改进方法,作者发现基于无词典的转录方式其实与真实的标签很接近,因此,作者首先采用无词典的转录方式获得转录文本
,然后用BK-tree从词典集中搜索与它编辑距离(有关编辑距离的概念可以参考这篇文章:《Edit Distance(编辑距离)》)小于
的词典,记为
,然后再从近邻词典里面计算每个字符序列的概率,选择概率最大的作为最后的转录文本,其计算公式如下:
2.2 模型的损失函数
CRNN的损失函数采用的是负对数似然函数,记训练集为,其中,
表示输入的图像,
表示真实的字符序列,则对应的损失函数为:
3.tensorflow实现
本文采用tensorflow对CRNN原理进行复现,项目的结构如图5所示,下面将对每个模块进行具体介绍。

首先是data路径,存放的是训练集和测试集,train_images存放的是训练时的数据集,test_images存放的是测试时的数据集,本文的数据有两种来源,一种是ICPR比赛数据集,一种是模拟的数据集。

dict下存放的是字符集文档,有三种可以选择,chinese.txt存放的是中文常用3000字,english.txt存放的是英文字母以及一些标点符号,而english_chinese.txt则是前面两个文档的集合,当选择english_chinese.txt时,则支持对中英文的文本识别,本文训练时默认使用的是english_chinese.txt。

fonts路径存放的是生成模拟数据时的字体文件,window系统一般可以在C:\Windows\Fonts下查找,这个可以自己选择字体文件。

images_base存放的是模拟数据的背景图像,models文件夹存放的是训练后的模型文件。接着,是各个py脚本文件的功能介绍,其中,charset_generate.py,该脚本存放的是字符集文本生成函数,从图像的label中提取字符集合,存生成charset.txt存放在data路径下。其代码如下:
import tqdm
from crnn import config as crnn_config
def generate_charset(labels_path, charset_path):
"""
generate char dictionary with text label
:param labels_path:label_path: path of your text label
:param charset_path: path for restore char dict
:return:
"""
with open(labels_path, 'r', encoding='utf-8') as fr:
lines = fr.read().split('\n')
dic = str()
for label in tqdm.tqdm(lines[:-1]):
for char in label:
if char in dic:
continue
else:
dic += char
with open(charset_path, 'w', encoding='utf-8')as fw:
fw.write(dic)
if __name__ == '__main__':
label_path = crnn_config.train_label_path
char_dict_path = crnn_config.charset_path
generate_charset(label_path, char_dict_path)
然后是data_provider.py文件,该文件一方面用于从自然场景图像中对文本进行切割,然后进行放射片段,并保存到data下的训练集和测试集路径下,用于训练和测试时使用,另一方面用于生成模拟的数据,模拟的数据也同样会存放在训练集路劲下。
import os
import cv2
import math
import random
import shutil
import numpy as np
from tqdm import trange
from collections import Counter
from crnn import charset_generate
from multiprocessing import Process
from crnn import config as crnn_config
from PIL import Image, ImageDraw, ImageFont
class TextCut(object):
def __init__(self,
org_images_path,
org_labels_path,
cut_train_images_path,
cut_train_labels_path,
cut_test_images_path,
cut_test_labels_path,
train_test_ratio=0.8,
filter_ratio=1.5,
filter_height=25,
is_transform=True,
angle_range=[-15.0, 15.0],
write_mode='w',
use_blank=False,
num_process=1):
"""
对ICPR原始图像进行切图
:param org_images_path: ICPR数据集原始图像路径,[str]
:param org_labels_path: ICPR数据集原始label路径,[str]
:param cut_train_images_path: 训练集切图的保存路径,[str]
:param cut_train_labels_path: 训练集切图对应label的保存路径,[str]
:param cut_test_images_path: 测试集切图的保存路径