[機(jī)器視覺]使用python自動識別驗(yàn)證碼詳解
前言
CAPTCHA全稱Completely Automated Public Turing Test to Tell Computers and Humans Apart,即全自動區(qū)分人機(jī)的圖靈測試。這也是驗(yàn)證碼誕生的主要任務(wù)。但是隨著近年來大數(shù)據(jù)運(yùn)算和機(jī)器視覺的發(fā)展,用機(jī)器視覺識別圖像已經(jīng)變得非常容易,過去用于區(qū)分人機(jī)的驗(yàn)證碼也開始變得不再安全。
接下來就讓我們從零開始,深入圖像處理和算法構(gòu)建,來看看使用機(jī)器視覺來識別過時的驗(yàn)證碼( 如下所示 )究竟可以有多簡單。
載入需要的程序包 & 設(shè)置全局變量
import requests import time from io import BytesIO from PIL import Image import os import numpy as np # 獲取驗(yàn)證碼的網(wǎng)址 CAPT_URL = "http://xxxxxxxxxxxx.cn/servlet/ImageServlet" # 驗(yàn)證碼的保存路徑 CAPT_PATH = "capt/" if not os.path.exists(CAPT_PATH): os.mkdir(CAPT_PATH) # 將驗(yàn)證碼轉(zhuǎn)為灰度圖時用到的"lookup table" THRESHOLD = 165 LUT = [0]*THRESHOLD + [1]*(256 - THRESHOLD)
從網(wǎng)站獲取驗(yàn)證碼
capt_fetch()方法非常簡單,我們直接從網(wǎng)站獲取驗(yàn)證碼,將其轉(zhuǎn)換為Image對象,等待被訓(xùn)練和測試等環(huán)節(jié)調(diào)用。
def capt_fetch(): """ 從網(wǎng)站獲取驗(yàn)證碼,將驗(yàn)證碼轉(zhuǎn)為Image對象 :require requests: import requests :require time: import time :require BytesIO: from io import BytesIO :require Image: from PIL import Image :param: :return capt: 一個Image對象 """ # 從網(wǎng)站獲取驗(yàn)證碼 capt_raw = requests.get(CAPT_URL) # 將二進(jìn)制的驗(yàn)證碼圖片寫入IO流 f = BytesIO(capt_raw.content) # 將驗(yàn)證碼轉(zhuǎn)換為Image對象 capt = Image.open(f) return capt
保存驗(yàn)證碼到本地
- 一個強(qiáng)大的機(jī)器學(xué)習(xí)模型,是離不開強(qiáng)大的訓(xùn)練集作支持的。這里我們也必須先有一個預(yù)先打好標(biāo)簽(預(yù)分類)的驗(yàn)證碼圖片集,才能開始訓(xùn)練模型。
- capt_download()方法就是我們用來建立訓(xùn)練圖像集的方法。它會調(diào)用capt_fetch()方法,將獲得的Image對象展示給用戶,等待用戶輸入驗(yàn)證碼中的字符,然后將圖片命名為用戶輸入的字符存儲起來。
- 當(dāng)然,為了避免文件名重復(fù)(比如獲取到了兩張字符完全相同的驗(yàn)證碼),capt_download()方法將系統(tǒng)時間也加入到了文件名中。
def capt_download(): """ 將Image類型的驗(yàn)證碼對象保存到本地 :require Image: from PIL import Image :require os: import os :require capt_fetch(): 從nbsc網(wǎng)站獲取驗(yàn)證碼 :require CAPT_PATH: 驗(yàn)證碼保存路徑 :param: :return: """ capt = capt_fetch() capt.show() text = raw_input("請輸入驗(yàn)證碼中的字符:") suffix = str(int(time.time() * 1e3)) capt.save(CAPT_PATH + text + "_" + suffix + ".jpg")
圖像預(yù)處理
- capt_process()方法會先將驗(yàn)證碼轉(zhuǎn)為灰度圖,然后再根據(jù)全局變量中定義的LUT將灰度圖轉(zhuǎn)化為黑白圖片。并按照驗(yàn)證碼中四個字符所在的位置進(jìn)行切割。
- 從彩色圖片到灰度圖,再到黑白圖,看似驗(yàn)證碼中的信息損失了很多,實(shí)際上這樣做的目的是為了使字符的特征更加明顯。
- 其實(shí)我們最終得到的黑白圖像會有一些噪點(diǎn)存在,這主要是由于前景色與背景色不存在嚴(yán)格的區(qū)分度,我們可以使用濾波器過濾掉這些噪點(diǎn),但少量的噪點(diǎn)會被訓(xùn)練模型當(dāng)作誤差處理,并不影響我們分類。至于過濾噪點(diǎn)的方法,我會專門寫一篇帖子。
def capt_process(capt): """ 圖像預(yù)處理:將驗(yàn)證碼圖片轉(zhuǎn)為二值型圖片,按字符切割 :require Image: from PIL import Image :require LUT: A lookup table, 包含256個值 :param capt: 驗(yàn)證碼Image對象 :return capt_per_char_list: 一個數(shù)組包含四個元素,每個元素是一張包含單個字符的二值型圖片 """ capt_gray = capt.convert("L") capt_bw = capt_gray.point(LUT, "1") capt_per_char_list = [] for i in range(4): x = 5 + i * 15 y = 2 capt_per_char = capt_bw.crop((x, y, x + 15, y + 18)) capt_per_char_list.append(capt_per_char) return capt_per_char_list
圖像預(yù)處理的效果如下:
原始圖像
灰度圖
黑白圖
按字符切分后的黑白圖像
由于字符寬窄有差異,這里我們按字符切分后,有些字符會多出來一部分,有些字符會丟失一部分。比如7多了一筆看起來像個三角形,M少了一豎看起來像N。但只要符號之間有區(qū)分度,依然能夠準(zhǔn)確分類。
提取圖像中的特征值
- 到了這一步,我們得到的圖像都是由單個字符組成的黑白圖片(0為黑色像素點(diǎn),1為白色像素點(diǎn))。此時,如果我們把圖片轉(zhuǎn)為數(shù)組,就會得到一個由0,1組成的矩陣,其長寬恰與圖片的大小相同, 每一個數(shù)字代表一個像素點(diǎn)。
- 接下來我們需要考慮如何提取能夠區(qū)分不同字符的特征值。我們可以直接用圖像中的每一個像素點(diǎn)作為一個特征值,也可以匯總圖像中共有多少黑色像素點(diǎn)(當(dāng)然,這樣每張圖片只能提取一個特征值),還可以按區(qū)域匯總圖像中的像素點(diǎn),比如先將圖片四等分,匯總四張“子圖片”中的像素點(diǎn),如果覺得特征不夠多,還可以繼續(xù)下分,直至精確到每一個像素點(diǎn)。
- 為了使代碼更加簡潔,我們這里直接匯總、分行列匯總了圖像像素點(diǎn)的個數(shù),共提取了1張圖片 + 15列 + 18行 ==> 34個特征值。至于按區(qū)域匯總的方法,還是等我們有空了單獨(dú)寫一篇帖子。
def capt_inference(capt_per_char): """ 提取圖像特征 :require numpy: import numpy as np :param capt_per_char: 由單個字符組成的二值型圖片 :return char_features:一個數(shù)組,包含 capt_per_char中字符的特征 """ char_array = np.array(capt_per_char) total_pixels = np.sum(char_array) cols_pixels = np.sum(char_array, 0) rows_pixels = np.sum(char_array, 1) char_features = np.append(cols_pixels, rows_pixels) char_features = np.append(total_pixels, char_features) return char_features.tolist()
生成訓(xùn)練集
這里我們會將預(yù)分類的每張驗(yàn)證碼分別讀入內(nèi)存,從它們的圖像中提取特征值,從它們的名稱中提取驗(yàn)證碼所對應(yīng)的文字(標(biāo)簽),并將特征值與標(biāo)簽分別匯總成列表,注意train_labels中的每個元素是與train_table中的每一行相對應(yīng)的。
def train(): """ 將預(yù)分類的驗(yàn)證碼圖片集轉(zhuǎn)化為字符特征訓(xùn)練集 :require Image: from PIL import Image :require os: import os :require capt_process(): 圖像預(yù)處理 :require capt_inference(): 提取圖像特征 :param: :return train_table: 驗(yàn)證碼字符特征訓(xùn)練集 :return train_labels: 驗(yàn)證碼字符預(yù)分類結(jié)果 """ files = os.listdir(CAPT_PATH) train_table = [] train_labels = [] for f in files: train_labels += list(f.split("_")[0]) capt = Image.open(CAPT_PATH + f) capt_per_char_list = capt_process(capt) for capt_per_char in capt_per_char_list: char_features = capt_inference(capt_per_char) train_table.append(char_features) return train_table, train_labels
定義分類模型
- 只要我們提取的特征值具有足夠的區(qū)分度(能夠區(qū)分不同字符),理論上我們可以使用任何機(jī)器學(xué)習(xí)的模型建立特征值與標(biāo)簽的相關(guān)。
- 我嘗試過使用knn,svm,Decision Tree,ANN等各種主流機(jī)器學(xué)習(xí)模型對提取的訓(xùn)練數(shù)據(jù)進(jìn)行分類,盡管不同模型表現(xiàn)各異,但識別準(zhǔn)確率都可以很輕松的達(dá)到90%以上。
- 這里我們不打算使用復(fù)雜的第三方模型完成學(xué)習(xí)過程,所以我們在這里自己寫了一個分類模型。模型實(shí)現(xiàn)的nnc算法是knn的一種,通過比對未知分類的一組特征值測試集與那一組已知分類的特征值訓(xùn)練集最接近,判定測試集的分類情況應(yīng)該和與其最接近的訓(xùn)練集的分類情況訓(xùn)練集標(biāo)簽相同。
- 當(dāng)然,我們也可以稍加改造直接實(shí)現(xiàn)knn算法,找到3組、5組或k組與未知分類的特征向量最接近的訓(xùn)練集中的特征向量,并通過票選與其對應(yīng)的標(biāo)簽,預(yù)測未知分類的特征向量在大概率上應(yīng)該屬于哪一類。
def nnc(train_table, test_vec, train_labels): """ Nearest Neighbour Classification(近鄰分類法), 根據(jù)已知特征矩陣的分類情況,預(yù)測未分類的特征向量所屬類別 :require numpy: import numpy as np :param train_table: 預(yù)分類的特征矩陣 :param test_vec: 特征向量, 長度必須與矩陣的列數(shù)相等 :param labels: 特征矩陣的類別向量 :return : 預(yù)測特征向量所屬的類別 """ dist_mat = np.square(np.subtract(train_table, test_vec)) dist_vec = np.sum(dist_mat, axis = 1) pos = np.argmin(dist_vec) return train_labels[pos]
測試模型分類效果
最后,我們需要測試我們的理論是否有效,通過調(diào)用test()方法,我們會先從網(wǎng)站獲取驗(yàn)證碼圖像,對圖像進(jìn)行處理、特征提取,然后調(diào)用nnc()方法對提取到的四組特征值做近鄰分類,分別得到驗(yàn)證碼中的四個字符。最后將驗(yàn)證碼圖像和識別到的字符傳出,方便我們比對識別結(jié)果。
def test(): """ 測試模型分類效果 :require Image: from PIL import Image :require capt_fetch(): 從nbsc網(wǎng)站獲取驗(yàn)證碼 :require capt_process(): 圖像預(yù)處理 :require capt_inference(): 提取圖像特征 :train_table, train_labels: train_table, train_labels = train() :param: :return capt: 驗(yàn)證碼圖片 :return test_labels: 驗(yàn)證碼識別結(jié)果 """ test_labels = [] capt = capt_fetch() capt_per_char_list = capt_process(capt) for capt_per_char in capt_per_char_list: char_features = capt_inference(capt_per_char) label = nnc(train_table, char_features, train_labels) test_labels.append(label) test_labels = "".join(test_labels) return capt, test_labels
訓(xùn)練數(shù)據(jù),識別驗(yàn)證碼
- 方法具備,接下來就是我們實(shí)踐的環(huán)節(jié)了。
- 首先,我們需要建立一個機(jī)器學(xué)習(xí)庫,即一個預(yù)分類的驗(yàn)證碼圖片集。這里我們僅僅獲取了120張驗(yàn)證碼作為訓(xùn)練集,相比tensorflow動輒成千上萬次的迭代,我們建立模型所需的樣本量非常之少。當(dāng)然,這也要感謝我們使用的nnc算法并不需要十分龐大的訓(xùn)練集支持,才使得我們能夠節(jié)省很多預(yù)分類時人工識別驗(yàn)證碼的精力。
- 接下來,我們會調(diào)用train()方法生成訓(xùn)練集和訓(xùn)練集標(biāo)簽,這兩個數(shù)組會被test()方法用到,但我們把這兩個數(shù)組存儲在全局變量里,所以不需要特意傳遞給test()方法。
# 下載120張圖片到本地 for i in range(120): capt_download() # 模型的訓(xùn)練與測試 train_table, train_labels = train() test_capt, test_labels = test()
最后我們調(diào)用test()方法驗(yàn)證我們的理論是否成立,識別效果如下:
獲取到的驗(yàn)證碼
識別結(jié)果
結(jié)語
至此,我們通過機(jī)器視覺識別驗(yàn)證碼的任務(wù)算是完成了。至于正確率,大概每10張驗(yàn)證碼,40各字符中會預(yù)測失誤一個字符。這已經(jīng)比較接近我們?nèi)祟惖淖R別準(zhǔn)確率了,當(dāng)然,我們還可以通過建立起更龐大的學(xué)習(xí)庫,使用knn或更復(fù)雜的模型,使用卷積核處理圖片等方式,使識別準(zhǔn)確率更高。
當(dāng)然,我們這里所用的方法,只適用于識別比較簡單的驗(yàn)證碼。對于更加復(fù)雜的驗(yàn)證碼(如下圖),以上方法是不起作用的,但這并不代表這樣的驗(yàn)證碼不能通過機(jī)器視覺進(jìn)行識別。
我們已經(jīng)看到,隨著機(jī)器視覺的發(fā)展,通過傳統(tǒng)的驗(yàn)證碼來區(qū)分人機(jī)已經(jīng)越來越難了。當(dāng)然,為了網(wǎng)絡(luò)的安全性,我們也可使用更復(fù)雜的驗(yàn)證碼,或者新型的驗(yàn)證方式,比如拖動滑塊、短信驗(yàn)證、掃碼登陸等。
以上所述是小編給大家介紹的python自動識別驗(yàn)證碼詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
opencv 圖像腐蝕和圖像膨脹的實(shí)現(xiàn)
這篇文章主要介紹了opencv 圖像腐蝕和圖像膨脹的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07

玩轉(zhuǎn)python爬蟲之URLError異常處理

python實(shí)現(xiàn)數(shù)據(jù)可視化超詳細(xì)講解

使用Python制作讀單詞視頻的實(shí)現(xiàn)代碼

Python 數(shù)據(jù)可視化之Matplotlib詳解