设计思路
前言
国家企业信用信息公示系统中的验证码是按语序点击汉字,如下图所示:
即,如果依次点击:‘无’,‘意’,‘中’,‘发’,‘现’,就会通过验证。
本项目的破解思路主要分为以下步骤:
- 使用目标探测网络YOLOV2进行汉字定位
- 设计算法进行汉字切割
- 使用darknet的分类器进行汉字识别
- 设计算法进行汉字纠错与语序识别
汉字定位与汉字识别
本项目的汉字定位和汉字识别部分都是基于darknet
框架进行训练的。本项目对它们使用的训练网络并没有太高要求,只需懂得如何使用darknet就可以了,关于如何使用darknet框架训练汉字定位模型和汉字识别模型可查阅模型训练文档以及官方文档的YOLO和Train a Classifier部分。那么,下面主要对汉字切割和语序识别进行讲解,最后再对整个破解程序进行讲解。
汉字切割算法
1 | def seg_one_img(img_path, rets): |
seg_one_img
函数是对一张验证码图片进行汉字切割,切割后的汉字图片保存在当前路径下的hanzi_img
文件夹中,并且返回由字典(key为汉字图片路径,value为坐标)组成的列表。需要注意的是,定位接口返回的定位框信息均是归一化信息,需要转换成实际的坐标信息,验证码图片大小信息为:344 × 384 × 3。如(0.25,,75)>> (0.25×344,0.75×384)
算法大体思路:
切割一张图片(图片路径,定位接口返回的定位框信息):
遍历定位框信息,对置信度大于0.5的定位框进行如下操作:
计算汉字定位框中心坐标和左上角坐标;
将汉字定位框向四周均匀扩大两个像素;
对越界的坐标进行修正;
对汉字进行切割;
定位框向四周扩大两个像素的目的:尽量将整个汉字切割下来。因为经过测试,有些定位框定位正确但是IOU不是很高,即汉字的某一小部分可能在定位框外部。扩大定位框可以更好的用于后面的汉字识别。
语序识别算法
语序识别算法结合了使用结巴分词识别语序和使用搜索引擎识别语序两个函数,下面分别对两个函数进行讲解。
使用结巴分词识别语序
本部分使用的是 Python 中文分词词库jieba
,关于结巴分词的基础知识请先阅读结巴分词Github文档,下面对使用结巴分词识别语序进行讲解。
1 | # 结巴分词 识别语序 |
下面我通过一个具体的实例来讲解算法思路:
输入:‘到马功成’
- 获得字符串长度:
l =4
获得字符串的全排列
1
['到马功成', '到马成功', '到功马成', '到功成马', '到成马功', '到成功马', '马到功成', '马到成功', '马功到成', '马功成到', '马成到功', '马成功到', '功到马成', '功到成马', '功马到成', '功马成到', '功成到马', '功成马到', '成到马功', '成到功马', '成马到功', '成马功到', '成功到马', '成功马到']
对每一个排列进行结巴分词,并打印其中字符串最长元素的索引
1 | ['到', '马', '功', '成'] |
遍历完之后,将l=4的字符串加入possible_words列表
1
['马到功成', '马到成功'] # possible_words列表
现在有两个词语语序是可能正确的,由于结巴分词词库中的词语是有词频的,比如:
1 | ...... |
每行的第二个元素代表词频,所以我们可以通过比较词频来确定最终的语序正确的词:
1 | 马到成功 |
使用搜索引擎识别语序
1 | # 搜索引擎搜索关键字,返回相关列表 |
同样,这里仍然用一个实例来讲解该算法思路:
输入:’现无中意发’
- 获得输入字符串的排列:
1 | ['现无中意发', '现无中发意', '现无意中发', '现无意发中', '现无发中意', '现无发意中', '现中无意发', '现中无发意', '现中意无发', '现中意发无', '现中发无意', '现中发意无', '现意无中发', '现意无发中', '现意中无发', '现意中发无', '现意发无中', '现意发中无', '现发无中意', '现发无意中', '现发中无意', '现发中意无', '现发意无中', '现发意中无', '无现中意发', '无现中发意', '无现意中发', '无现意发中', '无现发中意', '无现发意中', '无中现意发', '无中现发意', '无中意现发', '无中意发现', '无中发现意', '无中发意现', '无意现中发', '无意现发中', '无意中现发', '无意中发现', '无意发现中', '无意发中现', '无发现中意', '无发现意中', '无发中现意', '无发中意现', '无发意现中', '无发意中现', '中现无意发', '中现无发意', '中现意无发', '中现意发无', '中现发无意', '中现发意无', '中无现意发', '中无现发意', '中无意现发', '中无意发现', '中无发现意', '中无发意现', '中意现无发', '中意现发无', '中意无现发', '中意无发现', '中意发现无', '中意发无现', '中发现无意', '中发现意无', '中发无现意', '中发无意现', '中发意现无', '中发意无现', '意现无中发', '意现无发中', '意现中无发', '意现中发无', '意现发无中', '意现发中无', '意无现中发', '意无现发中', '意无中现发', '意无中发现', '意无发现中', '意无发中现', '意中现无发', '意中现发无', '意中无现发', '意中无发现', '意中发现无', '意中发无现', '意发现无中', '意发现中无', '意发无现中', '意发无中现', '意发中现无', '意发中无现', '发现无中意', '发现无意中', '发现中无意', '发现中意无', '发现意无中', '发现意中无', '发无现中意', '发无现意中', '发无中现意', '发无中意现', '发无意现中', '发无意中现', '发中现无意', '发中现意无', '发中无现意', '发中无意现', '发中意现无', '发中意无现', '发意现无中', '发意现中无', '发意无现中', '发意无中现', '发意中现无', '发意中无现'] |
对每一个排列进行百度搜索返回相关词。
其中的百度搜索是通过爬虫实现的,爬取的结点主要有两部分:1.每次搜索结果词条中红色的词。2.每次搜索结果最下面的相关搜索中的词。
1 | ['中意隆鑫航发基地', '我对你中意红包怎么发', '中意保险几号发工资', '当时不知曲中意现已成为曲中人', '中意空调现E5怎么办', '初闻不知曲中意现已成为曲中人', '中意', '中意在线', '初闻不知曲中意,再听已是曲中人', '发中意', '没有中意', '中意', '中意', '中意', '中意', '中意', '中意','中意', '中意', '没有', '中意', '中意', '没有', '中意', '中意隆鑫航发基地', '我对你中意红包怎么发', '中意保险几号发工资', '当时不知曲中意现已成为曲中人', '中意空调现E5怎么办', '初闻不知曲中意现已成为曲中人', '中意', '中意在线', '初闻不知曲中意,再听已是曲中人', '现发中意无','没有中意', '中意', '没有', '中意', '没有', '中意', '中意', '没有', '中意', '中意', '没有', '中意', '没有', '中意', '中发发型', '中发', '中发卷发', '中发烫发', '中发发型图片', '中发图片', '中发编发', '中发烫发图片', '中发白', '中发无意现', '中发', '中发', '无意', '中发', '中发', '中发', '中发', '中发', '中意隆鑫航发基地', '我对你中意红包怎么发', '中意保险几号发工资', '当时不知曲中意现已成为曲中人', '中意空调现E5怎么办', '初闻不知你', '中意在线用户登录', '我只中意你', '中意保险可靠吗', '中意', '中意', '中意', '无中意', '无中意', '中意', '发现', '中意', '中意', '无中意', '中意', '无中意', '意什么什么发', '意()()发', '发现的近意词是什么', '发现的进意词', '发现意', '微信里的发现是什么意是', '意料之中什么意思', '中译意', '发现', '无意中发现', |
- 通过一个嵌套循环来统计每一个排列在all_relalated列表中出现的次数(排列是列表元素的子串)。
1 | [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 125, 0, 0, 0, 0, 1,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] |
- 找到标志位最大的索引,返回word_list列表中该索引值对应的排列。
1 | 无意中发现 |
完整破解程序
程序讲解
通过多次试验会发现,使用结巴分词识别语序和搜索引擎识别语序各有利弊,使用结巴分词的优点是速度很快,缺点是对于一些不是词语的语序识别会识别不出来。而搜索引擎识别语序,语序识别能力强,但是比较慢。所以在破解程序中我将二者结合了一下,充分使用了各自的优点。
1 | # -*- coding: utf-8 -*- |
下面依然用一个实例来讲解破解程序思路:
输入:
1.使用汉字定位模型定位汉字,得到4个定位框信息,每个定位框的第一个元素为hanzi类,第二个元素为该定位框的置信度,第三个元素为定位框的归一化坐标信息(x,y,w,h)。
1 | [(b'hanzi', 0.8764635920524597, (0.672152578830719, 0.355495423078537, 0.17341256141662598, 0.16976206004619598)), (b'hanzi', 0.8573136329650879, (0.625790536403656, 0.7956624627113342, 0.15850003063678741, 0.13232673704624176)), (b'hanzi', 0.857090175151825, (0.8480002284049988, 0.5595549941062927, 0.18965952098369598, 0.1373395025730133)), (b'hanzi', 0.8561009168624878, (0.29499194025993347, 0.49679434299468994, 0.16142778098583221, 0.16253654658794403))] |
2.根据定位框切割图片,输出由字典组成的列表(key
为汉字图片的相对路径,value
为汉字的中心坐标)。
1 | [{'hanzi_img/15343037353537.jpg': (231, 136)}, {'hanzi_img/15343037353541.jpg': (215, 305)}, {'hanzi_img/15343037353543.jpg': (291, 214)}, {'hanzi_img/15343037353546.jpg': (101, 190)}] |
3.使用汉字识别模型识别汉字,因为汉字识别会有识别错误的情况出现,为了一定程度上纠正错误,我们针对汉字识别置信度小于0.95的汉字,先选取其top5,然后对汉字识别结果的这几个字进行组合。
1 | [(b'u7269', 0.9999469518661499), (b'u7545', 1.4645341252617072e-05), (b'u626c', 8.120928214339074e-06), (b'u629b', 6.87056399328867e-06), (b'u573a', 5.69164603803074e-06)] |
可以发现第二个和第四个汉字的置信度低于0.95,对置信度低于0.95的汉字选取其top5。
1 | [['物'], ['乳', '雅', '部', '型', '播'], ['动'], ['通', '埔', '铺', '哺', '厘']] |
可以发现,若仅选取置信度最高的,汉字识别结果是物,乳,动,通
,这样就是识别错了。所以我们对置信度低的先选取其top5。
对四个汉字列表进行组合得到:
1 | ['物乳动通', '物乳动埔', '物乳动铺', '物乳动哺', '物乳动厘', '物雅动通', '物雅动埔', '物雅动铺', '物雅动哺', '物雅动厘', '物部动通', '物部动埔', '物部动铺', '物部动哺', '物部动厘', '物型动通', '物型动埔', '物型动铺', '物型动哺', '物型动厘', '物播动通', '物播动埔', '物播动铺', '物播动哺', '物播动厘'] |
4.结合结巴分词和搜索引擎识别语序。对上一步中获得的组合进行遍历,先使用结巴分词识别语序,若结巴分词能识别出来,则直接返回;若结巴分词识别不出来,则仅对置信度最高的组合使用搜索引擎识别语序,将识别结果返回。本例中,结巴分词能正确识别,返回:
1 | 哺乳动物 |
5.返回汉字对应的坐标。
1 | [(101, 190), (215, 305), (291, 214), (231, 136)] |
总结:结巴分词识别一种汉字组合语序的时间大约为:0.000××(和电脑配置有关系),所以对多个组合进行遍历耗时也不会很多。而使用搜索引擎对一种组合识别语序耗时1-15妙(根据汉字个数有所不同),所以在结巴分词识别不出来时,仅对置信度最高的组合进行搜索引擎识别语序。这样的话,整体情况会比较不错。大部分语序识别耗时在1s以内,少部分通过搜索引擎识别的则会耗时2-16秒内,根据汉字个数有多不同。
测试正确率
文件准备:
python/valid
文件和python/valid.txt
文件,其中python/valid
文件内存放的是验证码图片,python/valid.txt
内存放的是验证码图片文件名及其对应的正确语序,如下:
1 | ...... |
测试脚本:
1 | # 加载汉字定位模型 |
注意:测试接口正确率的时候,需要将破解接口最后的返回值return centers
改为return rec_word
。
模型训练文档
本部分主要讲解如何使用定位器和分类器,其中包括训练数据准备、模型训练以及训练结果评估。
依赖
- python3.6
- opencv3
- numpy
文件结构
该文件结构图仅列出了一些比较重要的文件,并对文件作用进行了注解。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51.
├──Makefile darknet配置文件
├──README.md
├── cfg
│ ├── chinese.data 分类器训练配置文件
│ ├── chinese_character.cfg 分类器网络配置文件
│ ├── yolo-origin.cfg YOLOV2定位器网络配置文件
│ ├── yolo-origin.data YOLOV2定位器训练配置文件
│ ├── yolov3.cfg YOLOV3定位器网络配置文件
| └── yolov3.data YOLOV3定位器训练配置文件
├── chinese_classify
│ ├── backup 分类器权重存储文件
│ ├── data
│ │ ├── train 分类器训练集
│ │ ├── valid 分类器验证集
│ │ ├── labels.txt 分类器所有训练样本的标签
│ │ ├── train.list 分类器所有训练样本的路径
│ │ └── valid.list 分类器所有验证样本的路径
│ ├── new_img 分类器样本标记后存储在该文件夹
│ ├── old_img 分类器样本标记前存储在该文件夹
│ └── label_hanzi.py 标记分类器样本的脚本
├── darknet darknet二进制文件
├── examples
├── getmap.py 计算定位器的mAP
├── include
├── jiyan
│ ├── backup 定位器权重存储文件
│ ├── data
│ │ ├── train 定位器训练集
│ │ ├── valid 定位器验证集
│ │ ├── train.txt 定位器所有训练样本的路径
│ │ ├── valid.txt 定位器所有验证样本的路径
│ │ └── yolo.names 定位器标签仅一个,hanzi
│ ├── get_pic.py 爬取gsxt网站验证码的脚本
│ └── raw_img 最初爬取的2W张极验验证码图片(汉字识别已用过)
├── python
│ ├── hanzi_img 破解验证码时,存储的切割汉字图片的文件夹
│ ├── valid 测试集-500张(测试破解接口的准确率)
│ ├── valid.txt 测试集标记文件
│ ├── crack_pro.py 具有一定汉字纠错能力的验证码破解接口
│ ├── darknet.py 定位器和分类器调用接口
│ ├── recog_order.py 识别语序接口
│ ├── segment.py 切割汉字接口
│ └── dict.txt 识别语序时用到的带词频的字典
├── results 生成的anchor.txt会存在此文件
├── scripts
├── src darknet源代码
└── tools
├── voc_label.py .xml标签转换为.txt标签
├── generate_anchorsv2.py YOLOV2生成anchors脚本
└── generate_anchorsv3.py YOLOV3生成anchors脚本
Makefile文件配置
1 | GPU=1 # 置1,使用GPU训练 |
提示:修改Makefile或源码后,需在命令行先敲击
make clean
命令,再敲击make
命令,方能生效。
定位器训练
训练数据准备
样本的获取
训练定位器样本的获取方法:爬取国家企业信用信息公示系统,使用脚本jiyan/get_pic.py
进行爬取即可。
样本的标注
使用标注软件labelimg对样本图片进行标注。有关labelimg软件的安装与使用请自行百度,这里不再赘述。
通过labelimg将图片标注后,会生成该图片对应的.xml标签,训练数据时需要的是.txt标签,我们需要将.xml标签转化为.txt标签。转换脚本见tools/voc_label.py
。
文件准备
- 定位器网络配置文件
cfg/yolo-origin.cfg
1 | [net] |
重要参数讲解:
filter
应为30,其计算公式为:num
代表每个网格单元预测几个box。classes
代表一共有多少个类,本项目为1。coord
代表回归的四个位置,分别为<x><y><width><height>
代表物体中心点相对位置以及物体相对大小。如不
的标签为0 0.20 0.76 0.16 0.16
,忘
的标签为0 0.75 0.25 0.15 0.17
,初
的标签为0 0.25 0.20 0.17 0.17
,心
的标签为0 0.60 0.62 0.16 0.18
这张图的标签为:
1
2
3
40 0.25 0.20 0.17 0.17
0 0.75 0.25 0.15 0.17
0 0.60 0.62 0.16 0.18
0 0.20 0.76 0.16 0.16
- 定位器训练配置文件
cfg/yolo-origin.data
1 | classes= 1 # 类的个数,本项目为1 |
重要参数讲解:
train
:该参数为存放train.txt
文件的路径,train.txt
文件格式如下:1
2
3
4
5
6
7......
......
/home/geng/darknet/jiyan/data/train/1530946690.jpg
/home/geng/darknet/jiyan/data/train/3062060956.jpg
/home/geng/darknet/jiyan/data/train/3062062538.jpg
......
......即
train.txt
文件保存了所有训练样本的路径。在上述路径中的train文件内必须同时存放样本及其对应的标签,即:1
2
3
4
5
6
7
8......
......
1528780328740.jpg 1530947539.jpg 3062055104.jpg 3062097132.jpg
1528780328740.txt 1530947539.txt 3062055104.txt 3062097132.txt
1528780333363.jpg 1530947544.jpg 3062055126.jpg 3062097142.jpg
1528780333363.txt 1530947544.txt 3062055126.txt 3062097142.txt
......
......生成train.txt:通过以下命令行命令来生成路径文件
train.txt
:1
find `pwd`/train -name \*.jpg > train.txt
valid
:该参数为存放valid.txt
文件的路径,valid.txt
文件格式如下:1
2
3
4
5
6
7......
......
/home/geng/darknet/jiyan/data/valid/1530953866.jpg
/home/geng/darknet/jiyan/data/valid/1530954184.jpg
/home/geng/darknet/jiyan/data/valid/1530952279.jpg
......
......即
valid.txt
文件保存了所有验证样本的路径。需要注意的是:上述路径中的valid文件内必须同时存放样本及其对应的.txt
标签和.xml
标签,即:1
2
3
4
5
61530952510.jpg 1530953474.jpg 1530955039.jpg 3062099066.jpg 3062107032.jpg
1530952510.txt 1530953474.txt 1530955039.txt 3062099066.txt 3062107032.txt
1530952510.xml 1530953474.xml 1530955039.xml 3062099066.xml 3062107032.xml
1530952529.jpg 1530953479.jpg 1530955044.jpg 3062099078.jpg 3062107044.jpg
1530952529.txt 1530953479.txt 1530955044.txt 3062099078.txt 3062107044.txt
1530952529.xml 1530953479.xml 1530955044.xml 3062099078.xml 3062107044.xml生成valid.txt:通过以下命令行命令来生成路径文件
valid.txt
:1
find `pwd`/valid -name \*.jpg > valid.txt
模型训练
配置文件和数据集准备好之后,我们就可以开始训练了,训练命令如下:
1 | ./darknet detector train cfg/yolo-origin.data cfg/yolo-origin.cfg |
若在原有权重的基础上进行训练,使用如下命令:
1 | ./darknet detector train cfg/yolo-origin.data cfg/yolo-origin.cfg jiyan/backup/yolo-origin.backup |
训练结果评估
目标检测中衡量识别精度的指标是mAP(mean average precision),mAP越接近1,表示定位效果越好。在计算mAP时,需要先根据训练的模型生成检测结果,然后使用getmap.py
脚本计算mAP。
生成检测结果:
1 | ./darknet detector valid cfg/yolo-origin.data cfg/yolo-origin.cfg jiyan/backup/yolo-origin.backup |
生成的检测结果会存放在results/comp4_det_test_hanzi.txt
文件内。
计算mAP:
1 | python getmap.py results/comp4_det_test_hanzi.txt jiyan/data/valid.txt hanzi |
分类器训练
训练数据准备
样本的获取
分类器样本的获取方法是通过对定位的汉字进行切割获取的。汉字切割脚本见python/segment.py
。
样本的标注
分类器样本的标注,即对切割的汉字图片进行标记。实质上是修改汉字图片的文件名,所涉及的原理在下面的文件准备中有讲解。标注过程解释如下:
汉字 | 汉字对应的Unicode | 切割得到的汉字图片文件名 | 标记后的汉字图片文件名 |
---|---|---|---|
健 | \u5065 | 15310991303473_label.jpg | 15310991303473_u5065.jpg |
声 | \u58f0 | 15310991261628_label.jpg | 15310991261628_u58f0.jpg |
汉字识别的标注脚本已经写好,见label_hanzi.py
。
文件准备
- 分类器网络配置文件
cfg/chinese_character.cfg
1 | [net] |
重要参数讲解:
- 最后一个convolutional层的
filters
:此处filters的值为3604,但是需要注意的是:此值应该与chinese_classify/data/labels.txt
内的类标签个数一致。
- 分类器训练配置文件
cfg/chinese.data
1 | classes=3604 # 类的个数,此处即汉字的个数 |
重要参数讲解:
classes
代表分类个数train
该参数为存放train.list
文件的路径,train.list
文件内格式如下:1
2
3
4
5
6
7......
......
/home/geng/darknet/chinese_classify/data/train/15310999153792_u5347.jpg
/home/geng/darknet/chinese_classify/data/train/15310991401861_u4e58.jpg
/home/geng/darknet/chinese_classify/data/train/15310993054970_u4e4b.jpg
......
......即
train.txt
文件保存了所有训练样本的路径。在上述路径中的train文件内需要存放训练样本,即:1
2
3
4
5
6
7......
......
15310993303114_u5b9d.jpg 15310996251829_u5174.jpg 15310999168611_u6765.jpg
15310993303400_u90e8.jpg 15310996252114_u4efb.jpg 15310999168899_u5145.jpg
15310993303403_u5185.jpg 15310996252119_u4f55.jpg 15310999168901_u7535.jpg
......
......valid
该参数的格式与train
一样,不再赘述。只是该参数涉及的是验证集。labels
为存放类标签文件labels.txt
的路径,labels.txt
文件内格式如下:1
2
3
4
5
6
7
8
9......
......
u6bd3
u5347
u90f8
u5d58
u8426
......
......darknet是通过文件名与
labels.txt
中的字符串做匹配,匹配到则认为该标签为匹配到的字符串。如/home/geng/darknet/chinese_classify/data/train/15310999153792_u5347.jpg
,由于labels.txt
中出现u5347
,所以这张图的标签为u5347
。所以图片的路径绝对不可以出现多个labels.txt
中包含的字符串,如果路径为somepath1/data2/3.jpg
,labels.txt
包含1,2,3,则这张图片会被认为匹配1,2,3多个label从而报错。top
代表valid时取前多少计算正确率。如top100
代表分类时概率最大的前100类中出现了正确的标签就认为正确。
模型训练
配置文件和数据集准备好之后,我们就可以开始训练了,训练命令如下:
1 | ./darknet classifier train cfg/chinese.data cfg/chinese_character.cfg |
若在原有权重的基础上进行训练,使用如下命令:
1 | ./darknet classifier train cfg/chinese.data cfg/chinese_character.cfg chinese_classify/backup/chinese_character.backup |
需要注意的是:如果增加样本后label.txt
文件内增加了新的汉字标签,就不能在原有权重的基础上进行训练了,需要重新训练。
训练结果评估
分类器模型验证比较简单,直接用准确率来评估,即:验证集中分类正确的个数/分类错误的个数。分类器模型验证命令如下:
1 | ./darknet classifier valid cfg/chinese.data cfg/chinese_character.cfg chinese_classify/backup/chinese_character.backup |
参考文献
You Only Look Once: Unified ,Real-Time Object Detection
https://cos120.github.io/crack/
免责声明
该项目仅用于学术交流,不得任何商业使用!