实践目标
对汽车之家的验证码文字做识别处理
原图如下:
降噪处理+去除边框
噪点的存在会干扰图像文字识别,可以通过我们之前讲的识别噪点的算法来做,因为之家验证码的特性,这么做有些啰嗦了
思路:噪点颜色较浅->将接近白色的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的训练,能不能增加识别率,就不需要识别这么多次了,不过估计用户中心不会提供给我们这样的接口来做这样的训练,人工成本又太高。