Python下实现Tesseract OCR训练字符库(OpenCV-python边缘检测代替jTessBoxEditor手动矫正)

Posted Mirrracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python下实现Tesseract OCR训练字符库(OpenCV-python边缘检测代替jTessBoxEditor手动矫正)相关的知识,希望对你有一定的参考价值。

Python 下实现 Tesseract-OCR 训练字符库
(OpenCV 边缘检测代替 jTessBoxEditor 手动矫正)

作者:殷越

一、概述

本文详细介绍在Python下实现Tesseract-OCR训练字符库的方法。如果数据集较大,使用jTessBoxEditor对字符进行一一矫正工作量巨大,因此本文讲解如何利用opencv-python对字符进行边缘检测并自动获取最小矩形框坐标,最终生成.box文件,从而完全脱离jTessBoxEditor。

二、环境搭建

1. 下载 Tesseract OCR:https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-setup-3.05.01.exe

2. 下载 opencv-python 和 pytesseract(可直接在cmd中下载)

pip install opencv-python
pip install pytesseract

三、生成数据集和训练所需文件

1. 导入需要的包,以及自己写的一个简单的data_agumentation.py

import os
import cv2
import numpy as np
from data_augmentation import *

2. 看一下生成数据集函数的形参和函数内部我预设的参数

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

3. 生成 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))

文件内的格式:< fontname > < italic > < bold > < fixed > < serif > < fraktur >
此处参考:https://blog.csdn.net/qq_30534935/article/details/83794638

  • fontname:字体名称
  • italic:斜体(0/1)
  • bold:粗体(0/1)
  • fixed:默认字体(0/1)
  • serif:衬线字体(0/1)
  • fraktur:德文黑体(0/1)

4. 生成.tif图像和.box文件 (写字符、扩充数据集、检测边缘、获取最小外接矩形框)

这里我们直接生成一张大图,并用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))

我们将整张图看成一个个的小方格,并将每个方格作为一个子图,然后依次在每个子图里进行一系列操作:

  • 先获取每个方格左上角的坐标 top_left
  • 获取子图 subImg 以便直接对每张子图进行操作
  • 计算居中后的字符相对于每个 subImg 的坐标 text_org,转成左下角坐标(因为 putText() 函数中的 org 为字符左下角的坐标。
    下图中红色的 baseline 指的是真正意义上的 baseline,而黄色的 baseline 指的是 opencv 中 getTextSize() 函数获取到的 baseline 这个值。对于英文字母而言,所有大写字母和绝大部分小写字母都在 baseline 之上(如下图的 ‘b’ ),而少数小写字母会延伸到baseline之下(如下图的 ‘g’ )。但 getTextSize() 取到的 height 并不包含 baseline 之下的部分,因此如果要使每个字符都居中,则分别对两类字符进行不同的居中操作即可。但为了使生成的图像和我们平时看到的文字排列相同(以baseline为基准写的字),我们选择以延伸到baseline之下的这类字母为标准来居中,居中后的字符左下角坐标计算过程如下图。

    (图中字符的边框并不是其最小外接矩形,而是通过 getTextSize() 获取到的,并没有紧贴字符,由于此时还没有开始写字符,因此起始坐标只能通过这种方式获得)

  • 计算出可以使字符居中的左下角起始坐标后,使用 putText() 函数对 subImg 进行写字符的操作,org=text_org

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)
  • 由于滤波之后,字符边缘会往外扩散,所以如果直接用 getTextSize() 得到的 w, h, baseline 和 2) 中算到的起始坐标 text_org 来定位并框出字符的话,box的位置必然是不准确的:

  • 且经过实验发现,即使不进行滤波,getTextSize() 得到的 w, h, baseline本身也有一定偏差,框出的框并不是字符的(不旋转的)最小外接矩形:

  • 因此,为了找到字符轮廓并画出最小外接矩形,我们需要先通过图像二值化来凸显字符的轮廓:




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
  • 使用 boundingRect() 函数获取最小外接矩形的左上角坐标 x, y 以及 w, h。
  • 如果获取的 bounding box 长度大于1(即有不止一组轮廓最小外接矩形,如 i 和 j ),则我们需要将其组合。计算过程如下,其中 h 的计算,由于不确定两组坐标的先后,所以选取最大的。(此处只考虑了由两个不连通的图组成的情况,因为英文字母中,一个字母最多也只有两个不连通的图,而数字 0-9 更是不存在这种情况,因此 > 2 的情况暂时不考虑)


    若一个字符由2个以上的不连通的部分组成,则上述方法较为麻烦,因此,我决定不用 boudingRect() 函数。

方法二(寻找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 文件中的坐标是以左下角为原点的,因此需要做一个转换。

  • 首先看一下 .box 文件的格式:



  • 坐标转换计算:



8)将图像存储为.tif

tif = font + '.tif'
cv2.imwrite(filename=os.path.join(path, tif), img=img)
print('%s.tif generated successfully!' % font)

5. 生成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,原因未知

第三部分的完整代码:

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' \\
             

以上是关于Python下实现Tesseract OCR训练字符库(OpenCV-python边缘检测代替jTessBoxEditor手动矫正)的主要内容,如果未能解决你的问题,请参考以下文章

Tesseract-OCR5.0 Lstm傻瓜式训练工具使用教程

Python图像处理之图片文字识别(OCR)

Python图片文字识别——Windows下Tesseract-OCR的安装与使用

Python|基于百度API五行代码实现OCR文字高识别率

Python下Tesseract Ocr引擎及安装介绍

JAVA验证码识别:基于jTessBoxEditorFX和Tesseract-OCR训练样本