【PIL+numpy+pytesseract】識別汽車之家驗證碼
阿新 • • 發佈:2018-12-31
實踐目標
對汽車之家的驗證碼文字做識別處理
原圖如下:
降噪處理+去除邊框
噪點的存在會干擾影象文字識別,可以通過我們之前講的識別噪點的演算法來做,因為之家驗證碼的特性,這麼做有些囉嗦了
思路:噪點顏色較淺->將接近白色的RGB轉化成白色->去除邊框(將邊框變為白色)
程式碼實現如下:
def denoise(img, base_line=(200, 200, 200), border=1, zoom=1): """ 去除背景干擾 :param img: Image物件 :param base_line: (R, G, B) :param border: 邊框寬度,單位px :param zoom: 縮放比例,預設不縮放 :return: Image物件 """ img = img.convert('RGB') img = img.resize((zoom*i for i in img.size), Image.NEAREST) pixdata = img.load() for y in range(img.size[1]): for x in range(img.size[0]): if (border-1)<x<(img.size[0]-border) and (border-1)<y<(img.size[1]-border): if pixdata[x, y][0] > base_line[0] and pixdata[x, y][1] > base_line[0] \ and pixdata[x, y][2] > base_line[0]: pixdata[x, y] = 255, 255, 255 else: pixdata[x, y] = 0, 0, 0 else: pixdata[x, y] = 255, 255, 255 return img
去除噪點和邊框後的效果:
這個時候,文字還是不能識別,我們猜測,可能因為字是歪的,我們要把字做正
圖中的文字,有左傾和右傾的,所以我們無法對圖片整體旋轉,這就需要我們把字切開再分別旋轉
切割文字
思路:確定左右邊界->去左右邊->等切(最後一個切片做減法)
確定左右邊界
- 將圖片轉化為二值
- 從左邊迴圈對每列做切片,看最小值為FALSE,說明有黑色的畫素,第一個獲取到黑色畫素的列的X軸座標則為圖片的左邊界
- 從右邊迴圈對每列做切片,看最小值為FALSE,說明有黑色的畫素,第一個獲取到黑色畫素的列的X軸座標則為圖片的右邊界
程式碼實現如下:
def symmetrized_size(img): img = img.convert('1') # np.set_printoptions(threshold=np.nan) # print(np.array(img)) img_array = np.array(img) # print(img_array.shape) left = 0 right = img_array.shape[1]-1 for i in range(img_array.shape[1]): if not img_array[:, i].min(): left = i break for i in range(img_array.shape[1]): if not img_array[:, right-i].min(): right -= i break return left,right
對去除左右空白列後的圖片進行等切
- 獲取每個字的切圖座標(最後一個切片的右側橫座標為去左右白列後的最大橫座標)
- 對每個字的座標切圖
程式碼實現如下:
def slice(img, x=None, y=None, into=4): """ 對影象等比切片 :param img: :param x: 豎切的開始和結束位置(x0, x1) :param y: 橫切的開始和結束位置(y0, y1) :param into: 切成幾份 :return: 切分後圖片的list """ def get_region(obj, into): img_list = [] start = obj[0] total = obj[1] - obj[0] + 1 step = total // into for i in range(into): if i == into - 1: img_list.append([start, obj[1]]) else: img_list.append((start, start + step - 1)) start += step return img_list if x: return [img.crop((i, 0, j, img.size[1])) for i,j in get_region(x, into)] elif y: return [img.crop((0, i, img.size[0], j)) for i,j in get_region(y, into)] else: raise ValueError('x or y is needed.')
切割後效果:
旋轉識別文字
思路:字型的傾斜方向不超過45度->向左旋轉每次1度並識別直到45度->向右旋轉每次1度並識別直到45度,統計並返回識別最大概率的結果,因為汽車之家的驗證碼,字是隨機向兩側旋轉的,如果是單側旋轉,可以把角度入參區分左右
程式碼實現如下:
def recognize(img, angle=30, step=10, psm=8):
"""
旋轉一個字並識別
:param img:
:param angle: 最大旋轉角度
:param step: 每次旋轉增量的步長
:param psm: 預設識別一個字,同config="-psm {}".format(psm)
:return:
"""
def whtie_back(img):
back = Image.new('RGBA', img.size, (255,) * 4)
return Image.composite(img, back, img)
img = img.convert('RGBA')
rec_char_list = []
for i in range(-angle, angle+1, step):
rot_img = img.rotate(i, expand=1)
rot_img = whtie_back(rot_img)
rec_char = pytesseract.image_to_string(rot_img, config="-psm {}".format(psm))
if rec_char:
# return rec_char
print('識別出:{}'.format(rec_char))
rec_char_list.append(rec_char)
# raise ValueError('Can not recognize char.')
return sorted(dict(Counter(rec_char_list)).items(), key=lambda i: i[1], reverse=True)[0][0]
識別結果如下:
提升效率
先寫個簡單的計時器,測試下執行的時間
def timer(f):
@wraps(f)
def wrap(*args, **kwargs):
start_time = time.time()
func = f(*args, **kwargs)
print('耗時{}秒'.format(time.time()-start_time))
return func
return wrap
測試下順序執行的效率
@timer
def transaction():
img = Image.open('suBe.png')
img = denoise(img)
slice_list = slice(img, x=symmetrized_size(img))
result_str = ''
for j in [recognize(i) for i in slice_list]:
result_str += j
return result_str
執行結果:預設引數,相當於識別4*7=28次,耗時19秒
時間太長了,改成多執行緒的方式來做,完整程式碼如下:
__author__ = '老爺'
from PIL import Image
import numpy as np
import pytesseract
from collections import Counter
from functools import wraps
import threading, time
from queue import Queue
def denoise(img, base_line=(200, 200, 200), border=1, zoom=1):
"""
去除背景干擾
:param img: Image物件
:param base_line: (R, G, B)
:param border: 邊框寬度,單位px
:param zoom: 縮放比例,預設不縮放
:return: Image物件
"""
img = img.convert('RGB')
img = img.resize((zoom*i for i in img.size), Image.NEAREST)
pixdata = img.load()
for y in range(img.size[1]):
for x in range(img.size[0]):
if (border-1)<x<(img.size[0]-border) and (border-1)<y<(img.size[1]-border):
if pixdata[x, y][0] > base_line[0] and pixdata[x, y][1] > base_line[0] \
and pixdata[x, y][2] > base_line[0]:
pixdata[x, y] = 255, 255, 255
else:
pixdata[x, y] = 0, 0, 0
else:
pixdata[x, y] = 255, 255, 255
return img
def symmetrized_size(img):
img = img.convert('1')
img_array = np.array(img)
left = 0
right = img_array.shape[1]-1
for i in range(img_array.shape[1]):
if not img_array[:, i].min():
left = i
break
for i in range(img_array.shape[1]):
if not img_array[:, right-i].min():
right -= i
break
return left,right
def slice(img, x=None, y=None, into=4):
"""
對影象等比切片
:param img:
:param x: 豎切的開始和結束位置(x0, x1)
:param y: 橫切的開始和結束位置(y0, y1)
:param into: 切成幾份
:return: 切分後圖片的list
"""
def get_region(obj, into):
img_list = []
start = obj[0]
total = obj[1] - obj[0] + 1
step = total // into
for i in range(into):
if i == into - 1:
img_list.append([start, obj[1]])
else:
img_list.append((start, start + step - 1))
start += step
return img_list
if x:
return [img.crop((i, 0, j, img.size[1])) for i,j in get_region(x, into)]
elif y:
return [img.crop((0, i, img.size[0], j)) for i,j in get_region(y, into)]
else:
raise ValueError('x or y is needed.')
def recognize_list(img_list, angle=30, step=10, psm=8 , tn=None):
"""
識別切割後圖片列表
:param img_list: 切割後圖片列表
:param angle: 轉動夾角
:param step: 轉動步長
:param psm: 識別模式
:return: 圖片識別string
"""
img_list = [img.convert('RGBA') for img in img_list]
queue = Queue()
char_list = []
for n, i in enumerate(img_list):
char_list.append([])
for a in range(-angle, angle+1, step):
queue.put((n, a))
def whtie_back(img):
back = Image.new('RGBA', img.size, (255,) * 4)
return Image.composite(img, back, img)
class Consumer(threading.Thread):
def __init__(self, threadname, queue):
threading.Thread.__init__(self, name=threadname)
self.queue = queue
def run(self):
while True:
n, a = self.queue.get()
rot_img = img_list[n].rotate(a, expand=1)
rot_img = whtie_back(rot_img)
rec_char = pytesseract.image_to_string(rot_img, config="-l eng -psm {}".format(psm))
if rec_char:
char_list[n].append(rec_char)
self.queue.task_done()
if self.queue.qsize() == 0:
break
thread_pool = []
for i in range(tn or queue.qsize()):
thread_pool.append(Consumer('Consumer{}'.format(i), queue))
for t in thread_pool:
t.start()
for t in thread_pool:
t.join()
queue.join()
result_str = ''
for c in [sorted(dict(Counter(cs)).items(), key=lambda i: i[1], reverse=True)[0][0] for cs in char_list]:
result_str += c
return result_str
def timer(f):
@wraps(f)
def wrap(*args, **kwargs):
start_time = time.time()
func = f(*args, **kwargs)
print('識別驗證碼成功,耗時{}秒'.format(time.time()-start_time))
return func
return wrap
@timer
def example():
img = Image.open('suBe.png')
img = denoise(img)
slice_list = slice(img, x=symmetrized_size(img))
return recognize_list(slice_list, )
if __name__ == '__main__':
print('識別結果:'+example())
執行結果:
後記
比最早一個版本識別的準確率提升,step越小,識別越精準,識別時間越長,多執行緒對執行效率有一定的提升,但是提升不明顯,理論上識別28次,順序執行是19秒多,多執行緒執行緒池起了28個執行緒,識別耗時5秒,可見瓶頸在於Tesseract-OCR,查了很多資料,說Tesseract-OCR本身不支援多執行緒,沒有比較實在能解決問題的乾貨,後續看看通過Tesseract-OCR的訓練,能不能增加識別率,就不需要識別這麼多次了,不過估計使用者中心不會提供給我們這樣的介面來做這樣的訓練,人工成本又太高。