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


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

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

  • 相关阅读:
    2015.07-2015.08
    the last lecture
    强化的单例属性_Effective Java
    Socket通信客户端设计(Java)
    静态工场方法代替构造器
    如何控制Java中的线程,总结了3种方法...
    如何快速转型,比如C#...to...Java
    C#中var和dynamic
    How to use the Visual Studio
    mark blog
  • 原文地址:https://www.cnblogs.com/meelo/p/4311106.html
Copyright © 2020-2023  润新知