8

我正在为有限的字符集编写一个简单的 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 个缺失的那些)。缺失计数计算为 (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) 像素结合使用,以仅通过 2 次测试来唯一标识字母“O”。

当有 5 个字符时,这似乎是一个简单的算法,但是当有 26 个字母和更多像素重叠时,我该怎么做呢?例如,假设在 (3,2) 像素测试之后,它发现了 10 个包含该像素的不同字符(这是所有桶中最少的)。现在我需要找到与其他 9 个字符的区别,而不仅仅是 1 个其他字符。我将如何实现获得尽可能少的检查的目标,并确保我没有运行无关的测试?

4

7 回答 7

4

我没有答案,但这里是您最终解决方案的一些界限:

如果您想要直接“使用 X 像素作为键”,那么您至少需要ceiling(log2(number of characters))像素。您将无法消除位数较少的字母的歧义。在您的情况下,尝试找到 5 个像素等同于找到将字母分成独立分区的 5 个像素。恐怕没那么容易。

您还可以使用 Moron 的 (heheh)建议,并根据您正在扫描的语言的字母频率构建一棵树,类似于Huffman 编码。这将占用比每个字母 5 位更多的空间,但假设字母使用的幂律分布可能会更小。我会采用这种方法,因为它允许您搜索每个节点的特定分区,而不是搜索一组分区。

于 2010-02-12T18:10:31.207 回答
4

你可以创建一棵树。

选择一个像素,根据像素是白色还是黑色,将字母分成两个桶。然后选择第二个像素,将桶分成两个桶,每个桶都基于该像素,依此类推。

您可以尝试通过选择大小近似相等的像素来优化树的深度。

创建树是一次性预处理步骤。您不必多次执行此操作。

现在,当您获得要匹配的字母时,请根据设置/未设置的像素跟随树并获取您的字母。

于 2010-02-12T06:40:57.860 回答
1

我没有提供关键功能的算法,但这里有一些可能会有所帮助的东西。

首先,我不会太担心为每个字符寻找特征像素,因为平均而言,检查给定字符是否与二进制图像的给定条带 (5x5) 匹配不应该超过 5-7检查以判断没有匹配项。为什么?可能性。对于 7 个二进制像素,有 2**7=128 种不同的可能性。这意味着有 1/128 < 1% 的机会匹配高达 7 个像素的字符。只需确保在发现不匹配时立即停止比较。

其次,如果你不想做一个哈希表,那么你可以考虑使用trie来存储你所有的字符数据。它将使用更少的内存,并且您将一次检查所有字符。它不会像哈希表那样快速搜索,但您也不必转换为字符串。在树中的每个节点上,最多只能有 2 个后代。例如,如果您有两个 2x2 字符(我们称它们为 A 和 B):

A   B
01  00
10  11

您尝试在第一个节点上只有一个后代 - 仅在左侧(0 分支)。我们继续到下一个节点。它有两个后代,左 (0) 分支通向 B 的其余部分,右 (1) 分支通向 A 的其余部分。你明白了。如果这部分不清楚,请告诉我。

于 2010-02-12T06:41:16.033 回答
1

为什么不将图像视为 25 位整数?一个 32 位的 int 可以工作。例如,字母'I'可以看成十进制整数14815374,因为它的二进制表达式是0111000100001000010001110。你可以方便地比较两个图像,操作'=='作为两个整数。

于 2010-02-12T08:26:55.160 回答
1

一种方法是识别一个像素,该像素在大约一半的字母中为黑色,而在另一组中为白色。然后可以使用这将字母分成两组,递归地在两半上使用相同的算法,直到达到单个字符。

如果您找不到将集合分成两个的单个像素,您可能必须使用两个或更多像素的组,但希望使用单个像素应该足够好。

要找到像素,请从与字母大小相同的整数数组开始,将所有元素初始化为 0,然后如果字母中的相应像素(例如)黑色,则递增元素。您感兴趣的是(大致)10≤sum≤16 范围内的那些(对于顶级,较低的级别需要使用其他边界)。

于 2010-02-12T11:08:27.603 回答
0

我正在走类似的道路,试图发明一种算法,该算法将为我提供最少数量的测试,我可以使用它来将图像与我之前看到的图像进行匹配。我的应用程序是 OCR,但在从一组固定图像中尽可能快地识别图像的有限域中。

我的基本假设(我认为与您的相同,或者相同)是,如果我们可以识别一个唯一的像素(其中一个像素被定义为图像中的一个点加上一种颜色),那么我们已经找到了完美的 (最快)测试该图像。在您的情况下,您想查找字母。

如果我们找不到一个这样的像素,那么我们(不情愿地)寻找两个组合起来是唯一的像素。或三个。依此类推,直到我们对每个图像进行最小测试。

我应该注意到,我有一种强烈的感觉,即在我的特定领域中,我将能够找到如此独特的像素。对于您似乎有很多“重叠”的应用程序,它可能不一样。

在考虑了其他问题的评论(我刚刚开始对这个问题有所了解)和评论之后,我想我可能想出了一个可行的算法。

这是我到目前为止所得到的。我在下面描述的方法是在摘要中编写的,但在我的应用程序中,每个“测试”都是由一个点加上颜色标识的像素,而“结果”代表图像的身份。识别这些图像是我的最终目标。

考虑以下编号为 T1 到 T4 的测试。

  • T1:ABC
  • T2:乙
  • T3 : ACD
  • T4:广告

这个测试列表可以解释如下;

  • 如果测试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)的组合给出了预期的结果。

同样,可以通过 T4 和(不是 T1)的组合找到 D。

总之

A <- T4 && T1
B <- T2
C <- T3 && ¬T4
D <- T4 && ¬T1

(其中<-应读作“如果以下测试评估为真则可以找到”)

直觉和眯眼很好,但至少在 C# 5.0 之前我们可能不会将这些技术内置到语言中,因此这里尝试将方法形式化以在较少的语言中实现。

要查找结果R

  1. 找到Tr能够提供所需结果R和最少不需要结果的测试(最好没有其他结果)
  2. 如果测试给出了结果R而没有其他结果,我们就完成了。我们可以匹配RwhereTr为真。
  3. X对于测试中的每个不需要的结果Tr
    • Tn(a) 找出给出R但没有给出的最短测试X。如果我们找到这样的测试,我们就可以匹配Rwhere(T && Tn)
    • Tx(b) 如果没有测试符合条件 (a),则找到包含X但不包含的最短测试R。(这样的测试会X从 test中消除Tr)。然后我们可以测试在R哪里(T && ¬Tx)

现在我将尝试对每个期望的结果 A、B、C、D 遵循这些规则。

这里再次进行测试以供参考;

  • T1:ABC
  • T2:乙
  • T3 : ACD
  • T4:广告

为一个

根据规则 (1),我们从 T4 开始,因为它是给出结果 A 的最简单测试。但它也给出了结果“D”,这是一个不需要的结果。根据规则 (3),我们可以使用测试 T1,因为它包含“A”但不包含“D”。

因此我们可以用

A <- T4 && T1

对于乙

为了找到“B”,我们很快找到测试 T2,它是“B”的最短测试,因为它只给出结果“B”,所以我们完成了。

B <- T2

对于 C

为了找到“C”,我们从 T1 和 T3 开始。由于这些测试的结果同样短,我们任意选择 T1 作为起点。

现在根据 (3a) 我们需要找到一个包含“C”但不包含“A”的测试。由于没有测试满足这个条件,我们不能使用 T1 作为第一个测试。T3也有同样的问题。

由于无法找到满足 (3a) 的测试,我们现在寻找满足条件 (3b) 的测试。我们寻找一个给出“A”但不给出“C”的测试。我们可以看到测试 T4 满足这个条件,因此我们可以用

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)测试得到一个较短的列表。

于 2010-05-20T10:53:30.283 回答
0

好吧,我想出了解决方案。

您只需对每个像素和其他像素组合使用深度优先搜索,直到找到字母的一组独特特征。在执行深度优先搜索时,请确保您不要每次都从 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)

在做这一切之前,我还处理了整个字母表以找到始终为白色或始终为黑色的像素,因为这些像素永远无法使用。我将它们添加到 aList<Point> ignoredPixels中,每次迭代像素时,我总是使用if (ignoredPixels[x, y]) continue;.

这完美地工作并且非常快。尽管请记住,我的解决方案的这一部分根本不需要很快,因为它是一次性优化,以后可以帮助我。在我的每个“字母”集最多 8 个字符的测试用例中,它通常为每个字符产生一个或两个特征。我还没有在完整的 26 个字符上运行它。

于 2010-02-15T10:52:55.480 回答