背景:朋友在为"关山口男子职业技术学校"写一款校园应用,于是找MoonXue写一个学生选课系统的登录接口.为了搞定这个接口,不得不先搞定这个系统的验证码.
验证码大概是这个样子
看上去不怎么难,没有干扰线没有粘连没有扭曲.但还是没能用pytesser直接将它识别出来,因为当中有噪点和其他背景噪声的存在.MoonXue的工作就是去掉这些讨厌的东西
先介绍一下,我们的工具:
1.Pytesser 它是基于一个c语言实现名为tesser的识别工具的python封装.可惜比较笨,只能做最简单的识别而且不认识汉字
2.Requests 它是我们喜欢写爬虫的孩子的最爱,提供人性化的接口,代价是失去了一点效率(写python就别考虑效率啦)
3.BeautifulSoup 它和Requests是一对好机油,让提取文档中所需的内容变成一件简单的事情
4.PIL 它是今天的主角,PIL是专门用作图像处理的库,很好很强大.熟练的人甚至可以用它来P图
如何写爬虫去实现模拟登录此处不细说,下面说说怎么解决验证码识别
解决思路如下:
1.先用PIL对图像做一次图像增强,因为原图中数字的边缘和背景中的噪声并不是太分明,做了增强之后能将两者分离.如果不分离,可能会在去噪点的时候导致数字中有部分会缺失
im = Image.open("randomimage/randomImage11.jpg") im = ImageEnhance.Sharpness(im).enhance(3)
参数为3是经过实验之后感觉比较理想的值,太强不好,太弱也不好
2.做完预处理之后,就是去背景噪声了.背景噪声指的是背景中各种明暗变换的色块,肉眼也许不会注意到这个.但是它的存在会给识别带来影响.我最初的做法是将图像转换为只有黑白两色,这样自然就将噪声转换成了噪点.
效果如图
但我希望能去掉噪点,成为这样
最先想到的是种子染色法 ,什么是种子染色法请参看这个链接
为了防止坏链,此处做部分转载
种子染色法英文叫做Flood Fill ,实际上Flood Fill这个名称更贴切一点,因为这个方法作用在一个图的结点上时恰似洪水一样“淹没”与之相连的其他结点并以相同的方式蔓延出去,这个方法通常用于计算一个图的极大连通子图(这里的“图”是图论的概念)。设想一个无向图,我们从这个图中一个未标号(“标号”可以理解为“染色”)的结点开始,将此结点和从这个结点出发可达的所有结点都赋予相同的标号(染上相同的颜色),那么我们就得到了这些被标号的结点所组成的一个极大连通子图,搜索下一个未标号的结点并重复上述过程我们便可以找到所有的极大连通子图。“染色”的过程可以用DFS或者BFS实现,如果结点数为V,边数为E,因为我们在Flood Fill过程中“造访”每个结点两次,“造访”每条边两次,所以得到所有极大连通子图的时间复杂度为o(V+E) 。
来自Wikipedia的一个示例:
想象每个白色方块为图中的结点,相邻的方块(上下左右)有边相连,那么这个图就有三个极大连通子图,这演示了Flood Fill查找其中一个极大连通子图的过程。
在这是借要用种子染色法计算每块的面积,然后把小体积的块当作噪点去除.
代码在这
def check(j,i): try: if pix[j,i] == 0 and matrix[j][i] != -1: return True else: return False except: return False def juli(r,s): return abs(r[0]-s[0])+abs(r[1]-s[1])+abs(r[2]-s[2]) for i in range(w): for j in range(h): r = [0,0,0] s = [0,0,0] if pix[j,i] == 0: if check(j-1,i): r[0],r[1],r[2] = im2.getpixel((j,i)) s[0],s[1],s[2] = im2.getpixel((j-1,i)) print r print s print "-"*55 if juli(r,s) <=l: matrix[j][i] = matrix[j-1][i] maps[str(matrix[j][i])]+=1 elif check(j-1,i-1): r[0],r[1],r[2] = im2.getpixel((j,i)) s[0],s[1],s[2] = im2.getpixel((j-1,i-1)) if juli(r,s) <=l: matrix[j][i] = matrix[j-1][i-1] maps[str(matrix[j][i])]+=1 elif check(j,i-1): r[0],r[1],r[2] = im2.getpixel((j,i)) s[0],s[1],s[2] = im2.getpixel((j-1,i)) if juli(r,s) <=l: matrix[j][i] = matrix[j][i-1] maps[str(matrix[j][i])]+=1 elif check(j+1,i+1): r[0],r[1],r[2] = im2.getpixel((j,i)) s[0],s[1],s[2] = im2.getpixel((j+1,i+1)) if juli(r,s) <=l: matrix[j][i] = matrix[j+1][i+1] maps[str(matrix[j][i])]+=1 elif check(j,i+1): r[0],r[1],r[2] = im2.getpixel((j,i)) s[0],s[1],s[2] = im2.getpixel((j,i+1)) if juli(r,s) <=l: matrix[j][i] = matrix[j][i+1] maps[str(matrix[j][i])]+=1 elif check(j-1,i+1): pr[0],r[1],r[2] = im2.getpixel((j,i)) s[0],s[1],s[2] = im2.getpixel((j-1,i+1)) if juli(r,s) <=l: matrix[j][i] = matrix[j-1][i+1] maps[str(matrix[j][i])]+=1 elif check(j+1,i-1): r[0],r[1],r[2] = im2.getpixel((j,i)) s[0],s[1],s[2] = im2.getpixel((j+1,i-1)) if juli(r,s) <=l: matrix[j][i] = matrix[j+1][i-1] maps[str(matrix[j][i])]+=1 elif check(j+1,i): r[0],r[1],r[2] = im2.getpixel((j,i)) s[0],s[1],s[2] = im2.getpixel((j+1,i)) if juli(r,s) <=l: matrix[j][i] = matrix[j+1][i] maps[str(matrix[j][i])]+=1 else: n+=1 maps[str(n)]=1 matrix[j][i] = n for i in range(w): for j in range(h): if matrix[j][i]!=-1 and maps[str(matrix[j][i])]<=2: im.putpixel((j,i),255)
结果呢,不是很理想因为这个体积参数设小了,噪点没去干净,设大了数字部分可能也去了一小块.最重要的是这里噪点的大小不是很规律,很难找到一个不错的面积参数.
失败只是暂时的,经过观察发现背景噪声颜色明显比数字要浅的多.这也意味着它的RGB值要比数字小的多,通过分析RGB值能去掉大部分噪声,剩下来的噪点可以再通过种子染色法处理.也就是说,分别在两张图片(分别是黑白和彩色)上获取信息,在一张图片上做处理最后做识别
核心代码在这
r[0],r[1],r[2] = im2.getpixel((j,i)) if r[0]+r[1]+r[2]>=400 or r[0]>=250 or r[1]>=250 or r[2]>=250 : im2.putpixel((j,i),(255,255,255))
至此,本次识别的问题就搞定啦,成功率在50%以上基本满足接口的需求
想交流或想要源码的朋友可以邮件联系
-*-python之禅-*-
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.