OCR框架

  • 区块。图像中一块含有字符内容的区域。内容表示姓名、邮箱、手机号,等等。
  • 字段。字段是app内定义,描述图像中的区块表示什么含义。举个例子,图像是一张名片,字段就是名片中的一些属性,像姓名、邮箱、手机号。

实现的OCR框架用于非时实场景,关键技术基于OpenCV和Tensorflow。分三个步骤 ,一是定位区块(OpenCV),二是用户调整区块并绑定字段(GUI),三是识别字符(Tensorflow)。


第一步:定位区块

对应代码:(github.com/freeors/aism)中的locate_lines(...)函数。

使用OpenCV扫描图像,尽可能准确地提取内中区块。

locate_lines流程图

下面会一步一步参照上面的流程图,给出每个步骤的中间临时图片。要生成这些图片,可以把tocr::verbos_设置为true去开启调试模式。

要识别图像

1.1 灰度化。MSER提取图像中最大稳定极值区域,记为regions

为让cv::MSER得到更好效果,须先执行灰度化图像。给的示例图片背景是彩色,但操作是在灰度化后得出的。

白色、红色框都是cv::MSER搜出的区域,红色是要考虑区域(conside_region),怎么来的见1.2.1。

1.2.1 regions中,绘制出以区域的(w,h)为x、图像中该(w,h)出现的数量为y的直方图。汇集0.618*max_y的那些区域,记为conside_region

图中y值超过红线的就是此次要考虑的conside_region

1.2.2 以conside_region中的区域为基础,找出行的强种子

找强种子是在conside_region集合,找出y上认为是同一行的。

找出的矩形肯定是conside_region子集

1.2.3 从强种子扩展出行

扩展一行分三步,第一步校准,第二步向右扩展,第三步向左扩展。

深入扩展前先说下强种子。图中可看到强种子大致定位出了一些“字符”矩形,但最后不会用这些矩形。为什么不用?查找强种子靠的只是矩形判断,没有内容判断(不做内容判断是为了减少cpu消耗),定位出的矩形除了不能包含整个字符,更麻烦是可能包含多个字符(它们的各一部分被含在一个矩形),后者会让拆分变得非常困难。那强种子有什么用?1)大致确定行的位置。2)计算校准时基于的矩形,该矩形的x是第一个种子的x,y、w、h则是行中所有强种子的平圴值。

校准指的是基于第一个强种子进行“多次”定位(定位是怎么个操作见底下注1),找出第一个字符可能出现在的x、y,以及w、h。只有校准成功后才会执行扩展,同时校准出的字符会被扔掉,后面扩展出的才是第一个字符。

校准、扩展都是在grow_line执行,一次grow_line或是执行一次右扩展,或是一次左扩展。

grow_line流程

注1:字符一般要经过两次定位,第一次是粗定位,第二次是精准定位。粗定位是根据maybe_current_x算出裁剪矩形,然后向右或向左定位字符矩形。当向右粗定位时,字符矩形的或y、或y+h、或x+w等于裁剪矩形的相应值时,认为算出的字符矩形可能是不够的,须扩大裁剪矩形进行精准定位。精准定位指的是以粗定位搜出的矩形(称核心矩形)为中心向外扩展,精准定位出的矩形一定包含核心矩形。

locate_character执行粗/精准定位,定位步骤可归纳为:二值化、擦除孤立像素、取轮廓(分割x方向)、闭操作、取轮廓(分割y方向)。这些步骤详细述叙参考EasyPR--开发详解(4)形态学操作、尺寸验证、旋转等操作 - 计算机的潜意识 - 博客园

黑框(左下)标注的“8419326”中的2的各个过程

取轮廓(分割x方向)会产生个严重问题:左右结构的汉字被分割,像“ 强”。

粗定位和精准定位的第一个区别是二值化时使用的门限值。粗定位使用OTSU算法自动算门限,精准定位则固定使用之前粗定位算出的门限值。为什么这么做看以下这图。

图中第一列是原图像,第二列是cv::threshold后的二值图像,第三列是使用的门限值,白色表示OTSU自动计算,红色则使用固定值。第一行可认为是粗定位,由于算出的字符矩形y等于裁剪矩形y,于是这个向左的扩展会在左、上、下三个方向扩大裁剪矩形。第二列是对扩大的裁剪矩形使用OTSU,由于多出更多趋向于白色的像素,OTSU为让生成的图像中均衡黑、白像素数量,于是拉大门限值到195。门限值到195后,核心矩形部分出现更多黑像素,字符变得难分割了。第三行显示强制使用粗定位算出的门限值。

精准定位要使用之前粗定位门限值的直观原因,是裁剪矩形扩大后多出的像素中可能出现大比例失衡的背景、字体像素。当扩展到一行的头、尾,以及上、下行间隔较大时,多出的部分会出现很多背景像素。

粗定位和精准定位的第二个区别是怎么分割出轮廓。粗定位时,x方向一侧向内压、另一侧向外扩,y方向是两侧向内压。精准定位则是x、y方向都从中心(核心矩形)向外扩。为什么精准定位必须使用中心向外?grow_line知道字符会超过核心矩形,但扩展这个矩形使用的是预估,一旦预估出的裁剪矩形过大,极可能把上/下、左/右属于其它字符的包含进来。

除了较准,最多只进行一次精准定位。

注3。line.chars存在三种内容的char。-1:空白。[0, 255]:字符。256:空格。空格指在那位置是字间的空格符。空白则不认为是空格,它内部是有内容的,但这个内容不是有效字符,比如是图像。

locate_character算出一个字符矩形后,如何判断这矩形中内容是否是有效字符?——通过第一步MSER算出的regions。如果这矩形在regions集合中则认为是字符。这里要注意,可能要regions的多个区域才能拼出一个字符,像”清“这个字,regions中可能是存在两个区域,第一个是左侧的三点水,第二个是右侧的“青”。

关于line.chars几个结论。1)不可能全是空格。2)可能没有字符。即全是-1或256。3)如何计算区域矩形?x、w:所以char都有宽度,累加它们。y、h:有字符时,取所有字符的平均值。没有字符时,取所有-1的平均值。

注4:locate_character会去判断裁剪矩形中的内容是否是竖线,是竖线会立即结束此次扩展。

注5:精准定位时,locate_character调用enclosing_rect_from_kernel_rect,用于寻找含有核心矩形的最小矩形。定位过程要进行x、y两次分割,这函数会被调用两次。

enclosing_rect_from_kernel_rect

蓝框是核心矩形,为算出红色虚线框希望的结果,函数在垂直方向上分三个部分,图中数字表示了被处理顺序,即先中部,然后顶部、最后底部,三个部分组成一次循环。处理中部时,x向左、w向右,直到该列全是背景像素。在顶部,只取最靠近y的那一行,即y-1,从左到右(x-1到x+w)依次判断是否有字符像素,如果有就y--,并回到循环出发点开始新一轮循环,即处理中部。中部、顶部都没有可扩展时,处理底部。处理底部类似顶部,只取最靠近y+h-1的那一行,即y+h,从左到右(x-1到x+w)依次判断是否有字符像素,如果有就h++,并回到循环出发点开始新一轮循环。直到三个部分都没有再可扩展时,处理结束。


扩展后的各行。白框是各行占据的矩形

1.2.4 在regions,移除和此次定位出的行有交叉的区域

经过上面步骤,对regions的一轮处理结束了,回到1.2.1继续第二轮,直到该轮已找不到有强种子的行。


第二步:用户调整区块并绑定字段

调整区块并绑定字段窗口

将弹出上面窗口,用户在这窗口执行调整区块和绑定字段。整过程都是gui操作,不涉及到算法。

调整区块。第一步定位出的区块可能存在偏差,用户通过调整区块矩形从而调整该区块最后要包含哪些字符。

绑定字段。图中左侧是app提供的字段,拖拽到右侧某个区块让实现绑定。在该区块识别出的字符就属于该字段了。

如何提高这一步效率?1)调整区块。很显然,第一步的定位算法越智能,用户需要介入的操作就越少。2)绑定字段。可以引入个叫识别模板的概念,像身份证模板,名片模板,模板中存储着字段以及对应的区块的大致位置信息。第一次识别,用户生成模板并存储。之后的识别,代码则根据模板自动进行绑定。

用户调整、绑定后,按左上角的“识别”按钮,进入第三步识别字符。


第三步:识别字符

对应代码:(github.com/freeors/aism)中的inference_char(...)函数。

识别字符

识别字符是使用一个基于Tensorflow的汉字分类器,把上面定位出的区块内字符图像逐个输入分类器进行识别。识别出的结果按区块归类,填充到相应字段。字符串中会同时存在汉字、数字和字母,此分类器要能同时识别汉字和可打印ASCII字符。既然是模型,那一定得有训练它的数据集,识别汉字图像的数据集描述了一个数据集实例。

分类器复杂度和字符多少决定了识别需要的时间,为好的用户体验,界面上需要显示识别进度。除了进度提示,有人会想到把识别放在后台线程,个人不建议这么做。1)多开一个线程会使总的识别时间变长。2)运行OCR的往往是移动端app,app本身就占了整个屏幕,可以接受识别过程中界面不响应用户操作。


API

Rose已内置该框架,app要执行OCR只需两步。

一、把汉字分类器(之下的第三步会说分类器)复制向<res>/app-<app>/tensorflow。

假设分类器文件名是chinese-3982.pb,复制后的文件路径是<tensorflow>/ocr/chinese-3982.pb。除了分类器,同时还须要复制该分类器对应的UNICODE编码标注,除了扩展名是.txt,标注的路径、文件名和分类器一样,示例就是<tensorflow>/ocr/chinese-3982.txt。

2、在app中调用tensorflow2::ocr。

namespace tensorflow2 {
std::map<std::string, tocr_result> ocr(const config& app_cfg, display& disp, 
	const surface& surf, const std::vector<std::string>& fields, 
	const std::string& pb_short_path);
}
  • app_cfg:来自instance->app_cfg()。
  • disp:来自instance->disp()。
  • surf:nullptr时表示从摄像头实时图像中识别,非nullptr时表示从该图面识别。
  • fields:要绑定到的字段。
  • pb_short_path:分类器文件名中<tensorlfow>的后面部分。示例是ocr/chinese-3982.pb。

返回值是一个std::map,first是绑定到的字段,即参数fields中的某个单元,second是该字段对应的识别结果,用tocr_result表示。

struct tocr_result {
	std::string chars;
	uint32_t used_ms;
};

chars是识别出的字符串。used_ms是tensorflow在识别“chars”这几个字符时花费了的时间,单位是毫秒。

编辑于 2017-12-17

文章被以下专栏收录

    kOS+Rose+AI Smart。kOS:不是一个全新操作系统,是裁剪过只剩C/C++、针对无人值守设备优化过的Android。Rose:开发人工智能app的跨平台工具链,支持kOS、iOS、Android、Windows、Mac OS X和Linux。AI Smart:一个基于Rose的跨平台App,让测试并分享你的TensorFlow Lite模型。