识别验证码:寻找数字的位置(一)

时间:2024-02-23 16:34:59

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数据。

这纯粹是一个体力劳动,不过啪啦啪啦点来点去不用思考也挺好玩。