【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的训练,能不能增加识别率,就不需要识别这么多次了,不过估计用户中心不会提供给我们这样的接口来做这样的训练,人工成本又太高。





  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值