當前位置:
首頁 > 知識 > 用 KNN 來進行驗證碼識別

用 KNN 來進行驗證碼識別

(點擊

上方公眾號

,可快速關注)




來源:邱康singasong   


https://segmentfault.com/a/1190000006070219




前言

之前做了一個校園交友的APP,其中一個邏輯是通過用戶的教務系統來確認用戶是一名在校大學生,基本的想法是通過用戶的賬號和密碼,用爬蟲的方法來確認信息,但是許多教務系統都有驗證碼,當時是通過本地伺服器去下載驗證碼,然後分發給客戶端,然後讓用戶自己填寫驗證碼,與賬號密碼一併提交給伺服器,然後伺服器再去模擬登錄教務系統以確認用戶能否登錄該教務系統。驗證碼無疑讓我們想使得用戶快速認證的想法破滅了,但是當時也沒辦法,最近看了一些機器學習的內容,覺得對於大多數學校的那些極簡單的驗證碼應該是可以用KNN這種方法來破解的,於是整理了一下思緒,擼起袖子做起來!


分析


我們學校的驗證碼是這樣的:

,其實就是簡單地把字元進行旋轉然後加上一些微弱的噪點形成的。我們要識別,就得逆行之,具體思路就是,首先二值化去掉噪點,然後把單個字元分割出來,最後旋轉至標準方向,然後從這些處理好的圖片中選出模板,最後每次新來一張驗證碼就按相同方式處理,然後和這些模板進行比較,選擇判別距離最近的一個模板作為其判斷結果(亦即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目錄下面。如圖:


二值化


matlab豐富的圖像處理函數能給我們省下很多時間,,我們遍歷source文件夾,對每一張驗證碼圖片進行二值化處理,把處理過的圖片存入bw目錄下。代碼如下



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




處理結果如圖,一共2000個字元的圖片存在rotate文件夾中


模板選取


現在從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
"

,

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
"

,

codes

);


        

num

=

codes

-

realcodes

;


        

num

=

length

(

find

(

num

~=

0

));


        

singleerror

=

singleerror

+

num

;


        

if

num

>

0


            

uniterror

=

uniterror

+

1

;


        

end


        fprintf

(

"錯誤個數:%d
"

,

num

);


    

end


end


fprintf

(

"
-----結果統計如下-----

"

);


fprintf

(

"測試驗證碼的字元數量:%d
"

,

n

*

4

);


fprintf

(

"測試驗證碼的字元錯誤數量:%d
"

,

singleerror

);


fprintf

(

"單個字元識別正確率:%.2f%%
"

,(

1

-

singleerror

/

(

n

*

4

))

*

100

);


fprintf

(

"測試驗證碼圖的數量:%d
"

,

n

);


fprintf

(

"測試驗證碼圖的錯誤數量:%d
"

,

uniterror

);


fprintf

(

"填對驗證碼的概率:%.2f%%
"

,(

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

%



可見單個字元準確率是比較高的的了,但是綜合準確率還是不行,觀察結果至,錯誤的字元就是那些易混淆字元,比如E和F,C和L,5和3,4和A等,所以我們能做的事就是增加模板中的樣本數量,以期盡量減少混淆。



增加了幾十個樣本過後再次試驗,結果:






驗證碼實際字元

:

2B4E


驗證碼測試字元

:

2B4F


錯誤個數

:

1


驗證碼實際字元

:

4572


驗證碼測試字元

:

4572


錯誤個數

:

0


驗證碼實際字元

:

52CY


驗證碼測試字元

:

52LY


錯誤個數

:

1


驗證碼實際字元

:

83QG


驗證碼測試字元

:

83QG


錯誤個數

:

0


驗證碼實際字元

:

9992


驗證碼測試字元

:

9992


錯誤個數

:

0


驗證碼實際字元

:

A7Y7


驗證碼測試字元

:

A7Y7


錯誤個數

:

0


驗證碼實際字元

:

D993


驗證碼測試字元

:

D993


錯誤個數

:

0


驗證碼實際字元

:

F549


驗證碼測試字元

:

F5A9


錯誤個數

:

1


驗證碼實際字元

:

FMC6


驗證碼測試字元

:

FMLF


錯誤個數

:

2


驗證碼實際字元

:

R4N4


驗證碼測試字元

:

R4N4


錯誤個數

:

0


 


-----

結果統計如下

-----


 


測試驗證碼的字元數量

:

40


測試驗證碼的字元錯誤數量

:

5


單個字元識別正確率

:

87.50

%


測試驗證碼圖的數量

:

10


測試驗證碼圖的錯誤數量

:

4


填對驗證碼的概率

:

60.00

%




可見無論是單個字元識別正確率還是整個驗證碼正確的概率都有了提升。能夠預見:隨著模板數量的增多,正確率會不斷地提高。


總結


這種方法的可擴展性很弱,而且只適用於簡單的驗證碼,12306那種根本就別提了。



總之就是學習的道路還很長,我會慢慢的改善這種方法的。




看完本文有收穫?請轉

發分享給更多人


關注「P

ython開發者」,提升Python技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python開發者 的精彩文章:

面向對象:認真積極但絕不將就地找到那個他,相信會有奇妙的事件發生
恭喜 Python,基本是各年齡段開發者的最愛!

TAG:Python開發者 |