陆陆续续的学习了验证码的灰度、二值化、分割等方法,还了解了机器学习中最基本的3个分类方式——KNN、决策树、朴素贝叶斯。基于这些,今天结合这些工具来写一个简单的验证码识别程序,本来想使用现有的库来生成验证码,但无意间发现了之前写某个程序时下载的200个验证码,正好可以拿来练手。另外,虽然之前已经实现了上面3种算法,但这里还是会使用sklearn这个强大的三方库,学习原理是为了知其所以然,有现成工具还是要拿来用的。

原始验证码如图所示:

captchar

可以看出,字符红色,干扰线绿色,字符之间没有粘连扭曲,只包含数字和大写英文,经过查看后每个字符宽30像素,可以说是一种很简单的验证码。

首先去掉绿色的干扰线:

1
2
3
4
5
6
7
8
9
10
11
12
13
from PIL import Image
def denoise(img):
"""去除绿色干扰线"""
pixdata = img.load()
w, h = img.size
for y in range(h):
for x in range(w):
r, g, b = pixdata[x, y]
if r > 110 and g < 130:
pass
else:
img.putpixel((x, y), (255, 255, 255))
return img

使用putpixel函数把符合判断条件的元素改成白色,接下来就是分割、二值化等操作,之前有记录过不再赘述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import uuid
import numpy as np
def split_img(img):
"""验证码每个字符都比较固定,直接固定位置切割"""
w, h = img.size
cuts = [(0, 0, 30, h), (30, 0, 60, h), (60, 0, 90, h), (90, 0, w, h)]
for i, n in enumerate(cuts, 1):
tmp = img.crop(n)
tmp.save("split/%s.jpeg" % uuid.uuid1())

def binarizing(img, threshold):
"""传入image对象进行灰度、二值处理"""
img = img.convert("L") # 转灰度
pixdata = img.load()
w, h = img.size
# 遍历所有像素,大于阈值的为黑色
for y in range(h):
for x in range(w):
if pixdata[x, y] < threshold:
pixdata[x, y] = 0
else:
pixdata[x, y] = 255
return img

处理后,手动分类到不同的文件夹中(使用实际验证码就是坑在这点,需要手动打码,所以数据集较小),总共200个验证码切分出800个字符:

img2

然后就是加载数据进行训练了:

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
import os
from sklearn.metrics import accuracy_score
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier

train_data = []
train_label = []
test_data = []
test_label = []
for r, d, f in os.walk('data_train'):
for each in f:
imgname = os.path.join(r, each)
img = Image.open(imgname)
data = np.array(binarizing(img, 200))
label = r.split('/')[-1]
train_data.append(np.reshape(data, 30 * 30)) # 展开成1维数组
train_label.append(label)

for r, d, f in os.walk('data_test'):
for each in f:
imgname = os.path.join(r, each)
img = Image.open(imgname)
data = np.array(binarizing(img, 200))
label = r.split('/')[-1]
test_data.append(np.reshape(data, 30 * 30))
test_label.append(label)

gnb = GaussianNB()
gnb.fit(train_data, train_label)
clf = DecisionTreeClassifier()
clf.fit(train_data, train_label)
knn = KNeighborsClassifier()
knn.fit(train_data, train_label)

g = gnb.predict(test_data)
c = clf.predict(test_data)
k = knn.predict(test_data)

print("knn score:", accuracy_score(test_label, k))
print("bayes score:", accuracy_score(test_label, g))
print("description tree score:", accuracy_score(test_label, c))

输出如下:

1
2
3
knn score: 0.695
bayes score: 0.675
description tree score: 0.79

没想到决策树在这个情况中成功率可以达到0.79,最看好的贝叶斯居然是最低的。当然,这里仅仅演示都使用了默认参数进行训练,实际使用中还需要更多的进行调参。

虽然这个程序还很low,但总算把之前学习的各个知识点结合来起来。另外关于阈值的选取,有一个叫做 大津算法(OTSU),以后有机会可以试试看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def otsu_threshold(im):
pixel_counts = [np.sum(im == i) for i in range(256)]
# 得到图片的以0-255索引的像素值个数列表
s_max = (0, -10)
ss = []
for threshold in range(256):
# 遍历所有阈值,根据公式挑选出最好的
# 更新
w_0 = sum(pixel_counts[:threshold]) # 得到阈值以下像素个数
w_1 = sum(pixel_counts[threshold:]) # 得到阈值以上像素个数
mu_0 = sum([i * pixel_counts[i]
for i in range(0, threshold)]) / w_0 if w_0 > 0 else 0
# 得到阈值下所有像素的均值
mu_1 = sum([i * pixel_counts[i]
for i in range(threshold, 256)]) / w_1 if w_1 > 0 else 0
# 得到阈值上所有像素的均值
# 根据公式计算
s = 1.0 * w_0 * w_1 * (mu_0 - mu_1) ** 2
# 直接使用w_0 * w_1可能会造成整数相乘溢出,所以先乘一个1.0变为浮点数
ss.append(s)
# 取最大的
if s > s_max[1]:
s_max = (threshold, s)
return s_max[0]

最后把手工标记好的数据传到 github 上,有兴趣的可以拿去玩。