注意
本文純粹給未接觸過機器學習的同學看到,高手請繞路
背景
本人接觸了一個項目,需要登錄web系統。使用robotframework + python 做自動化測試,發現開發同學設置了驗證碼,首頁面根本就沒法登錄啊,而且還是變形的,使用基本的python庫識別效果不好。於是本人查詢了多位大俠的網站和文章,發現識別沒有這麼難,只是寫的還不夠白,所以我這次寫的白一點,讓大家看的更清楚一些。
本文只識別與上述這種不太複雜的圖片,太複雜的,特徵向量選取就比較麻煩
另外,
1、本文不介紹理論,直接使用爲主,快速入門爲目標
2、本文未考慮圖像干擾點清理的過程
涉及的技術內容概念
樣本:
對於本文來說,就是大量圖片,和未來待識別的內容完全一致的東西,根據這些圖片的內容,經過學習,生成學習後的model,用於後續的,預測,例如:
驗證集(預測集)
和上述完全一致,是一類東西,上述的用於學習model,輸出model文件後,那這個在用於預測下屬的圖片
最小樣本集:
實際上,機器學習,不是用上面的學習的,是要經過處理的,最終目標是識別某單個字符圖片的字符是什麼內容
觀察上述的樣本,有兩個明顯可以看出來的問題:
例如 一個圖片內容是“3456”,最終需要分解成四張小圖片去訓練
樣本來源(學習集和驗證集)
各顯其能吧,我是找開發要了他們原始的驗證碼和圖片生成代碼
樣本預處理(圖片數據清晰)
對於字符識別處理,基本都採用灰度處理,把字符變成了最終0和255兩個值,其餘的RGB信息全部去除
樣本特徵:
針對這樣的一個圖片,要數字化,才能去調用機器學習算法學習
一般使用向量標識
data = [[1,2,3,4,5,6,7,8,9,0], [1,2,3,4,5,6,7,8,9,0] ,[1,2,3,4,5,6,7,8,9,0] ,[1,2,3,4,5,6,7,8,9,0]]
上面是就是一個list,有四個元素,其中每個元素對應於一個10維空間的一個向量,每個向量描述一個字符
上述向量代表的值,沒對應什麼呢,對應於一一映射的另一個list
label = [3,4,5,6]
data和label 是兩個一一對應的list,分別對應圖片,及圖片的字符是什麼內容
學習模型(算法)
這個就是大家經常聽到的,學習算法是什麼,本文不講理論,只是讓大家快速用起來
所以使用最簡單的隨機森林算法RandomForestClassifier(),並使用其默認參數,而且會發現,即使默認參數,識別率就相當高
學習完成,會生成一個model文件
輸入是data和label
模型預測
使用上述生成model,就可以
整個過程圖
涉及的全部函數(後續再分解說)
公共函數
read_captcha_image(path)
讀取樣本圖像
get_croped_imgs(img)
我用它把圖片無效部分做了初步裁剪
def get_pure_image(image)
把無效的空格部分做進一步處理
image_transfer_to_gray
圖像二值化
get_feature(image, image_width = 27, image_height = 30)
這部分是圖片特數字化,向量化
詳細代碼和說明如下:
from PIL import Image
import os
import numpy as np
def read_captcha_image(path):
"""
1、獲取樣本圖像
2、大量樣本圖片時要用到numpy類型,否則會出現打開太多資源不足的情況
:param path:
:return:
"""
image_array = []
image_label = []
file_list = os.listdir(path) # 獲取captcha文件
for file in file_list:
fp = open(path + '/' + file,'rb')
image = Image.open(fp) # 打開圖片,以句柄方式
image = np.array(image)
fp.close()
image = Image.fromarray(image)
file_name = file.split(".")[0] #獲取文件名,此爲圖片標籤
image_array.append(image)
image_label.append(file_name)
return image_array, image_label
def get_croped_imgs(img):
"""
按照圖片的特點,進行切割,這個要根據具體的驗證碼來進行工作.
另外要把無關的數據切割掉
:param img:單個驗證碼image
:return:字符list,來源於字符中的圖片
"""
# 先截斷左側無效的部分(box目前看是[)這樣的關係)
img = img.crop(box=(12,0, 120, 30))
child_img_list = []
for i in range(4):
x = i * 27
y = 30
child_img = img.crop(box=(x,0, x+27, y))
child_img_list.append(child_img)
return child_img_list
def get_feature(image, image_width = 27, image_height = 30):
"""
1、特徵就是數字化
2、圖片字符特徵有很多種方法數字化,例如黑點佔全部等比例,某個區域的黑點佔全部的比例等等
本函數的實現方式是,參考了部分網上的部分實現,以及擴充了部分實現
以每行的黑色點數量,每列的黑色點數量,每圈的點數量(本例12圈),作爲向量
所以向量維數是(x+y+12)
:param image (單個圖像)
:return:feature (單個特徵)
"""
# 裁剪空白
image = get_pure_image(image)
# 標準化圖像格式
image = image.resize((image_width, image_height))
#再二值化
image_list = []
image_list.append(image)
image_list = image_transfer_to_gray(image_list, 100)# resize之後還需要二值化
#取回第一個圖片
image = image_list[0]
###################################################
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)
# 計算圈特徵
for index in range(12):
feature_circle = 0
# 上,下兩條邊
for y in [index, image_height - index - 1]:
for x in range(index, image_width - index):
if image.getpixel((x, y)) == 0:
feature_circle += 1
# # 左,右兩條邊
for x in [index, image_width - index - 1]:
for y in range(index, image_height - index):
if image.getpixel((x, y)) == 0:
feature_circle += 1
feature.append(feature_circle)
return feature
def get_pure_image(image):
"""
純粹的裁剪圖片無效的部分
把單個字符的圖片的無效空白處儘可能出掉,確保字符的上下左右都頂到邊緣
這樣能提升一定的準確率
:param image:
:return:
"""
dict_coordinate = {'x1': 0,'x2': image.size[0]-1, 'y1': 0,'y2': image.size[1]-1}
for y in range(0,image.size[1]):
for x in range(0,image.size[0]):
pix = image.getpixel((x, y))
if pix == 0: #等於0位黑色
dict_coordinate['y1'] = y
break
if dict_coordinate['y1'] != 0:
break
#######################################################################
for y in range(image.size[1]-1,0,-1 ):
for x in range (0,image.size[0]):
pix = image.getpixel((x, y))
if pix == 0:
dict_coordinate['y2'] = y + 1
break
if dict_coordinate['y2'] != image.size[1]-1:
break
###############################################################################
for x in range(0,image.size[0]): # 遍歷所有像素,將灰度超過閾值的像素轉變爲255(白)
for y in range(0,image.size[1]):
pix = image.getpixel((x, y))
if pix == 0:
dict_coordinate['x1'] = x
break
if dict_coordinate['x1'] != 0:
break
###############################################################################
for x in range(image.size[0]-1,0,-1 ):
for y in range(0,image.size[1]):
pix = image.getpixel((x, y))
if pix == 0:
dict_coordinate['x2'] = x + 1
break
if dict_coordinate['x2'] != image.size[0]-1:
break
image = image.crop(box=(dict_coordinate['x1'],dict_coordinate['y1'],dict_coordinate['x2'],dict_coordinate['y2']))
return image
def image_transfer_to_gray(image_arry, threshold_grey = 100):
"""
二值化圖片:
1、因爲圖片的顏色不重要,所以把圖片RGB,變成了一維的數字,由0到255標識
2、再經過變化,把圖片變成單一的0和255兩個值
:param image_arry: 圖像list,每個元素爲一副圖像
:return: image_arry: 二值化後的圖像list
"""
image_gray = []
for i, image in enumerate(image_arry):
image = image.convert('L') # 轉換爲灰度圖像,即RGB 3 維 變爲 1維
im_sub = 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: # 灰度閾值 默認100
im_sub.putpixel((x, y), 255)
else:
im_sub.putpixel((x, y), 0)
image_gray.append(im_sub)
return image_gray
學習模型主程序
"""
訓練
輸入:圖片
輸出:字符
"""
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.externals import joblib
from content_indentify.indentify_common import *
def trainModel(data, label,model_path):
"""
參數格式如下:圖片向量list和label list
data = [[0,0,0],[1,1,1],[2,2,2],[1,1,1],[2,2,2],[3,3,3],[1,1,1],[4,4,4]]
label = [0,1,2,1,2,3,1,4]
:param data:
:param label:
:return:
"""
print("開始模型擬合 >>>>>>>>>>>>>>>>>>>>>>")
model_learn = RandomForestClassifier()
scores = cross_val_score(model_learn, data, label, cv=10) # 交叉檢驗,計算模型平均準確率
print("model_learn: ", scores.mean())
model_learn.fit(data, label) # 擬合模型
joblib.dump(model_learn, model_path) # 模型保存到本地,以後預測就用這個了
print("模型保存完畢,恭喜!")
return model_learn
def data_generate(src_path, dir_path):
# 獲取目錄下的列表,每個元素,帶有圖片對象list和名字list
data = [] #圖片向量化
label = [] #label
#獲取樣本圖像
images_info = read_captcha_image(src_path)
images = list(images_info[0])
image_names = images_info[1]
#分解成四個單獨小圖片
for index1, image in enumerate (images):
image_list_cut = get_croped_imgs(image) # 把左側空白裁減掉
for index2 , image in enumerate(image_list_cut):
temp = []
temp.append(image)
# 第一步灰階處理
image_temp_list = image_transfer_to_gray(temp)
# 第二步,取特徵值,數字化
data_to_predict = get_feature(image_temp_list[0])
data.append(data_to_predict)
label.append(image_names[index1][index2])
return data, label
#待學習的樣本的位置
sample = r"C:\Users\user\Pictures\sample"
#圖像樣本數據化
data, label = data_generate(sample,r"C:\Users\user\Pictures\sample_data")
#生成模型並保存,待以後使用
trainModel(data,label,"g:/model")
識別驗證碼主程序
這個前期的讀取圖片,處理圖片,和模型學習完全一致
只是後面,使用已學習的model來預測
from sklearn.externals import joblib
from content_indentify.indentify_common import *
def test_captcha_sample(path):
# 獲取目錄下的列表,每個元素,帶有圖片對象list和名字list
images_info = read_captcha_image(path)
total_number = float(len(images_info[1]))
sucess_number = 0.0
images = list(images_info[0])
#分解成四個單獨小圖片
model = joblib.load("g:/model")
for index1, image in enumerate (images):
image_list_cut = get_croped_imgs(image) # 只處理文件夾裏的第一張圖片
data_set =[]
for index2 , image in enumerate(image_list_cut):
temp = []
temp.append(image)
# 第一步灰階處理
image_temp_list = image_transfer_to_gray(temp)
# 第二步,取特徵值
data_to_predict = get_feature(image_temp_list[0])
data_set.append(data_to_predict)
#使用模型進行預測,返回預測值
target_list = model.predict(data_set)
target = "".join(target_list)
if target == images_info[1][index1][0:4]:
sucess_number += 1.0
print("success_rate = ",sucess_number/total_number)
path = r"C:\Users\user\Pictures\驗證集"
test_captcha_sample(path)
最終效果
G:\pics 有三種圖片
執行之後,返回驗證碼實際數據如下
['4aQ3', 'Bd4X', 'CT4m'] 即使是默認也很高,在這種字符變形的情況下,識別率在99.8%
如果大家想練手的話,我提供一個集合給大家使用:
樣本集和驗證集文件路徑
https://download.csdn.net/download/weixin_41357300/12117690