您的位置:首页 > 其它

lstm+CTC_loss识别freetype库和captcha库生成的不定长验证码

2018-11-29 18:17 1381 查看
版权声明:站在巨人的肩膀上学习。 https://blog.csdn.net/zgcr654321/article/details/84634382

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个神经元)。

然后删除以前存储的模型,重新训练。

具体结果大家可以试一下(我没有试过)。

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: