1 研究背景
人工智能是研究开发能够模拟、延伸和扩展人类智能的理论、方法、技术及应用系统的一门新的技术科学,研究目的是促使智能机器会听(语音识别、机器翻译等)、会看(图像识别、文字识别等)、会说(语音合成、人机对话等)、会思考(人机对弈、定理证明等)、会学习(机器学习、知识表示等)、会行动(机器人、自动驾驶汽车等)。
人工智能充满未知的探索道路曲折起伏。如何描述人工智能自1956年以来60余年的发展历程,学术界可谓仁者见仁、智者见智。通过查阅资料将人工智能的发展历程划分为以下6个阶段:
一是起步发展期:1956年—20世纪60年代初。人工智能概念提出后,相继取得了一批令人瞩目的研究成果,如机器定理证明、跳棋程序等,掀起人工智能发展的第一个高潮。
二是反思发展期:20世纪60年代—70年代初。人工智能发展初期的突破性进展大大提升了人们对人工智能的期望,人们开始尝试更具挑战性的任务,并提出了一些不切实际的研发目标。然而,接二连三的失败和预期目标的落空(例如,无法用机器证明两个连续函数之和还是连续函数、机器翻译闹出笑话等),使人工智能的发展走入低谷。
三是应用发展期:20世纪70年代初—80年代中。20世纪70年代出现的专家系统模拟人类专家的知识和经验解决特定领域的问题,实现了人工智能从理论研究走向实际应用、从一般推理策略探讨转向运用专门知识的重大突破。专家系统在医疗、化学、地质等领域取得成功,推动人工智能走入应用发展的新高潮。
四是低迷发展期:20世纪80年代中—90年代中。随着人工智能的应用规模不断扩大,专家系统存在的应用领域狭窄、缺乏常识性知识、知识获取困难、推理方法单一、缺乏分布式功能、难以与现有数据库兼容等问题逐渐暴露出来。
五是稳步发展期:20世纪90年代中—2010年。由于网络技术特别是互联网技术的发展,加速了人工智能的创新研究,促使人工智能技术进一步走向实用化。1997年国际商业机器公司(简称IBM)深蓝超级计算机战胜了国际象棋世界冠军卡斯帕罗夫,2008年IBM提出“智慧地球”的概念。以上都是这一时期的标志性事件。
六是蓬勃发展期:2011年至今。随着大数据、云计算、互联网、物联网等信息技术的发展,泛在感知数据和图形处理器等计算平台推动以深度神经网络为代表的人工智能技术飞速发展,大幅跨越了科学与应用之间的“技术鸿沟”,诸如图像分类、语音识别、知识问答、人机对弈、无人驾驶等人工智能技术实现了从“不能用、不好用”到“可以用”的技术突破,迎来爆发式增长的新高潮。
2 OCR文字识别
2.1什么是OCR?
图 自然场景OCR文字识别(无乎648水印为刘世鹏博客自带,后续也是)
OCR英文全称是Optical Character Recognition,中文叫做光学字符识别。它是利用光学技术和计算机技术把印在或写在纸上的文字读取出来,并转换成一种计算机能够接受、人又可以理解的格式。文字识别是计算机视觉研究领域的分支之一,而且这个课题已经是比较成熟了,并且在商业中已经有很多落地项目了。比如汉王OCR,百度OCR,阿里OCR等等,很多企业都有能力都是拿OCR技术开始挣钱了。其实我们自己也能感受到,OCR技术确实也在改变着我们的生活:比如一个手机APP就能帮忙扫描名片、身份证,并识别出里面的信息;汽车进入停车场、收费站都不需要人工登记了,都是用车牌识别技术;我们看书时看到不懂的题,拿个手机一扫,APP就能在网上帮你找到这题的答案。太多太多的应用了,OCR的应用在当今时代确实是百花齐放。
2.2 OCR的分类
如果要给OCR进行分类分为两类:手写体识别和印刷体识别。
这两个可以认为是OCR领域两个大主题了,当然印刷体识别较手写体识别要简单得多,也能从直观上理解,印刷体大多都是规则的字体,因为这些字体都是计算机自己生成再通过打印技术印刷到纸上。在印刷体的识别上有其独特的干扰:在印刷过程中字体很可能变得断裂或者墨水粘连,使得OCR识别异常困难。当然这些都可以通过一些图像处理的技术帮他尽可能的还原,进而提高识别率。总的来说,单纯的印刷体识别在业界已经能做到很不错了,但说100%识别是肯定不可能的,但是说识别得不错那是没毛病。
图 手写字体展示
印刷体已经识别得不错了,那么手写体呢?手写体识别一直是OCR界一直想攻克的难关,但是时至今天,感觉这个难关还没攻破,还有很多学者和公司在研究。为什么手写体识别的难度在于因为人类手写的字往往带有个人特色,每个人写字的风格基本不一样,印刷体一般都比较规则,字体都基本就那几十种,机器学习这几十种字体并不是一件难事,但是手写体,每个人都有一种字体的话,那机器该学习大量字体,这就是难度所在。
2.3 OCR流程
假如输入系统的图像是一页文本,那么识别时的第一件事情是判断页面上的文本朝向,因为得到的这页文档往往都不是很完美的,很可能带有倾斜或者污渍,那么要做的第一件事就是进行图像预处理,做角度矫正和去噪。然后要对文档版面进行分析,进每一行进行行分割,把每一行的文字切割下来,最后再对每一行文本进行列分割,切割出每个字符,将该字符送入训练好的OCR识别模型进行字符识别,得到结果。但是模型识别结果往往是不太准确的,需要对其进行识别结果的矫正和优化,比如可以设计一个语法检测器,去检测字符的组合逻辑是否合理。比如,考虑单词Because,设计的识别模型把它识别为8ecause,那么就可以用语法检测器去纠正这种拼写错误,并用B代替8并完成识别矫正。这样子,整个OCR流程就走完了。从大的模块总结而言,一套OCR流程可以分为:
从上面的流程图可以看出,要做字符识别并不是单纯一个OCR模块就能实现的(如果单纯的OCR模块,识别率相当低),都要各个模块的组合来保证较高的识别率。上面的流程分的比较粗,每个模块下还是有很多更细节的操作,每个操作都关系着最终识别结果的准确性。做过OCR的童鞋都知道,送入OCR模块的图像越清晰(即预处理做的越好),识别效果往往就越好。那现在对这流程中最为重要的字符识别技术做一个总结。
2.4 OCR的简单应用
图 瓶盖的生产日期识别
在一些简单环境下OCR的准确度已经比较高了(比如电子文档),但是在一些复杂环境下的字符识别,在当今还没有人敢说自己能做的很好。现在大家都很少会把目光还放在如何对电子文档的文字识别该怎么进一步提高准确率了,因为他们把目光放在更有挑战性的领域。OCR传统方法在应对复杂图文场景的文字识别显得力不从心,越来越多人把精力都放在研究如何把文字在复杂场景读出来,并且读得准确作为研究课题,用学界术语来说,就是场景文本识别(文字检测+文字识别)。
对图片进行水平投影,找到每一行的上界限和下界限,进行行切割
对切割出来的每一行,进行垂直投影,找到每一个字符的左右边界,进行单个字符的切割。
首先是行切割。这里提到了水平投影的概念。水平投影,就是对一张图片的每一行元素进行统计(就是往水平方向统计),然后根据这个统计结果画出统计结果图,进而确定每一行的起始点和结束点。下面提到的垂直投影也是类似的,只是它的投影方向是往下的,即统计每一列的元素个数。
根据上面的解释,可以写出一个用于水平投影和垂直投影的函数。
#define V_PROJECT 1 //垂直投影(vertical)
#define H_PROJECT 2 //水平投影(horizational)
typedef struct
{
int begin;
int end;
}char_range_t;
//获取文本的投影以用于分割字符(垂直,水平),默认图片是白底黑色
int GetTextProjection(Mat& src, vector<int>& pos, int mode)
{
if (mode == V_PROJECT)
{
for (int i = 0; i < src.rows; i++)
{
uchar* p = src.ptr<uchar>(i);
for (int j = 0; j < src.cols; j++)
{
if (p[j] == 0) //是黑色像素
{
pos[j]++;
}
}
}
}
else if (mode == H_PROJECT)
{
for (int i = 0; i < src.cols; i++)
{
for (int j = 0; j < src.rows; j++)
{
if (src.at<uchar>(j, i) == 0)
{
pos[j]++;
}
}
}
}
return 0;}
下面是画出水平(垂直)投影图的代码实现。
void draw_projection(vector<int>& pos, int mode)
{
vector<int>::iterator max = std::max_element(std::begin(pos), std::end(pos)); //求最大值
if (mode == H_PROJECT)
{
int height = pos.size();
int width = *max;
Mat project = Mat::zeros(height, width, CV_8UC1);
for (int i = 0; i < project.rows; i++)
{
for (int j = 0; j < pos[i]; j++)
{
project.at<uchar>(i, j) = 255;
}
}
imshow("horizational projection", project);
}
else if (mode == V_PROJECT)
{
int height = *max;
int width = pos.size();
Mat project = Mat::zeros(height, width, CV_8UC1);
for (int i = 0; i < project.cols; i++)
{
for (int j = project.rows - 1; j >= project.rows - pos[i]; j--)
{
//std::cout << "j:" << j << "i:" << i << std::endl;
project.at<uchar>(j, i) = 255;
}
}
imshow("vertical projection", project);
}
waitKey();
}
投影图:
图 行分割投影图
通过上面的水平投影图,很容易就能确定每一行文字的位置,确定的思路如下:可以以每个小山峰的起始结束点作为文本行的起始结束点,当然要对这些山峰做些约束,比如这些山峰的跨度不能太小。这样子就得到每一个文本行的位置,接着就根据这些位置将每个文本行切割下来用于接下来的单个字符的切割。
//获取每个分割字符的范围,min_thresh:波峰的最小幅度,min_range:两个波峰的最小间隔
int GetPeekRange(vector<int>& vertical_pos, vector<char_range_t>& peek_range, int min_thresh = 2, int min_range = 10)
{
int begin = 0;
int end = 0;
for (int i = 0; i < vertical_pos.size(); i++)
{
if (vertical_pos[i] > min_thresh && begin == 0)
{
begin = i;
}
else if (vertical_pos[i] > min_thresh && begin != 0)
{
continue;
}
else if (vertical_pos[i] < min_thresh && begin != 0)
{
end = i;
if (end - begin >= min_range)
{
char_range_t tmp;
tmp.begin = begin;
tmp.end = end;
peek_range.push_back(tmp);
begin = 0;
end = 0;
}
}
else if (vertical_pos[i] < min_thresh || begin == 0)
{
continue;
}
else
{
//printf("raise error!\n");
}
}
return 0;
}
图 人工智能课本识别图
3 文字数据加强
首先是定义好输入参数,其中包括输出目录、字体目录、测试集大小、图像尺寸、图像旋转幅度等等。
def args_parse() :
#解析输入参数
parser = argparse.ArgumentParser(
description = description, formatter_class = RawTextHelpFormatter)
parser.add_argument('--out_dir', dest = 'out_dir',
default = None, required = True,
help = 'write a caffe dir')
parser.add_argument('--font_dir', dest = 'font_dir',
default = None, required = True,
help = 'font dir to to produce images')
parser.add_argument('--test_ratio', dest = 'test_ratio',
default = 0.2, required = False,
help = 'test dataset size')
parser.add_argument('--width', dest = 'width',
default = None, required = True,
help = 'width')
parser.add_argument('--height', dest = 'height',
default = None, required = True,
help = 'height')
parser.add_argument('--no_crop', dest = 'no_crop',
default = True, required = False,
help = '', action = 'store_true')
parser.add_argument('--margin', dest = 'margin',
default = 0, required = False,
help = '', )
parser.add_argument('--rotate', dest = 'rotate',
default = 0, required = False,
help = 'max rotate degree 0-45')
parser.add_argument('--rotate_step', dest = 'rotate_step',
default = 0, required = False,
help = 'rotate step for the rotate angle')
parser.add_argument('--need_aug', dest = 'need_aug',
default = False, required = False,
help = 'need data augmentation', action = 'store_true')
args = vars(parser.parse_args())
return args
接下来需要将第一步得到的对应表读入内存,因为这个表示ID到汉字的映射,在做一下转换,改成汉字到ID的映射,用于后面的字体生成。
#将汉字的label读入,得到(ID:汉字)的映射表label_dict
label_dict = get_label_dict()
char_list = [] # 汉字列表
value_list = [] # label列表
for (value, chars) in label_dict.items() :
print(value, chars)
char_list.append(chars)
value_list.append(value) # 合并成新的映射关系表:(汉字:ID)
lang_chars = dict(zip(char_list, value_list))
font_check = FontCheck(lang_chars)
对旋转的角度存储到列表中,旋转角度的范围是[-rotate,rotate]。
if rotate < 0:
roate = -rotate
if rotate > 0 and rotate <= 45:
all_rotate_angles = []
for i in range(0, rotate + 1, rotate_step) :
all_rotate_angles.append(i)
for i in range(-rotate, 0, rotate_step) :
all_rotate_angles.append(i)
#print(all_rotate_angles)
现在说一下字体图像是怎么生成的,首先使用的工具是PIL。PIL里面有很好用的汉字生成函数,我们用这个函数再结合我们提供的字体文件,就可以生成我们想要的数字化的汉字了。我们先设定好我们生成的字体颜色为黑底白色,字体尺寸由输入参数来动态设定。
生成字体图像
```python
class Font2Image(object) :
def __init__(self,
width, height,
need_crop, margin) :
self.width = width
self.height = height
self.need_crop = need_crop
self.margin = margin
def do(self, font_path, char, rotate = 0) :
find_image_bbox = FindImageBBox()
# 黑色背景
img = Image.new("RGB", (self.width, self.height), "black")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype(font_path, int(self.width * 0.7), )
# 白色字体
draw.text((0, 0), char, (255, 255, 255),
font = font)
if rotate != 0:
img = img.rotate(rotate)
data = list(img.getdata())
sum_val = 0
for i_data in data :
sum_val += sum(i_data)
if sum_val > 2:
np_img = np.asarray(data, dtype = 'uint8')
np_img = np_img[:, 0]
np_img = np_img.reshape((self.height, self.width))
cropped_box = find_image_bbox.do(np_img)
left, upper, right, lower = cropped_box
np_img = np_img[upper:lower + 1, left : right + 1]
if not self.need_crop:
preprocess_resize_keep_ratio_fill_bg = \
PreprocessResizeKeepRatioFillBG(self.width, self.height,
fill_bg = False,
margin = self.margin)
np_img = preprocess_resize_keep_ratio_fill_bg.do(
np_img)
# cv2.imwrite(path_img, np_img)
return np_img
else:
print("img doesn't exist.")
额外的图像增强
第三步生成的汉字图像是最基本的数据集,它所做的图像处理仅有旋转这么一项,如果想在数据增强上再做多点东西,想必最终训练出来的OCR模型的性能会更加优秀。使用opencv来完成定制的汉字图像增强任务。
因为生成的图像比较小,仅仅是30*30,如果对这么小的图像加噪声或者形态学处理,得到的字体图像会很糟糕,所以在做数据增强时,把图片尺寸适当增加,比如设置为100×100,再进行相应的数据增强,效果会更好。
噪音增加
```python
def add_noise(cls, img) :
for i in range(20) : #添加点噪声
temp_x = np.random.randint(0, img.shape[0])
temp_y = np.random.randint(0, img.shape[1])
img[temp_x][temp_y] = 255
return img
适当腐蚀
def add_erode(cls, img) :
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img = cv2.erode(img, kernel)
return img
适当膨胀
def add_dilate(cls, img) :
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
img = cv2.dilate(img, kernel)
return img
然后做随机扰动
def do(self, img_list = []) :
aug_list = copy.deepcopy(img_list)
for i in range(len(img_list)) :
im = img_list[i]
if self.noiseand random.random() < 0.5 :
im = self.add_noise(im)
if self.dilateand random.random() < 0.25 :
im = self.add_dilate(im)
if self.erodeand random.random() < 0.25 :
im = self.add_erode(im)
aug_list.append(im)
return aug_list
使用这种生成的图像如下图所示,第一数据集扩大了两倍,第二图像的丰富性进一步提高,效果还是明显的。当然,如果要获得最好的效果,还需要调一下里面的参数。
图 增加噪音后的数据
图 采集的试卷上的数据集
上图为通过试卷上的手写文字采集到数据集,通过文字定位与分割,将分割后的文字进行人工标注分类。
4 文本检测CTPN
2016年出了一篇很有名的文本检测的论文:《Detecting Text in Natural Image withConnectionist Text Proposal Network》,这个深度神经网络叫做CTPN,直到今天这个网络框架一直是OCR系统中做文本检测的一个常用网络,极大地影响了后面文本检测算法的方向。
回顾一下Faster RCNN做目标检测的一个缺点就是,没有考虑带文本自身的特点。文本行一般以水平长矩形的形式存在,而且文本行中每个字都有间隔。针对这个特点,CTPN剔除一个新奇的想法,把文本检测的任务拆分,第一步检测文本框中的一部分,判断它是不是一个文本的一部分,当对一幅图里所有小文本框都检测之后,将属于同一个文本框的小文本框合并,合并之后得到一个完整的、大的文本框了,也就完成了文本的检测任务。这个想法很有创造性,有点像“分治法”,先检测大物体的一小部分,等所有小部分都检测出来,大物体也就可以检测出来了。
图 RPN和CTPN对比
如图所示,左边的图是直接使用Faster RCNN中的RPN来进行候选框提取,可以看出,这种候选框太粗糙了,效果并不好。而右图是利用许多小候选框来合并成一个大文本预测框,可以看出这个算法的效果非常不错,需要说明的是,红色框表示这个小候选框的置信度比较高,而其他颜色的候选框的置信度比较低,可以看到,一个大文本的边界都是比较难预测的,那怎么解决这个边界预测不准的问题呢?后面会提到。
刚提到CTPN的其中一个闪光点,即检测小框代替直接检测大文本框。除了这个新意,CTPN还提出了在文本检测中应加入RNN来进一步提升效果。为什么要用RNN来提升检测效果?文本具有很强的连续字符,其中连续的上下文信息对于做出可靠决策来说很重要。RNN常用于序列模型,比如事件序列,语言序列等等,那CTPN算法中,把一个完整的文本框拆分成多个小文本框集合,其实这也是一个序列模型,可以利用过去或未来的信息来学习和预测,所以同样可以使用RNN模型。而且,在CTPN中,用的还是BiLSTM(双向LSTM),因为一个小文本框,对于它的预测,不仅与其左边的小文本框有关系,而且还与其右边的小文本框有关系!这个解释就很有说服力了,如果仅仅根据一个文本框的信息区预测该框内含不含有文字其实是很草率的,应该多参考这个框的左边和右边的小框的信息后(尤其是与其紧挨着的框)再做预测准确率会大大提升。
图 CTPN候选框
如上图所示,如果单纯依靠1号框内的信息来直接预测1号框中否存在文字(或者说是不是文本的一部分),其实难度相当大,因为1号框只包含文字的很小一部分。但是如果把2号框和3号框的信息都用上,来预测1号框是否存在文字,那么就会有比较大的把握来预测1号框确实有文字。还可以看看为什么边缘的文本框的置信度会较中间的低呢?个人认为很大一部分原因就在于因为这些框都位于总文本的边缘,没有办法充分利用左右相邻序列的信息做预测(比如位于最左的文本框丢失了其右边的信息)。这就是双向LSTM的作用,把左右两个方向的序列信息都加入到学习的过程中去。
CTPN借助了Faster RCNN中anchor回归机制,使得RPN能有效地用单一尺寸的滑动窗口来检测多尺寸的物体。当然CTPN根据文本检测的特点做了比较多的创新。比如RPN中anchor机制是直接回归预测物体的四个参数(x,y,w,h),但是CTPN采取之回归两个参数(y,h),即anchor的纵向偏移以及该anchor的文本框的高度,因为每个候选框的宽度w已经规定为16个像素,不需要再学习,而x坐标直接使用anchor的x坐标,也不用学习,所以CTPN的思路就是只学习y和h这两个参数来完成小候选框的检测!跟RPN相类似,CTPN中对于每个候选框都使用了K个不同的anchors(k在这里默认是10),但是与RPN不同的是,这里的anchors的width是固定的16个像素,而height的高度范围为11~273(每次对输入图像的height除以0.7,一共K个高度)。当然CTPN中还是保留了RPN大多数的思路,比如还是需要预测候选框的分数score(该候选框有文本和无文本的得分)。
文本行构建很简单,通过将那些text/no-text score > 0.7的连续的text proposals相连接即可。文本行的构建如下。首先,为一个proposal Bi定义一个邻居(Bj):Bj−>Bi,其中,Bj在水平距离上离Bi最近,该距离小于50 pixels它们的垂直重叠(vertical overlap) > 0.7,另外,如果同时满足Bj−>Bi和Bi−>Bj,会将两个proposals被聚集成一个pair。接着,一个文本行会通过连续将具有相同proposal的pairs来进行连接来构建。
图 CTPN网络架构
首先CTPN的基础网络使用了VGG16用于特征提取,在VGG的最后一个卷积层CONV5,CTPN用了3×3的卷积核来对该feature map做卷积,这个CVON5 特征图的尺寸由输入图像来决定,而卷积时的步长却限定为16,感受野被固定为228个像素。卷积后的特征将送入BLSTM继续学习,最后接上一层全连接层FC输出要预测的参数:2K个纵向坐标y,2k个分数,k个x的水平偏移量。看到这里大家可能有个疑问,这个x的偏移到底是什么,为什么需要回归这个参数?如果需要X的参数,为什么不在候选框参数回归时直接预测成(x,y,h)三个参数呢,而要多此一举把该参数单独预测,这个X的作用作者提到这也是他们论文的一大亮点,称之为Side-refinement,可以理解为文本框边缘优化。回顾一下上面提到的一个问题,文本框检测中边缘部分的预测并不准确。那么改咋办,CTPN就是用这个X的偏移量来精修边缘问题。这个X是指文本框在水平方向的左边界和右边界,通过回归这个左边界和右边界参数进而可以使得对文本框的检测更为精准。在这里想举个例子说明一下回归这个x参数的重要性。
通过观察下图,第一幅图张看到有很多小候选框,位于左边的候选框我标记为1、2、3、4号框,1号框和2号框为蓝色,表明得分不高就不把这两个框合并到大文本框内,对于3号框和4号框那就比较尴尬了,如果取3号框作为文本框的边缘框,那么显然左边边缘留白太多,精准度不够,但如果去掉3号框而使用4号框作为左边缘框,则有些字体区域没有检测出来,同样检测精度不足。这种情况其实非常容易出现,所以CTPN采取了Side-refinement 思路进一步优化边缘位置的预测即引入回归X参数,X参数直接标定了完整文本框的左右边界,做到精确的边界预测。第二幅图中的红色框就是经过Side-refinement后的检测结果,可以看出检测准确率有了很大的提升。 side-refinement确实可以进一步提升位置准确率,在SWT的Multi-Lingual datasets上产生2%的效果提升。
再看多几幅图,体验一下Side-refinement后的效果。
最后总结一下CTPN这个流行的文本检测框架的三个闪光点:
将文本检测任务转化为一连串小尺度文本框的检测;
引入RNN提升文本检测效果;
Side-refinement(边界优化)提升文本框边界预测精准度。
当然,CTPN也有一个很明显的缺点:对于非水平的文本的检测效果并不好。CTPN论文中给出的文本检测效果图都是文本位于水平方向的,显然CTPN并没有针对多方向的文本检测有深入的探讨。
5 运行效果展示
6总结
通过查阅相关资料学习什么是OCR并且查阅了如何实现OCR文字识别中的文字检测,并且通过理论实现流程并且用代码对CPTN文字检测进行复现。通过对文字数据预处理并且进行文字字符分割,看似简单,做起来其实很难做得很好,我们也对此查阅了很多论文,发现其实很多论文也谈到了,汉字确实很那做到一个高正确率的分割,直至现在还没有一统江湖的解决方案。汉字切割的失败,就会直接导致了后面OCR识别的失败,这也是当前很多一些很厉害的OCR公司都没法把汉字做到100%识别的一个原因。所以这个问题就必须得到很好的解决。最后我们解决汉字切割的较好方法是,在OCR识别中再把它修正。并且通过文字分割后对数据进行数据增强生成了大量数据防止模型的过拟合。并且通过学习CPTN论文,学习到了思路上的创新,在检测水平上的文字置信度很高,但是也有一些弊端对于非水平的文本检测效果并不好。
7引用文献
[1] Detecting Text in Natural Image with Connectionist Text Proposal Network.作者: Zhi Tian; Weilin Huang; Tong He; Pan He; Yu Qiao 0001
[2]基于深度学习的汉字识别方法研究[D].任凤丽.东华大学. 2021
[3]基于深度学习的光学字符识别技术研究[D].冯亚南.南京邮电大学 2020
[4]基于卷积神经网络的手写数字识别研究与设计[D].刘辰雨.成都理工大学2018
[5]基于CRNN的中文手写识别方法研究[J]. 石鑫,董宝良,王俊丰.信息技术. 2019(11)