寫在前面
本章將要介紹一下如何識別簡單的驗證碼。會涉及到一些影象的概念以及機器學習的知識。
我們本次識別的驗證碼來自csdn,長相如下:
在學習之前,我們先安裝本章需要的三個庫:影象庫Pillow、機器學習庫Scikit-Learn、科學計算庫Numpy。通過pip命令就可以進行安裝。
pip install pillow scikit-learn numpy
複製程式碼
原始碼介紹
本章節的案例稍微複雜,見:USTBCrawlers/lesson8
這裡主要有三個部分:下載器、分割器、與識別器。我們可以先把程式碼clone下來,然後進入到lesson8這個目錄下。
影象基本概念
下面我們先來對影象有個基本的介紹。影象是由一個一個畫素點構成的,其內部結構是一個二維的矩陣,或者理解成一個二維陣列。
例如,一個 M x N 的影象,可以表示成以下的格式:
影象的座標和我們平時學的直角座標並不相同,直角座標的原點是在左下角;而影象的座標 起點是在左上角,如下圖所示。在python中,我們可以使用PIL(Pillow)對影象進行操作。如下,我們開啟了我們的驗證碼,並呼叫convert("L")
方法把圖片轉為灰度影象。
from PIL import Image
im = Image.open("csdn.png").convert("L")
im.show() # 顯示影象
複製程式碼
然而我們真正操作並不是影象物件,而是一個矩陣,或者說是二維陣列,我們可以把影象轉成numpy陣列。
可以看到我們的驗證碼是20*48的。
影象直方圖
影象是由一個個的畫素構成的,畫素有灰度值,從0-255,一共256個灰度級。直方圖的作用是觀察每個灰度級所佔畫素的多少。
可以呼叫Image.histogram()
獲取Image物件的直方圖。
比如說對於我們的驗證碼圖片,一共有20*48=960個畫素點,其中灰度級為94的畫素有754個,而灰度級為255的有129個。
再次觀察一下我們灰度化的驗證碼,可以看到驗證碼的字母是白色的,也就是灰度級為255。周圍的背景是灰色的,灰度級為94。
影象二值化
在處理驗證碼的時候,背景很多時候並不是同一個灰度級的,為了減少背景對資料的影響。一般都會講驗證碼進行二值化。
所謂二值化,其實就是把灰度影象變成只由純黑、或純白兩種畫素組成的影象。
方法很簡單,我們可以設定一個閾值,灰度大於100的畫素都變成純白(255),而灰度小於100的畫素都變成純黑(0)。
驗證碼字元分割
因為驗證碼包含四個數字,所以需要把每個字母分割開。筆者為讀者準備的驗證碼是很好分割的型別,只需要對指定區域進行篩選即可。
這裡的圖片數字的寬度都是8,起點分別位於5、14、23、32。分割程式碼如下:
def split_and_save(path):
path = "../downloader/captchas/" + path
pix = np.array(Image.open(path).convert("L"))
# threshold image
pix = (pix > 100) * 255
col_ranges = [
[5, 5 + 8],
[14, 14 + 8],
[23, 23 + 8],
[32, 32 + 8]
]
# split and save
for col_range in col_ranges:
letter = pix[:, col_range[0]: col_range[1]]
im = Image.fromarray(np.uint8(letter))
save_path = "./letters/" + str(uuid.uuid4()) + ".png"
im.save(save_path)
複製程式碼
我們每個驗證碼字元的大小為:20*8。
建立資料集
對驗證碼分割後,我們就會得到一堆字母的圖片了。
但是這些圖片都沒有標註,下面我們使用的機器學習演算法是資料驅動的,所以需要一些已經標註好的驗證碼資料。我這裡標註的方法比較簡單,因為畢竟只有0-9十種字母,我就每種資料標註6個。直接通過檔名稱進行標識。
機器學習之KNN演算法
演算法描述
K近鄰演算法的定義十分簡單,在百度百科上有這樣的解釋:如果一個樣本在特徵空間中的k個最相似(即特徵空間中最鄰近)的樣本中的大多數屬於某一個類別,則該樣本也屬於這個類別。
也就是說,需要找到要識別的字母在訓練樣本中K個最近的字母,然後找出這K個字母中最多的是某個類的?要識別的圖片也就是該類的。
演算法舉例
上面那麼描述可能稍微有點兒晦澀,那我們舉個例子。
這裡以電影分類作為例子,電影題材可分為愛情片,動作片等。這裡假定將電影分為愛情片和動作片兩類,直觀感受的話,如果一部電影中接吻鏡頭很多,打鬥鏡頭較少,顯然是屬於愛情片,反之為動作片。
這裡我們的資料有兩個特徵:一個是接吻鏡頭的數目,一個是打鬥鏡頭的數目。下面我們有一組已知的資料。
我們這裡的目標是利用已知的四個電影資料,預測未知電影的型別。
我們把愛情電影定義為紅色的叉子,動作電影定義為綠色的圓圈,未知電影為問號。這裡可以畫個圖直觀感受一下。
假設我們K取3的話,那麼從圖中可以很清晰的看到,離未知電影最近的三個電影分別是:愛情電影、愛情電影、動作電影。愛情電影占比大,所以我們未知的電影是愛情片。
scikit-learn中使用KNN
我們將使用scikit-learn來實現KNN,所以不需要關注演算法的實現(雖然實現也很簡單),只要有資料和標籤就好了。我們來看看怎麼實現上面的預測電影型別的功能。
這裡X是我們已知型別的四部電影,y是四部電影的標籤。0代表愛情電影,1代表動作電影。然後呼叫scikit-learn中的KNeighborsClassifier
先通過fit
擬合資料,再呼叫predict
預測就好了。
可以看到我們最後預測出未知電影的標籤為0,也就是愛情電影,和想法一致。
驗證碼識別
瞭解了以上知識後,我們可以編寫驗證碼識別指令碼了。
我們這裡首先編寫載入資料的函式,載入之前標註好的驗證碼字母資料。我們驗證碼字母資料是20*8的,也就是相當於有160個特徵。
def load_dataset():
X = []
y = []
for i in range(60):
path = "./dataset/%d%d.png" % (i / 6, i % 6 + 1)
pix = np.array(Image.open(path).convert("L"))
X.append(pix.reshape(8*20))
y.append(i/6)
return np.array(X), np.array(y)
複製程式碼
然後對資料進行擬合:
X, y = load_dataset()
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X, y.astype('uint8'))
複製程式碼
最後先分割圖片,再使用擬合好資料的knn進行預測。
def split_letters(path):
pix = np.array(Image.open(path).convert("L"))
# threshold image
pix = (pix > 100) * 255
col_ranges = [
[5, 5 + 8],
[14, 14 + 8],
[23, 23 + 8],
[32, 32 + 8]
]
letters = []
for col_range in col_ranges:
letter = pix[:, col_range[0]: col_range[1]]
letters.append(letter.reshape(8*20))
return letters
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python recognizer.py <image_filename>")
letters = split_letters(sys.argv[1])
print(knn.predict(letters))
複製程式碼
我們執行一下,可以看到以下識別的結果,都識別出來了。
內容補充
以上的驗證碼識別只是一個基本的操作流程。現在只要有足夠多的資料,利用深度學習基本上所有的驗證碼都能識別出來。
深度學習由於需要讀者有數學基礎以及相關的背景知識,這裡筆者就提供一些我自己寫過的驗證碼相關的資料,如果感興趣可以自己去學習。
如果讀者感興趣可以進行深入學習。
筆者的驗證碼相關的一個專案:
筆者的驗證碼相關的部落格: