ocr文字识别技术总结(代码片段)

GoAI GoAI     2023-03-09     636

关键词:

📝导读:在上一篇文章中我们对文字识别算法理论部分进行详细总结,本篇将继续介绍文字识别CRNN网络实战部分,下面将从CRNN实践代码出发,进一步说明文字识别实战流程,具体分为算法介绍、代码解读、项目实战等几个部分。

本系列目录:

1️⃣OCR系列第一章:OCR文字识别技术总结(一)
2️⃣OCR系列第二章:OCR文字识别技术总结(二)
3️⃣OCR系列第三章:OCR文字识别技术总结(三)
4️⃣OCR系列第四章:OCR文字识别技术总结(四)
5️⃣OCR系列第五章:OCR文字识别技术总结(五)

文本识别实战

一、CRNN网络介绍

CRNN论文链接:An End-to-End Trainable Neural Network for Image-based Sequence Recognition and Its Application to Scene Text Recognition

CRNN学习博客参考:CRNN文本识别网络详解

CRNN是最早一批采用CNN与RNN结合的方式进行自然场景图片识别的基于深度学习的算法。文中提出的CRNN算法是一种能将特征提取、序列建模和转录整合到统一框架中的新型神经网络架
构。与之前的场景文本识别系统相比,该架构表现出几个不同的特点:
1)与之前的通过部分到整体的算法相比,可以进行端到端的训练,而 不是各个部分单独训练(字的特征部分与标签预测部分等分别进行训练);
2)借用了在自然语言处理模型中序列标注(Sequence Labeling) 任务的思想,将序列标注算法嵌套在现有的深度卷积网络中,组成完整的支持端到端梯度反向传播的算法;
3)在论文设计的实验中,该算法 在IIIT5K、SVT以及ICDAR系列的标准数据集中取得了优于现有算法的结果。

CRNN使用CNN提取图像特征,RNN进行序列推理,配合CTC的不定长字符识别,是文本和语音识别的一个重要模型。

首先,从图中看出模型包括三个部分,分别为卷积层、循环层以及转录层。从下到上依次为:
(1)卷积层。作用是从输入图像中提取特征序列。
(2)循环层。作用是预测从卷积层获取的特征序列的标签(真实值)分布。
(3)转录层。作用是把从循环层获取的标签分布通过去重整合等操作转换成最终的识别结果。

图1 CRNN网络结构图

二、推理流程:

1.首先对图像进行预处理,高度必须为16的倍数(这里为32),将输入图像缩放至:32W3
2.之后利用CNN提取后图像卷积特征,得到的大小为:1 W/4 512
3.以seq_len=W/4, input_size=512送入LSTM,提取序列特征,得到:W/4*n的后验概率矩阵
4最后利用CTC,使标签和输出无需一一对应,也能进行训练。

注:RNN最后的嵌入层的输出维度为我们总共要预测的字符数+1(blank),最后的输出可以认为是一种概率,最后进行解码即可。

三、CRNN构成:

1.卷积层

① 预处理

CRNN对输入图像先做了缩放处理,把所有输入图像缩放到相同高度,默认是32,宽度可任意长。

原因:CRNN模型中的卷积层由一系列的卷积层、池化层、BN层构造而成。就像其他的CNN模型一样,它将输入的图片转化为具有特征信息的特征图,作为后面循环层的输入。首先,为了使提取的特征图尺寸相同,输入的图像要缩放到固定的大小。

由于卷积神经网络中卷积层和最大池化层的存在,使其具有平移不变性的特点。卷积神经网络中的感受野指的是经过卷积层输出的特征图中每个像素对应的原输入图像区域的大小,它与特征图上的像素从左到右,从上到下是一一对应的,如图所示。因此,可以将特征图作为图像特征的表示。

② 卷积运算

CRNN的卷积层具体的网络结构如图所示,它是在VGG网络的基础上改造而成。卷积层对于VGG主要对两个地方进行了改动:

  • 将第2层和第3层的MaxPooling的卷积核的大小从 改成了 。
  • 第5层和第6层的卷积层后面都添加了一个BN(BatchNormalization)层。因为BN层可以对输入数据进行归一化,加速网络的收敛速度。

CRNN共包含7层卷积层,2层双向LSTM,输入的图像为灰度图。值得注意的是,网络在对特征降维的时候最大值池化采用的窗口高度固定为2,这就意味着每次池化高度都会减少一半,经过5次池化,高度缩减为1,宽度为原图长度的1/4。因此,序列的长度必须超过图片中单词的长度,这样才能够预测出完整的词语。

2.循环层

循环层由一个双向LSTM循环神经网络构成,预测特征序列中的每一个特征向量的标签分布。

由于LSTM需要有个时间维度,在本模型中把序列的 width 当作LSTM 的时间 time steps。

其中,“Map-to-Sequence”自定义网络层主要是做循环层误差反馈,与特征序列的转换,作为卷积层和循环层之间连接的桥梁,从而将误差从循环层反馈到卷积层。

3.转录层

转录层是将LSTM网络预测的特征序列的结果进行整合,转换为最终输出的结果。

在CRNN模型中双向LSTM网络层的最后连接上一个CTC模型,从而做到了端对端的识别。所谓CTC模型(Connectionist Temporal Classification,联接时间分类),主要用于解决输入数据与给定标签的对齐问题,可用于执行端到端的训练,输出不定长的序列结果。

由于输入的自然场景的文字图像,由于字符间隔、图像变形等问题,导致同个文字有不同的表现形式,但实际上都是同一个词,如下图:

而引入CTC就是主要解决这个问题,通过CTC模型训练后,对结果中去掉间隔字符、去掉重复字符(如果同个字符连续出现,则表示只有1个字符,如果中间有间隔字符,则表示该字符出现多次),如下图所示:

四、CRNN代码讲解

CRNN DEMO代码:

注:项目模型参数一般都存储在config文件下,可以按需修改。

(1)数据预处理及制作:

主要是对于labletxt的处理,定义strLabelConverter类,将两者进行转换,encode是将str转化为lable;decod将lable转化为str,decode中用到了ctc中的对应规则。

class strLabelConverter(object):
    """Convert between str and label.

    NOTE:
        Insert `blank` to the alphabet for CTC.

    Args:
        alphabet (str): set of the possible characters.
        ignore_case (bool, default=True): whether or not to ignore all of the case.
    """

    def __init__(self, alphabet, ignore_case=False):
        self._ignore_case = ignore_case
        if self._ignore_case:
            alphabet = alphabet.lower()
        self.alphabet = alphabet + '-'  # for `-1` index

        self.dict = 
        for i, char in enumerate(alphabet):
            # NOTE: 0 is reserved for 'blank' required by wrap_ctc
            self.dict[char] = i + 1

    def encode(self, text):
        """Support batch or single str.

        Args:
            text (str or list of str): texts to convert.

        Returns:
            torch.IntTensor [length_0 + length_1 + ... length_n - 1]: encoded texts.
            torch.IntTensor [n]: length of each text.
        """

        length = []
        result = []
        decode_flag = True if type(text[0])==bytes else False

        for item in text:

            if decode_flag:
                item = item.decode('utf-8','strict')
            length.append(len(item))
            for char in item:
                index = self.dict[char]
                result.append(index)
        text = result
        return (torch.IntTensor(text), torch.IntTensor(length))

    def decode(self, t, length, raw=False):
        """Decode encoded texts back into strs.

        Args:
            torch.IntTensor [length_0 + length_1 + ... length_n - 1]: encoded texts.
            torch.IntTensor [n]: length of each text.

        Raises:
            AssertionError: when the texts and its length does not match.

        Returns:
            text (str or list of str): texts to convert.
        """
        if length.numel() == 1:
            length = length[0]
            assert t.numel() == length, "text with length:  does not match declared length: ".format(t.numel(), length)
            if raw:
            	# ''.join将序列中的元素以指定的字符连接生成一个新的字符串。
                return ''.join([self.alphabet[i - 1] for i in t])
            else:
                char_list = []
                for i in range(length):
                    if t[i] != 0 and (not (i > 0 and t[i - 1] == t[i])):
                        char_list.append(self.alphabet[t[i] - 1])
                return ''.join(char_list)
        else:
            # batch mode
            assert t.numel() == length.sum(), "texts with length:  does not match declared length: ".format(t.numel(), length.sum())
            texts = []
            index = 0
            for i in range(length.numel()):
                l = length[i]
                texts.append(
                    self.decode(
                        t[index:index + l], torch.IntTensor([l]), raw=raw))
                index += l
            return texts

(2)模型代码解析

CRNN模型解析:首先是定义rnn的类,输入为输出,隐层和输出的特征维数,由一个BiLSTM和一个全连接层组成,方便下一步直接调用。

RNN部分

class BidirectionalLSTM(nn.Module):
    # Inputs hidden units Out
    def __init__(self, nIn, nHidden, nOut):
        super(BidirectionalLSTM, self).__init__()
        self.rnn = nn.LSTM(nIn, nHidden, bidirectional=True)
        self.embedding = nn.Linear(nHidden * 2, nOut)

    def forward(self, input):
        recurrent, _ = self.rnn(input)
        #seq_len, batch, hidden_size * num_directions
        T, b, h = recurrent.size()
        t_rec = recurrent.view(T * b, h)
        output = self.embedding(t_rec)  # [T * b, nOut]
        output = output.view(T, b, -1)
        return output

RNN部分使用了双向LSTM,隐藏层单元数为256,CRNN采用了两层BiLSTM来组成这个RNN层,RNN层的输出维度将是(s,b,class_num) ,其中class_num为文字类别总数。

下面为参考文章具体解释模型代码,注意代码与上面有出入,但思路是一样的!

http://www.javashuo.com/article/p-bxzirubp-kz.html

值得注意的是:Pytorch里的LSTM单元接受的输入都必须是3维的张量(Tensors).每一维表明的意思不能弄错。第一维体现的是序列(sequence)结构,第二维度体现的是小块(mini-batch)结构,第三位体现的是输入的元素(elements of input)。若是在应用中不适用小块结构,那么能够将输入的张量中该维度设为1,但必需要体现出这个维度。

LSTM的输入

input of shape (seq_len, batch, input_size): tensor containing thefeatures of the input sequence. The input can also be a packed variable length sequence.
input shape(a,b,c)
a:seq_len -> 序列长度
b:batch
c:input_size 输入特征数目

根据LSTM的输入要求,咱们要对CNN的输出作些调整,即把CNN层的输出调整为[seq_len, batch, input_size]形式,下面为具体操做:先使用squeeze函数移除h维度,再使用permute函数调整各维顺序,即从原来[w, b, c]的调整为[seq_len, batch, input_size],具体尺寸为[16,batch,512],调整好以后便可以将该矩阵送入RNN层。

x = self.cnn(x)
b, c, h, w = x.size()
# print(x.size()): b,c,h,w
assert h == 1   # "the height of conv must be 1"
x = x.squeeze(2)  # remove h dimension, b *512 * width
x = x.permute(2, 0, 1)  # [w, b, c] = [seq_len, batch, input_size]
x = self.rnn(x)

RNN层输出格式以下,由于咱们采用的是双向BiLSTM,因此输出维度将是hidden_unit * 2

Outputs: output, (h_n, c_n)
output of shape (seq_len, batch,num_directions * hidden_size)
h_n of shape (num_layers *num_directions, batch, hidden_size)
c_n (num_layers * num_directions,batch, hidden_size)

而后咱们再经过线性变换操做self.embedding1 = torch.nn.Linear(hidden_unit * 2, 512)是的输出维度再次变为512,继续送入第二个LSTM层。第二个LSTM层后继续接线性操做torch.nn.Linear(hidden_unit * 2, class_num)使得整个RNN层的输出为文字类别总数。

定义整体模型的类:

输入分别为图片的高,config里为32;输入的channel,这里为1;rnn输出特征的维数,就是字母表的大小。作者写了一个convRelu的函数,当i等于0时输入通道为送入图片的通道数,否则为上一层的输出通道数,每层的输出通道在nm中,卷积核大小为3,步长为1,padding为1,使用relu为激活函数。

最终的cnn模型与VGG16基本相同,rnn模型为两个bilstm级联。从cnn得到的特征,以width为seq,batch不变,channel为输入特征维度,来送入rnn,输出为[seq_len, batch, nh]的概率矩阵

class CRNN(nn.Module):
    def __init__(self, imgH, nc, nclass, nh, n_rnn=2, leakyRelu=False):
        super(CRNN, self).__init__()
        assert imgH % 16 == 0, 'imgH has to be a multiple of 16'

        ks = [3, 3, 3, 3, 3, 3, 2]
        ps = [1, 1, 1, 1, 1, 1, 0]
        ss = [1, 1, 1, 1, 1, 1, 1]
        nm = [64, 128, 256, 256, 512, 512, 512]

        cnn = nn.Sequential()

        def convRelu(i, batchNormalization=False):
        	#i==0成立则nIn = nc,否则nIn = nm[i - 1]
            nIn = nc if i == 0 else nm[i - 1]
            nOut = nm[i]
            cnn.add_module('conv0'.format(i),
                           nn.Conv2d(nIn, nOut, ks[i], ss[i], ps[i]))
            if batchNormalization:
                cnn.add_module('batchnorm0'.format(i), nn.BatchNorm2d(nOut))
            if leakyRelu:
                cnn.add_module('relu0'.format(i),
                               nn.LeakyReLU(0.2, inplace=True))
            else:
                cnn.add_module('relu0'.format(i), nn.ReLU(True))

        convRelu(0)
        cnn.add_module('pooling0'.format(0), nn.MaxPool2d(2, 2))  # 64x16x64
        convRelu(1)
        cnn.add_module('pooling0'.format(1), nn.MaxPool2d(2, 2))  # 128x8x32
        convRelu(2, True)
        convRelu(3)
        cnn.add_module('pooling0'.format(2),
                       nn.MaxPool2d((2, 2), (2, 1), (0, 1)))  # 256x4x16
        convRelu(4, True)
        convRelu(5)
        cnn.add_module('pooling0'.format(3),
                       nn.MaxPool2d((2, 2), (2, 1), (0, 1)))  # 512x2x16
        convRelu(6, True)  # 512x1x16

        self.cnn = cnn
        self.rnn = nn.Sequential(
            BidirectionalLSTM(512, nh, nh),
            BidirectionalLSTM(nh, nh, nclass))

    def forward(self, input):

        # conv features
        conv = self.cnn(input)
        b, c, h, w = conv.size()
        print(conv.size())
        assert h == 1, "the height of conv must be 1"
        conv = conv.squeeze(2) # b *512 * width
        conv = conv.permute(2, 0, 1)  # [w, b, c]
        output = F.log_softmax(self.rnn(conv), dim=2)

        return output

参数权重初始化和类的实例化:

def weights_init(m):
    #get class name
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        m.weight.data.normal_(0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        m.weight.data.normal_(1.0, 0.02)
        m.bias.data.fill_(0)

def get_crnn(config):

    model = CRNN(config.MODEL.IMAGE_SIZE.H, 1, config.MODEL.NUM_CLASSES + 1, config.MODEL.NUM_HIDDEN)
    model.apply(weights_init)

    return model

(3)Train

首先定义了一个读取config的函数:argparse是用来从命令行传入参数的,其用法可以参考:https://zhuanlan.zhihu.com/p/56922793

def parse_arg():
    parser = argparse.ArgumentParser(description="train crnn")

    parser.add_argument('--cfg', help='experiment configuration filename', required=True, type=str)

    args = parser.parse_args()

    with open(args.cfg, 'r') as f:
        # config = yaml.load(f, Loader=yaml.FullLoader)
        config = yaml.load(f)
        config = edict(config)

    config.DATASET.ALPHABETS = alphabets.alphabet
    config.MODEL.NUM_CLASSES = len(config.DATASET.ALPHABETS)

    return config

创建日志文件:

# create output folder
output_dict = utils.create_log_folder(config, phase='train')

# cudnn
cudnn.benchmark = config.CUDNN.BENCHMARK
cudnn.deterministic = config.CUDNN.DETERMINISTIC
cudnn.enabled = config.CUDNN.ENABLED

# writer dict
writer_dict = 
    'writer': SummaryWriter(log_dir=output_dict['tb_dir']),
    'train_global_steps': 0,
    'valid_global_steps': 0,

其他工作:包括模型,损失函数和优化器

1.模型运行在gpu上;

2.损失函数是Pytorch自带的ctc函数。

model = crnn.get_crnn(config)

# get device
if torch.cuda.is_available():
    device = torch.device("cuda:".format(config.GPUID))
else:
    device = torch.device("cpu:0")

model = model.to(device)

# define loss function
criterion = torch.nn.CTCLoss()

3.优化器的初始化

关于优化器的知识可以参考:https://blog.csdn.net/weixin_40170902/article/details/80092628
优化器config里设置为adam,并且利用torch.optim.lr_scheduler来调整学习率,在指定epoch后将lr降低指定倍数,可以参考:https://blog.csdn.net/qyhaill/article/details/103043637

optimizer = utils.get_optimizer(config, model)
    if isinstance(config.TRAIN.LR_STEP, list):
        lr_scheduler = torch.optim.lr_scheduler.MultiStepLR(
            optimizer, config.TRAIN.LR_STEP,
            config.TRAIN.LR_FACTOR, last_epoch-1
        )
    else:
        lr_scheduler = torch.optim.lr_scheduler.StepLR(
            optimizer, config.TRAIN.LR_STEP,
            config.TRAIN.LR_FACTOR, last_epoch - 1
        )

之后是finetune和resume的选择,以及与训练模型的载入。
fintune讲解:https://zhuanlan.zhihu.com/p/35890660,这里的fintune冻结了cnn,其参数不更新。

if config.TRAIN.FINETUNE.IS_FINETUNE:
    model_state_file = config.TRAIN.FINETUNE.FINETUNE_CHECKPOINIT
    if model_state_file == '':
        print(" => no checkpoint found")
    checkpoint = torch.load(model_state_file, map_location='cpu')
    if 'state_dict' in checkpoint.keys():
        checkpoint = checkpoint['state_dict']

    from collections import OrderedDict
    model_dict = OrderedDict()
    for k, v in checkpoint.items():
        if 'cnn' in k:
            model_dict[k[4:]] = v
    model.cnn查看详情  

ocr文字识别经典论文详解

📝OCR文字识别综述合集:1️⃣OCR系列第一章:OCR文字识别技术总结(一)2️⃣OCR系列第二章:OCR文字识别技术总结(二)3️⃣OCR系列第三章:OCR文字识别技术总结(三)4️⃣OCR系列第... 查看详情

机器学习基础---应用实例(图片文字识别)和总结(代码片段)

一:问题描述和流程图将介绍一种机器学习的应用实例:照片OCR技术,介绍它的原因:(1)首先,展示一个复杂的机器学习系统是如何被组合起来的;(2)接着,介绍一下机器学习流水线的有关概念以及如何分配资源来对下一... 查看详情

ocr文字识别

...码OCR技术系列之一:字符识别技术总览OCR技术系列之二:文字定位与分割OCR技术系列之三:大批量生成文字训练集OCR技术系列之四:基于深度学习的文字识别(3755个汉字)腾讯OCR:自动识别技术腾讯:OCR技术之检测篇腾讯:OCR... 查看详情

[深度学习][原创]常用ocr框架和技术总结

...平投影垂直投影2、模板匹配3、查找轮廓findcontours常用的文字检测框架:1、DBNet2、CTPN3、EAST常用的文字识别框架:CRNNdeep-text-recognition-benchmark常用的文字检测和文字识别一体框架:paddleocreasyocrchineseocrpytorchocr 查看详情

验证码识别tesseract的简单使用和总结(代码片段)

...通过电子设备扫描纸上的打印的字符,然后翻译成计算机文字的过程。也就是说通过输入图片,经过识别引擎,去识别图片上的文字。Tesseract是一种适用于各种操作系统的光学字符识别引擎,最早是hp公司的软件,2005年开源,200... 查看详情

项目管理javaocr实现图片文字识别(代码片段)

【项目管理】JavaOCR实现图片文字识别1.项目前言1.1项目需求1.2OCR引擎选择1.3Tess4j介绍2.项目实现2.1项目搭建2.2主要实现代码3.效果演示3.1中文识别3.1.1需要识别的图片3.1.2识别过程3.1.3识别结果3.1.4总结3.2英文识别3.2.1需要识别的图... 查看详情

ocr技术系列之一字符识别技术总览

...别。它是利用光学技术和计算机技术把印在或写在纸上的文字读取出来,并转换成一种计算机能够接受、人又可以理解的格式。文字识 查看详情

ffmpegtesseract-ocr识别文字滤镜中文识别输出文本(代码片段)

ffprobe-show_entriesframe_tags=lavfi.ocr.text-flavfi-i"movie=in.tif,ocr=datapath=tessdata:language=chi_sim:whitelist=">ocr.txtNote:识别中文时whitelist一定要设置为空;默认是英文字幕和数字,无法识别中文  查看详情

百度ocr文字识别接口使用(代码片段)

最近有个需求需要识别图片中的文字,所以就用到了百度的ocr接口,结果在测试的过程中被图片格式搞的有点晕,试了好久终于成功了,在此记录一下。附ocr接口文档地址:https://cloud.baidu.com/doc/OCR/OCR-API.html#.E8.BF.94.E5.9B.9E.E8.AF.B4... 查看详情

用python写一个图像文字识别ocr工具(代码片段)

...代码参考链接引言最近在技术交流群里聊到一个关于图像文字识别的需求,在工作、生活中常常会用到,比如票据、漫画、扫描件、照片的文本提取。博主基于PyQt+PaddleOCR写了一个桌面端的OCR工具,用于快速实现图... 查看详情

ocr识别技术文档识别怎么用

OCR识别技术文档识别的概括  我们常说的OCR、文字识别、OCR识别技术文档识别是指通过电子设备等将纸质上的文字识别出来,形成可编辑的文字。   OCR识别技术文档识别的流程  随着扫描仪的普及与广泛应用,再加上... 查看详情

2021aiwin手写体ocr识别竞赛总结(任务一)(代码片段)

2021AIWIN手写体OCR识别竞赛总结(任务一)参加了“世界人工智能创新大赛”——手写体OCR识别竞赛(任务一),取得了Top1的成绩。下面通过这篇文章来介绍我们队伍的方案。队伍随机组的,有人找我我就... 查看详情

从图片提取文字的终极解决方法——通用文字识别api(代码片段)

写在前面相信你用过类似对进行图片中的文字提取的功能,但是你了解过背后的原理吗?本文将从图片中文字提取的原理以及应用案例等多方面进行讲述,希望一文能为你讲透通用文字识别。通用文字识别是什么技术... 查看详情

python图片文字识别——windows下tesseract-ocr的安装与使用(代码片段)

Python图片文字识别——Windows下Tesseract-OCR的安装与使用前言Windows下Tesseract-OCR的安装与配置Tesseract-OCR简介与版本选择tesseract-OCR安装Tesseract-OCR配置安装Python调用TesseractAPI所需依赖项Tesseract-OCR测试与使用命令行模式使用Python调用Tess... 查看详情

顶像滑块/通用滑块识别/文字点选/图标点选/推理/ocr文字识别(代码片段)

背景验证码识别一直都是一个重要的话题,近日有一些公司询问本地DLL验证码识别定制的事,可以联系QQ:【167231471】定制本地离线DLL验证码识别。另外给大家普及一下通用验证码识别和滑块缺口检测的解决方案【网... 查看详情

顶像滑块/通用滑块识别/文字点选/图标点选/推理/ocr文字识别(代码片段)

背景验证码识别一直都是一个重要的话题,近日有一些公司询问本地DLL验证码识别定制的事,可以联系QQ:【167231471】定制本地离线DLL验证码识别。另外给大家普及一下通用验证码识别和滑块缺口检测的解决方案【网... 查看详情

tesseract-ocr+java实现图片文字识别(代码片段)

  ocr是OpticalCharacterRecognition的简写,就是光学字符识别技术。主要是对包含文本资料的图片进行识别,获取文本信息的技术。  目前tesseract-ocr这个工具可以很方便的在Windows、Linux、Mac下安装。  windows下的安装链接࿱... 查看详情