(python3.4)
本回答针对该验证码的识别,主要分以下五部分:验证码分析
下载验证码样本
生成初始字符图片样本
识别验证码
测试与总结
涉及的脚本及文件(脚本按照出现的先后顺序排列)
.../captcha_char
.../captcha_example
.../download_captcha_0.py
.../download_captcha_1.py
.../pretreat_image.py
.../cut_all_char.py
.../new_char_example.py
.../load_char_example.py
.../distinguish_captcha.py
验证码分析
下载验证码观察:
# download_captcha_0.py
# python3.4
from urllib import request
host_url = 'http://kmustjwcxk3.kmust.edu.cn/jwweb/'
captcha_url = host_url+'sys/ValidateCode.aspx'
for k in range(10):
request.urlretrieve(captcha_url,'%d.jpg'%k)
0.jpg
1.jpg
2.jpg
观察结论:字符为数字+大写英文字母.后面发现没有数字0,1,字母I,L,O.应该是考虑到人也不易区分这几个字符.字符有两种字体,且都是斜体字.
有五条随机的直线,上方有噪点,有一外框.干扰很小.
一般字体的变形有两类:线性的,非线性的.线性的又可分为:平移,倾斜,旋转,伸缩.很少有对称的.这里只有垂直方向的平移,较易处理.
总之,这是一个很弱的验证码.
(以下涉及两个文件夹captcha_example,captcha_char.放置在python脚本所在路径下.)
下载验证码样本
# download_captcha_1.py
# 手动建立文件夹:captcha_example
from urllib import request
# 该站点速度更快
host_url = 'http://jwmis.hhtc.edu.cn/'
captcha_url = host_url+'sys/ValidateCode.aspx'
for k in range(300):
request.urlretrieve(captcha_url,'captcha_example/%d.jpg'%k)
生成初始字符图片样本
分3步进行:验证码预处理,主要是降噪
分割图片,获取单个字符的图片
生成初始字符图片样本
验证码预处理,主要是降噪
预处理前(原始验证码图片,jpg格式):
预处理后(红色边框内部的是结果,png格式):
原图片的格式为JFIF(jpg).为了不失真,处理后的图片一律保存为png格式
# pretreat_image.py
# 注意:二值图像0/1统一转化为0/255
from PIL import Image,ImageDraw,ImageChops
import os
# 验证码预处理,主要是降噪
# 预处理结束后返回0/255二值图像
# 降噪,参考 http://blog.csdn.net/xinghun_4/article/details/47864949
def pretreat_image(image):
# 将图片转换成灰度图片
image = image.convert("L")
# 二值化,得到0/255二值图片
# 阀值threshold = 180
image = iamge2imbw(image,180)
# 对二值图片进行降噪
# N = 4
clear_noise(image,4)
# 去除外边框
# 原图大小:122*54
# 左上右下,左 <= x < 右
box = ( 8, 10, 118, 50 )
image = image.crop(box)
return image
# 灰度图像二值化,返回0/255二值图像
def iamge2imbw(image,threshold):
# 设置二值化阀值
table = []
for i in range(256):
if i < threshold:
table.append(0)
else:
table.append(1)
# 像素值变为0,1
image = image.point(table,'1')
# 像素值变为0,255
image = image.convert('L')
return image
# 根据一个点A的灰度值(0/255值),与周围的8个点的值比较
# 降噪率N: N=1,2,3,4,5,6,7
# 当A的值与周围8个点的相等数小于N时,此点为噪点
# 如果确认是噪声,用该点的上面一个点的值进行替换
def get_near_pixel(image,x,y,N):
pix = image.getpixel((x,y))
near_dots = 0
if pix == image.getpixel((x - 1,y - 1)):
near_dots += 1
if pix == image.getpixel((x - 1,y)):
near_dots += 1
if pix == image.getpixel((x - 1,y + 1)):
near_dots += 1
if pix == image.getpixel((x,y - 1)):
near_dots += 1
if pix == image.getpixel((x,y + 1)):
near_dots += 1
if pix == image.getpixel((x + 1,y - 1)):
near_dots += 1
if pix == image.getpixel((x + 1,y)):
near_dots += 1
if pix == image.getpixel((x + 1,y + 1)):
near_dots += 1
if near_dots < N:
# 确定是噪声,用上面一个点的值代替
return image.getpixel((x,y-1))
else:
return None
# 降噪处理
def clear_noise(image,N):
draw = ImageDraw.Draw(image)
# 外面一圈变白色
Width,Height=image.size
for x in range(Width):
draw.point((x,0),255)
draw.point((x,Height-1),255)
for y in range(Height):
draw.point((0,y),255)
draw.point((Width-1,y),255)
# 内部降噪
for x in range(1,Width - 1):
for y in range(1,Height - 1):
color = get_near_pixel(image,x,y,N)
if color != None:
draw.point((x,y),color)
if __name__ == '__main__':
image = Image.open('captcha_example/0.jpg')
image = pretreat_image(image)
image.show()
分割图片,获取单个字符的图片
预处理前:
预处理后(红色边框内部的是结果):
分割后(红色边框内部的是结果):
# cut_all_char.py
# 注意:二值图像0/1统一转化为0/255
from PIL import Image,ImageDraw,ImageChops
import os
from pretreat_image import *
# 分割图片,获取单个字符的图片
# 二值图片的分割
def cut_one_char(image):
# 再次降噪
# N = 4
clear_noise(image,4)
CharWidth=25
CharHeight=26
Width,Height=image.size
# 找出image上出现黑点的第一列
x = find_first_column(image)
# 第一行
# 左上右下,左 <= x < 右
box = (x,0,x+CharWidth,Height)
image2 = crop_white(image,box)
y = find_first_row(image2)
# 切割出一个字符
box = (x,y,x+CharWidth,y+CharHeight)
image_char = crop_white(image,box)
# 剩下的图片
if x+CharWidth > Width:
image_residue = None
else:
box = (x+CharWidth,0,Width,Height)
image_residue = crop_white(image,box)
return [image_char,image_residue]
# 没有字符W的情况下,切割的都比较好.
# 出现W的概率为1-(1-1/36)^4≈10.66%
# 这样一来准确率无法超过90%
# 这里处理4个字符的情况
def cut_all_char(image):
image_char1,image = cut_one_char(image)
image_char2,image = cut_one_char(image)
image_char3,image = cut_one_char(image)
image_char4,image = cut_one_char(image)
return [image_char1,image_char2,image_char3,image_char4]
# 如果box超出原图范围,默认会以黑色填充
# 因此为了让图片超出部分以白色填充,进行反色处理,最后再反色回来
def crop_white(image,box):
# 255 - old
image = ImageChops.invert(image)
image = image.crop(box)
return ImageChops.invert(image)
# 找出image上出现黑点的第一列
def find_first_column(image):
Width,Height=image.size
for x in range(Width):
for y in range(Height):
if image.getpixel( (x,y) ) == 0:
return x
# 如果没有黑点,返回第一列
return 0
# 找出image上出现黑点的第一行
def find_first_row(image):
Width,Height=image.size
for y in range(Height):
for x in range(Width):
if image.getpixel( (x,y) ) == 0:
return y
# 如果没有黑点,返回第一行
return 0
if __name__ == '__main__':
image = Image.open('captcha_example/0.jpg')
image = pretreat_image(image)
image_char_list = cut_all_char(image)
image_char_list[0].show()
生成初始字符图片样本
过程示意图1:
在此发现字符0,1,I,L,O并不出现.
过程示意图2:
# new_char_example.py
from PIL import Image,ImageDraw,ImageChops
import os
from pretreat_image import *
from cut_all_char import *
# 生成存放样本的文件夹
def new_char_folder():
# 0-9
for k in range(48,58):
try:
os.mkdir( 'captcha_char/%c' % k )
except:
pass
# A-Z
for k in range(65,91):
try:
os.mkdir( 'captcha_char/%c' % k )
except:
pass
# 生成样本字符,然后手动将对应字符移动到上面生成的文件夹中
# 每个文件夹手动移动10个以上
def new_char_example():
new_char_folder()
for s in range(300):
image = pretreat_image( 'captcha_example/%d.jpg' % s )
image.save( 'captcha_char/%d.png' % s )
image_char_list = cut_all_char(image)
for k in range(4):
image_char_list[k].save( 'captcha_char/%d_%d.png' % (s,k) )
if __name__ == '__main__':
pass
识别验证码
分4步进行(其中前两步与前面生成初始字符图片样本的步骤一样):验证码预处理,主要是降噪
分割图片,获取单个字符的图片
加载字符图片样本
分割的字符图片与字符图片样本进行比对
加载字符图片样本
# load_char_example.py
# 注意:二值图像0/1统一转化为0/255
from PIL import Image,ImageDraw,ImageChops
import os
# 遍历指定目录,显示目录下的所有文件名
def eachfile(filepath):
dir_list = os.listdir(filepath)
all_dir = []
for dir in dir_list:
child = '%s%s' % (filepath, dir)
all_dir.append(child)
return all_dir
# 共31个字符
char_set = [
'2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G',
'H', 'J','K', 'M','N',
'P','Q','R','S','T',
'U','V','W','X','Y','Z'
]
# 加载字符图片样本
# 将对应字符的样本按照上述 char_set 的顺序加载
def load_char_example():
global char_set
char_example = []
for char in char_set:
char_example.append([])
folder_path = 'captcha_char/%c/'%char
image_name_list = eachfile(folder_path)
for image_name in image_name_list:
image = Image.open(image_name)
# 注意这里读取的数据为灰度图像,像素值为0,255
# 为此将其他地方出现的0/1二值图像统一处理成0/255二值图像
char_example[-1].append(image)
return char_example
if __name__ == '__main__':
load_char_example()
分割的字符图片与字符图片样本进行比对
# distinguish_captcha.py
# 注意:二值图像0/1统一转化为0/255
# 涉及文件夹:captcha_example,captcha_char
from PIL import Image,ImageDraw,ImageChops
import os
from pretreat_image import *
from cut_all_char import *
from load_char_example import *
# 比较两个0/255二值图像
# 计算相似度,公式:相似度 = 相等的像素点数 / 总像素点数
def compare2imbw(imbw1,imbw2):
# out = abs(img1, img2),相同的点变为0,不同的变为255
image = ImageChops.difference(imbw1,imbw2)
# 统计相同的点的个数
# 直方图统计,返回长度为256的list
a = image.histogram()
same_pixel = a[0]
Width,Height=imbw1.size
all_pixel = Width*Height
return same_pixel/all_pixel
# 与样本比较,设置一个评分体系
# 分别取最大的五个相似度相加,再取最大的对应的字符
def distinguish_one_char(char_example,image_char):
global char_set
score_set=[]
for image_list in char_example:
score_set.append([])
for image in image_list:
score_set[-1].append( compare2imbw(image,image_char) )
# 对于score_set[k],取分值最高的5个相加
char_num=len(char_set)
for k in range(char_num):
# 从小到大排序
score_set[k].sort()
# 逆序
score_set[k].reverse()
# 调试打印节点1
# print(char_set[k],score_set[k][0:1])
# 取前5
score_set[k] = score_set[k][0:5]
# 前5相加,保存在score_set[k]中
score_set[k] = sum(score_set[k])
# 获得相似度最大的字符,并返回
# index 返回找到的第一个值的位置
a = score_set.index( max(score_set) )
# 调试打印节点2
# print(char_set[a])
return char_set[a]
def distinguish_all_char(char_example,image_char_list):
s = ""
for image_char in image_char_list:
s += distinguish_one_char(char_example,image_char)
return s
def distinguish_captcha(image):
# 预处理图片,返回0/255二值图像
image = pretreat_image(image)
# 切割二值图像
image_char_list = cut_all_char(image)
# 加载样本数据
char_example = load_char_example()
# 比对识别各个字符
result = distinguish_all_char(char_example,image_char_list)
return result
if __name__ == '__main__':
image = Image.open('captcha_example/0.jpg')
s = distinguish_captcha(image)
print(s)
测试与总结凡是 W 后面一个字符的识别基本上出错,这是由于 W 较宽,导致 W 与其右边一个字符出现粘合,从而切割时出错.如果 W 只出现在最后一个位置,错误率不高.因为切割从左向右进行, W 在最右边时,不会影响到其他字符.
计算理想的准确率,即 W 不出现在前面三个位置的概率.没有 W 的概率
, W 只在最后一个位置出现的概率
.理想的准确率为上述两者相加:
.
如有必要,需改进分割字符.
进行3次网络测试,每次为100个验证码.正确率为:87%,87%,88%.由此,正确率稳定在87%左右,与90.63%相差的部分应该是由其他误差导致的.
第一次在知乎长文回答,发现代码高亮和 tex 都挺好用的.
20170211