CRNN:文本序列识别
文本序列识别是图像领域的一个常见问题。一般来说,从自然场景图片中识别文字需要两步,首先定位图像中的文字位置,然后对文字序列进行识别。
文字检测:解决的问题是哪里有文字,文字的范围有多长。
文字识别:对定位好的文字区域进行识别,主要解决的问题是每个文字是什么,将图像中的文字区域进转化为字符信息。
一、相关背景介绍
常用的文字识别算法主要有两种框架,本文主要介绍第一种框架:
1、CNN + RNN + CTC (CRNN + CTC)
2、CNN + Seq2Seq + Attention
CRNN的全称为Convolutional Recurrent Neural Network,主要用于端到端地对不定长的文本序列进行识别。它不用先对单个文字进行切割,而是将文本识别转化为时序依赖的序列学习问题,直接基于图像进行文字序列识别。
CRNN借鉴了语音识别中的LSTM+CTC的建模方法,输入到LSTM的特征,不再是语音领域的声学特征,而是CNN网络提取的图像特征。CRNN算法最大的贡献,在于把CNN做图像特征工程的潜力与LSTM做序列化识别的潜力进行结合,它既提取了鲁棒特征,又通过序列识别避免了传统算法中难度极高的单字符切分与单字符识别。
二、CRNN模型结构
第一部分:卷积特征提取。
这里有一个很精彩的改动,一共有四个最大池化层,但是最后两个池化层的窗口尺寸由 2x2 改成了 1x2,也就是图片的高度减半了四次,而宽度则只减半了两次。这是因为文本图像多数都是高较小而宽较长,所以feature map也设计成这种高小宽长的矩形形状,使用1×2的池化窗口可以尽量保证不丢失在宽度方向的信息,更适合英文字母识别(比如区分i和l)。
(1)输入层:inputs = Input(shape=(256, 64, 1)) 。
(2)卷积:Conv2D(64, (3, 3), padding=‘same’)。
(3)批量归一化:BatchNormalization()。
(4)激活层:Activation(‘relu’)。
(5)最大池化:MaxPooling2D(pool_size=(2, 2))。
(6)卷积:Conv2D(128, (3, 3), padding=‘same’)。
(7)批量归一化:BatchNormalization()。
(8)激活层:Activation(‘relu’)。
(9)最大池化:MaxPooling2D(pool_size=(2, 2))。
(10)卷积:Conv2D(256, (3, 3), padding=‘same’)。
(11)批量归一化:BatchNormalization()。
(12)激活层:Activation(‘relu’)。
(13)卷积:Conv2D(256, (3, 3), padding=‘same’)。
(14)批量归一化:BatchNormalization()。
(15)激活层:Activation(‘relu’)。
(16)最大池化:MaxPooling2D(pool_size=(1, 2))。
(17)卷积:Conv2D(512, (3, 3), padding=‘same’)。
(18)批量归一化:BatchNormalization()。
(19)激活层:Activation(‘relu’)。
(20)卷积:Conv2D(512, (3, 3), padding=‘same’)。
(21)批量归一化:BatchNormalization()。
(22)激活层:Activation(‘relu’)。
(23)最大池化:MaxPooling2D(pool_size=(1, 2))。
(24)卷积:Conv2D(512, (2, 2), padding=‘same’)。
(25)批量归一化:BatchNormalization()。
(26)激活层:Activation(‘relu’)。
第二部分:Map to Sequence。
我们不能直接将CNN提取到的特征图送入RNN进行训练,需要进行一些调整,将特征图处理成RNN需要的特征向量序列。
卷积层,最大池化层只在局部区域上执行,它们是平移不变的,所以特征图沿channel的每一串,都对应于原始图像的一个矩形区域,并且这些矩形区域与特征图沿channel的每一串具有相同的位置顺序。因此,特征图沿channel的每一串序列正好可以作为循环层的输入。
(27)Reshape(target_shape=(32, -1))。
(28)Dense(64, activation=‘relu’)。
第三部分:双向LSTM循环神经网络。
Simple RNN有梯度消失的问题,所以CRNN中使用的是LSTM,可以捕获长距离依赖。LSTM 是单向的,它只使用过去的信息,然而在基于图像的序列中,两个方向的上下文是相互有用且互补的,因此,可以将两个LSTM一个向前和一个向后组合成一个双向LSTM。
(29)LSTM(256, return_sequences=True)。
(30)LSTM(256, return_sequences=True, go_backwards=True)。
(31)Lambda(lambda lstm_tensor: K.reverse(lstm_tensor, axes=1)。
(32)add([lstm_1, reversed_lstm_1b])。
(33)BatchNormalization()。
(34)LSTM(256, return_sequences=True)。
(35)LSTM(256, return_sequences=True, go_backwards=True)。
(36)Lambda(lambda lstm_tensor: K.reverse(lstm_tensor, axes=1))。
(37)concatenate([lstm_2, reversed_lstm_2b]) 。
(38)BatchNormalization()。
(39)Dense(num_class)。
(40)Activation(‘softmax’)。
三、实验结果
输入网络中训练的train_x:(batch, 256, 64, 1)。先将RGB图像灰度化,再resize到(64, 256)大小,最后像素值除以255归一化。
输入网络中训练的train_y:(batch, 6)。每张图像的label是一个6维的向量,代表序列含6个字符,用0-33来分别记录每个字符类别信息。训练过程中由于调用了ctc内部算法,部分数据编码转换已被封装,不需要我们再专门处理成one-hot编码形式。
训练50轮,优化器ada = Adadelta(),最佳模型权重val loss达到0.436,此时测试集中文本序列识别精度达到91.2%。进一步检验可知,识别失误的情况大多是图片清晰度不够,部分字符模糊到甚至人眼也无法准确判别,比如D和0、裁减部分丢失等等,识别错误也情有可原。
四、深入思考
Ques 1:为什么在定长序列识别模型中也最好引入blank机制?
对于Recurrent Layers,如果使用常见的Softmax cross-entropy loss,则每一列输出都需要对应一个字符元素。那么训练时候每张样本图片都需要标记出每个字符在图片中的位置,再通过CNN感受野对齐到Feature map的每一列获取该列输出对应的Label才能进行训练。
在实际情况中,标记这种对齐样本非常困难(除了标记字符,还要标记每个字符的位置,工作量非常大。另外,由于每张样本的字符数量不同,字体样式不同,字体大小不同,导致每列输出并不一定能与每个字符一一对应。所以CTC提出一种对不需要对齐的Loss计算方法,用于训练网络,被广泛应用于文本行识别和语音识别中。
Ques 2:源码中是如何实现Map to Sequence操作的?如何将CNN提取的feature map,通过某种转换从而能够输入RNN中?
图像尺寸的矩阵形式是(height, width, channel),在经过CNN提取特征后,feature map的尺寸变为(f_height, f_width, f_channel)。对于RNN循环网络模型,一次性需要输入一个序列长度的元素,这里也就是一次性要输入f_width长度的元素,这里每个元素可以是个向量。
我们在网络转换中需要把f_width固定保存下来,将f_height和f_channel的信息进行融合。为了统一到reshape框架,我们在源码最开始的模型搭建时,不用(height, width, channel)的框架形式,而改用(width, height, channel)的形式,特意对输入图像数据取个转置。
输入层结构:(None, 256, 64, 1)。
经过CNN卷积池化:(None, 64, 4, 512)。
将第2个维度f_height和第3个维度f_channel进行融合,保留f_width这个维度:(None, 64, 2048)。
进行信息冗余合并:(None, 64, 64)。
此时就可以将这个(64, 64)的特征放入LSTM进行训练了:第一个64代表RNN序列长度为64,也就是一张图片在width上被64等分,每一小块预测得到一个字符结果;第二个64代表RNN的每个时间步上都是64维的向量,其记录了许多信息。
我发现我源码结构中有些bug。输入图像原始尺寸为(256,64),经过卷积池化,width除以4倍,height除以16倍,此时变成(64,4)。既然此时width只有64,那reshape层应该就得处理成(64, 2048),我却套用了源码结果处理成了(32, 4096)。源码中原始输入只是(128, 64),自然reshape的时候保留了固定32这个数值,而我这里情况不一样,应做修正。
Ques 3:训练时的解码过程和预测时的解码过程并不相同。
训练时,计算loss需要用到最大似然原理。由于引入了blank机制,一种真实label可以对应多种模型输出结果,因此在计算概率时需要将这多种情况下的每个小概率值累加起来。累加过程比较复杂,需要借助HMM的Forward-Backward算法思路,利用动态规划算法求解。
预测时,用训练好的神经网络来识别新的文本序列图像,这时我们不知道真实label,如果像上面一样将每种可能文本的所有路径计算出来,对于长时间步和长字符序列来说,计算量非常庞大。于是干脆进行简化,将每一个时间步的输出作为字符类别的概率分布,取其中最大概率的字符作为该时间步的输出字符,然后将所有时间步得到一个字符进行拼接得到一个序列路径,再根据合并序列方法得到最终的预测文本结果。
五、源码
main函数调用:
import numpy as np
import cv2
from read_data_path import make_data
from c_rnn_model import get_model
from train import SequenceData
from train import train_network
from train import load_network_then_train
import itertools
from predict import predict_test_data
class_dictionary = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'J': 18, 'K': 19,
'L': 20, 'M': 21, 'N': 22, 'P': 23, 'Q': 24, 'R': 25, 'S': 26, 'T': 27, 'U': 28, 'V': 29,
'W': 30, 'X': 31, 'Y': 32, 'Z': 33}
if __name__ == "__main__":
train_x, train_y, val_x, val_y, test_x, test_y = make_data() # 图片尺寸 : (64, 256, 3)
train_generator = SequenceData(train_x, train_y, 16)
test_generator = SequenceData(test_x, test_y, 16)
train_network(train_generator, test_generator, epoch=50)
# load_network_then_train(train_generator, test_generator, epoch=20,
# input_name='first_weights.hdf5', output_name='second_weights.hdf5')
# 经过50轮训练之后, train loss : 86.15 - 1.4070e-04 , val loss : 30.06 - 0.7583
predict_test_data(test_x, test_y)
# 最佳训练效果: c-rnn--37--0.436.hdf5 , val loss达到0.436
# 字符识别精度到达91.2%,经过检验可知,识别失误的情况大多是图片清晰度不够,
# 图片模糊情况下甚至人眼也无法准确判别,比如D和0的区别,裁减部分丢失等等,模型识别错误情有可原。
CRNN模型结构:
from keras import backend as K
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Input, Dense, Activation
from keras.layers import Reshape, Lambda, BatchNormalization
from keras.layers.merge import add, concatenate
from keras.models import Model
from keras.layers.recurrent import LSTM
# 放缩后图片长宽分别是256,64,总共需要识别6个字符(字母+数字),加上blank总共分类类别数为34+1
img_w = 256
img_h = 64
char_num = 6
num_class = 34 + 1
# Loss and train functions, network architecture
def ctc_lambda_func(args):
y_pre, labels, input_length, label_length = args
# the 2 is critical here, since the first couple outputs of the RNN tend to be garbage:
y_pre = y_pre[:, 2:, :]
return K.ctc_batch_cost(labels, y_pre, input_length, label_length)
def get_model(loss_model=True):
input_shape = (img_w, img_h, 1) # (256, 64, 1)
inputs = Input(name='the_input', shape=input_shape, dtype='float32') # (None, 256, 64, 1)
# Convolution layer (VGG)
inner = Conv2D(64, (3, 3), padding='same', name='conv1',
kernel_initializer='he_normal')(inputs) # (None, 256, 64, 64)
inner = BatchNormalization()(inner) # (None, 256, 64, 64)
inner = Activation('relu')(inner) # (None, 256, 64, 64)
inner = MaxPooling2D(pool_size=(2, 2), name='max1')(inner) # (None, 128, 32, 64)
inner = Conv2D(128, (3, 3), padding='same', name='conv2',
kernel_initializer='he_normal')(inner) # (None, 128, 32, 128)
inner = BatchNormalization()(inner) # (None, 128, 32, 128)
inner = Activation('relu')(inner) # (None, 128, 32, 128)
inner = MaxPooling2D(pool_size=(2, 2), name='max2')(inner) # (None, 64, 16, 128)
inner = Conv2D(256, (3, 3), padding='same', name='conv3',
kernel_initializer='he_normal')(inner) # (None, 64, 16, 256)
inner = BatchNormalization()(inner) # (None, 64, 16, 256)
inner = Activation('relu')(inner) # (None, 64, 16, 256)
inner = Conv2D(256, (3, 3), padding='same', name='conv4',
kernel_initializer='he_normal')(inner) # (None, 64, 16, 256)
inner = BatchNormalization()(inner) # (None, 64, 16, 256)
inner = Activation('relu')(inner) # (None, 64, 16, 256)
inner = MaxPooling2D(pool_size=(1, 2), name='max3')(inner) # (None, 64, 8, 256)
inner = Conv2D(512, (3, 3), padding='same', name='conv5',
kernel_initializer='he_normal')(inner) # (None, 64, 8, 512)
inner = BatchNormalization()(inner) # (None, 64, 8, 512)
inner = Activation('relu')(inner) # (None, 64, 8, 512)
inner = Conv2D(512, (3, 3), padding='same', name='conv6')(inner) # (None, 64, 8, 512)
inner = BatchNormalization()(inner) # (None, 64, 8, 512)
inner = Activation('relu')(inner) # (None, 64, 8, 512)
inner = MaxPooling2D(pool_size=(1, 2), name='max4')(inner) # (None, 64, 4, 512)
inner = Conv2D(512, (2, 2), padding='same', name='con7',
kernel_initializer='he_normal')(inner) # (None, 64, 4, 512)
inner = BatchNormalization()(inner) # (None, 64, 4, 512)
inner = Activation('relu')(inner) # (None, 64, 4, 512)
# CNN to RNN, Map to Sequence
inner = Reshape(target_shape=(32, -1), name='reshape')(inner) # (None, 32, 4096)
inner = Dense(64, activation='relu', name='dense1',
kernel_initializer='he_normal')(inner) # (None, 32, 64)
# RNN layer
lstm_1 = LSTM(256, return_sequences=True, name='lstm1',
kernel_initializer='he_normal')(inner) # (None, 32, 256)
lstm_1b = LSTM(256, return_sequences=True,
go_backwards=True, name='lstm1_b',
kernel_initializer='he_normal')(inner) # (None, 32, 256)
reversed_lstm_1b = Lambda(lambda lstm_tensor:
K.reverse(lstm_tensor, axes=1))(lstm_1b) # (None, 32, 256)
lstm1_merged = add([lstm_1, reversed_lstm_1b]) # (None, 32, 256)
lstm1_merged = BatchNormalization()(lstm1_merged) # (None, 32, 256)
lstm_2 = LSTM(256, return_sequences=True, name='lstm2',
kernel_initializer='he_normal')(lstm1_merged) # (None, 32, 256)
lstm_2b = LSTM(256, return_sequences=True,
go_backwards=True, name='lstm2_b',
kernel_initializer='he_normal')(lstm1_merged) # (None, 32, 256)
reversed_lstm_2b = Lambda(lambda lstm_tensor:
K.reverse(lstm_tensor, axes=1))(lstm_2b) # (None, 32, 256)
lstm2_merged = concatenate([lstm_2, reversed_lstm_2b]) # (None, 32, 512)
lstm2_merged = BatchNormalization()(lstm2_merged) # (None, 32, 512)
# transforms RNN output to character recognition matrix:
inner = Dense(num_class, name='dense2',
kernel_initializer='he_normal')(lstm2_merged) # (None, 32, 66)
y_pre = Activation('softmax', name='softmax')(inner) # (None, 32, 66)
# create loss layer
labels = Input(name='the_labels',
shape=[char_num], dtype='float32') # (None, 7)
input_length = Input(name='input_length',
shape=[1], dtype='int64') # (None, 1)
label_length = Input(name='label_length',
shape=[1], dtype='int64') # (None, 1)
# Keras doesn't currently support loss funcs with extra parameters
# so CTC loss is implemented in a lambda layer
loss_out = Lambda(ctc_lambda_func, output_shape=(1,),
name='ctc')([y_pre, labels,
input_length, label_length]) # (None, 1)
if loss_model:
c_rnn_loss = Model(inputs=[inputs, labels, input_length, label_length], outputs=loss_out)
return c_rnn_loss
else:
c_rnn = Model(inputs=[inputs], outputs=y_pre)
return c_rnn
# Total params: 7,564,930
# Trainable params: 7,558,914
# Non-trainable params: 6,016
数据集准备:
import numpy as np
import cv2
import os
class_dictionary = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'J': 18, 'K': 19,
'L': 20, 'M': 21, 'N': 22, 'P': 23, 'Q': 24, 'R': 25, 'S': 26, 'T': 27, 'U': 28, 'V': 29,
'W': 30, 'X': 31, 'Y': 32, 'Z': 33}
def read_path():
data_x = []
data_y = []
# 所有训练数据分四个文件夹存储,把它们全部读取到列表data_x, data_y里
# 第1个文件夹:669张图片
filename1 = os.listdir('1')
filename1.sort()
for name in filename1:
path = '1/' + name
data_x.append(path)
obj1 = name.split('.')
obj2 = obj1[0].split('_')
obj3 = obj2[1]
label_name = obj3
data_y.append(label_name)
# 第2个文件夹:315张图片
filename2 = os.listdir('2')
filename2.sort()
for name in filename2:
path = '2/' + name
data_x.append(path)
obj1 = name.split('.')
obj2 = obj1[0].split('_')
obj3 = obj2[1]
label_name = obj3
data_y.append(label_name)
# 第3个文件夹:167张图片
filename3 = os.listdir('3')
filename3.sort()
for name in filename3:
path = '3/' + name
data_x.append(path)
obj1 = name.split('.')
obj2 = obj1[0].split('_')
obj3 = obj2[1]
label_name = obj3
data_y.append(label_name)
# 第4个文件夹:674张图片
filename4 = os.listdir('4')
filename4.sort()
for name in filename4:
path = '4/' + name
data_x.append(path)
obj1 = name.split('.')
obj2 = obj1[0].split('_')
obj3 = obj2[1]
label_name = obj3
data_y.append(label_name)
return data_x, data_y
def make_data():
data_x, data_y = read_path()
print('all image quantity : ', len(data_y))
# 取前1700张图片用作训练,后125张图片用作测试
train_x = data_x[:1700]
train_y = data_y[:1700]
val_x = data_x[1700:]
val_y = data_y[1700:]
test_x = data_x[1700:]
test_y = data_y[1700:]
return train_x, train_y, val_x, val_y, test_x, test_y
训练过程:
import cv2
import os
import random
import numpy as np
from keras.utils import Sequence
import math
from c_rnn_model import get_model
from keras.optimizers import Adadelta
from keras.callbacks import ModelCheckpoint
class_dictionary = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'J': 18, 'K': 19,
'L': 20, 'M': 21, 'N': 22, 'P': 23, 'Q': 24, 'R': 25, 'S': 26, 'T': 27, 'U': 28, 'V': 29,
'W': 30, 'X': 31, 'Y': 32, 'Z': 33}
img_w = 256
img_h = 64
char_num = 6
class SequenceData(Sequence):
def __init__(self, data_x, data_y, batch_size):
self.batch_size = batch_size
self.data_x = data_x
self.data_y = data_y
self.indexes = np.arange(len(self.data_x))
def __len__(self):
return math.floor(len(self.data_x) / float(self.batch_size))
def on_epoch_end(self):
np.random.shuffle(self.indexes)
# 训练过程中由于调用了ctc内部算法,部分数据编码处理已被封装,不需要我们再做转换。
# x_data :(batch, 256, 64, 1),每一个维度存储图片矩阵信息。
# y_data :(batch, 6),每一个维度仅仅是个向量,用0-33来记录每个字符类别信息,而不需要我们专门转化为one-hot编码形式。
def __getitem__(self, idx):
batch_index = self.indexes[idx * self.batch_size:(idx + 1) * self.batch_size]
batch_x = [self.data_x[k] for k in batch_index]
batch_y = [self.data_y[k] for k in batch_index]
x_data = np.ones([self.batch_size, img_w, img_h, 1]) # (batch, 256, 64, 1)
y_data = np.ones([self.batch_size, char_num]) # (batch, 6)
input_length = np.ones((self.batch_size, 1)) * (img_w // 8 - 2) # (batch, 1)
label_length = np.zeros((self.batch_size, 1)) # (batch, 1)
for i in range(self.batch_size):
img = cv2.imread(batch_x[i])
img1 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img2 = img1 / 255
img3 = img2.T
img4 = img3[:, :, np.newaxis]
x_data[i, :, :, :] = img4
text = batch_y[i]
label = np.zeros(char_num)
# print(text)
# cv2.namedWindow("Image")
# cv2.imshow("Image", img2)
# cv2.waitKey(0)
for j in range(char_num):
c = text[j]
index = class_dictionary[c]
label[j] = index
y_data[i] = label
label_length[i] = len(label)
inputs = {
'the_input': x_data,
'the_labels': y_data,
'input_length': input_length,
'label_length': label_length
}
outputs = {'ctc': np.zeros([self.batch_size])}
return inputs, outputs
# create model and train and save
def train_network(train_generator, validation_generator, epoch):
model = get_model()
ada = Adadelta()
checkpoint = ModelCheckpoint(filepath='c-rnn--{epoch:02d}--{val_loss:.3f}.hdf5', monitor='loss',
verbose=1, mode='min', period=1)
# the loss calc occurs elsewhere, so use a dummy lambda func for the loss
model.compile(loss={'ctc': lambda y_true, y_pre: y_pre}, optimizer=ada)
# captures output of softmax so we can decode the output during visualization
model.fit_generator(
train_generator,
steps_per_epoch=len(train_generator),
epochs=epoch,
validation_data=validation_generator,
validation_steps=len(validation_generator),
callbacks=[checkpoint]
)
model.save_weights('first_weights.hdf5')
def load_network_then_train(train_generator, validation_generator, epoch, input_name, output_name):
model = get_model()
model.load_weights(input_name, by_name=True, skip_mismatch=True)
ada = Adadelta()
checkpoint = ModelCheckpoint(filepath='c-rnn--{epoch:02d}--{val_loss:.3f}.hdf5', monitor='loss',
verbose=1, mode='min', period=1)
# the loss calc occurs elsewhere, so use a dummy lambda func for the loss
model.compile(loss={'ctc': lambda y_true, y_pre: y_pre}, optimizer=ada)
# captures output of softmax so we can decode the output during visualization
model.fit_generator(
train_generator,
steps_per_epoch=len(train_generator),
epochs=epoch,
validation_data=validation_generator,
validation_steps=len(validation_generator),
callbacks=[checkpoint]
)
model.fit_generator(
train_generator,
steps_per_epoch=len(train_generator),
epochs=epoch,
validation_data=validation_generator,
validation_steps=len(validation_generator),
callbacks=[checkpoint]
)
model.save_weights(output_name)
测试过程:
import numpy as np
import cv2
from c_rnn_model import get_model
import itertools
import os
# ctc算法的blank机制,在原有类别基础上多添加一个空白分类
ctc_blank = 34
class_dictionary = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'J': 18, 'K': 19,
'L': 20, 'M': 21, 'N': 22, 'P': 23, 'Q': 24, 'R': 25, 'S': 26, 'T': 27, 'U': 28, 'V': 29,
'W': 30, 'X': 31, 'Y': 32, 'Z': 33}
class_list = list(class_dictionary.keys())
def predict_test_data(test_x, test_y):
c_rnn_model = get_model(loss_model=False)
c_rnn_model.summary()
c_rnn_model.load_weights('c-rnn--37--0.436.hdf5')
print('total test quantity : ', len(test_x))
accuracy_count = 0
for i in range(len(test_x)):
img = cv2.imread(test_x[i])
img1 = cv2.resize(img, (256, 64), interpolation=cv2.INTER_AREA)
img2 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img3 = img2 / 255
img4 = img3.T
img5 = img4[np.newaxis, :, :, np.newaxis]
out1 = c_rnn_model.predict(img5) # out.shape : (1, 32, 66)
out2 = np.argmax(out1[0, 2:], axis=1) # get max index -> len = 32
out3 = [k for k, g in itertools.groupby(out2)] # remove overlap value
out4 = ''
for j in range(len(out3)):
index = int(out3[j])
if index < ctc_blank:
plate_char = class_list[index]
out4 = out4 + str(plate_char)
y_pre = out4
y_true = test_y[i]
if y_pre == y_true:
accuracy_count = accuracy_count + 1
else:
print('算法识别错误 : ', test_x[i])
print('y_pre : ', y_pre)
print('y_true :', y_true)
# cv2.namedWindow("Image")
# cv2.imshow("Image", img3)
# cv2.waitKey(0)
print('The test accuracy is : ', accuracy_count/len(test_x))
六、项目链接
如果代码跑不通,或者想直接使用我自己制作的数据集,可以去下载项目链接:
https://blog.csdn.net/Twilight737