1. 程式人生 > >【PIL+numpy+pytesseract】識別汽車之家驗證碼

【PIL+numpy+pytesseract】識別汽車之家驗證碼

實踐目標

對汽車之家的驗證碼文字做識別處理

原圖如下:

降噪處理+去除邊框

噪點的存在會干擾影象文字識別,可以通過我們之前講的識別噪點的演算法來做,因為之家驗證碼的特性,這麼做有些囉嗦了

思路:噪點顏色較淺->將接近白色的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

去除噪點和邊框後的效果:


這個時候,文字還是不能識別,我們猜測,可能因為字是歪的,我們要把字做正

圖中的文字,有左傾和右傾的,所以我們無法對圖片整體旋轉,這就需要我們把字切開再分別旋轉

切割文字

思路:確定左右邊界->去左右邊->等切(最後一個切片做減法)

確定左右邊界

  1. 將圖片轉化為二值
  2. 從左邊迴圈對每列做切片,看最小值為FALSE,說明有黑色的畫素,第一個獲取到黑色畫素的列的X軸座標則為圖片的左邊界
  3. 從右邊迴圈對每列做切片,看最小值為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

對去除左右空白列後的圖片進行等切

  1. 獲取每個字的切圖座標(最後一個切片的右側橫座標為去左右白列後的最大橫座標)
  2. 對每個字的座標切圖

程式碼實現如下:

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的訓練,能不能增加識別率,就不需要識別這麼多次了,不過估計使用者中心不會提供給我們這樣的介面來做這樣的訓練,人工成本又太高。