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 }
});

代わりに多次元配列を使用することでこの部分を最適化できる可能性があることはわかっていますが、今のところ、これは説明のためであることを無視しましょう。すべての文字はまったく同じ寸法で、10px x 11pxです(10px x 11pxは、実際のプログラムでの文字の実際の寸法です。この投稿では、0を使用して文字を「描画」する方がはるかに簡単なので、これを5pxx5pxに簡略化しました。小さい画像では1)。

これで、OCRで分析する画像の10px x 11pxの部分を指定すると、すべてのピクセル(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)に黒のピクセルがあるという独自の特性を定義できます。これは、他の文字がそのピクセルを黒として持っていないためです。そのため、「I」文字の一致について110ピクセルをテストする代わりに、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ポイントでそれを行う方法があるかもしれません。例はできました)。


私が思いついた別の解決策は、ハッシュテーブルを使用することです。これにより、ハッシュテーブルが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表記では、ページ上で処理するN文字に対してO(110N)= O(2860N)= O(N)であるため、アルゴリズムは同じです。ただし、それでも26の一定の係数で改善されており、大幅に改善されています(たとえば、26分かかる代わりに、1分かかります)。


更新:これまでに提供されたソリューションのほとんどは、キャラクターの固有の特性を識別する問題に対処しておらず、代わりのソリューションを提供しています。私が知る限り、最速の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)であることを確認します。実は1文字しかないので、この場合は「I」に違いないので、独自の特徴があります。

白になるピクセルに対しても同じことができます。バケット(2,0)には、「L」を除くすべての文字が含まれていることに注意してください。これは、ホワイトピクセルテストとして使用できることを意味します。同様に、(2,4)には「A」が含まれていません。

これらのピクセルは一意の特性(例:1,1; 4,0; 0,1; 4,4)を定義するのに役立たないため、すべての文字を含むバケット、または文字を含まないバケットはすぐに破棄できます。

たとえば、「O」と「B」の場合のように、文字に対して1ピクセルのテストがない場合は、さらに注意が必要です。'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個の異なる文字が見つかったとします(これはすべてのバケットの中で最も少なかった)。ここで、他の1文字だけでなく、他の9文字との違いを見つける必要があります。できるだけ少ない量のチェックを取得するという目標をどのように達成し、無関係なテストを実行していないことを確認するにはどうすればよいですか?

4

7 に答える 7

4

あなたは木を作ることができます。

ピクセルを選択し、ピクセルが白または黒であることに基づいて、文字を2つのバケットに分割します。次に、2番目のピクセルを選択し、バケットをそのピクセルに基づいて2つのバケットに分割します。

サイズがほぼ等しいバケットを提供するピクセルを選択することで、ツリーの深さを最適化することができます。

ツリーの作成は、1回限りの前処理ステップです。あなたはそれを複数回行う必要はないはずです。

一致するアルファベットを取得したら、設定されている/設定されていないピクセルに基づいてツリーをたどり、文字を取得します。

于 2010-02-12T06:40:57.860 に答える
4

答えはありませんが、最終的な解決策にはいくつかの限界があります。

「Xピクセルをキーとして使用する」というストレートアップが必要な場合は、少なくともceiling(log2(number of characters))ピクセルが必要です。より少ないビットで文字を明確にすることはできません。あなたの場合、5ピクセルを見つけようとすることは、文字を独立したパーティションに分割する5ピクセルを見つけることと同じです。おそらくそれほど簡単ではありません。

また、Moron(heheh)の提案を使用して、ハフマンコーディングと同様に、スキャンしている言語の文字頻度に基づいてツリーを構築することもできます。これは、1文字あたり5ビットよりも多くのスペースを占有しますが、文字の使用法のべき乗則分布を想定すると、おそらくより小さくなります。パーティションのセットを検索するのではなく、ノードごとに特定のパーティションを検索できるため、このアプローチを採用します。

于 2010-02-12T18:10:31.207 に答える
1

画像を25ビット整数と見なしてみませんか?32ビット整数が機能する場合があります。たとえば、文字「I」は、2進式が0111000100001000010001110であるため、10進数の整数14815374として扱うことができます。2つの整数としての演算「==」を使用して2つの画像を比較すると便利です。

于 2010-02-12T08:26:55.160 に答える
1

重要な機能を提供するアルゴリズムはありませんが、役立つ可能性のあるものがいくつかあります。

まず、各文字の特徴的なピクセルを探すことについてあまり心配する必要はありません。平均して、特定の文字がバイナリ画像の特定のスワス(5x5)と一致するかどうかを確認するのに5〜7を超えることはないからです。一致するものがないことを確認するためにチェックします。なんで?確率。7つのバイナリピクセルの場合、2 ** 7=128の異なる可能性があります。つまり、最大7ピクセルでも、文字が一致する可能性は1/128 <1%です。不一致を見つけたら、必ず比較を停止してください。

次に、ハッシュテーブルを作成したくない場合は、トライを使用してすべての文字データを格納することを検討してください。使用するメモリが少なくなり、すべての文字を一度にチェックできます。ハッシュテーブルほど検索は速くありませんが、文字列に変換する必要もありません。ツリーの各ノードには、最大2つの子孫しか存在できません。たとえば、2x2の文字が2つある場合(AとBと呼びましょう):

A   B
01  00
10  11

トライでは、最初のノードに子孫が1つだけあります。つまり、左側(0ブランチ)だけです。この次のノードに進みます。2つの子孫があり、左側の(0)ブランチはBの残りの部分につながり、右側(1)のブランチはAの残りの部分につながります。画像が表示されます。この部分が明確でない場合はお知らせください。

于 2010-02-12T06:41:16.033 に答える
1

1つの方法は、文字の約半分が黒で、他のセットが白のピクセルを識別することです。次に、これを使用して、個々の文字に到達するまで、両方の半分で同じアルゴリズムを再帰的に使用して、文字を2つのグループに分割できます。

セットを2つに分割する単一のピクセルが見つからない場合は、2つ以上のピクセルのグループに移動する必要があるかもしれませんが、うまくいけば、単一のピクセルを使用するだけで十分です。

ピクセルを見つけるには、文字と同じサイズの整数の配列から始めて、すべての要素を0に初期化し、文字の対応するピクセルが(たとえば)黒の場合は要素をインクリメントします。関心のあるものは、(おおよそ)10≤sum≤16の範囲のものです(トップレベルの場合、下位レベルでは他の境界を使用する必要があります)。

于 2010-02-12T11:08:27.603 に答える
0

了解しました。解決策を見つけました。

文字の固有の特性のセットが見つかるまで、他のすべてのピクセルの組み合わせですべてのピクセルに対して深さ優先探索を使用するだけです。深さ優先探索を実行するときは、各組み合わせを1回だけ処理するため、毎回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;

これは完璧に機能し、本当に高速です。ただし、これは1回限りの最適化であり、後で役立つため、ソリューションのこの部分は高速である必要はまったくないことに注意してください。「アルファベット」セットごとに最大8文字のテストケースでは、通常、文字ごとに1つまたは2つの特性が生成されます。私はまだ26文字のフルセットでそれを実行していません。

于 2010-02-15T10:52:55.480 に答える
0

私は、画像を以前に見たものと一致させるために使用できる最小限の数のテストを提供するアルゴリズムを発明しようとして、同様の道を進んでいます。私のアプリケーションはOCRですが、固定された画像セットから画像をできるだけ速く認識するという限られた領域にあります。

私の基本的な仮定(私はあなたのものと同じ、または同じだと思います)は、1つの一意のピクセル(ピクセルは画像内の点と色として定義されます)を識別できれば、完璧なものを見つけたということです(最速)その画像をテストします。あなたの場合、あなたは手紙を見つけたいです。

そのようなピクセルが1つ見つからない場合は、(不機嫌に)組み合わせて一意の2つのピクセルを探します。または3つ。以下同様に、各画像の最小限のテストが完了するまで続けます。

私の特定のドメインでは、そのようなユニークなピクセルを見つけることができると強く感じていることに注意する必要があります。「オーバーラップ」が多いように見えるアプリケーションでは、同じではない場合があります。

この別の質問(問題の感触をつかみ始めたばかりです)のコメントとここでのコメントを検討した後、実行可能なアルゴリズムを思いついたのではないかと思います。

これが私がこれまでに得たものです。以下で説明する方法は要約で書かれていますが、私のアプリケーションでは、各「テスト」は点と色で識別されるピクセルであり、「結果」は画像のアイデンティティを表します。これらの画像の識別は私の最終目標です。

T1からT4までの番号が付けられた次のテストについて考えてみます。

  • T1:ABC
  • T2:B
  • T3:ACD
  • T4:AD

このテストのリストは次のように解釈できます。

  • テストT1が真の場合、AまたはBまたはCの結果があると結論付けます。
  • テストT2が真の場合、結果はBであると結論付けます。
  • テストT3が真の場合、AまたはCまたはDの結果があると結論付けます。
  • テストT4が真の場合、AまたはDの結果があると結論付けます。

個々の結果A、B、C、Dごとに、明確な結果をテストできるようにするテストの組み合わせ(理想的には1つのテスト)を見つけたいと思います。

直感を適用し、画面を少し目を細めることで、次のテストの配置にたどり着くことができます。

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

<-次のテストがtrueと評価された場合、'は'と読む必要があります)

直感と目を細めることは問題ありませんが、少なくともC#5.0まではこれらの手法を言語に組み込むことはおそらくないので、ここでは、より少ない言語で実装するためのメソッドを形式化する試みを行います。

結果を見つけるにはR

  1. Tr目的の結果Rと不要な結果が最も少ない(理想的には他の結果がない)テストを見つけます
  2. テストで結果が得られ、R他に何も得られない場合は、終了です。RどこTrが真実であるかを一致させることができます。
  3. Xテストでのすべての不要な結果に対してTr;
    • Tn(a)与えるRが、与えない最短のテストを見つけますX。そのようなテストが見つかった場合は、Rどこに一致させることができます(T && Tn)
    • (b)条件(a)に一致するテストがない場合はTx、を含むXが含まない最短のテストを見つけますRX(そのようなテストは、テストの結果として排除されますTr)。R次に、どこでテストできますか(T && ¬Tx)

次に、A、B、C、Dの各結果について、これらのルールに従うようにします。

参考までに、ここでもテストを行います。

  • T1:ABC
  • T2:B
  • T3:ACD
  • T4:AD

のために

ルール(1)に従って、結果Aを与える最も単純なテストであるT4から始めます。しかし、それはまた、望ましくない結果である結果'D'を与えます。ルール(3)によれば、テストT1には「A」が含まれているが「D」は含まれていないため、テストT1を使用できます。

したがって、Aをテストできます。

A <- T4 && T1

Bの場合

「B」を見つけるために、「B」の最短テストであるテストT2をすばやく見つけ、結果「B」のみが得られるため、終了します。

B <- T2

Cの場合

「C」を見つけるために、T1とT3から始めます。これらのテストの結果も同様に短いため、開始点としてT1を任意に選択します。

(3a)によれば、「C」を含み、「A」を含まないテストを見つける必要があります。この条件を満たすテストはないため、最初のテストとしてT1を使用することはできません。T3にも同じ問題があります。

(3a)を満たすテストが見つからないため、条件(3b)を満たすテストを探します。「C」ではなく「A」を与えるテストを探します。テストT4がこの条件を満たすことがわかります。したがって、Cをテストできます。

C <- T1 && ¬T4

Dの場合

Dを見つけるために、T4から始めます。T4には不要な結果Aが含まれています。結果Dは得られるがAは得られないテストは他にないため、Aは得られるがDは得られないテストを探します。テストT1はこの条件を満たすため、次のようにDをテストできます。

D <= T4 && ¬T1

これらの結果は良好ですが、100%の信頼性を得るのに十分なほどこのアルゴリズムをデバッグしたとは思いません。私はそれについてもう少し考えて、多分それがどのように持ちこたえるかを見るためにいくつかのテストをコード化するつもりです。残念ながら、アルゴリズムは非常に複雑であるため、慎重に実装するには数分以上かかります。私がさらに何かを結論付けるまでには数日かかるかもしれません。

アップデート

(a)を探してから(b)を探すよりも、(a)または(b)を満たすテストを同時に探すのが最適であることがわかりました。最初に(a)を探すと、いくつかの(b)テストを許可することで、より短いリストを取得した場合に、テストの長いリストを取得する可能性があります。

于 2010-05-20T10:53:30.283 に答える