要做文字识别,第一步要考虑的就是怎么将每一个字符从图片中切割下来,然后才可以送入我们设计好的模型进行字符识别。现在就以下面这张图片为例,说一说最一般的字符切割的步骤是哪些。

当然,我们实际上要识别的图片很可能没上面那张图片如此整洁,很可能是倾斜的,或者是带噪声的,又或者这张图片是用手机拍下来下来的,变得歪歪扭扭,所以需要进行图片预处理,把文本位置矫正,把噪声去除,然后才可以进行进一步的字符分割和文字识别。这些预处理的方法在我的前面几篇博客都有提到了,大家可以参考参考:

透视矫正

水平矫正

在预处理工作做好之后,我们就可以开始切割字符了。最普通的切割算法可以总结为以下几个步骤:

  1. 对图片进行水平投影,找到每一行的上界限和下界限,进行行切割
  2. 对切割出来的每一行,进行垂直投影,找到每一个字符的左右边界,进行单个字符的切割

一看只有两个步骤,好像不太难,马上编程实现看看效果。

首先是行切割。这里提到了水平投影的概念,估计有的读者没听过这个名词,我来解释一下吧。水平投影,就是对一张图片的每一行元素进行统计(就是往水平方向统计),然后我们根据这个统计结果画出统计结果图,进而确定每一行的起始点和结束点。下面提到的垂直投影也是类似的,只是它的投影方向是往下的,即统计每一列的元素个数。

根据上面的解释,我们可以写出一个用于水平投影和垂直投影的函数。

#define V_PROJECT 1  //垂直投影(vertical)
#define H_PROJECT 2 //水平投影(horizational) typedef struct
{
int begin;
int end; }char_range_t; //获取文本的投影以用于分割字符(垂直,水平),默认图片是白底黑色
int GetTextProjection(Mat &src, vector<int>& pos, int mode)
{
if (mode == V_PROJECT)
{
for (int i = 0; i < src.rows; i++)
{
uchar* p = src.ptr<uchar>(i);
for (int j = 0; j < src.cols; j++)
{
if (p[j] == 0) //是黑色像素
{
pos[j]++;
}
}
}
}
else if (mode == H_PROJECT)
{
for (int i = 0; i < src.cols; i++)
{ for (int j = 0; j < src.rows; j++)
{
if (src.at<uchar>(j, i) == 0)
{
pos[j]++;
}
}
}
} return 0;
}

上面代码提到的vector pos就是用于存储垂直投影和水平投影的位置的,我们可以根据它来确定行的位置。我们先把水平投影画出来。

下面是画出水平(垂直)投影图的代码实现。

void draw_projection(vector<int>& pos, int mode)
{
vector<int>::iterator max = std::max_element(std::begin(pos), std::end(pos)); //求最大值
if (mode == H_PROJECT)
{
int height = pos.size();
int width = *max;
Mat project = Mat::zeros(height, width, CV_8UC1);
for (int i = 0; i < project.rows; i++)
{
for (int j = 0; j < pos[i]; j++)
{
project.at<uchar>(i, j) = 255;
}
}
imshow("horizational projection", project); }
else if (mode == V_PROJECT)
{
int height = *max;
int width = pos.size();
Mat project = Mat::zeros(height, width, CV_8UC1);
for (int i = 0; i < project.cols; i++)
{
for (int j = project.rows - 1; j >= project.rows - pos[i]; j--)
{
//std::cout << "j:" << j << "i:" << i << std::endl;
project.at<uchar>(j, i) = 255;
}
}
imshow("vertical projection", project);
} waitKey();
}

水平投影图:

通过上面的水平投影图,我们很容易就能确定每一行文字的位置,确定的思路如下:我们可以以每个小山峰的起始结束点作为我们文本行的起始结束点,当然我们要对这些山峰做些约束,比如这些山峰的跨度不能太小。这样子我们就得到每一个文本行的位置,接着我们就根据这些位置将每个文本行切割下来用于接下来的单个字符的切割。

//获取每个分割字符的范围,min_thresh:波峰的最小幅度,min_range:两个波峰的最小间隔
int GetPeekRange(vector<int> &vertical_pos, vector<char_range_t> &peek_range, int min_thresh = 2, int min_range = 10)
{
int begin = 0;
int end = 0;
for (int i = 0; i < vertical_pos.size(); i++)
{
if (vertical_pos[i] > min_thresh && begin == 0)
{
begin = i;
}
else if (vertical_pos[i] > min_thresh && begin != 0)
{
continue;
}
else if (vertical_pos[i] < min_thresh && begin != 0)
{
end = i;
if (end - begin >= min_range)
{
char_range_t tmp;
tmp.begin = begin;
tmp.end = end;
peek_range.push_back(tmp);
begin = 0;
end = 0;
} }
else if (vertical_pos[i] < min_thresh || begin == 0)
{
continue;
}
else
{
//printf("raise error!\n");
}
} return 0;
}

切割每一行,然后我们得到了一行文本,我们继续对这行文本进行垂直投影。

紧接着我们根据垂直投影求出来每个字符的边界值进行单个字符切割。方法与垂直投影的方法一样,只不过,因为字符排列得比较紧密,仅通过投影确定字符得到的结果往往不够准确的。不过先不管了,先切下来看看。

从上图看出,切割效果不太好,那多切割几行再看看。

效果确实不咋滴,那换成英文文档来测试这个切割算法。

比如切割这个英语文本图片

切割效果还是很不错的:

那为什么英语的切割效果很好,但中文效果一般呢?

分析其原因,这其实跟中文的字体复杂度有关的,中文的字符的笔画和形态都比英文的多,更重要的是英文字母都是绝大部分都是联通体,切割起来很简单,但是汉字多存在左右结构和上下结构,很容易造成过度切割,即把一个左右偏旁的汉字切成了两份,比如上面的“则”字。

针对行字符分割,左右偏旁的字难以分割的情况,我觉得可以做以下处理:

  1. 先用通用的分割方法切割字符,得到一堆候选的切割字符集合。
  2. 统计字符集合的大多数字符的尺寸,作为标准尺寸。
  3. 根据标准尺寸选出标准的字符,切割保存。并对切割保存好的字符原位置涂成白色
  4. 对剩下下来的图片进行腐蚀,让字体粘连。
  5. 用1中算法再次分割,得到完整字体集合。

因为以上的思路可能只适应于纯汉字文本,所以就不贴代码了。

最后贴几张分割字符的图吧,感觉分割效果不太让人满意,主要是汉字的分割确实很有难度,左右偏旁的字经常分割错误。

英文的切割还是比较简单的,毕竟英文字母基本都是联通体,而且没有像汉字那样的左右结构。

对于字体间隔比较宽的汉字文档,总的看来分割任务基本完成,但是左右结构的汉字依然难以正确分割。

最后看一下一些字体较小,字体间隔较窄的情况。这类情况确实分割效果大打折扣,因为每个字体粘连过于接近,字体的波谷很难确定下来,进而造成切割字符失败。

总结

汉字字符切割,看似简单,做起来其实很难做得很好,我也对此查阅了很多论文,发现其实很多论文也谈到了,汉字确实很那做到一个高正确率的分割,直至现在还没有一统江湖的解决方案。汉字切割的失败,就会直接导致了后面OCR识别的失败,这也是当前很多一些很厉害的OCR公司都没法把汉字做到100%识别的一个原因吧。所以这个问题就必须得到很好的解决。现在解决汉字切割失败(过切割,一个字被拆成两个)的较好方法是,在OCR识别中再把它修正。比如“刺”字被分为两部分了,那么我们就直接将这两个“字”送去识别,结果当然是得到一个置信度很低的一个反馈,那么我们就将这两个部分往他们身边最近的、而且没被成功识别的部分进行合并,再将这个合并后的字送进OCR识别,这样子我们就可以通过识别反馈来完成汉字的正确分割和识别了。既然一些基于图像处理的方法基本很难把汉字分割的效果做得很好,那深度学习呢?我先去试试,效果好的话再分享给大家。

代码在我的github

【OCR技术系列之二】文字定位与切割的更多相关文章

  1. OCR技术浅探 : 文字定位和文本切割(2)

    文字定位 经过前面的特征提取,我们已经较好地提取了图像的文本特征,下面进行文字定位. 主要过程分两步: 1.邻近搜索,目的是圈出单行文字: 2.文本切割,目的是将单行文本切割为单字. 邻近搜索 我们可 ...

  2. 【OCR技术系列之二】文字定位于切割

    要做文字识别,第一步要考虑的就是怎么将每一个字符从图片中切割下来,然后才可以送入我们设计好的模型进行字符识别.现在就以下面这张图片为例,说一说最一般的字符切割的步骤是哪些. 当然,我们实际上要识别的图 ...

  3. 【OCR技术系列一】光学字符识别技术介绍

    注:此篇内容主要是综合整理了光学字符识别 和OCR技术系列之一]字符识别技术总览,详情见文末参考文献 什么是 OCR? OCR(Optical Character Recognition,光学字符识别 ...

  4. 【OCR技术系列之七】端到端不定长文字识别CRNN算法详解

    在以前的OCR任务中,识别过程分为两步:单字切割和分类任务.我们一般都会讲一连串文字的文本文件先利用投影法切割出单个字体,在送入CNN里进行文字分类.但是此法已经有点过时了,现在更流行的是基于深度学习 ...

  5. 【OCR技术系列之四】基于深度学习的文字识别(3755个汉字)

    上一篇提到文字数据集的合成,现在我们手头上已经得到了3755个汉字(一级字库)的印刷体图像数据集,我们可以利用它们进行接下来的3755个汉字的识别系统的搭建.用深度学习做文字识别,用的网络当然是CNN ...

  6. 【OCR技术系列之四】基于深度学习的文字识别

    上一篇提到文字数据集的合成,现在我们手头上已经得到了3755个汉字(一级字库)的印刷体图像数据集,我们可以利用它们进行接下来的3755个汉字的识别系统的搭建.用深度学习做文字识别,用的网络当然是CNN ...

  7. 【OCR技术系列之一】字符识别技术总览

    最近入坑研究OCR,看了比较多关于OCR的资料,对OCR的前世今生也有了一个比较清晰的了解.所以想写一篇关于OCR技术的综述,对OCR相关的知识点都好好总结一遍,以加深个人理解. 什么是OCR? OC ...

  8. 【OCR技术系列之三】大批量生成文字训练集

    放假了,终于可以继续可以静下心写一写OCR方面的东西.上次谈到文字的切割,今天打算总结一下我们怎么得到用于训练的文字数据集.如果是想训练一个手写体识别的模型,用一些前人收集好的手写文字集就好了,比如中 ...

  9. 【OCR技术系列之八】端到端不定长文本识别CRNN代码实现

    CRNN是OCR领域非常经典且被广泛使用的识别算法,其理论基础可以参考我上一篇文章,本文将着重讲解CRNN代码实现过程以及识别效果. 数据处理 利用图像处理技术我们手工大批量生成文字图像,一共360万 ...

随机推荐

  1. runtime--小白看过来

    目录 RunTime 概述 RunTime消息机制 RunTime交换方法 RunTime消息转发 RunTime关联对象 RunTime实现字典与模型互转 1.RunTime 概述 我们在面试的时候 ...

  2. Codeforces Round #378 (Div. 2)-C. Epidemic in Monstropolis

    C. Epidemic in Monstropolis time limit per test 1 second memory limit per test 256 megabytes input s ...

  3. 关于easyui隐藏后数据不能刷新??

    原因是div用display属性隐藏后不能重新加载table数据 解决方法:使用hide()方法在初始化时隐藏 $("#two").hide(); //点击按钮隐藏与显示表单域 $ ...

  4. margin负值的相关应用

    1.页面上实现css sprite背景定位效果   其实margin:-40px 0 0 -160px;与background-position:-160px -40px;实现的原理是一致的,而差别就 ...

  5. salesforce零基础学习(八十二)审批邮件获取最终审批人和审批意见

    项目中,审批操作无处不在.配置审批流时,我们有时候会用到queue,related user设置当前步骤的审批人,审批人可以一个或者多个.当审批人有多个时,邮件中获取当前记录的审批人和审批意见就不能随 ...

  6. 深入理解java虚拟机_第二章_读书笔记

    1.本章内容目录: 概述 运行时数据区域 程序计数器 java虚拟机栈 本地方法栈 java堆 方法区 运行时常量池 直接内存 HotSpot虚拟机对象探秘 对象的创建 对象的内存布局 对象的访问定位 ...

  7. 【开源】canvas图像裁剪、压缩、旋转

    前言 前段时间遇到了一个移动端对图像进行裁剪.压缩.旋转的需求. 考虑到已有各轮子的契合度都不高,于是自己重新造了一个轮子. 关于图像裁剪.压缩 在HTML5时代,canvas的功能已经非常强大了,可 ...

  8. 一起写框架-MVC框架-基础功能-Date类型数据绑定(七)

    实现功能 表单请求传递的数据,格式为以下格式的日期时间数据. (1):yyyy-MM-dd hh:mm:ss (2):yyyy-MM-dd 执行方法可以使用Date类型接收. 实现思路 1.获得表单字 ...

  9. c#3.0提供的扩展方法

    在c#3.0之前,想要为内置的类型添加一个方法显然是不可能的.但是,c#3.0提供的扩展方法可以解决这个问题.具体代码如下: public static class ExtendedClass {pu ...

  10. WPF学习之路一

    前段时间一直在学习MVC,工作需要,现在需要180度急转弯,搞WPF,MVVM,只能找资料学习了. WPF中有一个消息机制,就是当前台控件绑定的值改变时,会自动通知到指定的事件来改变VM的值,反之亦然 ...