[機器學習]利用KNN演算法進行驗證碼識別
前言
對那些及其簡單的驗證碼,可以用KNN演算法來破解,這裡整理了一個思路,可以瞭解瞭解.那麼KNN演算法原理是什麼呢?
KNN原理
kNN演算法的核心思想是如果一個樣本在特徵空間中的k個最相鄰的樣本中的大多數屬於某一個類別,則該樣本也屬於這個類別,並具有這個類別上樣本的特性。該方法在確定分類決策上只依據最鄰近的一個或者幾個樣本的類別來決定待分樣本所屬的類別。 kNN方法在類別決策時,只與極少量的相鄰樣本有關。由於kNN方法主要靠周圍有限的鄰近的樣本,而不是靠判別類域的方法來確定所屬類別的,因此對於類域的交叉或重疊較多的待分樣本集來說,kNN方法較其他方法更為適合。
演算法流程
- 準備資料,對資料進行預處理
- 選用合適的資料結構儲存訓練資料和測試元組
- 設定引數,如k
4.維護一個大小為k的的按距離由大到小的優先順序佇列,用於儲存最近鄰訓練元組。隨機從訓練元組中選取k個元組作為初始的最近鄰元組,分別計算測試元組到這k個元組的距離,將訓練元組標號和距離存入優先順序佇列- 遍歷訓練元組集,計算當前訓練元組與測試元組的距離,將所得距離L 與優先順序佇列中的最大距離Lmax
- 進行比較。若L>=Lmax,則捨棄該元組,遍歷下一個元組。若L < Lmax,刪除優先順序佇列中最大距離的元組,將當前訓練元組存入優先順序佇列。
- 遍歷完畢,計算優先順序佇列中k 個元組的多數類,並將其作為測試元組的類別。
- 測試元組集測試完畢後計算誤差率,繼續設定不同的k值重新進行訓練,最後取誤差率最小的k 值。
優點
1.簡單,易於理解,易於實現,無需估計引數,無需訓練
2.適合對稀有事件進行分類(例如當流失率很低時,比如低於0.5%,構造流失預測模型)
3.特別適合於多分類問題(multi-modal,物件具有多個類別標籤),例如根據基因特徵來判斷其功能分類,kNN比SVM的表現要好
缺點
1.懶惰演算法,對測試樣本分類時的計算量大,記憶體開銷大,評分慢
可解釋性較差,無法給出決策樹那樣的規則。
下面開始著重講如何利用KNN演算法;來實現驗證碼的識別.
具體實現
其實就是簡單地把字元進行旋轉然後加上一些微弱的噪點形成的。我們要識別,就得逆行之.
具體思路
1)獲得驗證碼
2) 首先二值化去掉噪點
3)然後把單個字元分割出來
4)最後旋轉至標準方向,然後從這些處理好的圖片中選出模板
5)最後每次新來一張驗證碼就按相同方式處理,然後和這些模板進行比較,選擇判別距離最近的一個模板作為其判斷結果(亦即KNN的思想,本文取K=1)。
獲得驗證碼
#-*- coding:UTF-8 -*-
import urllib,urllib2,cookielib,string,Image
def getchk(number):
#建立cookie物件
cookie = cookielib.LWPCookieJar()
cookieSupport= urllib2.HTTPCookieProcessor(cookie)
opener = urllib2.build_opener(cookieSupport, urllib2.HTTPHandler)
urllib2.install_opener(opener)
#首次與教務系統連結獲得cookie#
#偽裝browser
headers = {
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding':'gzip,deflate',
'Accept-Language':'zh-CN,zh;q=0.8',
'User-Agent':'Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36'
}
req0 = urllib2.Request(
url ='http://mis.teach.ustc.edu.cn',
headers = headers #請求頭
)
# 捕捉http錯誤
try :
result0 = urllib2.urlopen(req0)
except urllib2.HTTPError,e:
print e.code
#提取cookie
getcookie = ['',]
for item in cookie:
getcookie.append(item.name)
getcookie.append("=")
getcookie.append(item.value)
getcookie = "".join(getcookie)
#修改headers
headers["Origin"] = "http://mis.teach.ustc.edu.cn"
headers["Referer"] = "http://mis.teach.ustc.edu.cn/userinit.do"
headers["Content-Type"] = "application/x-www-form-urlencoded"
headers["Cookie"] = getcookie
for i in range(number):
req = urllib2.Request(
url ="http://mis.teach.ustc.edu.cn/randomImage.do?date='1469451446894'",
headers = headers #請求頭
)
response = urllib2.urlopen(req)
status = response.getcode()
picData = response.read()
if status == 200:
localPic = open("./source/"+str(i)+".jpg", "wb")
localPic.write(picData)
localPic.close()
else:
print "failed to get Check Code "
if __name__ == '__main__':
getchk(500)
這裡下載了500張驗證碼到source目錄下面。如圖:
二值化
mydir='./source/';
bw = './bw/';
if mydir(end)~='\'
mydir=[mydir,'\'];
end
DIRS=dir([mydir,'*.jpg']); %副檔名
n=length(DIRS);
for i=1:n
if ~DIRS(i).isdir
img = imread(strcat(mydir,DIRS(i).name ));
img = rgb2gray(img);%灰度化
img = im2bw(img);%0-1二值化
name = strcat(bw,DIRS(i).name)
imwrite(img,name);
end
end
結果
分割
mydir='./bw/';
letter = './letter/';
if mydir(end)~='\'
mydir=[mydir,'\'];
end
DIRS=dir([mydir,'*.jpg']); %副檔名
n=length(DIRS);
for i=1:n
if ~DIRS(i).isdir
img = imread(strcat(mydir,DIRS(i).name ));
img = im2bw(img);%二值化
img = 1-img;%顏色反轉讓字元成為聯通域,方便去除噪點
for ii = 0:3
region = [ii*20+1,1,19,20];%把一張驗證碼分成四個20*20大小的字元圖片
subimg = imcrop(img,region);
imlabel = bwlabel(subimg);
% imshow(imlabel);
if max(max(imlabel))>1 % 說明有噪點,要去除
% max(max(imlabel))
% imshow(subimg);
stats = regionprops(imlabel,'Area');
area = cat(1,stats.Area);
maxindex = find(area == max(area));
area(maxindex) = 0;
secondindex = find(area == max(area));
imindex = ismember(imlabel,secondindex);
subimg(imindex==1)=0;%去掉第二大連通域,噪點不可能比字元大,所以第二大的就是噪點
end
name = strcat(letter,DIRS(i).name(1:length(DIRS(i).name)-4),'_',num2str(ii),'.jpg')
imwrite(subimg,name);
end
end
end
旋轉
接下來進行旋轉,哪找一個什麼標準呢?據觀察,這些字元旋轉不超過60度,那麼在正負60度之間,統一旋轉至字元寬度最小就行了。程式碼如下
if mydir(end)~='\'
mydir=[mydir,'\'];
end
DIRS=dir([mydir,'*.jpg']); %副檔名
n=length(DIRS);
for i=1:n
if ~DIRS(i).isdir
img = imread(strcat(mydir,DIRS(i).name ));
img = im2bw(img);
minwidth = 20;
for angle = -60:60
imgr=imrotate(img,angle,'bilinear','crop');%crop 避免影象大小變化
imlabel = bwlabel(imgr);
stats = regionprops(imlabel,'Area');
area = cat(1,stats.Area);
maxindex = find(area == max(area));
imindex = ismember(imlabel,maxindex);%最大連通域為1
[y,x] = find(imindex==1);
width = max(x)-min(x)+1;
if width<minwidth
minwidth = width;
imgrr = imgr;
end
end
name = strcat(rotate,DIRS(i).name)
imwrite(imgrr,name);
end
end
模板選取
現在從rotate資料夾中選取一套模板,涵蓋每一個字元,一個字元可以選取多個圖片,因為即使有前面的諸多處理也不能保證一個字元的最終呈現形式只有一種,多選幾個才能保證覆蓋率。把選出來的模板圖片存入samples資料夾下,這個過程很耗時耗力。可以找同學幫忙~,如圖
測試
測試程式碼如下首先對測試驗證碼進行上述操作,然後和選出來的模板進行比較,採用差分值最小的模板作為測試樣本的字元選擇,程式碼如下
% 具有差分最小值的圖作為答案
mydir='./test/';
samples = './samples/';
if mydir(end)~='\'
mydir=[mydir,'\'];
end
if samples(end)~='\'
samples=[samples,'\'];
end
DIRS=dir([mydir,'*.jpg']); %擴充套件?
DIRS1=dir([samples,'*.jpg']); %副檔名
n=length(DIRS);%驗證碼總圖數
singleerror = 0;%單個錯誤
uniterror = 0;%一張驗證碼錯誤個數
for i=1:n
if ~DIRS(i).isdir
realcodes = DIRS(i).name(1:4);
fprintf('驗證碼實際字元:%s\n',realcodes);
img = imread(strcat(mydir,DIRS(i).name ));
img = rgb2gray(img);
img = im2bw(img);
img = 1-img;%顏色反轉讓字元成為聯通域
subimgs = [];
for ii = 0:3
region = [ii*20+1,1,19,20];%奇怪,為什麼這樣才能均分?
subimg = imcrop(img,region);
imlabel = bwlabel(subimg);
if max(max(imlabel))>1 % 說明有雜點
stats = regionprops(imlabel,'Area');
area = cat(1,stats.Area);
maxindex = find(area == max(area));
area(maxindex) = 0;
secondindex = find(area == max(area));
imindex = ismember(imlabel,secondindex);
subimg(imindex==1)=0;%去掉第二大連通域
end
subimgs = [subimgs;subimg];
end
codes = [];
for ii = 0:3
region = [ii*20+1,1,19,20];
subimg = imcrop(img,region);
minwidth = 20;
for angle = -60:60
imgr=imrotate(subimg,angle,'bilinear','crop');%crop 避免影象大小變化
imlabel = bwlabel(imgr);
stats = regionprops(imlabel,'Area');
area = cat(1,stats.Area);
maxindex = find(area == max(area));
imindex = ismember(imlabel,maxindex);%最大連通域為1
[y,x] = find(imindex==1);
width = max(x)-min(x)+1;
if width<minwidth
minwidth = width;
imgrr = imgr;
end
end
mindiffv = 1000000;
for jj = 1:length(DIRS1)
imgsample = imread(strcat(samples,DIRS1(jj).name ));
imgsample = im2bw(imgsample);
diffv = abs(imgsample-imgrr);
alldiffv = sum(sum(diffv));
if alldiffv<mindiffv
mindiffv = alldiffv;
code = DIRS1(jj).name;
code = code(1);
end
end
codes = [codes,code];
end
fprintf('驗證碼測試字元:%s\n',codes);
num = codes-realcodes;
num = length(find(num~=0));
singleerror = singleerror + num;
if num>0
uniterror = uniterror +1;
end
fprintf('錯誤個數:%d\n',num);
end
end
fprintf('\n-----結果統計如下-----\n\n');
fprintf('測試驗證碼的字元數量:%d\n',n*4);
fprintf('測試驗證碼的字元錯誤數量:%d\n',singleerror);
fprintf('單個字元識別正確率:%.2f%%\n',(1-singleerror/(n*4))*100);
fprintf('測試驗證碼圖的數量:%d\n',n);
fprintf('測試驗證碼圖的錯誤數量:%d\n',uniterror);
fprintf('填對驗證碼的概率:%.2f%%\n',(1-uniterror/n)*100);
驗證碼實際字元:2B4E
驗證碼測試字元:2B4F
錯誤個數:1
驗證碼實際字元:4572
驗證碼測試字元:4572
錯誤個數:0
驗證碼實際字元:52CY
驗證碼測試字元:52LY
錯誤個數:1
驗證碼實際字元:83QG
驗證碼測試字元:85QG
錯誤個數:1
驗證碼實際字元:9992
驗證碼測試字元:9992
錯誤個數:0
驗證碼實際字元:A7Y7
驗證碼測試字元:A7Y7
錯誤個數:0
驗證碼實際字元:D993
驗證碼測試字元:D995
錯誤個數:1
驗證碼實際字元:F549
驗證碼測試字元:F5A9
錯誤個數:1
驗證碼實際字元:FMC6
驗證碼測試字元:FMLF
錯誤個數:2
驗證碼實際字元:R4N4
驗證碼測試字元:R4N4
錯誤個數:0
-----結果統計如下-----
測試驗證碼的字元數量:40
測試驗證碼的字元錯誤數量:7
單個字元識別正確率:82.50%
測試驗證碼圖的數量:10
測試驗證碼圖的錯誤數量:6
填對驗證碼的概率:40.00%