1. 程式人生 > 其它 >乾貨:用js實現的簡單驗證碼識別

乾貨:用js實現的簡單驗證碼識別

很高興大家喜歡!Github:leonof/imgRecJs,剛剛上傳,程式碼還需要完善~因為有不少同學表示訓練和識別有疑問,我做了個小介面放在最後,可以方便大家先把流程走通。

後續會更新:將js程式碼等打包成chrome擴充套件程式,這樣就可以讓瀏覽器自動識別,完全傻瓜式使用啦~!(更新啦:利用chrome擴充套件,讓瀏覽器執行我們的指令碼

其實整篇文章難度不高,網上也有很多java、c等的程式碼。只是當時我寫程式碼的時候,沒有找到純js可以用的程式碼和庫,不能打包成chrome擴充套件,用起來還是不太方便的。所以在驗證了思路的可行性後,我就大致寫下來,給他人以方便吧。

目前有多種驗證碼識別思路,限於能力有限,我只好採用了最簡單的機器學習。目標驗證碼也比較簡單,如:

(含字母也一樣)

。識別控制速度在0.1秒以內的話,正確率在99.99999%(因為一直是識別正確哈哈哈)。

在動手之前,先梳理一下大致思路,方便比較獨立的同學自己嘗試完成程式碼:

1、先分析網頁DOM結構,載入驗證碼圖片。

2、將圖片畫到canvas上,拿到圖片的畫素資料。

3、先後對圖片進行二值化、腐蝕膨脹、切割、旋轉、縮放處理。

4、記錄處理後的單個數字的二值化資料,並人工錄入真實數字。

5、重複訓練。

6、識別時,用處理後的影象與庫中資料對比,取得最相近的資料,得到真實數字。

(以下優化)

7、資料量大時,可以取前幾個相似資料,並按權重從中選出最可能的數字,以提高準確度。

8、也可查詢到相似度足夠高時停止搜尋,取其作為最後識別結果,以提升效率。

大神們可以直接去寫了,我這低階簡單的程式碼會遭你們嘲笑的。。。比較急於求成的同學也可以不用看了,回頭直接拿demo去修改吧!

====================================================================

好吧既然你看到這裡了,我就儘量說的清楚明白一點。

在動手之前,我簡單模擬一下需要輸入驗證碼的網站,效果如下:

好吧,是真的簡單…點選圖片可以更換驗證碼,輸入框用來輸入,按鈕模擬提交,如下:

我們就假裝他作為我們要自動識別的目標。

一、分析網頁DOM結構,載入驗證碼圖片。

我們可以看到,驗證碼的url是:img/0.jpg。我這裡的url會變化,是為了模擬更換驗證碼的過程。但實際上,由於驗證碼絕大多數為後臺生成的,所以地址是固定的。那麼我們很容易就可以拿到圖片資料:new一個Image,賦值url即可(直接get到img元素也行)。參考程式碼:

var img = document.getElementById("img");

二、將圖片畫到canvas上,拿到圖片的畫素資料。

要將圖片畫到canvas上,首先要建立一個canvas並初始化。參考程式碼:

var canvas1 = document.createElement("canvas");
document.getElementsByTagName("body")[0].appendChild(canvas1);
canvas1.style.backgroundColor = "cornsilk";
var ctx1 = canvas1.getContext("2d");

隨後,將圖片繪製上去。參考程式碼:

ctx1.drawImage(img,0,0,img.width,img.height);

然後我們就可以利用canvas,拿到圖片的畫素資料。參考程式碼:

var imgData = ctx1.getImageData(0,0,WIDTH,HEIGHT);

三、先後對圖片進行二值化、腐蝕膨脹、切割、旋轉、縮放處理。

這部分是影象識別的重點,直接影響到識別準確率和速度。複雜的驗證碼還應加上去躁等處理過程。比如可以檢測貫穿的橫線並消除,或者將顏色高度統一的背景去掉等等。我們的圖片幾乎沒有干擾,只有簡單的旋轉和縮放,故直接進行二值化操作(二值化也能去掉少量的干擾)。

1、二值化操作的思路是:計算圖片的平均灰度作為閾值,比閾值大的置為純黑,反之純白。參考程式碼:

function toHex(fromImgData){//二值化影象
    var fromPixelData = fromImgData.data;
    var greyAve = 0;
    for(var j=0;j<WIDTH*HEIGHT;j++){
        var r = fromPixelData[4*j];
        var g = fromPixelData[4*j+1];
        var b = fromPixelData[4*j+2];
        greyAve += r*0.3 + g*0.59 + b*0.11;
    }
    greyAve /= WIDTH*HEIGHT;//計算平均灰度值。
    for(j=0;j<WIDTH*HEIGHT;j++){
        r = fromPixelData[4*j];
        g = fromPixelData[4*j+1];
        b = fromPixelData[4*j+2];
        var grey = r*0.333 + g*0.333 + b*0.333;//取平均值。
        grey = grey>greyAve?255:0;
        fromPixelData[4*j] = grey;
        fromPixelData[4*j+1] = grey;
        fromPixelData[4*j+2] = grey;
    }
    return fromImgData;
}//二值化影象

二值化後,效果如圖:

可以發現,簡單的背景色是可以去掉的。

二值化處理之後,就可以將圖片轉換成陣列(存0或1)來儲存了。參考程式碼如下:

function toXY(fromImgData){
    var result = new Array(HEIGHT);
    var fromPixelData = fromImgData.data;
    for(var j=0;j<HEIGHT;j++){
        result[j] = new Array(WIDTH);
        for(var k=0;k<WIDTH;k++){
            var r = fromPixelData[4*(j*WIDTH+k)];
            var g = fromPixelData[4*(j*WIDTH+k)+1];
            var b = fromPixelData[4*(j*WIDTH+k)+2];

            result[j][k] = (r+g+b)>500?0:1;//賦值0、1給內部陣列
        }
    }
    return result;
}//影象轉陣列

2、接下來是腐蝕、膨脹。腐蝕的基本思路在於,將所有白色周圍的畫素都置成白色,以此來消除遊離的個別黑色畫素點噪聲。膨脹正好相反,將黑色周圍置成黑色,消除數字內部的個別白色。同時,腐蝕、膨脹的操作可以讓圖片更加平滑。參考程式碼:

function corrode(fromArray){
    for(var j=1;j<fromArray.length-1;j++){
        for(var k=1;k<fromArray[j].length-1;k++){
            if(fromArray[j][k]==1&&fromArray[j-1][k]+fromArray[j+1][k]+fromArray[j][k-1]+fromArray[j][k+1]==0){
                fromArray[j][k] = 0;
            }
        }
    }
    return fromArray;
}//腐蝕(簡單)

function expand(fromArray){
    for(var j=1;j<fromArray.length-1;j++){
        for(var k=1;k<fromArray[j].length-1;k++){
            if(fromArray[j][k]==0&&fromArray[j-1][k]+fromArray[j+1][k]+fromArray[j][k-1]+fromArray[j][k+1]==4){
                fromArray[j][k] = 1;
            }
        }
    }
    return fromArray;
}//膨脹(簡單)

由於我們的圖片背景干擾不是很強烈,所以基本看不出差別。不過對於計算機來說,還是有不同的喲~尤其是背景複雜的圖片,這一步很好用。

3、切割。

由於我們的圖片內各數字沒有粘連,所以切割時只需要從上至下,從左至右掃描圖片,發現圖片某一豎行均為白色,就切一刀。有粘連的驗證碼比較困難,暫時不討論了。參考程式碼:

function split(fromArray,count){
    var numNow = 0;
    var status = false;
    var w = fromArray[0].length;
    for(var k=0;k<w;k++) {//遍歷影象
        var sumUp = 0;
        for (var j=0;j<fromArray.length;j++) //檢測整列是否有影象
            sumUp += fromArray[j][k];
        if(sumUp == 0){//切割
            for (j=0;j<fromArray.length-1;j++)
                fromArray[j].remove(k);
            w --;
            k --;
            status = false;
            continue;
        }
        else{//切換狀態
            if(!status)
                numNow ++;
            status = true;
        }
        if(numNow!=count){//不是想要的數字
            for (j=0;j<fromArray.length-1;j++)
                fromArray[j].remove(k);
            w --;
            k --;
        }
    }
    return fromArray;
}//切割,獲取特定數字

切割後,左右的空白因為都被切了,就沒有了。但是上下仍然存在空白,所以進行處理。這裡比較簡單,就不放程式碼了,思路和切割類似,但簡單很多。

4、旋轉、縮放。

其實旋轉不是必要的。沒有旋轉的步驟,可以用更多的資料量訓練來彌補。同理,縮放也不是必須的。先大致講一下思路:旋轉和縮放都再次利用了canvas,將圖片畫上去之後,利用canvas的方法操作圖片旋轉或縮放,之後再把資料拿下來,就像我們最開始讀圖片時做的一樣。旋轉時,取順時針逆時針各90度,取左右寬度最窄的角度,當作數字站立的旋轉角度。縮放時,直接按預設長寬畫圖即可。這裡我就只寫了縮放。處理後再轉換回陣列形式。參考程式碼:

function zoomToFit(fromArray){
    var imgD = fromXY(fromArray);
    var w = lastWidth;
    var h = lastHeight;
    var tempc1 = document.createElement("canvas");
    var tempc2 = document.createElement("canvas");
    tempc1.width = fromArray[0].length;
    tempc1.height = fromArray.length;
    tempc2.width = w;
    tempc2.height = h;
    var tempt1 = tempc1.getContext("2d");
    var tempt2 = tempc2.getContext("2d");
    tempt1.putImageData(imgD,0,0,0,0,tempc1.width,tempc1.height);
    tempt2.drawImage(tempc1,0,0,w,h);
    var returnImageD = tempt2.getImageData(0,0,WIDTH,HEIGHT);
    fromArray = toXY(returnImageD);
    fromArray.length = h;
    for(var i=0;i<h;i++)
        fromArray[i].length = w;
    return fromArray;
}//尺寸歸一化

處理後效果如圖:

四、記錄處理後的單個數字的二值化資料,並人工錄入真實數字。

到這裡,影象處理就搞定了,後面的工作就比較簡單了。我們把上一步得到的陣列和真實的數字一起儲存起來。這個過程可以有很多方法。我當時採取了大家一起錄入的方式,所以搭建了PHP+MySQL的伺服器,用資料庫儲存。這塊就不詳述了,大家各顯神威。

五、重複訓練

為了方便訓練,我直接在頁面裡增加了手動輸入的地方,提交後重新整理驗證碼,繼續提交。提交20個驗證碼(20*4=80個數字)後,便經常可以正確識別出4位驗證碼,在單個數字的資料量在300左右時(大約需要300/4=75個驗證碼),識別效率已經在95%以上。在500左右時已經基本見不到錯誤識別的情況了,這時候已經可以寫程式碼實現自我訓練了。此時識別一次大約需要0.06秒。

六、識別時,用處理後的影象與庫中資料對比,取得最相近的資料,得到真實數字。

這塊也比較簡單。訓練完成後,我將資料庫資料匯出,儲存成了一個大的陣列,直接用js就可以讀了。識別時遍歷所有的資料,按畫素點逐一比較。由於尺寸做了歸一化,所以直接數有多少畫素匹配即可。匹配數量最多的即為識別出的結果。我只找到了最開始寫的PHP程式碼,先放一下吧,有點懶得再寫js了…:

function check($str)
{
    $str = str_split($str,1);
    $length = count($str);
    $tempNum = 0;
    $tempSimmiar = 0;
    $query = "SELECT * FROM numkeys";
    $sth = execSql($query);
    while ($RES = $sth->fetch()) {
        $thisSimmiar = 0;
        $thisFeature = str_split($RES["feature"],1);
        $thisNum = $RES["resultnum"];
        for($i=0;$i<$length;$i++){
            if($thisFeature[$i]==$str[$i]){
                $thisSimmiar ++;
            }
        }
        if($thisSimmiar>$tempSimmiar){
            $tempSimmiar = $thisSimmiar;
            $tempNum = $thisNum;
        }
    }
    return $tempNum;
}

七、優化部分

這塊就大家自己看著來吧,因為我的圖片不是很複雜,資料量也不是很大(千條級別),所以也沒啥優化的必要,每次識別大約0.1秒吧。所以我只是沒事幹,做了之前大綱裡寫了那兩個優化。其實我感覺主要的優化方向還是影象處理那塊,儘量減少干擾,才能提高效率,也能檢測更復雜的驗證碼。

PS:訓練和識別的介面:

訓練:POST傳送username(使用者名稱)、password(密碼)、n1(第一個陣列)、n2、n3

、n4、num(真實四位字元)至。參考程式碼:

function sendData() {
    var str = prompt("請輸入驗證碼:", "");
    if(!str)
        return false;
    postData = {//整合資料包
        username: 'pdgzfx',
        password: 'pdgzfx',
        nums: str,
        n1: numsArray[0],
        n2: numsArray[1],
        n3: numsArray[2],
        n4: numsArray[3]
    };
    $.ajax({
        url: 'http://www.leonszone.cn/test/yanzhengma/train.php',
        type: 'POST',
        data: postData,
        success: function (data) {
            console.log(data);
            setTimeout(function () {
                location.reload();
            },1000);
        }
    });
}

識別:POST傳送username(使用者名稱)、password(密碼)、n1(第一個陣列)、n2、n3、n4至。參考程式碼:

function getData() {
    postData = {//整合資料包
        username: 'pdgzfx',
        password: 'pdgzfx',
        nums: 'help!!!',
        n1: numsArray[0],
        n2: numsArray[1],
        n3: numsArray[2],
        n4: numsArray[3]
    };
    $.ajax({
        url: 'http://www.leonszone.cn/test/yanzhengma/check.php',
        type: 'POST',
        data: postData,
        success: function (data) {
            $("#Vercode").val(data);
            console.log(data);
        }
    });
}

註冊使用者名稱密碼(防止大家的庫混淆):POST或GET傳送username(使用者名稱)、password(密碼)至。參考程式碼:

function getData() {
    postData = {//整合資料包
        username: 'pdgzfx',
        password: 'pdgzfx',
        };
    $.ajax({
        url: 'http://www.leonszone.cn/test/yanzhengma/regist.php',
        type: 'POST',
        data: postData,
        success: function (data) { 
            console.log(data); } }); }

或直接瀏覽器訪問:http://www.leonszone.cn/test/yanzhengma/regist.php?username= 你的使用者名稱 &amp;amp;amp;password=你的密碼

轉自https://zhuanlan.zhihu.com/p/28483558