零、准备工作
1、python3+
2、工具:pycharm
3、模块:os、sklearn、urllib.request、numpy、pandas、PIL(建议下载anaconda)
4、一颗对输入验证码感到厌烦的心
一、批量得到验证码图片
import urllib.request
def download(n):
for i in range(1, n+1):
img_src = 'http:xxxxx'
i = str(i)
urllib.request.urlretrieve(img_src, './jpg/' + i + '.jpg') # 图片可能不是jpg格式,请注意
想要识别验证码,首先要有验证码:使用循环访问n次验证码网址,下载n张二维码到当前同级目录
下的jpg文件夹中。
得到验证码的生成网址 要在浏览器中使用开发者工具,即按F12。在右边栏中可以找到服务器发给浏
览器的验证码,然后 copy一下验证码网址就行。
下载500例,可以看到验证码是彩色的,而且有奇奇怪怪的线条,我们接下来就要搞定它们
二、转成黑白图片--二值化
def getTable(threshold=140):
table = []
for i in range(256):
table.append(0) if i < threshold else table.append(1)
return table
from PIL import Image
image = Image.open("jpg/418.jpg")
imgry = image.convert('L') # 转化为灰度图
table = get_bin_table() # table是list[]
out = imgry.point(table, '1') # out是image类型
out.show()
out的图片如上所示,可见其变成了黑白二色,这代表out是由0和1构成的,黑色表示0,白色表示1。
这样更容易统计得到图片的特征,比如数字3 由几个0,1构成,进而得到各个数字的规律。
三、去除线条--除噪点
可以看到,验证码图片中通常会有设置的干扰物,这会影响SVM训练时的效果。但是可以发现的是,
这种验证码图片的 噪点 并不多,而且处于边缘位置。需要说明的是,SVM训练的是一小块字符截图,所以
截图位置外的噪点皆可漠视,因此我 决定暂时不去除噪点,先看看截图效果。
四、得到字符截图
在pycharm中放大图片,开启网格,可以看到一些细节:z离左边缘4个像素;3离顶部4个像素;字
符之间的距离是2或3个 像素;所有字符离底边缘6个像素。看起来不好办啊!字符之间的距离竟然是浮动
的。然而仔细观察后我发现,字符似乎最大长 度都 是高12px,最大宽度10px。也就是说12*10的区域可
以恰好包含z,而不包含3。 对于字符z,截下(4,4,14,16);对于字符3,截下(14,4,24,16);以此类推。
def get_crop_imgs(img):
child_img_list = []
for i in range(4):
x = 4 + i * 10 # 第1个字符的截图位置是(4,4,14,16)
y = 4
child_img = img.crop((x, y, x + 10, y + 12))
child_img_list.append(child_img) # child_img是image类型,child_img_list是含image类型元素的列表
return child_img_list
list=get_crop_imgs(img)
list[0].show()
这就是截下来的字符,看得出噪点依然存在,但是不用急,大部分的字符截图都没有噪点,
五、保存字符截图
每一张验证码都会截下四张字符截图,n张验证码会产生4n张字符截图,需要注意的一点是,字符截图
数不可太多, 因为 这是监督学习,是需要人为标注的,也就是你要手动分类,数字1归一类,数字2归一类,
等等。分别用文件夹储存。 经过后来的尝试我发现每类20个就可以训练得不错,这侧面反映这确实是很简单
的验证码。
def saveSpiltPicture(self, n):
for i in range(0, n):
i = i + 1 # 循环为0时,i=1;循环为1时,i=2;方便计算
image = Image.open('./jpg/' + str(i) + '.jpg')
imgry = image.convert('L') # 转化为灰度图
out = imgry.point(self.table, '1')
img_list = get_crop_imgs(out)
img_list[0].save('./bmp/' + str(4 * i + 1) + '.bmp')
img_list[1].save('./bmp/' + str(4 * i + 2) + '.bmp')
img_list[2].save('./bmp/' + str(4 * i + 3) + '.bmp')
img_list[3].save('./bmp/' + str(4 * i + 4) + '.bmp')
六、手动分类字符截图
首先要统计字符频率:共11类:123abcmnxvz。创建文件夹,命名为char,里面再创建11个文件夹,
分别以类名命名。
手动将bmp文件夹下的截图分类, 每类二三十个就可以了。
这里7.bmp对应2.jpg内的第三个字符。
七、获得图片特征
(1)文件操作
为什么又命名jpg文件夹,又命名bmp文件,甚至还有char文件夹呢?
因为我要使用文件操作,自动对char下的每个文件夹下的每个图片使用“获取特征函数”,省时省力。
import os
def sortTable():
list = []
files_list = os.listdir('char') # files_list->['1','2',...'x','z']
num = 0 # 第num张截图
for i in files_list: # i是第几个文件夹
files = os.listdir('char/' + i) # 返回的files->['7.bmp','xx.bmp',...]
for j in range(0, len(files)): # j是第几张图片
image = Image.open('char/' + i + '/' + files[j]) # files[0]->'7.bmp'
list.append(get_feature(image)) # 使用获取特征函数返回list类型加在list后,所以list是二维数组
list[num].append(i) # 向list中的某list的尾加上文件名 *1
num += 1 #最后大list中含n个小list,每个小list都含有截图特征信息
return list
*1:为什么要加上文件名?打比方说,获得‘1’文件夹下某个字符‘1’的特征后,得再加一个特征,说前22个特征指向的
是字符‘1’,不然谁知道22个特征指向的是谁,是x还是z?所以新加的特征就是文件夹名。
(2)获取特征函数
def get_feature(img):
"""
获取指定图片的特征值,
一张截图为高12,宽10。统计各行的黑点,得到12个信息,统计各列的黑点,得到10个信息,用列表返回22个信息
"""
pixel_cnt_list = []
height = 12
width = 10
for y in range(height):
pix_cnt_x = 0
for x in range(width):
if img.getpixel((x, y)) == 0: # 黑色点
pix_cnt_x += 1
pixel_cnt_list.append(pix_cnt_x)
for x in range(width):
pix_cnt_y = 0
for y in range(height):
if img.getpixel((x, y)) == 0: # 黑色点
pix_cnt_y += 1
pixel_cnt_list.append(pix_cnt_y)
return pixel_cnt_list
(3)保存数据集
import pandas as pd
def save():
names = [x for x in range(1, 24)]
list=sortTable()
test = pd.DataFrame(list, columns=names) # test类型为Dataframe
test.to_csv('data.txt') # 保存数据集
储存的好处是直接保存计算结果,调试时可以节省重复计算的时间,上图为data.txt的部分内
有23列, 表示23个特征, 末位特征表示此行指向什么字符,每行首位是行号。看得出虽然是4个字
符‘1’, 但是某些特征还是不同的。
八、SVM训练
from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import SVC
from sklearn.cross_validation import train_test_split
from sklearn.externals import joblib
def train():
data = pd.read_csv("data.txt")
clf = OneVsRestClassifier(SVC(kernel='linear'))
X = data.ix[:, 1:23] # 选取所有行的1-22列
y = np.array(data.ix[:, 23]).astype(str) # 选取所有行的第23列,类型转换
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) #数据集分四类
clf.fit(X_train, y_train) # 训练ing
y_pred = clf.predict(X_test) # 测试ing
rf = pd.DataFrame(list(zip(y_pred, y_test)), columns=['predicted', 'actual']) # rf类型是dataframe
rf['correct'] = rf.apply(lambda r: 1 if r['predicted'] == r['actual'] else 0, axis=1)
print(len(rf[rf['correct'] == 1]) / len(rf))
joblib.dump(clf, "train_model.m") # 保存训练模型
test_size参数表示分组规模,0.3表示70%的data拿去训练,30%的data拿去测试。
我的data大概有900多行的特征向量,test_size=0.99时,准确率不到60%,test_size=0.9时,准确率就接近100%了。
而且我的字符中m宽度有15个px,截图只截下半个多m,然而训练后可以准确识别出m,且不怎么受噪点影响,所以除噪
也没什么必要了。
如果训练效果不好,可能是算法有问题,或是训练量不够。
九、使用训练模型识别验证码
def seePicture():
clf = joblib.load("train_model.m")
image = Image.open("jpg/340.jpg")
imgry = image.convert('L') # 转化为灰度图
table = get_bin_table()
out = imgry.point(table, '1')
img_list = get_crop_imgs(out)
for x in range(0, 4):
list = get_feature(img_list[x])
y_pred = clf.predict([list]) # predict接受二维数组参数
print(len(y_pred))
识别成功,任务完成。
十、小结
这是我第一次写博客,如有错误或不详尽之处请指出。大概50%是在前人的基础上完成的,
在研究的同时走了许多弯路,所以就写下这篇尽量完整详尽的博客(所以才有那么多注释),希
望所有人无论是新手都能看的明白。至于文章中的一些格式错误,我没办法了,编辑时是粗体,
发表时变成斜体,晕。 以下是类代码
from PIL import Image
import pandas as pd
import numpy as np
import urllib.request
from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import SVC
from sklearn.cross_validation import train_test_split
from sklearn.externals import joblib
import os
class verification_code:
data = []
table = []
threshold = 140
def __init__(self):
for i in range(256):
self.table.append(0) if i < self.threshold else self.table.append(1)
def download(self, n, URL):
for i in range(1, n + 1):
img_src = URL
i = str(i)
urllib.request.urlretrieve(img_src, './jpg/' + i + '.jpg')
def get_crop_imgs(self, img):
child_img_list = []
for i in range(4):
x = 4 + i * 10 # 见原理图
y = 4
child_img = img.crop((x, y, x + 10, y + 12))
child_img_list.append(child_img)
return child_img_list
def get_feature(self, img):
pixel_cnt_list = []
height = 12
width = 10
for y in range(height):
pix_cnt_x = 0
for x in range(width):
if img.getpixel((x, y)) == 0: # 黑色点
pix_cnt_x += 1
pixel_cnt_list.append(pix_cnt_x)
for x in range(width):
pix_cnt_y = 0
for y in range(height):
if img.getpixel((x, y)) == 0: # 黑色点
pix_cnt_y += 1
pixel_cnt_list.append(pix_cnt_y)
return pixel_cnt_list
def saveSpiltPicture(self, n):
for i in range(0, n):
i = i + 1
image = Image.open('./jpg/' + str(i) + '.jpg')
imgry = image.convert('L') # 转化为灰度图
out = imgry.point(self.table, '1')
img_list = self.get_crop_imgs(out)
img_list[0].save('./bmp/' + str(4 * i + 1) + '.bmp')
img_list[1].save('./bmp/' + str(4 * i + 2) + '.bmp')
img_list[2].save('./bmp/' + str(4 * i + 3) + '.bmp')
img_list[3].save('./bmp/' + str(4 * i + 4) + '.bmp')
def sorttable(self):
list = []
files_list = os.listdir('char')
num = 0
for i in files_list: # i是第几个文件夹
files = os.listdir('char/' + i)
for j in range(0, len(files)): # j是第几张图片
image = Image.open('char/' + i + '/' + files[j])
list.append(self.get_feature(image))
list[num].append(i)
num += 1
names = [x for x in range(1, 24)]
test = pd.DataFrame(list, columns=names) # test类型为Dataframe
test.to_csv('data.txt') # 保存数据集
def train(self):
data = pd.read_csv("data.txt")
clf = OneVsRestClassifier(SVC(kernel='linear'))
X = data.ix[:, 1:23]
y = np.array(data.ix[:, 23]).astype(str)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
rf = pd.DataFrame(list(zip(y_pred, y_test)), columns=['predicted', 'actual'])
rf['correct'] = rf.apply(lambda r: 1 if r['predicted'] == r['actual'] else 0, axis=1)
print(len(rf[rf['correct'] == 1]) / len(rf))
joblib.dump(clf, "train_model.m")
print(rf)
def seePicture(self, img):
clf = joblib.load("train_model.m")
image = Image.open(img)
image.show()
imgry = image.convert('L') # 转化为灰度图
out = imgry.point(self.table, '1')
img_list = self.get_crop_imgs(out)
for x in range(0, 4):
list = self.get_feature(img_list[x])
y_pred = clf.predict([list])
print(y_pred)
def workFrist(self, n, URL):
self.download(n, URL)
self.saveSpiltPicture(n)
# 手动分组
def workSecond(self):
self.sorttable()
self.train()
def workTest(self, img):
self.seePicture(img)
code=verification_code()
code.workFrist(100, 'http://csujwc.its.csu.edu.cn/verifycode.servlet')
code.workSecond()
code.workTest('jpg/1.jpg')
十一、参考文献
1、字符型图片验证码识别完整过程及Python实现 点击打开链接