优化的OCR黑白像素算法
Posted
技术标签:
【中文标题】优化的OCR黑白像素算法【英文标题】:Optimized OCR black/white pixel algorithm 【发布时间】:2011-01-16 00:48:59 【问题描述】:我正在为一组有限的字符编写一个简单的 OCR 解决方案。也就是说,我知道字母表中所有 26 个字母的样子。我正在使用 C#,并且能够轻松确定给定像素应该被视为黑色还是白色。
我正在为每个字符生成一个黑/白像素矩阵。例如,字母 I(大写 i)可能如下所示:
01110
00100
00100
00100
01110
注意:我在本文后面使用的所有点都假设左上角的像素是 (0, 0),右下角的像素是 (4, 4)。 1 代表黑色像素,0 代表白色像素。
我会像这样在 C# 中创建一个相应的矩阵:
CreateLetter("I", new List<List<bool>>()
new List<bool>() false, true, true, true, false ,
new List<bool>() false, false, true, false, false ,
new List<bool>() false, false, true, false, false ,
new List<bool>() false, false, true, false, false ,
new List<bool>() false, true, true, true, false
);
我知道我可以通过使用多维数组来优化这部分,但我们暂时忽略它,这是为了说明目的。每个字母的尺寸完全相同,10 像素 x 11 像素(10 像素 x 11 像素是我真实程序中字符的实际尺寸。我在这篇文章中将其简化为 5 像素 x 5 像素,因为使用 0 更容易“绘制”字母和 1 在较小的图像上)。
现在,当我给它一个 10 像素 x 11 像素的图像部分以使用 OCR 分析时,它需要在每个像素 (10 * 11 = 110) 上的每个字母 (26) 上运行,这意味着 2,860 (26 * 110) 次迭代(在最坏的情况下)每个字符。
我认为这可以通过定义每个角色的独特特征来优化。因此,例如,假设字符集仅包含 5 个不同的字母:I、A、O、B 和 L。它们可能如下所示:
01110 00100 00100 01100 01000
00100 01010 01010 01010 01000
00100 01110 01010 01100 01000
00100 01010 01010 01010 01000
01110 01010 00100 01100 01110
在分析了每个角色的独特特征后,我可以显着减少测试角色所需执行的测试次数。例如,对于“I”字符,我可以将它的独特特征定义为在坐标 (3, 0) 中具有黑色像素,因为没有其他字符具有该像素为黑色。因此,我没有测试 110 像素来匹配“I”字符,而是将其缩减为 1 像素测试。
这就是所有这些字符的样子:
var LetterI = new OcrLetter()
Name = "I",
BlackPixels = new List<Point>() new Point (3, 0)
var LetterA = new OcrLetter()
Name = "A",
WhitePixels = new List<Point>() new Point(2, 4)
var LetterO = new OcrLetter()
Name = "O",
BlackPixels = new List<Point>() new Point(3, 2) ,
WhitePixels = new List<Point>() new Point(2, 2)
var LetterB = new OcrLetter()
Name = "B",
BlackPixels = new List<Point>() new Point(3, 1) ,
WhitePixels = new List<Point>() new Point(3, 2)
var LetterL = new OcrLetter()
Name = "L",
BlackPixels = new List<Point>() new Point(1, 1), new Point(3, 4) ,
WhitePixels = new List<Point>() new Point(2, 2)
对于 5 个字符手动执行此操作具有挑战性,并且添加的字母数量越多,难度就越大。您还希望保证您拥有最少的字母独特特征集,因为您希望尽可能优化它。
我想创建一个算法来识别所有字母的独特特征,并生成与上述类似的代码。然后我会使用这个优化的黑白矩阵来识别字符。
我如何获取填充了所有黑色/白色像素的 26 个字母(例如 CreateLetter 代码块)并将它们转换为一组优化的定义字母的独特特征(例如新的 OcrLetter() 代码块)?我如何保证它是最有效的独特特征定义集(例如,不是将 6 个点定义为独特特征,可能有一种方法可以用 1 或 2 个点来完成,就像我的字母“I”示例能够)。
我提出的另一种解决方案是使用哈希表,这会将其从 2,860 次迭代减少到 110 次迭代,减少了 26 次。它可能是这样工作的:
我会用类似于以下的数据填充它:
Letters["01110 00100 00100 00100 01110"] = "I";
Letters["00100 01010 01110 01010 01010"] = "A";
Letters["00100 01010 01010 01010 00100"] = "O";
Letters["01100 01010 01100 01010 01100"] = "B";
现在,当我到达图像中要处理的位置时,我将其转换为字符串,例如:“01110 00100 00100 00100 01110”,然后在哈希表中找到它。这个解决方案看起来很简单,然而,这仍然需要 110 次迭代才能为每个字母生成这个字符串。
在大 O 表示法中,算法是相同的,因为 O(110N) = O(2860N) = O(N) 在页面上处理 N 个字母。但是,它仍然以 26 倍的常数改进,这是一个显着的改进(例如,它需要 1 分钟,而不是需要 26 分钟)。
更新:目前提供的大多数解决方案都没有解决识别角色独特特征的问题,而是提供了替代解决方案。我仍在寻找这种解决方案,据我所知,它是实现最快 OCR 处理的唯一方法。
我只是想出了一个部分解决方案:
对于网格中的每个像素,将具有该像素的字母存储为黑色像素。
使用这些字母:
I A O B L
01110 00100 00100 01100 01000
00100 01010 01010 01010 01000
00100 01110 01010 01100 01000
00100 01010 01010 01010 01000
01110 01010 00100 01100 01110
你会有这样的东西:
CreatePixel(new Point(0, 0), new List<Char>() );
CreatePixel(new Point(1, 0), new List<Char>() 'I', 'B', 'L' );
CreatePixel(new Point(2, 0), new List<Char>() 'I', 'A', 'O', 'B' );
CreatePixel(new Point(3, 0), new List<Char>() 'I' );
CreatePixel(new Point(4, 0), new List<Char>() );
CreatePixel(new Point(0, 1), new List<Char>() );
CreatePixel(new Point(1, 1), new List<Char>() 'A', 'B', 'L' );
CreatePixel(new Point(2, 1), new List<Char>() 'I' );
CreatePixel(new Point(3, 1), new List<Char>() 'A', 'O', 'B' );
// ...
CreatePixel(new Point(2, 2), new List<Char>() 'I', 'A', 'B' );
CreatePixel(new Point(3, 2), new List<Char>() 'A', 'O' );
// ...
CreatePixel(new Point(2, 4), new List<Char>() 'I', 'O', 'B', 'L' );
CreatePixel(new Point(3, 4), new List<Char>() 'I', 'A', 'L' );
CreatePixel(new Point(4, 4), new List<Char>() );
现在对于每个字母,为了找到独特的特征,您需要查看它属于哪些桶,以及桶中其他字符的数量。那么让我们以“我”为例。我们转到它所属的所有存储桶 (1,0; 2,0; 3,0; ...; 3,4) 并看到具有最少其他字符的存储桶是 (3,0)。事实上,它只有一个字符,这意味着在这种情况下它必须是一个“我”,我们发现了我们的独特之处。
您也可以对白色的像素执行相同的操作。请注意,桶 (2,0) 包含除“L”之外的所有字母,这意味着它可以用作白色像素测试。同样,(2,4) 不包含“A”。
可以立即丢弃包含所有字母或不包含任何字母的桶,因为这些像素无法帮助定义独特的特征(例如 1,1;4,0;0,1;4,4)。
如果您没有对字母进行 1 像素测试,例如在“O”和“B”的情况下,它会变得更加棘手。让我们来看看 'O' 的测试...
它包含在以下存储桶中:
// Bucket Count Letters
// 2,0 4 I, A, O, B
// 3,1 3 A, O, B
// 3,2 2 A, O
// 2,4 4 I, O, B, L
此外,我们还有一些白色像素测试可以提供帮助:(我只列出了最多 2 个缺失的那些)。 Missing Count 计算为 (5 - Bucket.Count)。
// Bucket Missing Count Missing Letters
// 1,0 2 A, O
// 1,1 2 I, O
// 2,2 2 O, L
// 3,4 2 O, B
所以现在我们可以取最短的黑色像素桶 (3,2) 并看到当我们测试 (3,2) 时,我们知道它是“A”还是“O”。所以我们需要一种简单的方法来区分“A”和“O”。我们可以寻找包含“O”但不包含“A”的黑色像素桶(例如 2,4)或包含“O”但不包含“A”的白色像素桶(例如 1,1)。其中任何一个都可以与 (3,2) 像素结合使用,以唯一标识字母“O”,只需 2 次测试。
当有 5 个字符时,这似乎是一个简单的算法,但是当有 26 个字母和更多像素重叠时,我该怎么做呢?例如,假设在 (3,2) 像素测试之后,它发现了 10 个包含该像素的不同字符(这是所有桶中最少的)。现在我需要找到与其他 9 个字符的区别,而不仅仅是 1 个其他字符。我将如何实现我的目标,即尽可能少地进行检查,并确保我没有运行无关的测试?
【问题讨论】:
我数了 26 位。您可以使用 32 位 int 来表示您的字母,然后使用数组而不是列表。 【参考方案1】:我没有答案,但以下是您最终解决方案的一些界限:
如果您想要直接“使用 X 像素作为键”,那么您至少需要 ceiling(log2(number of characters))
像素。您将无法消除位数较少的字母的歧义。在您的情况下,尝试找到 5 个像素相当于找到将字母分成独立分区的 5 个像素。恐怕没那么容易。
您还可以使用 Moron 的 (heheh) suggestion 并根据您正在扫描的语言的字母频率构建一棵树,类似于 Huffman coding。这将占用比每个字母 5 位更多的空间,但假设使用 power-law distribution 的字母可能会更小。我会采用这种方法,因为它允许您搜索每个节点的特定分区,而不是搜索一组分区。
【讨论】:
【参考方案2】:你可以创建一棵树。
选择一个像素,根据像素是白色还是黑色,将字母分成两个桶。然后选择第二个像素,根据该像素将桶分成两个桶,依此类推。
您可以尝试通过选择大小近似相等的像素来优化树的深度。
创建树是一次性预处理步骤。您不必多次这样做。
现在,当您找到要匹配的字母时,根据设置/未设置的像素跟随树并获取您的字母。
【讨论】:
【参考方案3】:我没有提供关键功能的算法,但这里有一些可能会有所帮助。
首先,我不会太担心为每个字符寻找特征像素,因为平均而言,检查给定字符是否与二进制图像的给定条带 (5x5) 匹配不应该超过5-7 检查以判断没有匹配项。为什么?可能性。对于 7 个二进制像素,有 2**7=128 种不同的可能性。这意味着有 1/128
其次,如果你不想做一个哈希表,那么你可以考虑使用trie 来存储你所有的字符数据。它将使用更少的内存,并且您将一次检查所有字符。它不会像哈希表那样快速搜索,但您也不必转换为字符串。在树中的每个节点上,最多只能有 2 个后代。例如,如果您有两个 2x2 字符(我们称它们为 A 和 B):
A B
01 00
10 11
您尝试在第一个节点处只有一个后代 - 仅在左侧(0 分支)。我们继续到下一个节点。它有两个后代,左 (0) 分支通向 B 的其余部分,右 (1) 分支通向 A 的其余部分。你明白了。如果这部分不清楚,请告诉我。
【讨论】:
抱歉,我对 10x11 和 5x5 不是很清楚。 10x11 是我程序中角色的实际尺寸。 5x5 是我在这篇文章中的简化版本。我也在上面的帖子中澄清了这一点。 特里是有道理的。使用压缩的 trie 会更有益。【参考方案4】:为什么不将图像视为 25 位整数?一个 32 位的 int 可以工作。例如字母'I'可以看成十进制整数14815374,因为它的二进制表达式是0111000100001000010001110。你可以很方便的比较两张图片,用'=='操作作为两个整数。
【讨论】:
是的,这与哈希表的解决方案基本相同。不过,它实际上需要是一个 110 位整数。而且您仍然需要将要处理的图像转换为整数,这意味着您在其上运行 110 次迭代。 这个“要处理的图像”是什么?我们所知道的是,它保证上面有字符,这些字符方便地总是相同的大小。它以什么数据格式保存? 1位黑白?【参考方案5】:一种方法是识别一个像素,该像素在大约一半的字母中为黑色,而在另一组中为白色。这可以用来将字母分成两组,递归地对两半使用相同的算法,直到找到单个字符。
如果您找不到将集合分成两部分的单个像素,则可能需要使用两个或更多像素的组,但希望使用单个像素应该足够好。
要找到像素,从一个与字母大小相同的整数数组开始,将所有元素初始化为 0,然后如果字母中的对应像素是(比如说)黑色,则递增元素。您感兴趣的是(大约)10≤sum≤16 范围内的那些(对于***,较低的级别需要使用其他边界)。
【讨论】:
【参考方案6】:好的,我想出了解决办法。
您只需对每个像素和其他像素组合使用深度优先搜索,直到找到字母的一组独特特征。在执行深度优先搜索时,请确保您不要每次都从 x=0 和 y=0 开始,因为您只想处理每个组合一次,因此您最终要做的是递增每个组合中的 x 和 y 值迭代。
我创建了一个包含这些属性的辅助对象:
public Point LastPoint get; set;
public List<OcrChar> CharsWithSimilarProperties get; set;
public List<Point> BlackPixels get; set;
public List<Point> WhitePixels get; set;
对于每次迭代,如果我找不到独特的特征(例如,所有其他字母的这个像素为黑色,但这个字母的像素为白色......或相反)我将所有后续像素添加到队列中正在处理,通过创建一个具有正确设置属性的上述对象的实例。
一些伪代码:
rootNode.LastPoint = new Point(-1, -1)
rootNode.CharsWithSimilarProperties = all letters in alphabet except for this one
queue.Add(rootNode)
while queue.HasNodes()
for each pixel after node.LastPoint
if node.IsBlackPixel(pixel) && node.CharsWithSimilarProperties.IsAlwaysWhite(pixel)
node.BlackPixels.Add(pixel)
return node.BlackPixels and node.WhitePixels
if node.IsWhitePixel(pixel) && node.CharsWithSimilarProperties.IsAlwaysBlack(pixel)
node.WhitePixels.Add(pixel)
return node.BlackPixels and node.WhitePixels
newNode = new Node();
newNode.BlackPixels = node.BlackPixels.Copy();
newNode.WhitePixels = node.WhitePixels.Copy();
newNode.LastPoint = pixel
if node.IsBlackPixel(pixel)
newNode.BlackPixels.Add(pixel)
newNode.CharsWithSimilarProperties = list of chars from node.CharsWithSimilarProperties that also had this pixel as black
else
newNode.WhitePixels.Add(pixel)
newNode.CharsWithSimilarProperties = list of chars from node.CharsWithSimilarProperties that also had this pixel as white
queue.Add(newNode)
要确定是“node.CharsWithSimilarProperites.IsAlwaysWhite()”还是“IsAlwaysBlack()”,可以在队列的每次迭代中生成compositeMap:
for each pixel after node.LastPoint
for each char in node.CharsWithSimilarProperties
if char.IsBlackPixel(pixel)
compositeMap[pixel].Add(char)
在做这一切之前,我还处理了整个字母表以找到始终为白色或始终为黑色的像素,因为这些像素永远无法使用。我将它们添加到List<Point> ignoredPixels
,每次迭代像素时,我总是使用if (ignoredPixels[x, y]) continue;
。
这非常有效,而且速度非常快。尽管请记住,我的解决方案的这一部分根本不需要很快,因为它是一次性优化,以后可以帮助我。在我的每个“字母”集最多 8 个字符的测试用例中,它通常为每个字符产生一个或两个特征。我还没有在完整的 26 个字符集上运行它。
【讨论】:
@eagle:你为什么偏爱“独特”的特征?它对于特定角色可能是最佳选择,但请考虑您将如何实际使用该“独特”特征。您是否不必平均(至少)进行 13 个像素比较(对于 26 个字母)?使用基于字母频率的树(请参阅 MSN 的答案),您平均可能只进行 6-7 像素比较(当然,除非我们知道数据是什么,否则无法确定)。不过,也许我误解了您的问题/解决方案。 你说得对……使用这种方法效率更高,这也是我最终要使用的解决方案。如果有人需要我提供的解决方案,我仍然会留下这个答案。【参考方案7】:我正在走类似的道路,试图发明一种算法,让我可以使用最少的测试来将图像与我之前看到的图像进行匹配。我的应用程序是 OCR,但在从一组固定图像中尽可能快地识别图像的有限领域。
我的基本假设(我认为与您的相同,或者相同)是,如果我们可以识别一个唯一的像素(其中一个像素被定义为图像中的一个点加上一种颜色),那么我们发现该图像的完美(最快)测试。在您的情况下,您想查找字母。
如果我们找不到一个这样的像素,那么我们(不情愿地)寻找两个组合起来是唯一的像素。或三个。以此类推,直到我们对每张图片都进行了最低限度的测试。
我应该注意,我有一种强烈的感觉,即在我的特定领域中,我将能够找到如此独特的像素。对于您似乎有很多“重叠”的应用程序,情况可能不一样。
在考虑了this other question 中的 cmets(我刚刚开始对这个问题有所了解)和这里的 cmets 之后,我想我可能想出了一个可行的算法。
这是我到目前为止所得到的。我在下面描述的方法是在摘要中编写的,但在我的应用程序中,每个“测试”都是由一个点加上颜色标识的像素,而“结果”代表图像的身份。识别这些图像是我的最终目标。
考虑以下编号为 T1 到 T4 的测试。
T1:A B C T2:B T3:A C D T4:A D这个测试列表可以解释如下;
如果测试 T1 为真,我们会得出结论,我们的结果是 A 或 B 或 C。 如果测试 T2 为真,我们得出结论,我们有 B 的结果。 如果测试 T3 为真,我们会得出结论,我们的结果是 A 或 C 或 D。 如果测试 T4 为真,我们会得出结论,我们的结果为 A 或 D。对于每个单独的结果 A、B、C、D,我们希望找到一个测试组合(理想情况下只有一个测试),以便我们测试一个明确的结果。
运用直觉并稍微眯着眼看屏幕,我们可以摸索到以下测试安排。
对于 A,我们可以测试 T4(A 或 D)和 T1(A 但不是 D)的组合
B 很容易,因为有一个测试 T2 给出了结果 B,没有别的。
C 有点难,但最终我们可以看到 T3(A 或 C 或 D)和 NOT T4(非 A 和非 D)的组合给出了预期的结果。
同样,D 可以通过 T4 和(不是 T1)的组合找到。
总结
A <- T4 && T1
B <- T2
C <- T3 && ¬T4
D <- T4 && ¬T1
(其中<-
应读作“如果以下测试评估为真则可以找到”)
直觉和眯眼很好,但我们可能至少要到 C# 5.0 才能将这些技术内置到语言中,所以这里尝试将这种方法形式化以用较少的语言实现。
要查找结果R
,
-
找到测试
Tr
,它可以提供所需结果R
和最少不需要的结果(最好没有其他结果)
如果测试给出了R
的结果而没有其他结果,我们就完成了。我们可以匹配R
,其中Tr
为真。
对于测试中的每个不需要的结果X
Tr
;
(a) 找到最短的测试Tn
,它给出R
但不是X
。如果我们找到这样的测试,我们可以匹配R
where (T && Tn)
(b) 如果没有测试与条件 (a) 匹配,则找到包含 X
但不包含 R
的最短测试 Tx
。 (这样的测试将消除X
作为测试Tr
的结果)。然后我们可以测试R
,其中(T && ¬Tx)
现在我将尝试对每个期望的结果(A、B、C、D)遵循这些规则。
这里再次测试以供参考;
T1:A B C T2:B T3:A C D T4:A D对于 A
根据规则 (1),我们从 T4 开始,因为它是给出结果 A 的最简单测试。但它也给出结果“D”,这是一个不需要的结果。根据规则 (3) 我们可以使用测试 T1,因为它包含“A”但不包含“D”。
因此我们可以用
A <- T4 && T1
对于 B
为了找到“B”,我们快速找到测试 T2,它是“B”的最短测试,因为它只给出结果“B”,所以我们完成了。
B <- T2
对于 C
要找到“C”,我们从 T1 和 T3 开始。由于这些测试的结果同样短,我们任意选择 T1 作为起点。
现在根据 (3a) 我们需要找到一个包含“C”但不包含“A”的测试。由于没有测试满足这个条件,我们不能使用 T1 作为第一个测试。 T3也有同样的问题。
由于无法找到满足 (3a) 的测试,我们现在寻找满足条件 (3b) 的测试。我们寻找一个给出“A”但不给出“C”的测试。我们可以看到测试 T4 满足这个条件,因此我们可以测试 C 与
C <- T1 && ¬T4
对于 D
要找到 D,我们从 T4 开始。 T4 包含不需要的结果 A。没有其他测试给出结果 D 但不给出 A,因此我们寻找给出 A 但不给出 D 的测试。测试 T1 满足此条件,因此我们可以使用以下方法测试 D
D <= T4 && ¬T1
这些结果很好,但我认为我对这个算法的调试还不够充分,无法有 100% 的信心。我会再考虑一下,也许会编写一些测试来看看它是如何成立的。不幸的是,该算法非常复杂,需要花费几分钟才能仔细实施。我可能还需要几天的时间才能做出进一步的结论。
更新
我发现最好同时寻找满足 (a) OR (b) 的测试,而不是寻找 (a) 然后 (b)。如果我们首先查找 (a),我们可能会得到一个很长的测试列表,而我们可能会通过允许一些 (b) 测试得到一个较短的列表。
【讨论】:
以上是关于优化的OCR黑白像素算法的主要内容,如果未能解决你的问题,请参考以下文章