KNN算法實戰:驗證碼的識別

識別驗證碼的方式很多,如tesseract、SVM等。今天主要學習的是如何使用KNN進行驗證碼的識別。

數據準備

本次實驗採用的是CSDN的驗證碼做演練

目前接口返回的驗證碼共2種:

KNN算法實戰:驗證碼的識別

純數字、干擾小的驗證碼,簡單進行圖片去除背景、二值化和閾值處理後,使用kNN算法即可識別。

KNN算法實戰:驗證碼的識別

字母加數字、背景有干擾、圖形字符位置有輕微變形,進行圖片去除背景、二值化和閾值處理後,使用kNN算法識別

這裡選擇第二種進行破解。由於兩種驗證碼的圖片大小不一樣,所以可以使用圖片大小來判斷哪個是第一種驗證碼,哪個是第二種驗證碼。

下載驗證碼

 import requests
import uuid
from PIL import Image
import os
url = "http://download.csdn.net/index.php/rest
/tools/validcode/source_ip_validate/10.5711163911089325"
for i in range(1000):
resp = requests.get(url)
filename = "./captchas/" + str(uuid.uuid4()) + ".png"
with open(filename, 'wb') as f:
for chunk in resp.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
f.flush()
f.close()
im = Image.open(filename)
if im.size != (70, 25):
im.close()
os.remove(filename)
else:
print(filename)

分割字符

下載過後,就需要對字母進行分割。分割字符還是一件比較麻煩的工作。

灰度化

將彩色的圖片轉化為灰度圖片,便於後面的二值化處理,示例代碼:

 from PIL import Image

file = ".\\\\captchas\\\\0a4a22cd-f16b-4ae4-bc52-cdf4c081301d.png"
im = Image.open(file)
im_gray = im.convert('L')
im_gray.show()

處理前:

KNN算法實戰:驗證碼的識別

處理後:

KNN算法實戰:驗證碼的識別

二值化

灰度化以後,有顏色的像素點為0-255之間的值。二值化就是將大於某個值的像素點都修改為255,小於該值的修改為0,示例代碼:

 from PIL import Image
import numpy as np
file = ".\\\\captchas\\\\0a4a22cd-f16b-4ae4-bc52-cdf4c081301d.png"
im = Image.open(file)
im_gray = im.convert('L')
# im_gray.show()

pix = np.array(im_gray)
print(pix.shape)
print(pix)

threshold = 100 #閾值

pix = (pix > threshold) * 255
print(pix)

out = Image.fromarray(pix)
out.show()

二值化輸出的結果:

KNN算法實戰:驗證碼的識別

去除邊框

從二值化輸出的結果可以看到除了字符,還存在邊框,在切割字符前還需要先將邊框去除。

border_width = 1

new_pix = pix[border_width:-border_width,border_width:-border_width

字符切割

由於字符與字符間沒有存在連接,可以使用比較簡單的“投影法”進行字符的切割。原理就是將二值化後的圖片先在垂直方向進行投影,根據投影后的極值來判斷分割邊界。分割後的小圖片再在水平方向進行投影。

KNN算法實戰:驗證碼的識別

代碼實現:

 def vertical_image(image):
height, width = image.shape
h = [0] * width
for x in range(width):
for y in range(height):
s = image[y, x]
if s == 255:
h[x] += 1
new_image = np.zeros(image.shape, np.uint8)
for x in range(width):
cv2.line(new_image, (x, 0), (x, h[x]), 255, 1)
cv2.imshow('vert_image', new_image)
cv2.waitKey()
cv2.destroyAllWindows()

整體代碼

 from PIL import Image
import cv2
import numpy as np
import os
import uuid


def clean_bg(filename):
im = Image.open(filename)
im_gray = im.convert('L')
image = np.array(im_gray)
threshold = 100 # 閾值
pix = (image > threshold) * 255
border_width = 1
new_image = pix[border_width:-border_width, border_width:-border_width]
return new_image


def get_col_rect(image):
height, width = image.shape
h = [0] * width
for x in range(width):
for y in range(height):
s = image[y, x]
if s == 0:
h[x] += 1
col_rect = []
in_line = False
start_line = 0
blank_distance = 1
for i in range(len(h)):
if not in_line and h[i] >= blank_distance:

in_line = True
start_line = i
elif in_line and h[i] < blank_distance:
rect = (start_line, i)
col_rect.append(rect)
in_line = False
start_line = 0
return col_rect


def get_row_rect(image):
height, width = image.shape
h = [0] * height
for y in range(height):
for x in range(width):
s = image[y, x]
if s == 0:
h[y] += 1
in_line = False
start_line = 0
blank_distance = 1
row_rect = (0, 0)
for i in range(len(h)):
if not in_line and h[i] >= blank_distance:
in_line = True
start_line = i
elif in_line and i == len(h)-1:
row_rect = (start_line, i)
elif in_line and h[i] < blank_distance:
row_rect = (start_line, i)
break
return row_rect


def get_block_image(image, col_rect):
col_image = image[0:image.shape[0], col_rect[0]:col_rect[1]]
row_rect = get_row_rect(col_image)
if row_rect[1] != 0:
block_image = image[row_rect[0]:row_rect[1], col_rect[0]:col_rect[1]]
else:
block_image = None
return block_image


def clean_bg(filename):
im = Image.open(filename)
im_gray = im.convert('L')
image = np.array(im_gray)
threshold = 100 # 閾值
pix = (image > threshold) * 255

border_width = 2
new_image = pix[border_width:-border_width, border_width:-border_width]
return new_image

def split(filename):
image = clean_bg(filename)
col_rect = get_col_rect(image)
for cols in col_rect:
block_image = get_block_image(image, cols)
if block_image is not None:
new_image_filename = 'letters/' + str(uuid.uuid4()) + '.png'
cv2.imwrite(new_image_filename, block_image)


if __name__ == '__main__':
for filename in os.listdir('captchas'):
current_file = 'captchas/' + filename
split(current_file)
print('split file:%s' % current_file)

數據集準備

在完成圖像切割後,需要做將切分的字母建立由標籤的樣本。即將切分後的字符梳理到正確的分類中。比較常見的方式是人工梳理。

由於圖像比較多,這裡使用使用Tesseract-OCR進行識別。

官方項目地址: https://github.com/tesseract-ocr/tesseract

Windows安裝包地址: https://github.com/UB-Mannheim/tesseract/wiki

Tesseract-OCR的安裝

下載完安裝包後,直接運行安裝即可,比較重要的是環境變量的設置。

將安裝目錄(D:\\Program Files (x86)\\Tesseract-OCR)添加進PATH

新建TESSDATA_PREFIX系統變量,值為tessdata 文件夾的路徑(D:\\Program Files (x86)\\Tesseract-OCR\\tessdata)

安裝Python包pytesseract(pip install pytesseract)

Tesseract-OCR的使用

使用起來非常的簡單,代碼如下:

 from PIL import Image
import pytesseract
import os


def copy_to_dir(filename):
image = Image.open(filename)
code = pytesseract.image_to_string(image, config="-c tessedit"
"_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
" --psm 10"
" -l osd"
" ")
if not os.path.exists("dataset/" + code):
os.mkdir("dataset/" + code)
image.save("dataset/" + code + filename.replace("letters", ""))
image.close()


if __name__ == "__main__":
for filename in os.listdir('letters'):
current_file = 'letters/' + filename
copy_to_dir(current_file)
print(current_file)

由於Tesseract-OCR識別的準確率非常的低,完全不能使用,放棄~,還是需要手工整理。

圖片尺寸統一

在完成人工處理後,發現切割後的圖片大小不一。在字符識別前需要對圖片進行的尺寸進行統一。

具體實現方法:

 import cv2

def image_resize(filename):
img = cv2.imread(filename, cv2.IMREAD_GRAYSCALE) #讀取圖片時採用單通道
print(img)
if img.shape[0] != 10 or img.shape[1] != 6:
img = cv2.resize(img, (6, 10), interpolation=cv2.INTER_CUBIC)
print(img)
cv2.imwrite(filename, img)

使用cv2.resize時,參數輸入是 寬×高×通道,這裡使用的時單通道的,interpolation的選項有:

INTER_NEAREST 最近鄰插值

INTER_LINEAR 雙線性插值(默認設置)

INTER_AREA 使用像素區域關係進行重採樣。 它可能是圖像抽取的首選方法,因為它會產生無雲紋理的結果。 但是當圖像縮放時,它類似於INTER_NEAREST方法。

INTER_CUBIC 4×4像素鄰域的雙三次插值

INTER_LANCZOS4 8×8像素鄰域的Lanczos插值

另外為了讓數據更加便於利用,可以將圖片再進行二值化的歸一。具體代碼如下:

 import cv2
import numpy as np

def image_normalize(filename):
img = cv2.imread(filename, cv2.IMREAD_GRAYSCALE) #讀取圖片時採用單通道

if img.shape[0] != 10 or img.shape[1] != 6:
img = cv2.resize(img, (6, 10), interpolation=cv2.INTER_CUBIC)
normalized_img = np.zeros((6, 10)) # 歸一化
normalized_img = cv2.normalize(img, normalized_img, 0, 1, cv2.NORM_MINMAX)
cv2.imwrite(filename, normalized_img)

歸一化的類型,可以有以下的取值:

NORM_MINMAX:數組的數值被平移或縮放到一個指定的範圍,線性歸一化,一般較常用。

NORM_INF:此類型的定義沒有查到,根據OpenCV 1的對應項,可能是歸一化數組的C-範數(絕對值的最大值)

NORM_L1 : 歸一化數組的L1-範數(絕對值的和)

NORM_L2: 歸一化數組的(歐幾里德)L2-範數

字符識別

字符圖片 寬6個像素,高10個像素 ,理論上可以最簡單粗暴地可以定義出60個特徵:60個像素點上面的像素值。但是顯然這樣高維度必然會造成過大的計算量,可以適當的降維。比如:

每行上黑色像素的個數,可以得到10個特徵

每列上黑色像素的個數,可以得到6個特徵

 from sklearn.neighbors import KNeighborsClassifier
import os
from sklearn import preprocessing

import cv2
import numpy as np
import warnings
warnings.filterwarnings(module='sklearn*', action='ignore', category=DeprecationWarning)


def get_feature(file_name):
img = cv2.imread(file_name, cv2.IMREAD_GRAYSCALE) # 讀取圖片時採用單通道
height, width = img.shape

pixel_cnt_list = []
for y in range(height):
pix_cnt_x = 0
for x in range(width):
if img[y, x] == 0: # 黑色點
pix_cnt_x += 1

pixel_cnt_list.append(pix_cnt_x)

for x in range(width):
pix_cnt_y = 0
for y in range(height):
if img[y, x] == 0: # 黑色點
pix_cnt_y += 1

pixel_cnt_list.append(pix_cnt_y)

return pixel_cnt_list


if __name__ == "__main__":
test = get_feature("dataset/K/04a0844c-12f2-4344-9b78-ac1d28d746c0.png")
category = []
features = []
for dir_name in os.listdir('dataset'):
for filename in os.listdir('dataset/' + dir_name):
category.append(dir_name)
current_file = 'dataset/' + dir_name + '/' + filename
feature = get_feature(current_file)
features.append(feature)
# print(current_file)
le = preprocessing.LabelEncoder()
label = le.fit_transform(category)

model = KNeighborsClassifier(n_neighbors=1)
model.fit(features, label)
predicted= model.predict(np.array(test).reshape(1, -1))
print(predicted)
print(le.inverse_transform(predicted))

這裡直接使用了sklearn中的KNN方法


分享到:


相關文章: