lstm+CTC_loss识别freetype库和captcha库生成的不定长验证码
freetype库生成不定长验证码:
freetype库的介绍可以看这篇文章:
https://www.geek-share.com/detail/2753469982.html
使用freetype库生成不定长验证码的步骤:
创建一个face对象,加载特定的字体字库文件.ttf(相当于规定了字体);
输入一个字符串、字符串在图片起始的位置(以像素点为单位)、字符的字体大小(以像素点为单位)、字符的颜色;
对字符串的每一个字符创建对应的字形,规定该字形在图片上的位置(这时候以1/64像素点为单位);
将每个字形转化为位图画在背景图片上;
对画好的彩色图片加噪声,可以选择加高斯噪声或椒盐噪声;
加完噪声后,可以选择用方框滤波对图片进行降噪;
对彩色图片灰度化,并生成图片的一维数组形式(一个像素点用一个灰度值代表),这个一维数组就是可以送入深度学习模型训练的数据形式。
完整代码如下:
freeTypeGenerateTextImage.py
[code]import numpy as np import freetype import copy import random import cv2 import os # 使用FreeType库生成验证码图片时,我们输入的文本的属性pos位置和text_size大小是以像素点为单位的,但是将文本转化成字形时 # 需要把这些数据转换成1/64像素点单位计数的值,然后在画位图时,还要把相关的数据重新转化成像素点单位计数的值 # 这就是本class中几个方法主要做的工作 class PutChineseText(object): def __init__(self, ttf): # 创建一个face对象,装载一种字体文件.ttf self._face = freetype.Face(ttf) # 在一个图片(用三维数组表示)上绘制文本字符 def draw_text(self, image, pos, text, text_size, text_color): """ draw chinese(or not) text with ttf :param image: 一个图片平面,用三维数组表示 :param pos: 在图片上开始绘制文本字符的位置,以像素点为单位 :param text: 文本的内容 :param text_size: 文本字符的字体大小,以像素点为单位 :param text_color:文本字符的字体颜色 :return: 返回一个绘制了文本的图片 """ # self._face.set_char_size以物理点的单位长度指定了字符尺寸,这里只设置了宽度大小,则高度大小默认和宽度大小相等 # 我们将text_size乘以64倍得到字体的以point单位计数的大小,也就是说,我们认为输入的text_size是以像素点为单位来计量字体大小 self._face.set_char_size(text_size * 64) # metrics用来存储字形布局的一些参数,如ascender,descender等 metrics = self._face.size # 从基线到放置轮廓点最高(上)的距离,除以64是重新化成像素点单位的计数 # metrics中的度量26.6象素格式表示,即数值是64倍的像素数 # 这里我们取的ascender重新化成像素点单位的计数 ascender = metrics.ascender / 64.0 # 令ypos为从基线到放置轮廓点最高(上)的距离 ypos = int(ascender) # 如果文本不是unicode格式,则用utf-8编码来解码,返回解码后的字符串 if isinstance(text, str) is False: text = text.decode('utf-8') # 调用draw_string方法来在图片上绘制文本,也就是说draw_text方法其实主要是在定位字形位置和设定字形的大小,然后调用draw_string方法来在图片上绘制文本 img = self.draw_string(image, pos[0], pos[1] + ypos, text, text_color) return img # 绘制字符串方法 def draw_string(self, img, x_pos, y_pos, text, color): """ draw string :param x_pos: 文本在图片上开始的x轴位置,以1/64像素点为单位 :param y_pos: 文本在图片上开始的y轴位置,以1/64像素点为单位 :param text: unicode形式编码的文本内容 :param color: 文本的颜色 :return: 返回一个绘制了文本字形的图片(三维数组形式) """ prev_char = 0 # pen是笔位置或叫原点,用来定位字形 pen = freetype.Vector() # 设定pen的x轴位置和y轴位置,注意pen.x和pen.y都是以1/64像素点单位计数的,而x_pos和y_pos都是以像素点为单位计数的 # 因此x_pos和y_pos都左移6位即乘以64倍化成1/64像素点单位计数 pen.x = x_pos << 6 pen.y = y_pos << 6 hscale = 1.0 # 设置一个仿射矩阵 matrix = freetype.Matrix(int(hscale) * 0x10000, int(0.2 * 0x10000), int(0.0 * 0x10000), int(1.1 * 0x10000)) cur_pen = freetype.Vector() pen_translate = freetype.Vector() # 将输入的img图片三维数组copy过来 image = copy.deepcopy(img) # 一个字符一个字符地将其字形画成位图 for cur_char in text: # 当字形图像被装载时,对该字形图像进行仿射变换,这只适用于可伸缩(矢量)字体格式。set_transform()函数就是做这个工作 self._face.set_transform(matrix, pen_translate) # 装载文本中的每一个字符 self._face.load_char(cur_char) # 获取两个字形的字距调整信息,注意获得的值是1/64像素点单位计数的。因此可以用来直接更新pen.x的值 kerning = self._face.get_kerning(prev_char, cur_char) # 更新pen.x的位置 pen.x += kerning.x # 创建一个字形槽,用来容纳一个字形 slot = self._face.glyph # 字形图像转换成位图 bitmap = slot.bitmap # cur_pen记录当前光标的笔位置 cur_pen.x = pen.x # pen.x的位置上面已经更新过 # bitmap_top是字形原点(0,0)到字形位图最高像素之间的垂直距离,由于是像素点计数的,我们用其来更新cur_pen.y时要转换成1/64像素点单位计数 cur_pen.y = pen.y - slot.bitmap_top * 64 # 调用draw_ft_bitmap方法来画出字形对应的位图,注意这里是循环,也就是一个字符一个字符地画 self.draw_ft_bitmap(image, bitmap, cur_pen, color) # 每画完一个字符,将pen.x更新成下一个字符的笔位置(原点位置),advanceX即相邻两个原点的水平距离(字间距) pen.x += slot.advance.x # prev_char更新成当前新画好的字符的字形的位置 prev_char = cur_char # 返回包含所有字形的位图的图片(三维数组) return image # 将字形转化成位图 def draw_ft_bitmap(self, img, bitmap, pen, color): """ draw each char :param bitmap: 要转换成位图的字形 :param pen: 开始画字形的位置,以1/64像素点为单位 :param color: RGB三个通道值表示,每个值0-255范围 :return: 返回一个三维数组形式的图片 """ # 获得笔位置的x轴坐标和y轴坐标,这里右移6位是重新化为像素点单位计数的值 x_pos = pen.x >> 6 y_pos = pen.y >> 6 # rows即位图中的水平线数 # width即位图的水平象素数 cols = bitmap.width rows = bitmap.rows # buffer数一个指向位图象素缓冲的指针,里面存储了我们的字形在某个位置上的信息,即字形轮廓中的所有的点上哪些应该画成黑色,或者是白色 glyph_pixels = bitmap.buffer # 循环画位图 for row in range(rows): for col in range(cols): # 如果当前位置属于字形的一部分而不是空白 if glyph_pixels[row * cols + col] != 0: # 写入每个像素点的三通道的值 img[y_pos + row][x_pos + col][0] = color[0] img[y_pos + row][x_pos + col][1] = color[1] img[y_pos + row][x_pos + col][2] = color[2] # 快速设置带字符串的图片的属性 class GenerateCharListImage(object): # 初始化图片属性 def __init__(self): # 候选字符集为数字0-9 self.number = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] # 令char_set为候选字符集 self.char_set = self.number # 计算候选字符集的字符个数 self.len = len(self.char_set) # 生成的不定长验证码最大长度 self.max_size = 4 self.ft = PutChineseText('fonts/OCR-B.ttf') # 生成随机长度0-max_size之间的字符串,并返回字符串及对应的标签向量 def random_text(self): # 空字符串 text = '' # 空标签向量 text_vector = np.zeros((self.max_size * self.len)) # 设置字符串的长度是随机的 size = random.randint(1, self.max_size) # size = self.max_size # 逐个生成字符串和对应的标签向量 for index in range(size): c = random.choice(self.char_set) one_element_vector = self.one_char_to_one_element_vector(c) # 更新字符串和标签向量 text = text + c text_vector[index * self.len:(index + 1) * self.len] = np.copy(one_element_vector) # 返回字符串及对应的标签向量 return text, text_vector # 根据生成的字符串,生成验证码图片,返回图片数据和其标签,默认给图片添加高斯噪声 def generate_color_image(self, img_shape, noise): text, text_vector = self.random_text() # 创建一个图片背景,图片背景为黑色 img_background = np.zeros([img_shape[0], img_shape[1], 3]) # 设置图片背景为白色 img_background[:, :, 0], img_background[:, :, 1], img_background[:, :, 2] = 255, 255, 255 # (0, 0, 0)黑色,(255, 255, 255)白色,(255, 0, 0)深蓝色,(0, 255, 0)绿色,(0, 0, 255)红色 # 设置字体颜色为黑色 text_color = (0, 0, 0) # 设置文本在图片上起始位置和文本大小,单位都是像素点 pos = (20, 10) text_size = 20 # 画出验证码图片,返回的image是一个三维数组 image = self.ft.draw_text(img_background, pos, text, text_size, text_color) # 如果想添加噪声 if noise == "gaussian": # 添加20%的高斯噪声 image = self.image_add_gaussian_noise(image, 0.2) elif noise == "salt": # 添加20%的椒盐噪声 image = self.image_add_salt_noise(image, 0.1) elif noise == "None": pass # 返回三维数组形式的彩色图片 return image, text, text_vector # 给一张生 20000 成的图片加入随机椒盐噪声 def image_add_salt_noise(self, image, percent): rows, cols, dims = image.shape # 要添加椒盐噪声的像素点的数量,用全图像素点个数乘以一个百分比计算出来 salt_noise_num = int(percent * image.shape[0] * image.shape[1]) for i in range(salt_noise_num): # 获得随机的一个x值和y值,代表一个像素点 x = np.random.randint(0, rows) y = np.random.randint(0, cols) # 所谓的椒盐噪声就是随机地将图像中的一定数量(这个数量就是椒盐的数量num)的像素值取极大或者极小 # 即让维度0第x个,维度1第y个确定的一个像素点的数组(这个数组有三个元素)的三个值都为0,即噪点是黑色,因为我们的图片背景是白色 image[x, y, :] = 0 return image # 给一张生成的图片加入高斯噪声 def image_add_gaussian_noise(self, image, percent): rows, cols, dims = image.shape # 要添加的高斯噪点的像素点的数量,用全图像素点个数乘以一个百分比计算出来 gaussian_noise_num = int(percent * image.shape[0] * image.shape[1]) # 逐个给像素点添加噪声 for index in range(gaussian_noise_num): # 随机挑一个像素点 x_temp, y_temp = np.random.randint(0, rows), np.random.randint(0, cols) # 随机3个值,加到这个像素点的3个通道值上,为了不超过255,后面再用clamp函数限定其范围不超过255 value_temp = np.random.normal(0, 255, 3) for subscript in range(3): image[x_temp, y_temp, subscript] = image[x_temp, y_temp, subscript] - value_temp[subscript] if image[x_temp, y_temp, subscript] > 255: image[x_temp, y_temp, subscript] = 255 elif image[x_temp, y_temp, subscript] < 0: image[x_temp, y_temp, subscript] = 0 return image # 图片降噪函数 def image_reduce_noise(self, image): # 使用方框滤波,normalize如果等于true就相当于均值滤波了,-1表示输出图像深度和输入图像一样,(2,2)是方框大小 image = cv2.boxFilter(image, -1, (2, 2), normalize=False) return image # 将彩色图像转换成灰度图片的一维数组形式的数据形式 def color_image_to_gray_image(self, image): # 将图片转成灰度数据,并进行标准化(0-1之间) r, g, b = image[:, :, 0], image[:, :, 1], image[:, :, 2] gray = (0.2989 * r + 0.5870 * g + 0.1140 * b) / 255 # 生成灰度图片对应的一维数组数据,即输入模型的x数据形式 gray_image_array = np.array(gray).flatten() # 返回度图片的一维数组的数据形式 return gray_image_array # 单个字符转为向量 def one_char_to_one_element_vector(self, c): one_element_vector = np.zeros([self.len, ]) # 找每个字符是字符集中的第几个字符,是第几个就把标签向量中第几个元素值置1 for index in range(self.len): if self.char_set[index] == c: one_element_vector[index] = 1 return one_element_vector # 整个标签向量转为字符串 def text_vector_to_text(self, text_vector): text = '' text_vector_len = len(text_vector) # 找标签向量中为1的元素值,找到后index即其下标,我们就知道那是候选字符集中的哪个字符 for index in range(text_vector_len): if text_vector[index] == 1: text = text + self.char_set[index % self.len] # 返回字符串 return text if __name__ == '__main__': # 创建文件保存路径 if not os.path.exists("./free_type_image/"): os.mkdir("./free_type_image/") # 图片尺寸 image_shape = (40, 120) test_object = GenerateCharListImage() # 生成一个不加噪声的图片 test_color_image_no_noise, test_text_no_noise, test_text_vector_no_noise = test_object.generate_color_image( image_shape, noise="None") test_gray_image_array_no_noise = test_object.color_image_to_gray_image(test_color_image_no_noise) print(test_gray_image_array_no_noise) print(test_text_no_noise, test_text_vector_no_noise) cv2.imwrite("./free_type_image/test_color_image_no_noise.jpg", test_color_image_no_noise) # 显示这张不加噪声的图片 cv2.imshow("test_color_image_no_noise", test_color_image_no_noise) # 2000毫秒后刷新图像 cv2.waitKey(2000) # 生成一个加了高斯噪声的图片 test_color_image_gaussian_noise, test_text_gaussian_noise, test_text_vector_gaussian_noise = \ test_object.generate_color_image(image_shape, noise="gaussian") cv2.imwrite("./free_type_image/test_color_image_gaussian_noise.jpg", test_color_image_gaussian_noise) cv2.imshow("test_color_image_gaussian_noise", test_color_image_gaussian_noise) # 2000毫秒后刷新图像 cv2.waitKey(2000) # 高斯噪声图片降噪后的图片 test_color_image_reduce_gaussian_noise = test_object.image_reduce_noise(test_color_image_gaussian_noise) cv2.imwrite("./free_type_image/test_color_image_reduce_gaussian_noise.jpg", test_color_image_reduce_gaussian_noise) cv2.imshow("test_color_image_reduce_gaussian_noise", test_color_image_reduce_gaussian_noise) # 2000毫秒后刷新图像 cv2.waitKey(2000) # 生成一个加了椒盐噪声的图片 test_color_image_salt_noise, test_text_salt_noise, test_text_vector_salt_noise = test_object.generate_color_image( image_shape, noise="salt") cv2.imwrite("./free_type_image/test_color_image_salt_noise.jpg", test_color_image_salt_noise) cv2.imshow("test_color_image_salt_noise", test_color_image_salt_noise) # 2000毫秒后刷新图像 cv2.waitKey(2000) # 椒盐噪声图片降噪后的图片 test_color_image_reduce_salt_noise = test_object.image_reduce_noise(test_color_image_salt_noise) cv2.imwrite("./free_type_image/test_color_image_reduce_salt_noise.jpg", test_color_image_reduce_salt_noise) cv2.imshow("test_color_image_reduce_salt_noise", test_color_image_reduce_salt_noise) # 2000毫秒后刷新图像 cv2.waitKey(2000) cv2.destroyAllWindows()
运行测试用例后,可以依次生成无噪声彩色图片、加了高斯噪声的彩色图片、加了高斯噪声再降噪后的彩色图片、加了椒盐噪声的彩色图片、加了椒盐噪声再降噪后的彩色图片。
Captcha库生成不定长验证码:
使用Captcha库生成不定长验证码的步骤:
首先确定候选字符集和验证码最大长度;
随机生成不定长的字符串和其对应的标签向量;
利用上面生成的字符串生成对应的验证码图片(Captcha库会自动给图片添加一些噪声,噪声中会有一些不规则的线);
我们还可以给图片降噪,生成降噪后的彩色图片;
将彩色图片转换成灰度图片,再转换成可以送入深度学习模型训练的一维数组数据形式(一个像素点用一个灰度值表示)。
完整代码如下:
CaptchaGenerateTextImage.py
[code]from captcha.image import ImageCaptcha import random from PIL import Image import numpy as np import cv2 import os class GenerateCaptchaListImage(object): # 生成验证码的函数,生成的验证码序列长度为4 # 初始化图片属性 def __init__(self): # 候选字符集为数字0-9 self.number = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] # 令char_set为候选字符集 self.char_set = self.number # 计算候选字符集的字符个数 self.len = len(self.char_set) # 生成的不定长验证码最大长度 self.max_size = 4 def random_captcha_text(self): # 空字符串 text = '' # 空标签向量 text_vector = np.zeros((self.max_size * self.len)) # 设置字符串的长度是随机的 size = random.randint(1, self.max_size) # size = self.max_size # 逐个生成字符串和对应的标签向量 for index in range(size): c = random.choice(self.char_set) one_element_vector = self.one_char_to_one_element_vector(c) # 更新字符串和标签向量 text = text + c text_vector[index * self.len:(index + 1) * self.len] = np.copy(one_element_vector) # 返回字符串及对应的标签向量 return text, text_vector # 获取和生成的验证码对应的字符图片 def generate_color_image(self, img_shape): # 生成指定大小的图片 img = ImageCaptcha(height=img_shape[0], width=img_shape[1]) # 生成一个随机的验证码序列 text, text_vector = self.random_captcha_text() # 根据验证码序列生成对应的字符图片 image = img.generate(text) image = Image.open(image) # 因为图片是用RGB模式表示的,将其转换成数组即图片的分辨率160X60的矩阵,矩阵每个元素是一个像素点上的RGB三个通道的值 image = np.array(image) return image, text, text_vector # 图片降噪函数,对于captcha生成的验证码,使用中值滤波降噪效果较好 def image_reduce_noise(self, image): # 使用中值滤波,7表示中值滤波器使用7×7的范围来计算。 # 即对像素的中心值及其7×7邻域组成了一个数值集,对其进行处理计算,当前像素被其中位值替换掉。 # 这样,如果在某个像素周围有白色或黑色的像素,这些白色或黑色的像素不会选择作为中值(最大或最小值不用),而是被替换为邻域值。 image = cv2.medianBlur(image, 7) return image # 将彩色图像转换成灰度图片的一维数组形式的数据形式 def color_image_to_gray_image(self, image): # 将图片转成灰度数据,并进行标准化(0-1之间) r, g, b = image[:, :, 0], image[:, :, 1], image[:, :, 2] gray = (0.2989 * r + 0.5870 * g + 0.1140 * b) / 255 # 生成灰度图片对应的一维数组数据,即输入模型的x数据形式 gray_image_array = np.array(gray).flatten() # 返回度图片的一维数组的数据形式 return gray_image_array # 单个字符转为向量 def one_char_to_one_element_vector(self, c): one_element_vector = np.zeros([self.len, ]) # 找每个字符是字符集中的第几个字符,是第几个就把标签向量中第几个元素值置1 for index in range(self.len): if self.char_set[index] == c: one_element_vector[index] = 1 return one_element_vector # 整个标签向量转为字符串 def text_vector_to_text(self, text_vector): text = '' text_vector_len = len(text_vector) # 找标签向量中为1的元素值,找到后index即其下标,我们就知道那是候选字符集中的哪个字符 for index in range(text_vector_len): if text_vector[index] == 1: text = text + self.char_set[index % self.len] # 返回字符串 return text # 测试 if __name__ == '__main__': # 创建文件保存路径 if not os.path.exists("./captcha_image/"): os.mkdir("./captcha_image/") # 图片尺寸 image_shape = (60, 120) test_object = GenerateCaptchaListImage() # 生成一张有噪声的验证码图片(captcha生成的验证码图片默认就是带有噪声的) test_color_image_noise, test_text_noise, test_text_vector_noise = test_object.generate_color_image(image_shape) test_gray_image_array_noise = test_object.color_image_to_gray_image(test_color_image_noise) print(test_gray_image_array_noise) print(test_text_noise, test_text_vector_noise) cv2.imwrite("./captcha_image/test_color_image_noise.jpg", test_color_image_noise) cv2.imshow("test_color_image_noise", test_color_image_noise) # 2000毫秒后刷新图像 cv2.waitKey(2000) # 降噪后的图片 test_color_image_reduce_noise = test_object.image_reduce_noise(test_color_image_noise) cv2.imwrite("./captcha_image/test_color_image_reduce_noise.jpg", test_color_image_reduce_noise) cv2.imshow("test_color_image_reduce_noise", test_color_image_reduce_noise) # 2000毫秒后刷新图像 cv2.waitKey(2000) cv2.destroyAllWindows()
运行后依次生成一张有噪声的彩色图片和一张降噪后的彩色图片。
lstm+CTC_loss识别不定长验证码:
我们的代码可以用freetype库或captcha库生成的不定长验证码训练模型:
[code]# 如果用freetype生成的验证码,则为True;如果是用captcha生成的验证码,则为False use_freeType_or_captcha = True
不同库生成的验证码训练的结果保存在不同的文件夹中,测试时也会调用对应的存储模型来进行测试。
free_type_get_next_batch()或captcha_get_next_batch()函数用来取得一批训练样本,返回值为输入的x数据(图片数据)、压缩过的图片标签(注意这个标签不是独热编码,而是这批样本中每个样本代表的字符串)、seq_length(记录这批样本中每个样本中有多少个时间序列,即time_steps的数量)。
图片标签原本是一批样本的所有字符串(一个样本一个字符串)的集合,放在一个列表中,经过sparse_tuple_from函数转化为稀疏矩阵的记录形式,即有3个结构indices, values, shape。indices记录每一个字符在字符所在的样本中的位置(下标),values记录了所有的字符串中的字符,shape记录了样本个数和样本字符串最大长度;
训练模型定义了一个cell层,该层有64个神经元,输入图片的尺寸为40X120,每张图片的一列的像素点数据看成一个time_steps中输入的数据,所以time_steps=120。输入数据的shape=[batch_size,num_steps,input_dim]。dynamic_rnn函数的time_major=False,故cell层输出数据的shape=[batch_size,max_time_step,cell.output_size],cell.output_size即cell层神经元的数目。cell层后接一个全连接层,再经过一系列操作,得到的logits的shape=[max_time_step,batch_size,num_classes]。
定义ctc_loss为损失函数,输入真实标签(是一个经过sparse_tuple_from函数转化为稀疏矩阵的记录形式),预测标签logits,以及seq_len(每个样本的time_steps的数量);
通过 tf.nn.ctc_beam_search_decoder函数将预测标签解码成稀疏矩阵的记录形式(即压缩过的标签向量形式)。
每轮使用64个样本训练模型,每训练100次将预测标签和真实标签用decode_sparse_tensor()函数将其解码成字符串形式,然后比对,最后得到预测准确率。
完整代码如下:
[code]# LSTM+CTC_loss训练识别不定长数字字符图片 from CaptchaGenerateTextImage import GenerateCaptchaListImage from freeTypeGenerateTextImage import GenerateCharListImage import tensorflow as tf import numpy as np import time import os os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' os.environ["CUDA_VISIBLE_DEVICES"] = '0' # 超参数 # 要生成的图片的像素点大小 char_list_image_shape = (40, 120) # 隐藏层神经元数量 num_hidden = 64 # 初始学习率和学习率衰减因子 lr_start = 1e-3 lr_decay_factor = 0.9 # 一批训练样本和测试样本的样本数量,训练迭代次数,每经过test_report_step_interval测试一次模型预测的准确率 train_batch_size = 64 test_batch_size = 64 train_iteration = 5000 test_report_step_interval = 100 # 用来恢复标签用的候选字符集 char_set = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] # 如果用freetype生成的验证码,则为True;如果是用captcha生成的验证码,则为False use_freeType_or_captcha = False # 设定准确率达到多少后停止训练 acc_reach_to_stop = 0.95 obj_number = GenerateCharListImage() # 类别为10位数字+blank+ctc blank num_classes = obj_number.len + 1 + 1 # 生成batch_size个样本,样本的shape变为[batch_size,image_shape[1],image_shape[0]] # 输入的图片是把每一行的数据看成一个时间间隔t内输入的数据,然后有多少行就是有多少个时间间隔 # 使用freetype库生成一批样本 def free_type_get_next_batch(bt_size, img_shape): obj_batch = GenerateCharListImage() bt_x_inputs = np.zeros([bt_size, char_list_image_shape[1], char_list_image_shape[0]]) bt_y_inputs = [] for i in range(bt_size): # 生成不定长度的字符串及其对应的彩色图片 color_image, text, text_vector = obj_batch.generate_color_image(img_shape, noise="gaussian") # 图片降噪,然后由彩色图片生成灰度图片的一维数组形式 color_image = obj_batch.image_reduce_noise(color_image) gray_image_array = obj_batch.color_image_to_gray_image(color_image) # np.transpose函数将得到的图片矩阵转置成(image_shape[1],image_shape[0])形状的矩阵,且由行有序变成列有序 # 然后将这个图片的数据写入bt_x_inputs中第0个维度上的第i个元素(每个元素就是一张图片的所有数据) bt_x_inputs[i, :] = np.transpose(gray_image_array.reshape((char_list_image_shape[0], char_list_image_shape[1]))) # 把每个图片的标签添加到bt_y_inputs列表,注意这里直接添加了图片对应的字符串 bt_y_inputs.append(list(text)) # 将bt_y_inputs中的每个元素都转化成np数组 targets = [np.asarray(i) for i in bt_y_inputs] # 将targets列表转化为稀疏矩阵 sparse_matrix_targets = sparse_tuple_from(targets) # bt_size个1乘以char_list_image_shape[1],也就是batch_size个样本中每个样本(每个样本即图片)的长度上的像素点个数(或者说列数) # seq_length就是每个样本中有多少个时间序列 seq_length = np.ones(bt_x_inputs.shape[0]) * char_list_image_shape[1] # 得到的bt_x_inputs的shape=[bt_size, char_list_image_shape[1], char_list_image_shape[0]] return bt_x_inputs, sparse_matrix_targets, seq_length # 使用captcha库生成一批样本 def captcha_get_next_batch(bt_size, img_shape): obj_batch = GenerateCaptchaListImage() bt_x_inputs = np.zeros([bt_size, char_list_image_shape[1], char_list_image_shape[0]]) bt_y_inputs = [] for i in range(bt_size): # 生成不定长度的字符串及其对应的彩色图片 color_image, text, text_vector = obj_batch.generate_color_image(img_shape) # 图片降噪,然后由彩色图片生成灰度图片的一维数组形式 color_image = obj_batch.image_reduce_noise(color_image) gray_image_array = obj_batch.color_image_to_gray_image(color_image) # np.transpose函数将得到的图片矩阵转置成(image_shape[1],image_shape[0])形状的矩阵,且由行有序变成列有序 # 然后将这个图片的数据写入bt_x_inputs中第0个维度上的第i个元素(每个元素就是一张图片的所有数据) bt_x_inputs[i, :] = np.transpose(gray_image_array.reshape((char_list_image_shape[0], char_list_image_shape[1]))) # 把每个图片的标签添加到bt_y_inputs列表,注意这里直接添加了图片对应的字符串 bt_y_inputs.append(list(text)) # 将bt_y_inputs中的每个元素都转化成np数组 targets = [np.asarray(i) for i in bt_y_inputs] # 将targets列表转化为稀疏矩阵 sparse_matrix_targets = sparse_tuple_from(targets) # bt_size个1乘以char_list_image_shape[1],也就是batch_size个样本中每个样本(每个样本即图片)的长度上的像素点个数(或者说列数) seq_length = np.ones(bt_x_inputs.shape[0]) * char_list_image_shape[1] # 得到的bt_x_inputs的shape=[bt_size, char_list_image_shape[1], char_list_image_shape[0]] return bt_x_inputs, sparse_matrix_targets, seq_length # 转化一个序列列表为稀疏矩阵 def sparse_tuple_from(sequences, dtype=np.int32): """ :param sequences: 一个元素是列表的列表 :param dtype: 列表元素的数据类型 :return: 返回一个元组(indices, values, shape) """ indices = [] values = [] for index, seq in enumerate(sequences): # sequences存储了你的样本对应的字符串(由数字组成)的所有数字 # 每次取list中的一个元素,即一个数字,代表的是一个样本(即一个字符串)中的一个数字值,注意这个单独的数字是也是一个列表 # extend()函数用于在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)。 # zip()函数将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的一个对象。 # zip(a,b)函数分别从a和b中取一个元素组成元组,再次将组成的元组组合成一个新的迭代器。a与b的维数相同时,正常组合对应位置的元素。 # 每个seq是一个字符串,index即这是第几个字符串(第几个样本) indices.extend(zip([index] * len(seq), range(len(seq)))) # [index]的值为[0]、[1]、[2]。。。,len(seq)为每个字符串的长度 # 如[1]*4的结果是[1, 1, 1, 1] # * 操作符在实现上是复制了值的引用,而不是创建了新的对象。所以上述的list里面,是4个指向同一个对象的引用,所以4个值都是1 values.extend(seq) indices = np.asarray(indices, dtype=np.int64) values = np.asarray(values, dtype=dtype) shape = np.asarray([len(sequences), np.asarray(indices).max(0)[1] + 1], dtype=np.int64) # indices:二维int64的矩阵,代表元素在batch样本矩阵中的位置 # values:二维tensor,代表indice位置的数据值 # dense_shape:一维,代表稀疏矩阵的大小 # 假设sequences有2个,值分别为[1 3 4 9 2]、[ 8 5 7 2]。(即batch_size=2) # 则其indices=[[0 0][0 1][0 2][0 3][0 4][1 0][1 1][1 2][1 3]] # values=[1 3 4 9 2 8 5 7 2] # shape=[2 5] return indices, values, shape # 解压缩压缩过的所有样本的字符串的列表的集合,return为不压缩的所有样本的字符串的列表的集合 def decode_sparse_tensor(sparse_tensor): decoded_indexes = list() current_i = 0 current_seq = [] # sparse_tensor[0]即sparse_tuple_from函数的返回值中的indices # 这里是一批样本的字符串的列表集合经过sparse_tuple_from函数处理后的返回值中的indices # offset即indices中元素的下标,即indices中的第几个元素(每个元素是一个单字符,代表这个单字符在这批样本中的位置) # i_and_index即sparse_tensor[0]也就是indices中的每个元素,i_and_index[0]即sparse_tensor[0]中每个元素属于第几号样本 for offset, i_and_index in enumerate(sparse_tensor[0]): # i记录现在遍历到的sparse_tensor[0]元素属于第几号样本 i = i_and_index[0] # 如果新遍历到的sparse_tensor[0]元素和前一个元素不属于同一个样本 if i != current_i: # 每次属于同一个样本的sparse_tensor[0]元素遍历完以后,decoded_indexes添加这个样本的完整current_seq decoded_indexes.append(current_seq) # 更新i current_i = i # 对这样新编号的样本建立一个新的current_seq current_seq = list() # current_seq记录我们现在遍历到的sparse_tensor[0]元素在这批样本中的位置(下标) current_seq.append(offset) # for循环遍历完以后,添加最后一个样本的current_seq到decoded_indexes,这样decoded_indexes就记录了这批样本中所有样本的current_seq decoded_indexes.append(current_seq) result = [] # 遍历decoded_indexes,依次解码每个样本的字符串内容 # 实际上decoded_indexes就是记录了一批样本中每个样本中的所有字符在这批样本中的位置(下标) for index in decoded_indexes: result.append(decode_a_seq(index, sparse_tensor)) # result记录了这批样本中每个样本的字符串内容,result的每个元素就是一个样本的字符串的内容 # 这个元素是一个列表,列表每个元素是一个单字符 return result # 将压缩过的所有样本的字符串的列表的集合spars_tensor中取出第indexes号样本中的所有字符在这个样本中的位置(下标),解压缩成字符串 def decode_a_seq(indexes, spars_tensor): decoded = [] # indexes是decoded_indexes中第indexes号样本中的所有字符在这批样本中的位置(下标) # for循环取出的m就是这个样本中每个字符在这批样本中的位置(下标) for m in indexes: # spars_tensor[1][m]即spars_tensor中的values列表的第m个值 # ch即取出了m对应的spars_tensor中的values列表的第m个值,是一个字符 ch = char_set[spars_tensor[1][m]] # 把这个字符加到decoded列表中 decoded.append(ch) # decoded列表即存储一个样本中的所有字符 return decoded # 定义训练模型 def get_train_model(): x_inputs = tf.placeholder(tf.float32, [None, None, char_list_image_shape[0]]) # inputs的维度是[batch_size,num_steps,input_dim] # 定义ctc_loss需要的标签向量(稀疏矩阵形式) targets = tf.sparse_placeholder(tf.int32) # 每个样本中有多少个时间序列 seq_length = tf.placeholder(tf.int32, [None]) # 定义LSTM网络的cell层,这里定义有num_hidden个单元 # cell_multilayer = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.LSTMCell(num_units=size) for size in [64, 128]],state_is_tuple=True) cell = tf.contrib.rnn.LSTMCell(num_hidden, state_is_tuple=True) # state_is_tuple:如果为True,接受和返回的states是n-tuples,其中n=len(cells)。 # 如果cell选择了state_is_tuple=True,那final_state是个tuple,分别代表Ct和ht,其中ht与outputs中的对应的最后一个时刻的输出ht相等; # 如果time_major == False(default),输出张量形如[batch_size, max_time, cell.output_size]。 # 如果time_major == True, 输出张量形如:[max_time, batch_size, cell.output_size]。 # cell.output_size其实就是我们的num_hidden,即cell层的神经元的个数。 outputs, _ = tf.nn.dynamic_rnn(cell, x_inputs, seq_length, time_major=False, dtype=tf.float32) # ->[batch_size,max_time_step,num_features]->lstm # ->[batch_size,max_time_step,cell.output_size]->reshape # ->[batch_size*max_time_step,num_hidden]->affine projection AW+b # ->[batch_size*max_time_step,num_classes]->reshape # ->[batch_size,max_time_step,num_classes]->transpose # ->[max_time_step,batch_size,num_classes] # 上面最后的shape就是标签向量的shape,此时标签向量还未压缩 shape = tf.shape(x_inputs) # x_inputs的shape=[batch_size,image_shape[1],image_shape[0]] # 所以输入的数据是按列来排的,一列的像素为一个时间序列里输入的数据,一共120个时间序列 batch_s, max_time_steps = shape[0], shape[1] # 输出的outputs为num_hidden个隐藏层单元的所有时刻的输出 # reshape后的shape=[batch_size*max_time_step,num_hidden] outputs = tf.reshape(outputs, [-1, num_hidden]) # 相当于一个全连接层,做一次线性变换 w = tf.Variable(tf.truncated_normal([num_hidden, num_classes], stddev=0.1), name="w") b = tf.Variable(tf.constant(0., shape=[num_classes]), name="b") logits = tf.matmul(outputs, w) + b # 变换成和标签向量一致的shape logits = tf.reshape(logits, [batch_s, -1, num_classes]) # logits的维度交换,第1个维度和第0个维度互相交换 logits = tf.transpose(logits, (1, 0, 2)) # 注意返回的logits预测标签此时还未压缩,而targets真实标签是被压缩过的 return logits, x_inputs, targets, seq_length, w, b # test_targets即用sparse_tuple_from压缩过的所有样本的字符串的一个列表的集合,decoded_list也是一样 def report_accuracy(decoded_list, test_targets): # 将压缩的真实标签和预测标签解压缩,解压缩后都是一个列表,列表中存储了这批样本中的所有字符串。 # 列表中的每个元素都是一个列表,这个列表中包含一个样本中的所有字符。 original_list = decode_sparse_tensor(test_targets) detected_list = decode_sparse_tensor(decoded_list) # 本批样本中预测正确的次数 correct_prediction = 0 # 注意这里的标签不是指独热编码,而是这批样本的每个样本代表的字符串的集合 # 如果解压缩后的真实标签和预测标签的样本个数不一样 if len(original_list) != len(detected_list): print("真实标签样本个数:{},预测标签样本个数:{},真实标签与预测标签样本个数不匹配".format(len(original_list), len(detected_list))) return -1 print("真实标签(长度) <-------> 预测标签(长度)") # 注意这里的标签不是指独热编码,而是这批样本的每个样本代表的字符串的集合 # 如果真实标签和预测标签的样本个数吻合,则分别比对每一个样本的预测结果 # for循环从original_list中取出一个一个的字符串(注意字符串存在一个列表中,列表中每个元素是单个字符) for idx, true_number in enumerate(original_list): # detected_list[idx]即detected_list中第idx号字符串(注意字符串存在一个列表中,列表中每个元素是单个字符) detect_number = detected_list[idx] # signal即真实标签是否与预测标签相等的结果,相等则为true signal = (true_number == detect_number) # 打印true_number和detect_number直观对比 print(signal, true_number, "(", len(true_number), ") <-------> ", detect_number, "(", len(detect_number), ")") # 如果相等,统计正确的预测次数加1 if signal is True: correct_prediction += 1 # 计算本批样本预测的准确率 acc = correct_prediction * 1.0 / len(original_list) print("本批样本预测准确率:{}".format(acc)) return acc # 定义训练过程 def train(): global_step = tf.Variable(0, trainable=False) # tf.train.exponential_decay函数实现指数衰减学习率 learning_rate = tf.train.exponential_decay(lr_start, global_step, train_iteration, lr_decay_factor, staircase=True) logits, inputs, targets, seq_len, w, b = get_train_model() # 注意得到的logits此时是未压缩的标签向量 # 设置loss函数是ctc_loss函数 # CTC :Connectionist Temporal Classifier 一般译为联结主义时间分类器 ,适合于输入特征和输出标签之间对齐关系不确定的时间序列问题 # TC可以自动端到端地同时优化模型参数和对齐切分的边界。 # 本例40X120大小的图片,切片成120列,输出标签最大设定为4(即不定长验证码最大长度为4),这样就可以用CTC模型进行优化。 # 假设40x120的图片,数字串标签是"123",把图片按列切分(CTC会优化切分模型),然后分出来的每块再去识别数字 # 找出这块是每个数字或者特殊字符的概率(无法识别的则标记为特殊字符"-") # 这样就得到了基于输入特征序列(图片)的每一个相互独立建模单元个体(划分出来的块)(包括“-”节点在内)的类属概率分布。 # 基于概率分布,算出标签序列是"123"的概率P(123),当然这里设定"123"的概率为所有子序列之和,这里子序列包括'-'和'1'、'2'、'3'的连续重复 # tf.nn.ctc_loss(labels, inputs, sequence_length, preprocess_collapse_repeated=False, ctc_merge_repeated=True) # labels: label实际上是一个稀疏矩阵SparseTensor,即真实标签(被压缩过的) # inputs:是RNN的输出logits,shape=[max_time_step,batch_size,num_classes] # sequence_length: bt_size个1乘以char_list_image_shape[1],即bt_size个样本每个样本有多少个time_steps # preprocess_collapse_repeated: 设置为True的话, tensorflow会对输入的labels进行预处理, 连续重复的会被合成一个。 # ctc_merge_repeated: 连续重复的是否被合成一个。 cost = tf.reduce_mean(tf.nn.ctc_loss(labels=targets, inputs=logits, sequence_length=seq_len)) # 这里用Adam算法来优化 optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost, global_step=global_step) decoded, log_prob = tf.nn.ctc_beam_search_decoder(logits, seq_len, merge_repeated=False) # tf.nn.ctc_beam_search_decoder对输入中给出的logits执行波束搜索解码。 # ctc_greedy_decoder是ctc_beam_search_decoder中参数top_paths=1和beam_width=1(但解码器在这种特殊情况下更快)的特殊情况。 # 如果merge_repeated是True,则合并输出序列中的重复类。这意味着如果梁中的连续条目相同,则仅发出第一个条目。 # 如,当顶部路径为时A B B B B,返回值为:A B如果merge_repeated = True;A B B B B如果merge_repeated = False。 # inputs:3-D float Tensor,尺寸 [max_time x batch_size x num_classes]。输入是预测的标签向量。 # sequence_length:bt_size个1乘以char_list_image_shape[1],即bt_size个样本每个样本有多少个time_steps # beam_width:int标量> = 0(波束搜索波束宽度)。 # top_paths:int标量> = 0,<= beam_width(控制输出大小)。 # merge_repeated:布尔值。默认值:True。如果merge_repeated是True,则合并输出序列中的重复类。 # 返回值: # 元组(decoded, log_probabilities) # decoded:decoded是一组SparseTensor。由于我们每一次训练只输入一组训练数据,所以decoded里只有一个SparseTensor。 # 即decoded[0]就是我们这组训练样本预测得到的SparseTensor,decoded[0].indices就是其位置矩阵。 # log_probability:包含序列对数概率的float矩阵(batch_size)。 accuracy = tf.reduce_mean(tf.edit_distance(tf.cast(decoded[0], tf.int32), targets)) # tf.edit_distance(hypothesis, truth, normalize=True, name="edit_distance"),计算序列之间的(Levenshtein)莱文斯坦距离 # 莱文斯坦距离(LD)用于衡量两个字符串之间的相似度。莱文斯坦距离被定义为将字符串a变换为字符串b所需的删除、插入、替换操作的次数。 # hypothesis: SparseTensor,包含序列的假设.truth: SparseTensor, 包含真实序列. # normalize: 布尔值,如果值True的话,求出来的Levenshtein距离除以真实序列的长度. 默认为True # name: operation 的名字,可选。 def do_report(): # 生成一批样本数据,进行测试,根据使用的是freetype还是captcha生成的验证码,使用不同的批样本 # 为true时使用freetype生成验证码 if use_freeType_or_captcha is True: test_inputs, test_targets, test_seq_len = free_type_get_next_batch(test_batch_size, char_list_image_shape) else: test_inputs, test_targets, test_seq_len = captcha_get_next_batch(test_batch_size, char_list_image_shape) test_feed = {inputs: test_inputs, targets: test_targets, seq_len: test_seq_len} dd = sess.run(decoded[0], feed_dict=test_feed) report_acc = report_accuracy(dd, test_targets) # 返回准确率 return report_acc def do_batch(): # 生成一批样本数据,进行训练,根据使用的是freetype还是captcha生成的验证码,使用不同的批样本 # 为true时使用freetype生成验证码 if use_freeType_or_captcha is True: train_inputs, train_targets, train_seq_len = free_type_get_next_batch(train_batch_size, char_list_image_shape) else: train_inputs, train_targets, train_seq_len = captcha_get_next_batch(train_batch_size, char_list_image_shape) train_feed = {inputs: train_inputs, targets: train_targets, seq_len: train_seq_len} b_cost, b_lr, b_acc, steps, _ = sess.run([cost, learning_rate, accuracy, global_step, optimizer], feed_dict=train_feed) return b_cost, steps, b_acc, b_lr # 创建模型文件保存路径 if not os.path.exists("./free_type_image_lstm_model/"): os.mkdir("./free_type_image_lstm_model/") if not os.path.exists("./captcha_image_lstm_model/"): os.mkdir("./captcha_image_lstm_model/") saver = tf.train.Saver() # 创建会话,开始训练 with tf.Session() as sess: sess.run(tf.global_variables_initializer()) if use_freeType_or_captcha is True and os.path.exists("./free_type_image_lstm_model/checkpoint"): # 判断模型是否存在,如果存在则从模型中恢复变量 saver.restore(sess, tf.train.latest_checkpoint('./free_type_image_lstm_model/')) if use_freeType_or_captcha is False and os.path.exists("./captcha_image_lstm_model/checkpoint"): # 判断模型是否存在,如果存在则从模型中恢复变量 saver.restore(sess, tf.train.latest_checkpoint('./captcha_image_lstm_model/')) # 训练循环 while True: start = time.time() # 每轮将一个batch的样本喂进去训练 batch_cost, train_steps, acc, batch_lr = do_batch() batch_seconds = time.time() - start log = "iteration:{},batch_cost:{:.6f},batch_learning_rate:{:.12f},batch seconds:{:.6f}" print(log.format(train_steps, batch_cost, batch_lr, batch_seconds)) if train_steps % test_report_step_interval == 0: # 如果使用freetype生成的验证码,则生成的模型存在free_type_image_lstm_model文件夹 # 为True时使用freetype库生成验证码 if use_freeType_or_captcha is True: saver.save(sess, "./free_type_image_lstm_model/train_model", global_step=train_steps) # 如果使用captcha生成的验证码,则生成的模型存在captcha_image_lstm_model文件夹 else: saver.save(sess, "./captcha_image_lstm_model/train_model", global_step=train_steps) acc = do_report() if acc > acc_reach_to_stop: print("准确率已达到临界值{},目前准确率{},停止训练".format(acc_reach_to_stop, acc)) break if __name__ == '__main__': train()
运行结果:
使用freetype库生成的不定长验证码来训练模型,steps达到1200次左右预测准确率已可达到0.98,此时loss值大约0.1-0.2之间。
当使用captcha库生成的不定长验证码来训练模型时,steps训练12万次时loss值仍然在2.5左右,无法继续降低,此时准确率只有0.1-0.2左右。
说明这个模型对于captcha库生成的不定长验证码来说过于简单,不能作准确预测,原因是captcha库生成的不定长验证码会把字符随机地歪一些角度,并且字符会有一些放大或缩小,另外字符的位置也有一些偏移,这使得这个模型对于captcha库生成的不定长验证码来说过于简单了,无法预测。
我们可以尝试将该模型定义成多个cell层组成的lstm网络,增加模型的复杂度再训练。看看更复杂的模型是否可以预测。
比如将上面的模型中:
[code]cell = tf.contrib.rnn.LSTMCell(num_hidden, state_is_tuple=True) outputs, _ = tf.nn.dynamic_rnn(cell, x_inputs, seq_length, time_major=False, dtype=tf.float32)
改为:
[code]cell_multilayer = tf.nn.rnn_cell.MultiRNNCell([tf.nn.rnn_cell.LSTMCell(num_units=size) for size in [64, 128]], state_is_tuple=True) outputs, _ = tf.nn.dynamic_rnn(cell_multilayer, x_inputs, seq_length, time_major=False, dtype=tf.float32)
即将以前的一层cell层(64个神经元)改为2层cell层(64个神经元,128个神经元)。
然后删除以前存储的模型,重新训练。
具体结果大家可以试一下(我没有试过)。
阅读更多- tensorflow LSTM+CTC实现端到端的不定长数字串识别
- ASP.NET验证码生成与识别
- tensorflow实现验证码生成及识别(一)
- Python3 FreeType库介绍、Python3使用FreeType库生成不定长验证码的实现
- eoLinker-API_Shop_验证码识别与生成类API调用的代码示例合集:六位图片验证码生成、四位图片验证码生成、简单验证码识别等
- eoLinker-API_Shop_开发工具类API调用的代码示例合集:六位图片验证码生成、四位图片验证码生成、简单验证码识别等
- LSTM 与 CTC loss (以及DP、HMM)
- 利用keras框架cnn+ctc_loss识别不定长字符图片
- 基于python的验证码生成与识别1—生成简单的验证码
- 避免被软件识别的验证码生成
- 网友写的验证码生成方案,可防止绝大多数机械识别。
- 对tensorflow 中的lstm,ctc loss的调试
- Java 生成验证码
- python生成验证码
- Java基础--生成验证码
- mvc 如何生成空心3D风格的验证码 推荐
- jsp生成随机验证码图片
- 机器自动识别验证码的原理是怎么样的?
- thinkcmf生成的验证码不显示
- Python 使用Pillow模块生成验证码