本文详细介绍在Python下实现Tesseract-OCR训练字符库的方法。如果数据集较大,使用jTessBoxEditor对字符进行一一矫正工作量巨大,因此本文讲解如何利用opencv-python对字符进行边缘检测并自动获取最小矩形框坐标,最终生成.box文件,从而完全脱离jTessBoxEditor。
pip install opencv-python
pip install pytesseract
import os
import cv2
import numpy as np
from data_augmentation import *
def dataset_producing(path, text_list, lang, fontname, exp_num=0, italic=0, bold=0, fixed=0, serif=0, fraktur=0):
# 一、预设参数
space = 80 # 一个字符所占区域大小
row = 64 # 数据增强后排列组合的总数
img_size = (row * space, len(text_list) * space) # 画布大小(h, w)
img = np.zeros(img_size, np.uint8) # (h, w)
img.fill(255) # 填充白色背景
x_box = 0 # 左上角坐标 x
y_box = 0 # 左上角坐标 y
with open(os.path.join(path, 'font_properties.txt'), 'w') as fp:
fp.write('%s %d %d %d %d %d'
% (fontname, italic, bold, fixed, serif, fraktur))
文件内的格式:< fontname > < italic > < bold > < fixed > < serif > < fraktur >
此处参考:https://blog.csdn.net/qq_30534935/article/details/83794638
这里我们直接生成一张大图,并用opencv在图中依次写字符并进行增强,最后存储为.tif。当然如果是识别手写字符的话,可以掠过下述代码的1和2,直接从3开始。
下面我对每一个步骤做一个详细的解释(由于是生成一张很大的图像,有多行字符,所以这一部分代码在for循环中,以下先对for循环中的代码片段进行逐个的解释,这一块的完整代码在这一部分的最后)
1) 获取字体大小和基准线
text_size = cv2.getTextSize(text=text_list[j], fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=get_scale(i) / 22, thickness=get_thickness(i))
w, h, baseline = text_size[0][0], text_size[0][1], text_size[1]
因为是一行一行地写字符(第 i 行,第 j 列),所以先在写下字符之前通过 getTextSize() 获取到当前正准备写的字符的 w, h, baseline,以供后面计算每个字符在每个小格子里居中时的起始位置。
getTextSize()中各参数的定义可参考这篇博客:https://blog.csdn.net/u010970514/article/details/84075776
我简单手画了一个图,方便理解 getTextSize() 获取的 baseline 的含义(但真正意义上的baseline指的是第二条红线):
2) 写字符
top_left = (x_box, y_box) # 方框左上角绝对坐标
subImg = img[top_left[1]:top_left[1] + space, top_left[0]:top_left[0] + space] # 获取方框子图
text_org = (int((space - w) / 2), int((space - baseline + h) / 2)) # 字符转成左下角相对坐标并居中
cv2.putText(img=subImg, text=text_list[j], org=text_org, fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=get_scale(i) / 22, color=(0, 0, 0), thickness=get_thickness(i))
我们将整张图看成一个个的小方格,并将每个方格作为一个子图,然后依次在每个子图里进行一系列操作:
3)均值滤波
ksize = (get_ksize(i))
cv2.blur(src=subImg, ksize=ksize, dst=subImg)
使用 blur() 函数对每张 subImg 进行均值滤波(当然也可以选择其它类型的滤波,并排列组合)。ksize 是核的大小,我在另一个文件 data_augmentation.py 中写了一个简陋的 get_ksize() 函数,根据是第几行来选取不同的 ksize,这里大家可以自己设计。
4)图像二值化
ret, binary = cv2.threshold(src=subImg, thresh=254, maxval=255, type=cv2.THRESH_BINARY_INV)
5)寻找字符的边缘轮廓
contours, hierarchy = cv2.findContours(image=binary, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
binary 是 4) 中得到的二值化图像,**返回值 contours 是 n 组轮廓坐标( n 指该字符由 n 个连通的部分组成,对于大多数英文字母,n=1,对于 i 和 j,n=2)
6)寻找字符的最小外接矩形
方法一(使用 boundingRect() 函数来找到最小外接矩形)(不推荐):
bounding_boxes = [cv2.boundingRect(cnt) for cnt in contours] # x, y 为矩形左上角坐标
if len(bounding_boxes) > 1: # 组合两个不连通的图
x0, y0, w0, h0 = bounding_boxes[0][0], bounding_boxes[0][1], bounding_boxes[0][2], bounding_boxes[0][3]
x1, y1, w1, h1 = bounding_boxes[1][0], bounding_boxes[1][1], bounding_boxes[1][2], bounding_boxes[1][3]
bounding_boxes = [(min(x0, x1), min(y0, y1), max(w0, w1), max(y1 - y0 + h1, y0 - y1 + h0))]
xmin, ymin, w, h = bounding_boxes[0][0], bounding_boxes[0][1], bounding_boxes[0][2], bounding_boxes[0][3]
xmax, ymax = xmin + w, ymin + h
方法二(寻找xmin, xmax, ymin, ymax)(推荐使用):
pts_x = [] # 存储所有的轮廓横坐标
pts_y = [] # 存储所有的轮廓纵坐标
for part in contours:
for pts in range(len(part)):
pts_x.append(part[pts][0][0])
pts_y.append(part[pts][0][1])
xmin, ymin, xmax, ymax = min(pts_x), min(pts_y), max(pts_x), max(pts_y) # 找到最小外接矩形
该方法直接遍历在一个subImg中找到的所有轮廓坐标,并找出横纵坐标分别的最小和最大值,即可确定最小外接矩形。
7) 生成.box文件
xmin_new, xmax_new = x_box + xmin, x_box + xmax # 将子图坐标转成相对于原图的坐标
ymin_new, ymax_new = img_size[0] - y_box - ymax, img_size[0] - y_box - ymin # 转换成以左下角为原点的坐标系的坐标 (.box文件中以左下角为原点)
fp.write('%s %d %d %d %d %d'
% (text_list[j], xmin_new, ymin_new, xmax_new, ymax_new, 0)) # 将(字符、x、y-h、x+w、y、页码)写入txt文件
fp.write('\n')
这个步骤主要在做一些坐标转换。由于我们之前的操作都是以左上角为原点的,而 .box 文件中的坐标是以左下角为原点的,因此需要做一个转换。
8)将图像存储为.tif
tif = font + '.tif'
cv2.imwrite(filename=os.path.join(path, tif), img=img)
print('%s.tif generated successfully!' % font)
这一部分的问题还未解决,问题写在最后两行的注释里了,如果有人知道原因和解决方法,希望可以与我交流。
train_bat = 'echo Run Tesseract for Training.. \r' \
'tesseract.exe %s.tif %s nobatch box.train \r\n' \
'echo Compute the Character Set.. \r' \
'unicharset_extractor.exe %s.box \r' \
'mftraining -F font_properties.txt -U unicharset -O %s.unicharset %s.tr \r\n' \
'echo Clustering.. \r' \
'cntraining.exe %s.tr \r\n' \
'echo Rename Files.. \r' \
'rename normproto %s.normproto \r' \
'rename inttemp %s.inttemp \r' \
'rename pffmtable %s.pffmtable \r' \
'rename shapetable %s.shapetable \r\n' \
'echo Create Tessdata.. \r' \
'combine_tessdata.exe %s. \r\n' \
'echo. & pause' \
% (font, font, font, lang, font, font, lang, lang, lang, lang, lang)
with open(os.path.join(path, 'train.bat'), 'w') as fp:
fp.write(train_bat)
print('train.bat generated successfully!')
# 上述生成的.bat文件无法直接执行、具体原因尚不明确,需要将文件以编辑的形式打开、复制下来,粘贴到新建的txt文件中,再更改后缀为.bat
# 已经过多次排查,只有上述方法可以得到可以执行的批处理文件,即使文件内容由复制粘贴得来完全一样,文件大小相差14kb,原因未知
第三部分的完整代码:
import os
import cv2
import numpy as np
from data_augmentation import *
def dataset_producing(path, text_list, lang, fontname, exp_num=0, italic=0, bold=0, fixed=0, serif=0, fraktur=0):
# 一、预设参数
space = 80 # 一个字符所占区域大小
row = 64 # 数据增强后排列组合的总数
img_size = (row * space, len(text_list) * space) # 画布大小(h, w)
img = np.zeros(img_size, np.uint8) # (h, w)
img.fill(255) # 填充白色背景
x_box = 0 # 左上角x
y_box = 0 # 左上角y
# 二、生成font_properties.txt文件
with open(os.path.join(path, 'font_properties.txt'), 'w') as fp:
fp.write('%s %d %d %d %d %d' % (fontname, italic, bold, fixed, serif, fraktur))
print('font_properties.txt generated successfully!')
# 三、生成.tif图像和.box文件 (写字符、检测边缘、获取最小外接矩形框)
font = '%s.%s.exp%d' % (lang, fontname, exp_num)
with open(os.path.join(path, '%s.box' % font), 'w') as fp:
for i in range(int(img_size[0] / space)): # 写第i行
for j in range(int(img_size[1] / space)): # 写第j列
# 1、获取字体大小和基准线
text_size = cv2.getTextSize(text=text_list[j], fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=get_scale(i) / 22, thickness=get_thickness(i))
w, h, baseline = text_size[0][0], text_size[0][1], text_size[1]
# 2、写字符
top_left = (x_box, y_box) # 方框左上角绝对坐标
subImg = img[top_left[1]:top_left[1] + space, top_left[0]:top_left[0] + space] # 获取方框子图
text_org = (int((space - w) / 2), int((space - baseline + h) / 2)) # 字符转成左下角相对坐标并居中
cv2.putText(img=subImg, text=text_list[j], org=text_org, fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=get_scale(i) / 22, color=(0, 0, 0), thickness=get_thickness(i))
# 3、均值滤波 (对子图进行滤波操作时需要加上dst,否则不对原图产生改变)
ksize = (get_ksize(i))
cv2.blur(src=subImg, ksize=ksize, dst=subImg)
# 4、图像二值化
ret, binary = cv2.threshold(src=subImg, thresh=254, maxval=255, type=cv2.THRESH_BINARY_INV)
# 5、寻找字符边缘轮廓
contours, hierarchy = cv2.findContours(image=binary, mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
# 6、寻找字符最小外接矩形
# # 方法一:
# bounding_boxes = [cv2.boundingRect(cnt) for cnt in contours] # x, y 为矩形左上角坐标
# if len(bounding_boxes) > 1: # 组合两个不连通的图
# x0, y0, w0, h0 = bounding_boxes[0][0], bounding_boxes[0][1], bounding_boxes[0][2], bounding_boxes[0][3]
# x1, y1, w1, h1 = bounding_boxes[1][0], bounding_boxes[1][1], bounding_boxes[1][2], bounding_boxes[1][3]
# bounding_boxes = [(min(x0, x1), min(y0, y1), max(w0, w1), max(y1 - y0 + h1, y0 - y1 + h0))]
# xmin, ymin, w, h = bounding_boxes[0][0], bounding_boxes[0][1], bounding_boxes[0][2], bounding_boxes[0][3]
# xmax, ymax = xmin + w, ymin + h
# 方法二:
pts_x = [] # 存储所有的轮廓横坐标
pts_y = [] # 存储所有的轮廓纵坐标
for part in contours:
for pts in range(len(part)):
pts_x.append(part[pts][0][0])
pts_y.append(part[pts][0][1])
xmin, ymin, xmax, ymax = min(pts_x), min(pts_y), max(pts_x), max(pts_y) # 找到最小外接矩形
# # 画出最小外接矩形 (测试时可用)
# cv2.rectangle(img=subImg, pt1=(xmin, ymin), pt2=(xmax, ymax), color=(0, 255, 0), thickness=1)
# 7、生成box文件
xmin_new, xmax_new = x_box + xmin, x_box + xmax # 将子图坐标转成相对于原图的坐标
ymin_new, ymax_new = img_size[0] - y_box - ymax, img_size[0] - y_box - ymin # 转换成以左下角为原点的坐标系的坐标 (.box文件中以左下角为原点)
fp.write('%s %d %d %d %d %d'
% (text_list[j], xmin_new, ymin_new, xmax_new, ymax_new, 0)) # 将(字符、x、y-h、x+w、y、页码)写入txt文件
fp.write('\n')
x_box += space # 写下一列
x_box = 0 # x返回第一列
y_box += space # 写下一行
print('%s.box generated successfully!' % font)
# # 画网格(测试居中时使用)
# y = 0
# for i in range(int(img_size[0] / space)): # 画横线
# cv2.line(img=img, pt1=(0, y), pt2=(img_size[1], y), color=(0, 0, 0), thickness=1) # 画网格
# y += space
# x = 0
# for i in range(int(img_size[1] / space)): # 画竖线
# cv2.line(img=img, pt1=(x, 0), pt2=(x, img_size[0]), color=(0, 0, 0), thickness=1) # 画网格
# x += space
# 8、将图像存储为.tif以供训练
tif = font + '.tif'
cv2.imwrite(filename=os.path.join(path, tif), img=img)
print('%s.tif generated successfully!' % font)
# 四、生成train.bat批处理文件
train_bat = 'echo Run Tesseract for Training.. \r' \
'tesseract.exe %s.tif %s nobatch box.train \r\n' \
'echo Compute the Character Set.. \r' \
'unicharset_extractor.exe %s.box \r' \
'mftraining -F font_properties.txt -U unicharset -O %s.unicharset %s.tr \r\n' \
'echo Clustering.. \r' \
'cntraining.exe %s.tr \r\n' \
'echo Rename Files.. \r' \
'rename normproto %s.normproto \r' \
'rename inttemp %s.inttemp \r' \
'rename pffmtable %s.pffmtable \r' \
'rename shapetable %s.shapetable \r\n' \
'echo Create Tessdata.. \r' \
'combine_tessdata.exe %s. \r\n' \
'echo. & pause' \
% (font, font, font, lang, font, font, lang, lang, lang, lang, lang)
with open(os.path.join(path, 'train.bat'), 'w') as fp:
fp.write(train_bat)
print('train.bat generated successfully!')
# 上述生成的.bat文件无法直接执行、具体原因尚不明确,需要将文件以编辑的形式打开、复制下来,粘贴到新建的txt文件中,再更改后缀为.bat
# 已经过多次排查,只有上述方法可以得到可以执行的批处理文件,即使文件内容由复制粘贴得来完全一样,文件大小相差14kb,原因未知
画上网格和最小外接矩形后生成的图像大概是这样的(生成用于训练的图像时请把网格和矩形框去掉):
去掉网格线和最小外接矩形框,重新生成图像后,为了验证 .box 文件的正确性,也可在jTessBoxEditor 中打开该 tif 图像进行查看:
这就是我前面提到的写的一个简单的 data_agmentation.py,为了快速实现走通流程,我还没有使用很多的图像增强方式,之后会完善。目前里面暂时只包含了字体缩放、字体粗细、均值滤波。其中,字体缩放和字体粗细直接传到 opencv 的 putText() 函数的 fontScale 和 thickness 里,均值滤波则是 blur() 函数。因为写得比较简陋,而且大家可以根据自己的想法去设置这些函数,然后排列组合,所以我就不暂时展示我写的了。
接着第三步的生成了 train.bat 往后讲(注意最后两行我写的注释,需要重新新建 txt 文件,复制并粘贴上生成的 bat 文件中的代码,再将文件后缀改为 .bat,点开就执行了)。执行完之后,找到 .traineddata 文件,把它放入 Tesseract-OCR 文件夹下的 tessdata 中即可。
把待测的图像放入一个空文件下(如我代码中的 testing_pics 文件夹),即会遍历文件夹下的文件进行逐个识别。language 的值就是第五步中生成的 .traineddata 文件的名字。
import os
import pytesseract
from PIL import Image
# 将待测图像放入项目文件下的testing_pics文件下,进行依次测试
pics_path = 'testing_pics'
language = 'num'
def test(path, lang=None):
for img in os.listdir(pics_path):
name = img
img = Image.open(os.path.join(pics_path, img))
text = pytesseract.image_to_string(image=img, lang=lang)
print('testing result for', name, ':')
print(text)
if __name__ == '__main__':
test(path=pics_path, lang=language)
以上就是整个生成数据集、训练、测试的流程了,整个流程花了将近两天时间做完并完善到目前这一步。博客写了整整一天,写博客真的非常不容易,转载请注明出处,欢迎交流讨论。
——殷越(2021/12/19)