最近想重构我之前写的明日方舟公招计算器的代码,其中一个原因就是之前的方案识别速度太慢了,因为用的百度的 API,图片上传到服务器,提取出 tag,还要再上传到百度识别再返回,这个过程需要几秒的时间,如果遇到网络不好的情况就更慢了,所以我决定还是使用 Tesseract 在本地进行识别。

但是在本地识别还是有两个问题,一是明日方舟游戏内有全屏随机飘散的粒子特效,截图时难免会截到,这会对 OCR 造成干扰。其次是在使用 Tesseract 官方提供的 chi_sim.tessdata 字库的情况下,如果使用 legacy 引擎,速度依然很慢,个人体感和上传到百度的速度差不了多少;如果使用新的 LSTM 引擎,对 tag 的识别结果就是几乎不可用的状态。

图片处理

对于第一个问题,就要对图片进行一些处理,提取出文字部分,去除其它的干扰部分。之前我只对图片做了二值化处理,用百度的 API 识别的话基本是够用了,但是如果要用 Tesseract 在本地识别就有问题了。

首先,要识别的图片是这个样子:

1610526333498349 image-20210114191012223

对这些图片直接进行 OCR 是没有问题的。

但是还有一些是这个样子:

image-20210114191552913 image-20210114191619038

要想办法去除这些点,首先依然是对图片进行二值化处理。

img = cv2.imread(file_name)  # 读取图片
img = cv2.resize(img, (320, 100)) # 强制缩放图片的大小,方便后面处理
grayimg = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # # 转为灰度图
_, bwimg = cv2.threshold(grayimg, 127, 255, cv2.THRESH_BINARY)  # 二值化

cv2.threshold() 这个函数的四个参数分别为灰度图、二值化的阈值、高于或低于阈值后设置的新值、进行二值化的方法。

这个函数有两个返回值,第一个是阈值,这里用不到就直接忽略了,第二个就是二值化之后的图像。

show

现在要去掉图片上多余的点,首先想到的办法是腐蚀操作。

kernel = np.ones((5, 5), np.uint8) # 用于腐蚀的内核,大小为 5x5
erosion = cv2.erode(bwimg1, kernel, iterations=1)

erode() 的三个参数分别为被处理的图像、用于腐蚀的内核和迭代次数。

image-20210114193814648

但是出来的结果并不好,由于点的面积过大,无论怎么调整参数,都无法在保证文字完整的情况下腐蚀掉点。

那就换别的方法。

先找出图中的所有白色色块,并计算面积,因为点的面积一般比较小,所以设置一个阈值,将面积小于阈值的点都填充掉。

contours, _ = cv2.findContours(bwing, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 寻找轮廓

show

这里为了看起来清楚一点我把轮廓绘制在了原图上。

然后筛选出面积过小的区域。

noise = []

for contour in contours:
    area = cv2.contourArea(contour)
    if area <= 40:
        noise.append(contour)

image-20210114195147933

可以看到需要删除的点已经被选中了,然后将其填充为黑色。

cv2.fillPoly(bwimg1, noise, 0)

image-20210114195440020

效果很好,看起来问题已经完美解决了。

但是在使用的过程中,很快问题又出现了。由于有些字的笔画中包含一些面积较小的点,同样会被脚本识别为噪点被处理掉,导致文字不完整的情况出现。阈值设置小了清除不掉噪点,设置大了会误伤文字,那么这个方法也不能用了。

image-20210114195753453

继续寻找其它方法。

思考了一会,我突然想到,虽然没法准确地找到噪点,但是可以找到文字的位置啊!

图片上的文字虽然每个之间都是分开的,但是距离非常近,那么就可以先对图片进行一次膨胀操作。

kernel = np.ones((3, 11), np.uint8)
dilation = cv2.dilate(bwimg1, kernel, iterations=1)  # 膨胀

这样就可以让文字连接为一个整体了。因为文字只有一行,所以设置内核为 3x11 让它更多地横向膨胀。

image-20210114204324165

现在只要找到面积最大的区域,就是文字的位置了。

contours, _ = cv2.findContours(dilation, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)  # 寻找轮廓
text_area = max(contours, key=lambda x: cv2.contourArea(x))

再生成这个轮廓的最小外接矩形,框选出文字。

rect = cv2.minAreaRect(text_area)
box = cv2.boxPoints(rect)
box = np.int0(box)

image-20210114201852088

这样就完美地选出了文字的区域。

最后将这个区域外的部分填充为黑色

stencil = np.zeros(bwimg.shape).astype(bwimg.dtype)
color = [255, 255, 255]
cv2.fillPoly(stencil, [box], color)

result = cv2.bitwise_and(bwimg, stencil)

image-20210114202140440 image-20210114204656579

就得到了一个完美的,没有噪点只有文字的图片。

现在使用 Tesseract 官方的 chi_sim.tessdata 字库对其进行 OCR,已经可以 100% 准确识别了(至少我这里的 700+ 张图片没有出现错误)。

训练字库

因为我这个使用场景 OCR 的内容非常固定,所以针对其训练一个专用的字库能极大地提升识别速度。

我从一百多张游戏截图中提取了 700+ 张图片用于字库的训练,具体的训练方法参考了下面这些内容:

最后训练出的字库文件大小仅有 300 多 KB,相比 chi_sim.tessdata 的 40 多 MB 可以说几乎忽略不计了。而速度提升也是相当明显,在我的 MacBook Pro 对一份有 778 页的 tif 文件,分别采用两个字库进行识别,其它参数不变。chi_sim.tessdata 耗时一分半,而我训练出的字库不到 2 秒,可以说提升巨大。

If you think my article is useful to you, please feel free to appreciate