1. 程式人生 > >python 基於機器學習識別驗證碼

python 基於機器學習識別驗證碼

1、背景
    驗證碼自動識別在模擬登陸上使用的較為廣泛,一直有耳聞好多人在使用機器學習來識別驗證碼,最近因為剛好接觸這方面的知識,所以特定研究了一番。發現網上已有很多基於machine learning的驗證碼識別,本文主要參考幾位大牛的研究成果,集合自己的需求,進行改進、學習。

 

2、基本工具   
開發環境:

    python 3.5 + pycharm

模組:

    Pillow、sklearn、numpy及其他子模組

 

3、基本流程
描述整個識別流程:

①驗證碼清理並生成訓練集樣本

②驗證碼特徵提取

③擬合識別模型

④識別模型測試

 

4、關於資料集
        沒有特意網上找python的生成指令碼,用了一個java的驗證碼生成指令碼。驗證碼是數字+大寫字母+小寫字母的組合,即[0-9]+[A-Z]+[a-z]。檔名是驗證碼的正確數字標籤,例項如下

 

使用三個資料集:

①訓練集(training set):10000張驗證碼

②測試集(test set):100張驗證碼

③驗證集(validation set):100張驗證碼

 

5、驗證碼清理並生成訓練集樣本
(1)讀取圖片

    首先讀取該檔案路徑下的所有圖片檔名稱,並逐張開啟。返回結果image_array,每一個元素型別為“<class 'PIL.JpegImagePlugin.JpegImageFile'>”。

def read_captcha(path):
image_array = []
image_label = []
file_list = os.listdir(path) # 獲取captcha檔案
for file in file_list:
image = Image.open(path + '/' + file) # 開啟圖片
file_name = file.split(".")[0] #獲取檔名,此為圖片標籤
image_array.append(image)
image_label.append(file_name)
return image_array, image_label
(2)影象粗清理

    影象粗清理包括以下步驟:

step 1:原始影象是RGB影象,即維度為 (26, 80, 3)。將其轉換為灰度影象,維度變為(26, 80)。

原始影象:

 

 灰度影象:

 

step 2:對於將要識別的驗證碼,顯然,裡面出現了很多用於干擾作用的灰色線條。博主通過設定灰度閾值(預設100),對影象中大於閾值的畫素,賦值為255(灰度影象中畫素值範圍是0~255,其中255是白色,0是黑色)。發現對於此型別的驗證碼,這種方法很實用有木有。

 

def image_transfer(image_arry):
"""
:param image_arry:影象list,每個元素為一副影象
:return: image_clean:清理過後的影象list
"""
image_clean = []
for i, image in enumerate(image_arry):
image = image.convert('L') # 轉換為灰度影象,即RGB通道從3變為1
im2 = Image.new("L", image.size, 255)
for y in range(image.size[1]): # 遍歷所有畫素,將灰度超過閾值的畫素轉變為255(白)
for x in range(image.size[0]):
pix = image.getpixel((x, y))
if int(pix) > threshold_grey: # 灰度閾值
im2.putpixel((x, y), 255)
else:
im2.putpixel((x, y), pix)
image_clean.append(im2)
return image_clean

 


(3)影象細清理

    僅僅通過粗清理的辦法,無法完全去除所有噪聲點。此處引入了更細粒度的清理方法,參考這位大牛的清理方法。

主要有3大步驟:

    step 1:找出影象中所有的孤立點;

    step 2:計算黑色點近鄰9宮格中黑色點個數,若小於等於2個,那麼認為該點為噪聲點;

    step 3:去除所有噪聲點。

    經過細清理後,雖然可以看到還存在一個噪聲點,但效果其實很不錯了。

 

(4)單字元影象切割

    去除孤立點後,我們還是沒法一下子就識別出這四個字元,需要對經過處理後的圖片進行切分。(其實可以使用deep learning的方法進行識別,但本文僅介紹基於machine learning的識別方法)

    切割方式主要有一下步驟:

    step 1:找出圖片中所有分離影象的開始結束位置。遍歷width&height,當每出現一個黑色點,記為該字元開始位置;當新的一列出現全白色點,那麼記為結束位置。

[(8, 9), (14, 22), (29, 38), (42, 50), (57, 66)]

    step 2:儘管經過清理後,還是可能存在噪聲點。在找到所有切割開始結束位置後,計算並選出(結束值-開始值)最大的切割位置。

[(14, 22), (29, 38), (42, 50), (57, 66)]
切割後檢視如下:

 

 

 

 

code:

def image_split(image):
"""
:param image:單幅影象
:return:單幅影象被切割後的影象list
"""
inletter = False #找出每個字母開始位置
foundletter = False #找出每個字母結束位置
start = 0
end = 0
letters = []    #儲存座標
for x in range(image.size[0]):
for y in range(image.size[1]):
pix = image.getpixel((x, y))
if pix != True:
inletter = True
if foundletter == False and inletter == True:
foundletter = True
start = x
if foundletter == True and inletter == False:
foundletter = False
end = x
letters.append((start, end))
inletter = False

# 因為切割出來的影象有可能是噪聲點
# 篩選可能切割出來的噪聲點,只保留開始結束位置差值最大的位置資訊
subtract_array = []    # 儲存 結束-開始 值
for each in letters:
subtract_array.append(each[1]-each[0])
reSet = sorted(subtract_array, key=lambda x:x, reverse=True)[0:image_character_num]
letter_chioce = []    # 儲存 最終選擇的點座標
for each in letters:
if int(each[1] - each[0]) in reSet:
letter_chioce.append(each)

image_split_array = []    #儲存切割後的影象
for letter in letter_chioce:
im_split = image.crop((letter[0], 0, letter[1], image.size[1])) # (切割的起始橫座標,起始縱座標,切割的寬度,切割的高度)
im_split = im_split.resize((image_width, image_height)) # 轉換格式
image_split_array.append(im_split)

     return image_split_array[0:int(image_character_num)]

 


(5)儲存到訓練集

        將按上述方法切分後的單個數字、字母,儲存到新建的資料夾裡,專門用來作為模型的訓練集。

 

 

6、特徵提取
    特徵提取是針對每一個切割出後的單個字元,如6。此處構建特徵的方法較為簡單,統計每個字元影象每一行畫素值為黑色的總和(灰度值為0),加上每一列畫素值為黑色的總和。因為我們切割後的影象大小為8*26(width*height),故特徵個數為34=8+26。當然此處其實可以把單字元影象按畫素值展開為一個208=8*26的向量,以此作為特徵向量,也是可以的。示例結果如下所示:

feature vector: [7, 11, 13, 4, 4, 13, 11, 7, 0, 0, 0, 0, 0, 4, 6, 4, 6, 6, 6, 6, 6, 6, 6, 4, 6, 4, 0, 0, 0, 0, 0, 0, 0, 0]
    聰明的你可能會發現了一個致命的問題,如果使用新型別/不同畫素大小的驗證碼來做處理和特徵提取,那程式不就報錯了?我們已經在切割的步驟後面加上畫素大小的轉換:

im_split = im_split.resize((image_width, image_height)) # 轉換格式,im_split為切割後的影象,image_width為目標畫素寬度,iamge_height為目標畫素高度
    當然,在讀取影象的時候就轉換格式也是可以的~

code:

def feature_transfer(image):
"""
:param image (影象list)
:return:feature (特徵list)
"""
image = image.resize((image_width, image_height)) #標準化影象格式

feature = []#計算特徵
for x in range(image_width):#計算行特徵
feature_width = 0
for y in range(image_height):
if image.getpixel((x, y)) == 0:
feature_width += 1
feature.append(feature_width)

for y in range(image_height): #計算列特徵
feature_height = 0
for x in range(image_width):
if image.getpixel((x, y)) == 0:
feature_height += 1
feature.append(feature_height)
# print('feature length :',len(feature))
print("feature vector:",feature)
return feature

 

7、訓練識別模型
    關於訓練識別模型,使用全量學習的方式,訓練集用於擬合模型,測試集用於測試模型效果。本部落格對比了SVC、random forest。

    關於SVC的引數,使用了不同kernel(線性核、高斯核),以及在一定範圍內修改了正則項C,但測試效果不十分理想。對於正則項C,位於SVM模型的目標函式位置,當C越大時,模型對誤分類的懲罰增大,反之,減少。

 

    關於隨機森林的引數,調整了樹的深度、每個節點分支需要的最少樣本數,儘量簡化了每棵樹的結構。效果較SVC好。

    得到的結果如下

 

model parameters training accuracy test accuracy
SVC(linear) C=1.0 0.9125 0.65
SVC(rbf) C=1.0 0.9055 0.55
Random Foest max_depth=10, min_sample_split=10 0.9420 0.75
code:

def trainModel(data, label):
print("fit model >>>>>>>>>>>>>>>>>>>>>>")

    # svc_rbf = svm.SVC(decision_function_shape='ovo',kernel='rbf')    # rbf核svc
# svc_linear = svm.SVC(decision_function_shape='ovo',kernel='linear')    #linear核svc
rf = RandomForestClassifier(n_estimators=100, max_depth=10,min_samples_split=10, random_state=0)    #隨機森林
scores = cross_val_score(rf, data, label,cv=10)    #交叉檢驗,計算模型平均準確率
print("rf: ",scores.mean())
rf.fit(data, label)    # 擬合模型

joblib.dump(rf, model_path) # 模型持久化,儲存到本地
print("model save success!")

return rf

 

關於資料量問題:

    當訓練樣本非常大,如上千萬/億的時候,若使用傳統machine learning的全量學習方法,需要消耗大量內容,對個人使用者並不友好,此時可以引入增量學習。類似於訓練neural network,每次訓練只使用一個batch的小資料集,經過多次迭代,可達到非常robust的效果。

    scikit-learn中支援SGD、Naive Bayes等分類模型的增量學習。通過迭代的方式,每次生成小資料集batch,使用partial_fit()方法訓練模型。

    scikit-learn 0.19.1 支援如下模型的增量學習

 

8、模型測試效果
    得到的結果如下

 

model parameters training accuracy test accuracy
SVC(linear) C=1.0 0.9125 0.65
SVC(rbf) C=1.0 0.9055 0.55
Random Foest max_depth=10, min_sample_split=10 0.9420 0.75

9、識別預測流程
    經過上述步驟,我們已經訓練好了一個具有一定識別驗證碼能力的模型,為了能讓模型自動化實現輸入驗證碼檔案,輸出驗證碼識別結果,流程如下:

①讀取將要識別的驗證碼檔案

②驗證碼粗清理。將灰度值小於閾值的畫素值轉化為255。

③驗證碼細清理。找出所有孤立的噪聲點,並將該畫素值轉化為255。

④字元切割。找出所有字元的開始結束位置,並切割出4幅影象。

⑤影象特徵提取。對於4幅影象中的每一幅,分別從行、列統計其灰度值為0(黑色)的和,構建4個特徵向量。

⑥識別。讀取訓練好的模型,分別對4個特徵向量進行識別,得到4個預測結果。

⑦輸出。將識別出的4個字元結果,串起來,並輸出到結果檔案。

code:

#-*- coding:utf-8 -*

import os
from captcha_test.captcha_soc import image_process, image_feature, image_model, image_training
from sklearn.externals import joblib
from captcha_test.captcha_soc.config import *


#驗證碼資料清洗
def clean():
#驗證碼清理
image_array, image_label = image_process.read_captcha(test_data_path) #讀取待測試驗證碼檔案
print("待測試的驗證碼數量:", len(image_array))
image_clean = image_process.image_transfer(image_array) #轉換成灰度影象,並去除背景噪聲
image_array = [] #[[im_1_1,im_1_2,im_1_3,im_1_4],[im_2_1,im_2_2,im_2_3,im_2_4],...]
for each_image in image_clean:
image_out = image_process.get_clear_bin_image(each_image) #轉換為二值圖片,並去除剩餘噪聲點
split_result = image_process.image_split(image_out) #切割圖片
image_array.append(split_result)
return image_array, image_label


#特徵矩陣生成
def featrue_generate(image_array):
feature = []
for num, image in enumerate(image_array):
feature_each_image = []
for im_meta in image:
fea_vector = image_feature.feature_transfer(im_meta)
# print('label: ',image_label[num])
# print(feature)
feature_each_image.append(fea_vector)
# print(fea_vector)
# print(len(feature_each_image))
if len(feature_each_image) == 0:
feature_each_image = [[0]*(image_width+image_height)]*int(image_character_num)
# print(feature_each_image)
feature.append(feature_each_image)
print("預測資料的長度:", len(feature))
print("預測資料特徵示例:", feature[0])
return feature


#將結果寫到檔案
def write_to_file(predict_list):
file_list = os.listdir(test_data_path)
with open(output_path, 'w') as f:
for num, line in enumerate(predict_list):
if num == 0:
f.write("file_name\tresult\n")
f.write(file_list[num] + '\t' + line + '\n')
print("結果輸出到檔案:", output_path)

def main():
#驗證碼清理
image_array, image_label = clean()
#特徵處理
feature = featrue_generate(image_array)
#預測
predict_list = []
acc = 0
model = joblib.load(model_path)    #讀取模型
# print("預測錯誤的例子:")
for num, line in enumerate(feature):
# print(line)
predict_array = model.predict(line)
predict = ''.join(predict_array)
predict_list.append(predict)
if predict == image_label[num]:
acc += 1
else:
pass
print("-----------------------")
print("actual:",image_label[num])
print("predict:", predict)
print("測試集預測acc:", acc/len(image_label))
#輸出到檔案
write_to_file(predict_list)

if __name__ == '__main__':
main()

 

10、總結
    關於上述機器學習的驗證碼識別,只是作了一個簡單例子的過程演示。僅僅是針對某種特定型別的驗證碼,若換成其他型別的驗證碼做測試,不能保證識別的準確率。

    這就是傳統機器學習的不足:需要人工做資料清理和提煉特徵。有個辦法可以解決這種繁瑣的資料清理,以及人工提取驗證碼特徵的缺點,那就是深度學習的方法。

    博主使用深度學習的迴圈神經網路,訓練了一個識別模型,具體請跳轉到這裡。


11、相關部落格&文獻
https://www.cnblogs.com/TTyb/p/6156395.html?from=timeline&isappinstalled=0

https://www.cnblogs.com/beer/p/5672678.html

 

完整程式碼及資料集:github.com/wzzzd/captcha_ml


---------------------
作者:Neleuska
來源:CSDN
原文:https://blog.csdn.net/Neleuska/article/details/80040304
版權宣告:本文為博主原創文章,轉載請附上博文連結!