全自動區分計算機和人類的圖靈測試(英語:Completely Automated Public Turing test to tell Computers and Humans Apart,簡稱CAPTCHA),俗稱驗證碼。CAPTCHA這個詞最先是在2002年由卡內基梅隆大學的路易斯·馮·安、Manuel Blum、Nicholas J.Hopper以及IBM的John Langford所提出。CAPTCHA是一種區分用戶是計算機或人類的公共全自動程序,在CAPTCHA測試中,做爲服務器的計算機會自動生成一個問題 讓用戶來解答。這個問題能夠由計算機生成並評判,可是必須只有人類才能解答。由於計算機沒法解答CAPTCHA的問題,因此回答出問題的用戶就能夠被認爲 是人類。python
可是因爲這個測試是由計算機來考人類,而不是像標準圖靈測試中那樣由人類來考計算機,因此更確切的講CAPTCHA是一種反向圖靈測試。[ 1 ]正則表達式
文本驗證碼常以問答形式出現,如:給出問題要求用戶答案,給出古詩上舉要求用戶寫出下句等等。算法
由於全部的驗證碼問題和答案都要事先在數據庫中存好,因此這類驗證碼數量有限。攻擊者能夠先將問題庫中的全部問題先爬取下來並準備相應的答案庫,破解時只需利用正則表達式將驗證問題提取出來,而後從答案庫中找到相應答案便可。數據庫
靜態圖驗證碼是目前應用最廣的一類驗證碼,這類驗證碼要求用戶輸入驗證碼圖片上所顯示的文字或數字,經過扭曲、變形、干擾等方法避免被光學字符識別(OCR, Optical Character Recognition)之類的電腦程序自動辨識出圖片上的文字和數字。安全
可是因爲許多驗證碼的設計者對驗證碼的意義理解的不到位,而且缺少相關安全知識和經驗,因此目前在用的不少驗證碼都是能夠被輕鬆攻破的。服務器
動態圖驗證碼看似更爲複雜,可是實際上動態驗證碼提供了更大的信息冗餘,冗餘越大,提供的信息就越多,所以也越容易被識別。例如,在某一幀本來粘連嚴重的兩字字符很能在另外一幀中就比較好的分離開了。app
許多開發者考慮到部分視覺障礙者,提供了語音驗證碼的功能,經過播放語音,讓用戶輸入聽到的內容來完成驗證。圖片驗證碼的識別主要是基於圖像處理技術,而語音驗證碼的識別主要是基於音頻處理,可是他們在識別的基本原理上是相同的。測試
隨着手機的普及,如今不少網站、應用開始使用短信驗證碼。服務器將驗證碼發送到用戶預留的手機號中,而後要求用戶輸入收到的驗證碼內容。網站
短信驗證碼的設計目的與上述三種驗證碼稍有不一樣,它不只區分用戶是人類仍是計算機計算機,它還主要用於驗證是不是用戶本人操做。可是因爲部分開發人員的安全意識不足,這類驗證碼也可能被輕易地攻破。spa
驗證碼識別主要分紅三部分:預處理,字符分割,字符識別。下面以靜態圖驗證碼(後面將簡稱爲:圖像驗證碼)爲例來具體介紹識別原理。
預處理主要是將驗證碼圖片進行色度空間轉換、去除干擾、降噪、旋轉等操做,爲字符分割的時候提供一個質量較好的圖片。
在預處理是經常使用到色度空間轉換,其中最主要的一種色度空間的轉換就是二值化。二值化目的是將前景(主要爲有效信息,即驗證碼信息)與背景(多爲干擾信息)分離,盡最大程度講有效信息提取出來,下降色彩空間維度,下降複雜度。
統計一張圖片(彩色圖需轉成256色灰度圖)的灰度直方圖後能夠看到該圖片在各灰度級上的像素分佈數量。如下圖的驗證碼爲例,咱們能夠看到最左邊 (即純黑色)與右側其餘灰度級像素的分佈有明顯一段隔開的區域,而圖中純黑色區域正好是有效信息(即驗證碼)。所以咱們能夠在該段隔開的區域裏設一個閾 值,像素值大於閾值的置爲白色,小於像素值的置爲黑色。
下圖爲經過上述辦法二值化後的結果,背景已徹底被去除,而有效信息被完整的保留了下來。
可是有時當前景與背景像素的灰度值交織在一塊兒時,咱們則很難經過閾值法提取出有效信息。如下面這張驗證碼爲例,咱們能夠從其灰度直方圖中看到全部像素點幾乎都彙集在了一塊兒。
咱們將閾值設在峯值左側嘗試二值化,能夠從結果看出,這時有效信息非但沒有被提取出來,反而帶入了更強的干擾。對於此類驗證碼咱們則須要在二值化以前先去除干擾。
代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
def
Binarized(Picture):
Pixels
=
Picture.load()
(Width, Height)
=
Picture.size
Threshold
=
80
# 閾值
for
i
in
xrange
(Width):
for
j
in
xrange
(Height):
if
Pixels[i, j] > Threshold:
# 大於閾值的置爲背景色,不然置爲前景色(文字的顏色)
Pixels[i, j]
=
BACKCOLOR
else
:
Pixels[i, j]
=
TEXTCOLOR
return
Picture
|
上述實驗已證實對於一些干擾較大的驗證碼咱們須要先對其進行去幹擾處理。去幹擾的具體方法須要根據給定的驗證碼作有針對性的設計。 以某銀行驗證碼爲例,仔細觀察能夠發現驗證碼部分筆畫寬度相對較寬,而干擾線寬度僅爲1像素。針對此特性我設計了一種分離有效信息和干擾信息的算法。
具體算法過程以下:
將驗證碼轉成256色灰度圖像後,用一個33的窗口以此覆蓋圖像中的每個像素點,而後將窗口中9個點的像素值進行排序後取中值Vmid,比較Vmid與33窗口中中心像素的值。若是兩者差值大於預設的閾值,則判斷該點顏色接近於白色仍是黑色,若接近白色則將該點置爲白色(255),若接近於黑色則置爲黑色(0)。重複三次左右便可獲得一個基本穩定的結果。
經過對比能夠看出處理後的驗證碼區域灰度已被加深成黑色,與干擾線和背景的顏色已經明顯區分開。從處理後的灰度直方圖能夠看出,像素點已主要集中在黑色(0)和白色(255)兩個灰度級,這時在用閾值法二值化便可獲得一個比較使人滿意的結果了。
代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
def
Enhance(Picture):
'''分離有效信息和干擾信息'''
Pixels
=
Picture.load()
Result
=
Picture.copy()
ResultPixels
=
Result.load()
(Width, Height)
=
Picture.size
xx
=
[
1
,
0
,
-
1
,
0
,
1
,
-
1
,
1
,
-
1
]
yy
=
[
0
,
1
,
0
,
-
1
,
-
1
,
1
,
1
,
-
1
]
Threshold
=
50
Window
=
[]
for
i
in
xrange
(Width):
for
j
in
xrange
(Height):
Window
=
[i, j]
for
k
in
xrange
(
8
):
# 取3*3窗口中像素值存在Window中
if
0
<
=
i
+
xx[k] < Width
and
0
<
=
j
+
yy[k] < Height:
Window.append((i
+
xx[k], j
+
yy[k]))
Window.sort()
(x, y)
=
Window[
len
(Window)
/
2
]
if
(
abs
(Pixels[x, y]
-
Pixels[i, j]) < Threshold):
# 若差值小於閾值則進行「強化」
if
Pixels[i, j] <
255
-
Pixels[i,j]:
# 若該點接近黑色則將其置爲黑色(0),不然置爲白色(255)
ResultPixels[i, j]
=
0
else
:
ResultPixels[i, j]
=
255
else
:
ResultPixels[i, j]
=
Pixels[i, j]
return
Result
|
雖然上面結果的質量已經足以用於識別了,但咱們仍然能夠看到圖中存在明顯的噪聲,咱們還能夠經過降噪將其質量進一步提升。
降噪的主要目的是去除圖像中的噪聲,降噪方法有方法有不少如:平滑、低通濾波等……這裏介紹一種相對簡單的方法——平滑降噪。具體方法是經過統計每 個像素點周圍像素值的個數來判斷將改點置爲和值。若是一個點周圍白色點的個數大於某一閾值則將改點置爲白色,反之亦然。經過平滑降噪已經能夠將剩下的噪聲 點所有去除了。
這裏須要注意的是對二值圖像進行降噪時應注意強度,當驗證碼筆畫較細時,降噪強度過大可能會破壞驗證碼自己的信息,這可能會影響到後面的識別效果。
代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
def
Smooth(Picture):
'''平滑降噪'''
Pixels
=
Picture.load()
(Width, Height)
=
Picture.size
xx
=
[
1
,
0
,
-
1
,
0
]
yy
=
[
0
,
1
,
0
,
-
1
]
for
i
in
xrange
(Width):
for
j
in
xrange
(Height):
if
Pixels[i, j] !
=
BACKCOLOR:
Count
=
0
for
k
in
xrange
(
4
):
try
:
if
Pixels[i
+
xx[k], j
+
yy[k]]
=
=
BACKCOLOR:
Count
+
=
1
except
IndexError:
# 忽略訪問越界的狀況
pass
if
Count >
3
:
Pixels[i, j]
=
BACKCOLOR
return
Picture
|
獲得通過預處理的圖片後須要將每一個字符單獨分隔出來,這裏簡單介紹幾種字符分隔的方法。
投影法是根據圖片在投影方向上的像素個數進行分割的。
統計以前通過預處理圖像在豎直方向上的像素個數能夠看到每兩個字符之間的像素個數有明顯斷開的狀況。所以,咱們在這些斷開處進行分隔便可。
投影法對於處理字符在投影方向上分佈比較開的狀況有比較好的效果,可是若是遇到當兩個字符在有影方向上有交集的狀況則可能將兩個字符誤判成一個字符。
若是兩個點相鄰切顏色相同,則稱這兩個點是連通的。從一個點開始,全部與它直接或簡介連通的點集即爲一個連通區域。 連通區域法是從一個點開始找其連通區域,而後將每個連通區域分割成一個塊。
這樣每一個字符都將做爲一個連通區域沒分割出來。下圖中每一種顏色是一個連通區域。
連通區域法能夠很好解決兩個字符雖然在有影方向上有交集但是沒有粘連的狀況,可是若是兩個字符粘連在一塊兒的話連通區域法也會將兩個字符誤判成一個。
若是對於上述狀況咱們能夠經過最大字符寬度來判斷連個字符是否發生粘連。咱們能夠先統計一些字符,記下最大字符寬度,當用連通區域法分隔出的字符寬度大於最大字符寬度時,咱們則認爲這是粘連字符。
這裏介紹兩種處理粘連字符的方法:
咱們一樣先統計一些字符,記下平均字符寬度,當遇到兩個字符粘連時,從平均字符寬度處向兩側找豎直方向上有效像素個數的極小值點,而後從極小值點進行分割。
這種方法雖然在必定程度上能夠解決粘連字符的問題,可是可能會破壞部分字符,這樣可能對以後的識別形成干擾。
未解決上述問題提出了滴水算法。滴水算法的過程是從圖片頂部開始向下走,向水滴滴落同樣沿着字符輪廓下滑,當滴到輪廓凹處滲入筆畫,穿過筆畫後繼續滴落,最終水滴所通過的軌跡就構成了字符的分割路徑。[ 2 ]
從上圖能夠看出粘連字符較好的被分割開而且在最大程度上保護了每個字符的原貌。
代碼以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
def
SplitCharacter(Block):
'''根據平均字符寬度找極小值點分割字符'''
Pixels
=
Block.load()
(Width, Height)
=
Block.size
MaxWidth
=
20
# 最大字符寬度
MeanWidth
=
14
# 平均字符寬度
if
Width < MaxWidth:
# 若小於最大字符寬度則認爲是單個字符
return
[Block]
Blocks
=
[]
PixelCount
=
[]
for
i
in
xrange
(Width):
# 統計豎直方向像素個數
Count
=
0
for
j
in
xrange
(Height):
if
Pixels[i, j]
=
=
TEXTCOLOR:
Count
+
=
1
PixelCount.append(Count)
for
i
in
xrange
(Width):
# 從平均字符寬度處向兩側找極小值點,從極小值點處進行分割
if
MeanWidth
-
i >
0
:
if
PixelCount[MeanWidth
-
i
-
1
] > PixelCount[MeanWidth
-
i] < PixelCount[MeanWidth
-
i
+
1
]:
Blocks.append(Block.crop((
0
,
0
, MeanWidth
-
i
+
1
, Height)))
Blocks
+
=
SplitCharacter(Block.crop((MeanWidth
-
i
+
1
,
0
, Width, Height)))
break
if
MeanWidth
+
i < Width
-
1
:
if
PixelCount[MeanWidth
+
i
-
1
] > PixelCount[MeanWidth
+
i] < PixelCount[MeanWidth
+
i
+
1
]:
Blocks.append(Block.crop((
0
,
0
, MeanWidth
+
i
+
1
, Height)))
Blocks
+
=
SplitCharacter(Block.crop((MeanWidth
+
i
+
1
,
0
, Width, Height)))
break
return
Blocks
#!python
def
SplitPicture(Picture):
'''用連通區域法初步分隔'''
Pixels
=
Picture.load()
(Width, Height)
=
Picture.size
xx
=
[
0
,
1
,
0
,
-
1
,
1
,
1
,
-
1
,
-
1
]
yy
=
[
1
,
0
,
-
1
,
0
,
1
,
-
1
,
1
,
-
1
]
Blocks
=
[]
for
i
in
xrange
(Width):
for
j
in
xrange
(Height):
if
Pixels[i, j]
=
=
BACKCOLOR:
continue
Pixels[i, j]
=
TEMPCOLOR
MaxX
=
0
MaxY
=
0
MinX
=
Width
MinY
=
Height
# BFS算法從找(i, j)點所在的連通區域
Points
=
[(i, j)]
for
(x, y)
in
Points:
for
k
in
xrange
(
8
):
if
0
<
=
x
+
xx[k] < Width
and
0
<
=
y
+
yy[k] < Height
and
Pixels[x
+
xx[k], y
+
yy[k]]
=
=
TEXTCOLOR:
MaxX
=
max
(MaxX, x
+
xx[k])
MinX
=
min
(MinX, x
+
xx[k])
MaxY
=
max
(MaxY, y
+
yy[k])
MinY
=
min
(MinY, y
+
yy[k])
Pixels[x
+
xx[k], y
+
yy[k]]
=
TEMPCOLOR
Points.append((x
+
xx[k], y
+
yy[k]))
TempBlock
=
Picture.crop((MinX, MinY, MaxX
+
1
, MaxY
+
1
))
TempPixels
=
TempBlock.load()
BlockWidth
=
MaxX
-
MinX
+
1
BlockHeight
=
MaxY
-
MinY
+
1
for
y
in
xrange
(BlockHeight):
for
x
in
xrange
(BlockWidth):
if
TempPixels[x, y] !
=
TEMPCOLOR:
TempPixels[x, y]
=
BACKCOLOR
else
:
TempPixels[x, y]
=
TEXTCOLOR
Pixels[MinX
+
x, MinY
+
y]
=
BACKCOLOR
TempBlocks
=
SplitCharacter(TempBlock)
for
TempBlock
in
TempBlocks:
Blocks.append(TempBlock)
return
Blocks
|
這裏我將分隔出來的字符塊與模板庫中的字符信息進行比對,距離越小類似度越大。關於距離這裏推薦使用編輯距離(Levenshtein Distance),他與漢明距離相比能夠更好的抵抗字符因輕微的扭曲、旋轉等變換而帶來的偏差。
爲提升識別的精確度,我取了距離最小的前TopConut個字符信息來計算其中出現的每一個字符與待識別字符的加權距離。咱們令第i個字符的權重爲TopConut - i,那麼字符x與待識別字符的加權距離爲:
其中Disi是第i個字符信息與待識別字符的距離,i取前TopCount個字符信息中全部字符爲x的下標。
至此,一個驗證碼的識別已經所有完成了。
在整個驗證碼識別過程當中有兩個關鍵之處:一是有效信息的提取,只要提取出來較好質量的有效信息才能在識別時取得較高的識別率;二是字符的分割,現有的不少算法對單個字符的識別已經有較高的的識別率了,所以,如何較好的分隔字符也成爲了驗證碼識別的關鍵。
知道了攻擊的關鍵咱們就能夠有針對性的來改進咱們的驗證碼了。對於設計驗證碼的一個基本原則就是利用人類識別與機器自動識別的差別來設計。這裏我再給出幾個我我的認爲值得考慮的地方:
好的粘連能夠有效的避免常見的字符分割算法;
讓前景與背景具備相近的像素能夠避免直接利用閾值法除去幹擾信息;
在必定程度上要減小冗餘,冗餘越大,提供的信息越多,越容易被識別