开发语言:python
建议系统:ubuntu
OCR中,把每个字分割出来是最难的,因为分隔出来后就可以进行单个图片的识别(现在识别器太多了, 整体来说单个图片的识别工作难度不大,现在又很多的库可以用。)。
假设我们已经有个文本段,没有什么杂音,譬如平时的电脑截图。
为了方便,我们继续以这个图片为例子:
需要一些初始化代码
1 2 3 4 5 6 7 8 9 10 11 |
import os import cv2 import numpy as np import matplotlib.pyplot as plt base_dir = "/root/workspace/deep_ocr" path_test_image = os.path.join(base_dir, "test_data.png") image_color = cv2.imread(path_test_image) new_shape = (image_color.shape[1] * 2, image_color.shape[0] * 2) image_color = cv2.resize(image_color, new_shape) image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY) |
图片数据读到image_color和灰度image里面,为了方便查看,这里我把图片放大两倍了。
首先, 我们开始对图片进行二值化处理:
1 2 3 4 5 6 7 |
adaptive_threshold = cv2.adaptiveThreshold( image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\ cv2.THRESH_BINARY_INV, 11, 2) cv2.imshow('binary image', adaptive_threshold) cv2.waitKey(0) |
可以看到,二值化的效果还是非常理想的,没有什么噪点。
现在的目标就是提取每行,然后分割每个字符。
1 2 3 4 5 |
horizontal_sum = np.sum(adaptive_threshold, axis=1) plt.plot(horizontal_sum, range(horizontal_sum.shape[0])) plt.gca().invert_yaxis() plt.show() |
上面的代码是把图像往水平方向的求和,然后画出结果。可以见到文本行被明显分隔出来。注意,这里我把y轴反了,因为图像的坐标0点在左上角。
接着需要提取数组里面的峰值,然后找出文本行,所以这里定义提取峰值函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
def extract_peek_ranges_from_array(array_vals, minimun_val=10, minimun_range=2): start_i = None end_i = None peek_ranges = [] for i, val in enumerate(array_vals): if val > minimun_val and start_i is None: start_i = i elif val > minimun_val and start_i is not None: pass elif val < minimun_val and start_i is not None: end_i = i if end_i - start_i >= minimun_range: peek_ranges.append((start_i, end_i)) start_i = None end_i = None elif val < minimun_val and start_i is None: pass else: raise ValueError("cannot parse this case...") return peek_ranges |
有些图片比较多噪音,需要minimun_val和minimun_range用于过滤噪音。用这个函数提取峰值:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
peek_ranges = extract_peek_ranges_from_array(horizontal_sum) line_seg_adaptive_threshold = np.copy(adaptive_threshold) for i, peek_range in enumerate(peek_ranges): x = 0 y = peek_range[0] w = line_seg_adaptive_threshold.shape[1] h = peek_range[1] - y pt1 = (x, y) pt2 = (x + w, y + h) cv2.rectangle(line_seg_adaptive_threshold, pt1, pt2, 255) cv2.imshow('line image', line_seg_adaptive_threshold) cv2.waitKey(0) |
四行文字都被提取出来,然后可以对中文字分割。相类似, 使用垂直方向的求和把字符也可以找出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
vertical_peek_ranges2d = [] for peek_range in peek_ranges: start_y = peek_range[0] end_y = peek_range[1] line_img = adaptive_threshold[start_y:end_y, :] vertical_sum = np.sum(line_img, axis=0) vertical_peek_ranges = extract_peek_ranges_from_array( vertical_sum, minimun_val=40, minimun_range=1) vertical_peek_ranges2d.append(vertical_peek_ranges) ## Draw color = (0, 0, 255) for i, peek_range in enumerate(peek_ranges): for vertical_range in vertical_peek_ranges2d[i]: x = vertical_range[0] y = peek_range[0] w = vertical_range[1] - x h = peek_range[1] - y pt1 = (x, y) pt2 = (x + w, y + h) cv2.rectangle(image_color, pt1, pt2, color) cv2.imshow('char image', image_color) cv2.waitKey(0) |
最后我们可以获得这样的一个大致的文字分割图,是不是很好呢 :)
在没有文字信息情况下,还有是有一些细节问题可以解决,譬如有些包围盒连续在一起,譬如第三行最后“本上就”连在一起。
这个利用每行宽度的中位值进行切割来解决:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def median_split_ranges(peek_ranges): new_peek_ranges = [] widthes = [] for peek_range in peek_ranges: w = peek_range[1] - peek_range[0] + 1 widthes.append(w) widthes = np.asarray(widthes) median_w = np.median(widthes) for i, peek_range in enumerate(peek_ranges): num_char = int(round(widthes[i]/median_w, 0)) if num_char > 1: char_w = float(widthes[i] / num_char) for i in range(num_char): start_point = peek_range[0] + int(i * char_w) end_point = peek_range[0] + int((i + 1) * char_w) new_peek_ranges.append((start_point, end_point)) else: new_peek_ranges.append(peek_range) return new_peek_ranges |
上面的函数是利用中位置切割连在一起的包围盒。我们再分割一次,然后加上切割太长的包围盒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
vertical_peek_ranges2d = [] for peek_range in peek_ranges: start_y = peek_range[0] end_y = peek_range[1] line_img = adaptive_threshold[start_y:end_y, :] vertical_sum = np.sum(line_img, axis=0) vertical_peek_ranges = extract_peek_ranges_from_array( vertical_sum, minimun_val=40, minimun_range=1) vertical_peek_ranges = median_split_ranges(vertical_peek_ranges) vertical_peek_ranges2d.append(vertical_peek_ranges) ## Draw color = (0, 0, 255) for i, peek_range in enumerate(peek_ranges): for vertical_range in vertical_peek_ranges2d[i]: x = vertical_range[0] y = peek_range[0] w = vertical_range[1] - x h = peek_range[1] - y pt1 = (x, y) pt2 = (x + w, y + h) cv2.rectangle(image_color, pt1, pt2, color) cv2.imshow('splited char image', image_color) cv2.waitKey(0) |
下图是最终的结果,“本上就”被切割开了。
最后还有一个过分割的问题需要解决,但是这个需要识别器的配合才能解决。
简单来利用识别器识别包围盒的内容,假如包围盒的宽度比较小和属于数字,字幕,标点符号等,即不需要合并,否则如果是中文需要找附近的包围盒合并再次识别(譬如“比”字被切开两份)。
但是识别器超出这篇文章的内容。暂时不介绍,但是会在后续的文章继续介绍。好好继续玩玩图像识别 : )
源代码可以在这里下载
太棒了!感谢无私分享!期待更多好教程,谢谢!
博主您好,我目前正在做身份证识别这块。因身份证上的文字,因光照污迹扭曲等原因导致二值化不够理想,进而导致分割出问题。请问博主有在不进行分割的前提下进行识别吗?
暂时没有找到,我有空再找找,谢谢你的反馈。
请问,如果图片的文字是倾斜的,该怎么分割呢?
倾斜不粘在一起还好,还是可以靠空隙分割,最怕粘在一起,我写过一篇文章如何分割验证码。
楼主,用你的方法队繁体字切分不太准确,有什么办法优化吗
“最后还有一个过分割的问题需要解决,但是这个需要识别器的配合才能解决。” 这个是什么办法