Tesserast-OCR踩坑记录——训练一个能识别验证码的OCR模型
前言
公司项目的系统登录有一套验证码系统,之前想写一些自动化测试时总是会被这个验证码卡住,不能完全自动运行。去找开发同事关一下验证码,也是一开一关挺麻烦的,不能总麻烦人家。秉承着工作是自己的,麻烦到头来总要自己解决的原则,开始找方案。

第一个是发现可以把验证码图片给AI去解析,得到的答案正确率很高,就是不能调用太多,毕竟token是要花钱的,虽然量不大,但便宜归便宜,付费上班总归不是办法。然后又在网上找到了用OCR的方案,商用OCR当然也是要付费的,好在我们有伟大的开源方案,大不了自己训练嘛,这么简单的验证码,应该还挺好识别的吧...
主角
最后选择的就是开源的Tesseract-OCR,目前的版本是5.4.1,需要自己编译。好在也有懒人解决方案,在用户手册里可以找到第三方提供的编译好的安装包,提供了三个常用的系统,包括Windows。从链接进去就可以找到下载链接,不过Windows安装包提供的版本是5.4.0,并不是最新的...也不差多少是了。
下载安装,完成后会提供十数个命令行工具,其中用来做识别的是tesseract,其它的都不会用XD。
安装包只包含一个eng的训练数据(也就是模型),在安装位置的tessdata文件夹中,其它的模型需要到库中自行下载,就像它自己的介绍,提供了100+种语言。不过验证码是这样的,只需要数字和几个运算符,用英语也足够了,总之先尝试一下吧!
tesseract imageFile outputFile
# outputFile 使用 - 可以直接把结果打印在窗口中
# 使用 -l 选项指定使用的模型,只需要输入名称
很可惜,效果并不好,看来,想用的话只能自己训练了。
开始训练
训练是个很麻烦的事情,尤其是对我这种门外汉来说,在做的时候主要参考了这篇博客和官方用户手册,以及官方提供的一个简化训练步骤的脚本库。
1 准备训练图片
验证码的图片直接从登录页面拿就可以了,不过训练的话,即使构成简单还是要用几百张的,最好是用接口去获取图片,又因为训练脚本只支持TIFF和PNG类型的图片,其它类型的图片需要在保存时转换格式,还是要写个脚本来处理这个事情的。
import base64
from requests import get
for i in range(1, 101):
r = get("Url") # 验证码获取链接
if r.ok:
j = r.json()
s = j["data"]["code"] # 解析到图片数据,没有做验证,有问题的话就报错了
p = base64.b64decode(s) # base64数据需要解码
with open(f"./trainImage/{i}.png", "wb+") as f: # 创建文件
f.write(p)
print(f'write complete.{i}.png')
else: # 请求出错时调试用的
print(r.text)
break
这时,我遇到的第一个问题出现了,倒不是获取图片遇到问题,而是每个训练图片都是需要答案的,答案要保存在和图片同名的后缀为.gt.txt文件中,比如图片文件名是1.png,那么答案文件就是1.gt.txt,文件的内容只包含图片中的文本答案,比如上面那张图,它的答案应该写
6x4=?
不需要换行,但是应该包含图片中的所有目标字符。
注意到这个事情时,我已经保存了一百张图片,不多,但是手动创建文件还是很麻烦的,还要打开图片,还有写入文件。所以,这种事还是交给脚本来做吧XD~
我的思路是写一个UI程序,按顺序加载图片,再准备一个输入框,看到图片后我把答案输进去,再由脚本把答案写到对应文件里。非常的清晰XD。下面是我用的脚本
import logging
import tkinter as tk
class App(tk.Frame):
def __init__(self, master: tk.Tk) -> None:
super().__init__(master)
self.master = master
self.image = read_image()
self.create_widget()
def create_widget(self):
self.label = tk.Label(self.master, image=self.image)
self.label.grid(column=0,row=0)
self.input = tk.Entry(self.master, width=6 ,font=('Arial', 14))
self.input.bind('<Return>', self.complete)
self.input.grid(column=0,row=1)
self.label1 = tk.Label(self.master, text="=?", font=('Arial', 14))
self.label1.grid(row=1,column=1)
self.button2 = tk.Button(self.master, text="刷新", command= self.refresh_img)
self.button2.grid(row=0, column=1)
self.button = tk.Button(self.master, text="写入", command= self.complete)
self.button.grid(column=2,row=1)
def complete(self, event=''):
content = self.input.get()
if len(content) == 0 :
logging.warning('未输入数据')
return
save_file(content)
global I_NUM
I_NUM += 1
self.refresh_img()
self.input.delete(0, tk.END)
def refresh_img(self):
img = read_image()
if img:
self.image = img
self.label.configure(image= self.image)
def save_file(content):
file_name = f"{I_NUM}.gt.txt"
with open(image_dir + file_name, 'w+') as f:
i = f.write(content + '=?') # 我这个验证码最后两个都是'=?' 所以偷个懒XD
logging.info(f'成功向文件{file_name}写入{i}个字节') # 会打印在命令行里,UI里没写显示提示信息的功能
return i
def read_image():
image_name = f"{I_NUM}.png"
image = image_dir + image_name
return tk.PhotoImage(name= image_name, file= image)
# 文件不存在的话会报错,错误信息都会打印在命令行里,UI里没写显示提示信息的功能
I_NUM = 1 # 其实是图片的名称,这样我可以比较简单的控制从哪张图片开始
image_dir = "D:\\tesseract\\trainImage\\" # 包含图片的文件夹,答案文件也保存在这里了
gui = tk.Tk()
gui.geometry('400x250') # 设置了UI大小,不然太小我总是找不到
app = App(gui)
app.mainloop()
这样就可以只输入验证码来创建答案文件了XD
已经保存的图片创建完了,对新获取图片的话,只需要把上面两个脚本合并起来,再稍微修改一下read_image就好了。
# 省略其它代码
def read_image():
image_name = f"{I_NUM}.png"
image = image_dir + image_name
if get_image(image): # 获取失败的时候点【刷新】就能重新获取了
return tk.PhotoImage(name= image_name, file= image)
def get_image(image):
r = get("Url")
if r.ok:
j = r.json()
s = j["data"]["code"] # 解析到图片数据,没有做验证,有问题的话就报错了
p = base64.b64decode(s) # base64数据需要解码
with open(f"./trainImage/{I_NUM}.png", "wb+") as f: # 创建文件
f.write(p)
logging.debug(f'write complete.{I_NUM}.png')
return True
else:
logging.error("获取图片失败")
return False
把读取本地文件改成先获取再读就好了,缺少的依赖请自行添加。
这样手动准备个两三百张图片就差不多了。
2 尝试使用训练脚本进行训练
把训练脚本的库下载下来,然后按使用说明把环境配置好(Windows):
- 把Tesseract的目录添加到环境变量
- 安装Python3和依赖(在requirements.txt里)
- 需要一些Linux工具,有安装Git的话,直接用Git Bash就行
- 安装make(文档里是用winget安装的)
训练脚本的说明文档很清晰,如果能认真看的话,我就能少踩俩坑了:(
在训练前,在下载好的脚本文件夹里,先在data文件夹中新建一个MODEL_NAME-ground-truth的文件夹,再把训练用的图片数据复制到这个文件夹里。MODEL_NAME是训练出的模型的名称,只要不重复就行。然后在Git Bash里打开脚本文件夹。
脚本的入口是MakeFile,最简单的训练命令是
make training
但是只下载脚本库是运行不了。脚本里提供了一系列的变量,全部都有默认值。MODEL_NAME就是一个,指模型名称,现在只需要把这个变量填进去就能运行了;D
make training MODEL_NAME=模型名称
虽然运行起来了,但是这么多变量,显然不可能都用默认就能训练出合适的模型(比如我,这样直接训练出来的模型,正确率不到个位数,当然如果训练出来就能用那算你运气好)。这些变量中,NET_SPEC是比较重要的一个,包含了初始的训练参数。而其它变量大多数都是输出位置,关联文件位置等,即使使用默认,也不会影响训练效果。
不过,在调整参数前,还有一件事需要做。
3 图片处理
处理图片消耗了我不少的时间,毕竟这个东西我也要现找方案XD
其实在Tesseract的使用文档中,图片的处理也是放在前面的,就是提高质量。在文档中,它解释了清晰的图片才有准确的识别结果,并提供了一些处理图片的方法和工具。这些方法同样可以用来处理训练图片,而处理的目标,就是尽可能消除噪音、去掉背景、或是让图片更符合识别引擎,在这个过程中我参考了许多教程,比如这篇。
图片处理起来很费时,要选择方法,不断调整参数。调试时总是用一张图,然而一个方案在这张图上合适时,对其它图片也可能不尽人意。我做了三四个版本,每个版本的训练效果都没什么明显的提示。大概是我比较菜吧:(
最后我选择的处理方案就是不处理,除了把图片的宽高缩放到了原本的一半。原图片高是48像素,缩小后就是24像素。会这么做的原因是:之前的模型识别后,总是会多一些字符,所以我想,是不是图比较大,所以噪点部分容易被识别,如果把图片缩小一点儿,会不会就不会识别噪点了。
很奇怪的想法,但是实验一下很简单,所以就这样做了。把图片缩放后再去识别,同一张图片,真的就比原图识别结果贴近很多,至少没有了多很多字符。既然如此,把图片都处理好,接下来大概可以开始训练了?
4 调整训练参数
看来还不能开始训练,还有一个重要参数没有调整,就是NET_SPEC 😦
如果仔细查看训练脚本的文档,是可以知道,NET_SPEC和START_MODEL参数是二选一的。而START_MODEL的含义也很直白,就是初始模型。如果要从一个已有模型继续训练,使用START_MODEL就可以了;如果没有,就必须设置。虽然必须设置,但它是有默认值的
[1,36,0,1 Ct3,3,16 Mp3,3 Lfys48 Lfx96 Lrx96 Lfx256 O1c###]
看不懂?没关系,我也看不懂。当然官方文档中是有说明的,看这里。
虽然看不懂,我们可以先挑着比较简单的地方调整,比如前面四个逗号分隔的数字
1,36,0,1
第一个数字1,文档说这个数字目前忽略,但最终可能用来说明最小训练批次的大小。也就是说还用不到,所以不管它。
而剩下的3个数字指示了输入图片的期望属性,依次是高,宽,位深。对高和宽,如果是0的话,程序会自适应;对位深,1代表灰度图片,而3代表彩色,没有其它选项。根据已经调整好的图片就可以直接填写这三个值。
除此之外,还有一个部分需要修改,就是默认值中###的部分。需要填写一个数字,代表了训练输出的类(class)数量(也就是可识别的字符数量)。虽然文档中说这个数字被忽略,不过还是填上吧。
如果开始时运行了训练,会在data文件夹中创建一个名称和MODEL_NAME相同的文件夹,里面有一个MODEL_NAME.unicharset的文本文件,打开后在第一行就是根据答案文件.gt.txt计算出的可识别字符数;没运行的话,那就自己算一下,比如我这个验证码,就是10个数字、4个运算符和1个等号、1个问号,也就是16个。
如果要使用START_MODEL进行训练,就不用设置NET_SPEC了,不过需要让程序读取到设置的模型。脚本会读取变量TESSDATA中设置的文件夹,其默认值为./usr/share/tessdata,初始模型不在这个文件夹里的话,就需要再设置一下这个变量。
还有一个可能会用的变量是MAX_ITERATIONS(最大迭代数),默认值是10000,可能不够,具体的数量就要按需设置了。我一般会设置到200000。
5 执行训练
接下来,只要把调整后的图片和答案文件(.gt.txt)存放到数据文件夹中,然后执行训练脚本即可。当然,还要把变量设置到命令中。
make training MODEL_NAME=模型名称 NET_SPEC="[1,0,0,1 Ct3,3,16 Mp3,3 Lfys48 Lfx96 Lrx96 Lfx256 O1c1]" MAX_ITERATIONS=200000
还有更多的可设置变量,具体请参照脚本库文档按需修改。
训练一旦开始运行,接下来就只要等待就行了:)
程序有两种自动停止的情况:一是到达最大迭代次数(MAX_ITERATIONS),即上面设置的那个值;另一个是到达目标错误率(TARGET_ERROR_RATE),默认是0.01,可以使用变量更改。目标错误率并不是对所有训练图片的,而是其中一部分。脚本会自动从训练数据中分出一部分用于对训练结果进行验证,当这个错误率低于设定值时,训练程序会自动停止。
6 效果验证
当程序自动停止时,会自动生成模型文件,默认位置在data文件夹中。要使用这个模型,需要将模型文件复制到tesseract安装位置的tessdata文件夹中,然后使用该模型进行识别,这样可以查看模型的识别效果。
将训练数据逐一进行识别,再与答案文件的内容比较,可以得到一个初步的正确率。使用Python的pytesseract库可以方便地编写一个验证脚本来进行计算。
⚠️ 注意,如果使用新数据进行验证,记得对这些图片进行处理。
如果对模型效果不满意,可以继续训练。
7 继续训练
当训练的模型效果不合需求时,我们应该继续进行训练:对于在训练结束时没有达到目标错误率的模型,可以简单的增加迭代次数,直到错误率达标;而对于错误率达标的模型,因为训练的错误率是使用一部分训练数据进行验证的,我们可以调整程序中作为验证部分的数据后继续训练:一个简单的办法就是再起一个模型名称,修改-ground-truth文件夹的名称,然后使用这个名称从旧模型继续训练。
make training MODEL_NAME=模型名称-2 START_MODEL=模型名称 TESSDATA=./data MAX_ITERATIONS=200000 RANDOM_SEED=123
# 使用START_MODEL时,不需要设置NET_SPEC
# RANDOM_SEED用来产生不同的随机训练数据划分
这样就可以使用相同的训练数据,但是变更验证数据进行训练。
对于多次训练后效果仍不理想,可以考虑重新调整图片,增加训练图片等。
后记
到此为止,我们就完成了一个可以识别验证码图片的OCR模型。不像在文中记录的那样顺利,事实上很多错误是在反复训练中一点一点纠正的,甚至还有些问题是在最终模型完成后,我才注意到的。但毕竟只是训练,使用已经包装好的工具按说明去执行操作就可以了。而我则完全不了解LSTM、RNN这些底层的算法。当然,如果要训练一个更加通用的、更多字符量的识别模型,就不能只是学会使用工具,更要学习它的底层算法、构成理论,还要有足够的训练数据、更加精准的标注等。不过真的要做的这种程度的话,我觉得还是花点钱用商业的吧XD
我最终训练的模型识别验证码的正确率在85%左右,使用的时候写了一个3次重试,来避免识别错误报错。不过总归达到了目标,算是圆满结束,😄。
文中出现的链接:
- GitHub - tesseract-ocr/tesseract: Tesseract Open Source OCR Engine (main repository)
- Tesseract User Manual | tessdoc
- Tesseract-OCR5.0字体训练以及提高准确率、提升训练效率的方法_tesseract 训练-CSDN博客
- GitHub - tesseract-ocr/tesstrain: Train Tesseract LSTM with make
- Improving the quality of the output | tessdoc
- python 识别登陆验证码图片(完整代码)_python识别网页验证码-CSDN博客
- VGSL Specs - rapid prototyping of mixed conv/LSTM networks for images | tessdoc

浙公网安备 33010602011771号