最近做了一些验证码识别的工作,现在总结一下。
本文将介绍几种类型的验证码识别任务,只针对包含英文字母和数字的简单型OCR识别。
开始的时候,研究了很多关于验证码识别的相关资料,现在准确率较高的一般都是基于机器学习的,其大概可以分为两种,一种是将整个验证码图片作为分类器的输入,将相应输出作为标签,比如这篇博客:tensorflow-深度学习破解验证码。另外一种则是先分割再识别。比较这两种验证码识别方法,不得不说,第一种识别方法的适用性较差,因为需要学习的变换规律很复杂,要利用一个卷积神经网络完成分割和识别两种任务,因此需要大量的训练样本和较复杂的神经网络模型,一般使用captcha库来生成需要的训练样本。然而,在实际应用场景中,我们需要识别的验证码图像和captcha生成的图像相差较大(数据分布不同),因此适应性较低。举个例子,笔者跑出的试验模型在captcha图像库上准确率大概为99%,而在新图像库上准确率不到1%。
而第二种方法的过程比较直观,即先分割再识别,相比第一种方法,该方法的分类器专注于字母和数字识别而不用关心验证码所在的位置,因此更容易训练。本文先介绍一种比较简单的验证码识别任务,该任务的验证码图片类似于这种:
,,
可以看出,该类验证码排列很整齐,基本每一个元素都位于固定的位置,因此相对很好提取每个元素的位置。观察到验证码的分辨率为55×20,我们使用opencv库来提取每个元素的矩阵:
Mat crop1 = Mat(image,Range(5,17),Range(9,18)).clone();
Mat crop2 = Mat(image,Range(5,17),Range(18,27)).clone();
Mat crop3 = Mat(image,Range(5,17),Range(27,36)).clone();
Mat crop4 = Mat(image,Range(5,17),Range(36,45)).clone();
其中,image代表读取的验证码图像,crop1-crop4代表分割出的4个元素的矩阵,接下来进行归一化:
resize(crop1,crop1,Size(28,28),0,0,INTER_NEAREST);
resize(crop2,crop2,Size(28,28),0,0,INTER_NEAREST);
resize(crop3,crop3,Size(28,28),0,0,INTER_NEAREST);
resize(crop4,crop4,Size(28,28),0,0,INTER_NEAREST);
归一化的目的是将所有的分割出的图像归一化到同样大小,以便于输入到卷积神经网络模型中。
当然,这种分割方法是最简单的,也只适用于固定元素位置的验证码识别中,对于一些变化较大或者较复杂的图像,该方法不能工作。
接下来,将从数据准备,训练集制作,模型建立,以及最后的测试来说明。
数据准备其实很好弄,如果你会使用爬虫工具的话,这项工作将会很简单,这里我贴一个我在网上下载的爬取数据的代码:
import urllib.request
from multiprocessing.dummy import Pool as ThreadPool
import os
url = 'https://cx.hexinpass.com/index.php?default,dyncodeImg,d'
output = '.png'
targetDir = r"pic"
def destFile(path):
if not os.path.isdir(targetDir):
os.mkdir(targetDir)
t = os.path.join(targetDir, path)
return t
def saveimg(i):
urllib.request.urlretrieve(url, destFile(str(i)+output))
pool = ThreadPool(8)
pool.map(saveimg, range(1, 1000))
pool.close()
pool.join()
一般这个代码可以直接拿去用,是在python3环境下运行的一个脚本,可能需要配置一下库,直接pip安装即可。需要修改的是验证码图片的网址和要下载的图片数量。图片网址查看网页源代码,一般可以找到。
或者可以自己手动下载保存一些图片,对于这种较简单只包含数字的验证码来说,100张图像基本足够,下载完图像后,需要对图像进行标注作训练集。例如,我们使用1328.png作为文件名。
准备好数据集后,可以使用分割的方法作训练集,我们将使用dlib深度学习库来实现分类模型。我们直接将每一类的图像放在一个文件夹下即可。现在当前目录新建一个train文件夹,然后在train文件夹下建立十个空文件夹,名字分别从0-9,图像分割和保存的代码如下:
for(string file : directory(dir).get_files())
{
Mat image = imread(file);
Mat crop1 = Mat(image,Range(5,17),Range(9,18));
Mat crop2 = Mat(image,Range(5,17),Range(18,27));
Mat crop3 = Mat(image,Range(5,17),Range(27,36));
Mat crop4 = Mat(image,Range(5,17),Range(36,45));
resize(crop1,crop1,Size(28,28),0,0,INTER_NEAREST);
resize(crop2,crop2,Size(28,28),0,0,INTER_NEAREST);
resize(crop3,crop3,Size(28,28),0,0,INTER_NEAREST);
resize(crop4,crop4,Size(28,28),0,0,INTER_NEAREST);
matrix img1,img2,img3,img4;
assign_image(img1,cv_image(crop1));
assign_image(img2,cv_image(crop2));
assign_image(img3,cv_image(crop3));
assign_image(img4,cv_image(crop4));
cout<
在这里,我使用的是dlib的图像读取格式,当然也可以直接使用opencv的Mat格式,更简单。另外,我在保存每个分割出来的图像时,取得是file字符串的某一个位置的字母,这需要根据自己的情况修改一下代码。例如我的file字符串第37-40之间的内容为:1328.
我们使用CNN分类模型作为分类器,输出层的单元个数为10,模型的代码为:
using net_type = loss_multiclass_log<
fc<10,
relu>>>>>>>>>>>;
当然,这是在dlib库下的代码,我们使用的模型为2个卷积层后跟2个全连接层,相对很简单,速度很快。模型建立和训练的代码如下:
#include
#include
#include
#include
using namespace std;
using namespace dlib;
using net_type = loss_multiclass_log<
fc<10,
relu>>>>>>>>>>>;
int main(int argc,char** argv)
{
if(argc != 2)
{
cout<<"input the file_folder to train";
return 1;
}
dlib::set_dnn_prefer_smallest_algorithms();
string dir_name = argv[1];
auto objects = load_object_file(dir_name);
net_type net;
const double initial_learning_rate = 0.1;
const double weight_decay = 0.0001;
const double momentum = 0.9;
dnn_trainer trainer(net,sgd(weight_decay, momentum));
trainer.be_verbose();
trainer.set_iterations_without_progress_threshold(100000);
trainer.set_learning_rate(initial_learning_rate);
trainer.set_synchronization_file("captcha_resnet_sync", std::chrono::seconds(100));
std::vector> images;
std::vector labels;
dlib::pipe>> q_images(4);
dlib::pipe> q_labels(4);
auto data_loader = [&q_images,&q_labels,&objects](time_t seed)
{
dlib::rand rnd(time(0)+seed);
std::vector> images;
std::vector labels;
while(q_images.is_enabled())
{
try
{
load_mini_batch(16,rnd,objects,images,labels);
q_images.enqueue(images);
q_labels.enqueue(labels);
}
catch(std::exception& e)
{
cout << "EXCEPTION IN LOADING DATA" << endl;
cout << e.what() << endl;
}
}
};
std::thread data_loader1([data_loader](){ data_loader(1); });
std::thread data_loader2([data_loader](){ data_loader(2); });
std::thread data_loader3([data_loader](){ data_loader(3); });
std::thread data_loader4([data_loader](){ data_loader(4); });
while(trainer.get_learning_rate() >= 1e-6)
{
q_images.dequeue(images);
q_labels.dequeue(labels);
trainer.train_one_step(images, labels);
}
trainer.get_net();
cout << "done training" << endl;
net.clean();
serialize("captcha.dat") << net;
q_images.disable();
q_labels.disable();
data_loader1.join();
data_loader2.join();
data_loader3.join();
data_loader4.join();
}
具体的参数设置可以查阅dlib的相关介绍。
模型训练完成后,可以得到一个文件:captcha.dat的文件。该文件保存了我们训练好的模型参数。
最后是测试工作,我们需要的仅仅是图像预处理+前面训练出来的dat文件。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
using namespace dlib;
using namespace cv;
using net_type = loss_multiclass_log<
fc<10,
relu>>>>>>>>>>>;
void get_crop(const Mat& image,std::vector>& crop_images)
{
Mat crop1 = Mat(image,Range(5,17),Range(9,18)).clone();
Mat crop2 = Mat(image,Range(5,17),Range(18,27)).clone();
Mat crop3 = Mat(image,Range(5,17),Range(27,36)).clone();
Mat crop4 = Mat(image,Range(5,17),Range(36,45)).clone();
resize(crop1,crop1,Size(28,28),0,0,INTER_NEAREST);
resize(crop2,crop2,Size(28,28),0,0,INTER_NEAREST);
resize(crop3,crop3,Size(28,28),0,0,INTER_NEAREST);
resize(crop4,crop4,Size(28,28),0,0,INTER_NEAREST);
matrix img1,img2,img3,img4;
assign_image(img1,cv_image(crop1));
assign_image(img2,cv_image(crop2));
assign_image(img3,cv_image(crop3));
assign_image(img4,cv_image(crop4));
crop_images.push_back(img1);
crop_images.push_back(img2);
crop_images.push_back(img3);
crop_images.push_back(img4);
}
int main(int argc,char** argv)
{
if(argc != 2)
{
cout<<"invalid input! 2 arguments are expected.";
return 1;
}
string image_path = argv[1];
std::vector> crop_images;
Mat image_ = imread(image_path);
get_crop(image_,crop_images);
net_type net;
deserialize("captcha.dat") >> net;
std::vector predict_label = net(crop_images);
for(auto element : predict_label)
{
cout<
代码比较直观,当输入一张图像时,将直接输出验证码所代表的内容。当然,本文只是为了研究验证码识别的算法,并没有加入异常处理。
整个项目的代码网址:https://download.csdn.net/download/zsy162534/10500809。
最后,安利以下dlib深度学习库,里面集成了很多人脸识别的工具和深度学习框架,很实用,纯C++语言编写,容易上手。dlib的github地址为:https://github.com/davisking/dlib,官网地址为:http://dlib.net/。
后续的验证码识别将重点在图像分割的工作上,毕竟后续的模型建立和训练都可以使用相似的代码。