识别验证码:寻找数字的位置(一)
1 接下来
前面我用Python的pillow库生成了一些验证码,这些验证码都非常弱,没有其他线条的干扰,数字还没有混叠在一起,肯定能够被高手轻松破译。但那些简单原始的验证码,不失为学习如何识别图片中数字很好的原料,那就是我接下来要做的。
2 寻找数字的位置
要想计算机识别验证码的数字,必须找到数字的位置,这点上计算机和人相比可差远了。
计算机必须用对应一个数字大小的方框,来从左至右、从上至下遍历图片。如果有的验证码中数字的大小不同,那么就只能依次用从小至大的方框遍历了。
3 用小方块遍历图片
基本上就是两个循环,外循环指定小方块左上角的x坐标,内循环指定小方块左上角的y坐标。
遍历没必要一个像素接着一个像素,选择一个合适的步进值,能够分离出一个单独的数字即可,选择小方块大小的原则也是如此。
忘了说图片的坐标通常是一左上角为原点,向右为x轴,向下为y轴
pillow包有一个剪裁图片的函数crop(左上角x,左上角y,右下角x,右下角y),需要指定剪裁区域的位置——矩形左上角的位置及右下角的位置
class DecodeCaptcha(object): def __init__(self,filename): self.image = Image.open(filename) self.size = self.image.size def toBlack(self): self.image = self.image.convert('L') self.image = self.image.point(lambda x:0 if x<240 else 255) #值小于240的像素用0来表示 def detectLetter(self): winSize = (18,26) step = 3 # crops = [] for b in range(0,self.size[1]-winSize[1]+1,step): cropL = [] for a in range(0,self.size[0]-winSize[0]+1,step): crop = self.image.crop((a,b,a+winSize[0],b+winSize[1])) cropL.append(crop) crops.append(cropL) return crops
假定验证码的颜色不携带信息,其实不是,有的验证码不同字符的颜色不同,这肯定是分开字符的最好办法,但这不具有通用性。
为了简单起见,会把图片转换为L模式,及一个像素用,0~255来表示。更进一步只让每一个像素取0和255。
4 人工标记小方块是否包含数字
判断哪个小方块有数字就可以用机器学习了。
使用机器学习算法先人工标记哪些小方块清晰完整地包含数字数字(+样本),哪些小方块不包含数字或是不完整(-样本)。
这个过程选择用网页和服务器搭配最好不过了。网页展示图片的所有小方块供人来选择,人就可以选择最具有代表性的+样本和-样本。
Python可以很方便的实现一个简单的服务器,我使用的是Flask框架,也是第一次试用。
from flask import Flask, url_for, render_template,request from selectLetter import DecodeCaptcha from io import BytesIO import os import base64 import sqlite3 app = Flask(__name__) files = os.listdir(r'.\arialnb') def toBase64(img): #如何把图片嵌入到一个网页里,而不是以链接的形式指定图片的位置,将图片的二进制数据base64编码就是答案,嵌入到网页里会大大简化问题 output = BytesIO() img.save(output,'PNG') contents = base64.b64encode(output.getvalue()) output.close() return str(contents)[2:-1] @app.route('/select/<int:imgId>',methods=['GET','POST']) def select_letter(imgId): img = DecodeCaptcha(r'.\ARIALNB\%s' % files[imgId]) img.toBlack() crops = img.detectLetter() if request.method=='GET': crops = [[toBase64(c) for c in l] for l in crops] return render_template('select.html',imgId=imgId,whole=toBase64(img.image),crops=crops) else: form = request.form coln = int(form['coln']) rown = int(form['rown']) contain = int(form['contain']) con = sqlite3.connect('./letterImg.sqlite3') cur = con.cursor() cur.execute("INSERT INTO letterImg VALUES (?,?)", (crops[rown][coln].tobytes(),contain)) con.commit() cur.close() con.close() return 'Success' app.run(debug=True)
Flask框架使用jinja2模版引擎来生成网页,模版引擎简单说来就是用于动态的生成内容,即便是不同的验证码,网页的结构都是一样的,只是内容不同。模版引擎可以直接从程序里获取数据并生成网页
<html> <head> <style> img { border: 1px solid #66CD00; } </style> </head> <body> <h3>Captcha {{ imgId }}</h3> <img src="data:image/png;base64,{{ whole|safe }}"> <table cellpadding="10"> {% for l in crops %} {% set outer_loop = loop %} <tr> {% for c in l %} <td><img src="data:image/png;base64,{{ c|safe }}" rown={{ outer_loop.index0 }} coln={{ loop.index0 }}></td> {% endfor %} </tr> {% endfor %} </table> <a href='/select/{{ imgId+1 }}'>>>Captcha {{ imgId+1 }}</a> <script> function ajaxRequest(coln,rown,contain) { xmlHttpRequest = new XMLHttpRequest(); xmlHttpRequest.open("POST", "{{ imgId }}", true); xmlHttpRequest.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); xmlHttpRequest.send('coln='+coln+'&rown='+rown+'&contain='+contain); } function sendInfo(ths,contain) { var rown = ths.getAttribute('rown'); var coln = ths.getAttribute('coln'); ajaxRequest(coln,rown,contain); } var img = document.getElementsByTagName('img'); for(var i=1;i<img.length;i++){ img[i].onclick=function(){ this.style.border='1px solid #FF0000'; sendInfo(this,1); } img[i].oncontextmenu=function() { this.style.border='1px solid #009ACD'; sendInfo(this,0); return false; } } </script> </body> </html>
在浏览器打卡链接是,服务器会从一个给定的文件夹里,选择出指定文件名的图片,分割成一个个小方块,保存在网页里,再发送给浏览器。
效果是这样的
每一个小方块默认是绿色的边框,用鼠标左键点击选择+样本,同时边框变为红色,用鼠标右键点击选择-样本,同时边框变成蓝色。
对于每一张验证码我会选择最佳的原验证码的5个数字,对于人来说有时都难以抉择,机器肯定也会遇到同样的问题。随机选择几个有代表性的-样本。
在用鼠标点击图片的同时,浏览器会想服务器发送图片的数据,服务器这时会把数据存储到sqlite3数据库里。
这背后看不见的发送数据的一部分,需要用javascript来实现。我并不擅长这个,花了好些时间来实现这个功能。
5 人工标注
在2015年春节的一个深夜,花了至少一个小时,也许有两个小时来点击小方块,总共有100张验证码,每一个验证码有5个+样本和5个-样本,得到了来之不易的690KB数据。
这纯粹是一个体力劳动,不过啪啦啪啦点来点去不用思考也挺好玩。