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数据。
这纯粹是一个体力劳动,不过啪啦啪啦点来点去不用思考也挺好玩。