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
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!