Оптимизированный алгоритм черно-белых пикселей OCR - PullRequest
8 голосов
/ 12 февраля 2010

Я пишу простое решение для распознавания текста для конечного набора символов.То есть я точно знаю, как будут выглядеть все 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 на 11px (10px на 11px - это фактические размеры символа в моей настоящей программе. Я упростил это до 5px на 5px в этой публикации, так как «рисовать» буквы с помощью 0 гораздо прощеи 1 на меньшем изображении).

Теперь, когда я даю ему часть изображения размером 10 на 11 пикселей для анализа с помощью оптического распознавания символов, необходимо выполнить каждую букву (26) на каждом пикселе (10).* 11 = 110), что означало бы 2860 (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

После анализа уникальногохарактеристики каждого персонажа, я могу значительно сократить количество тестов, которые необходимо выполнить, чтобы проверить для персонажа.Например, для символа «я» я мог бы определить его уникальные характеристики как наличие черного пикселя в координате (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 баллами, как буква «я» в моемПример был в состоянии).


Альтернативное решение, которое я придумал, заключается в использовании хеш-таблицы, которая сократит ее с 2860 итераций до 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, что является значительным улучшением (например, вместо того, чтобы занимать 26 минут, это займет 1 минуту).


Обновление: Большинство предоставленных решений пока не решают проблему определения уникальных характеристик персонажа и скорее предоставляют альтернативные решения. Я все еще ищу это решение, которое, насколько я могу судить, является единственным способом добиться самой быстрой обработки распознавания.

Я только что нашел частичное решение:

Для каждого пикселя в сетке сохраняйте буквы с черным пикселем.

Используя эти буквы:

  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 символ, то есть в данном случае это должно быть «я», и мы нашли нашу уникальную характеристику.

Вы можете сделать то же самое для пикселей, которые будут белыми. Обратите внимание, что bucket (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), мы знаем, что это либо «А», либо «О». Поэтому нам нужен простой способ определить разницу между буквой «А» и буквой «О». Мы могли бы искать черный пиксельный сегмент, который содержит «О», но не «А» (например, 2,4), или белый пиксельный сегмент, который содержит «О», но не «А» (например, 1,1). Любой из них можно использовать в сочетании с (3,2) пикселем, чтобы однозначно идентифицировать букву «О» только с двумя тестами.

Это похоже на простой алгоритм, когда есть 5 символов, но как бы я это сделал, если перекрываются 26 букв и намного больше пикселей? Например, предположим, что после (3,2) пиксельного теста было найдено 10 различных символов, которые содержат пиксель (и это было наименьшим из всех сегментов). Теперь мне нужно найти отличия от 9 других персонажей, а не только от 1 другого персонажа. Как бы я достиг своей цели - получить как можно меньше проверок и убедиться, что я не выполняю посторонние тесты?

Ответы [ 7 ]

4 голосов
/ 12 февраля 2010

У меня нет ответа, но вот некоторые границы вашего возможного решения:

Если вы хотите, чтобы "использовать X пикселей в качестве ключа", то вам нужно по крайней мере ceiling(log2(number of characters)) пикселей. Вы не сможете устранить неоднозначность букв с меньшим количеством битов. В вашем случае попытка найти 5 пикселей эквивалентна поиску 5 пикселей, которые разбивают буквы на независимые разделы. Это, вероятно, не так просто.

Вы также можете использовать предложение Морона (хе-хе) и построить дерево, основанное на частотах букв языка, который вы сканируете, аналогично кодированию Хаффмана . Это заняло бы больше места, чем 5 битов на букву, но, вероятно, было бы меньше при условии степенного распределения использования букв. Я хотел бы пойти с этим подходом, поскольку он позволяет вам искать определенный раздел для каждого узла, а не искать набор разделов.

4 голосов
/ 12 февраля 2010

Вы можете создать дерево.

Выберите пиксель и разделите буквы на два сегмента, исходя из того, что пиксель белый или черный. Затем выберите второй пиксель, разделите сегменты на два сегмента, каждый на основе этого пикселя и т. Д.

Вы можете попытаться оптимизировать глубину дерева, выбрав пиксели, которые дают сегменты, приблизительно равные по размеру.

Создание дерева - это однократный шаг предварительной обработки. Вам не нужно делать это несколько раз.

Теперь, когда вы получите соответствующий алфавит, следуйте по дереву на основе установленных / не установленных пикселей и получите ваше письмо.

1 голос
/ 12 февраля 2010

Один из способов - определить черный пиксель примерно в половине букв и белый в другом наборе. Затем его можно использовать для разделения букв на две группы, используя один и тот же алгоритм рекурсивно для обеих половин, пока вы не достигнете отдельных символов.

Если вы не можете найти один пиксель, который разделяет наборы на два, вам, возможно, придется перейти к группе из двух или более пикселей, но, надеюсь, использование отдельных пикселей должно быть достаточно хорошим.

Чтобы найти пиксель, начните с массива целых чисел, того же размера, что и ваши буквы, инициализируйте все элементы до 0, а затем увеличьте элементы, если соответствующий пиксель в букве (скажем) черный. Вас интересуют те, которые находятся в (примерно) диапазоне 10 & le; sum & le; 16 (для верхнего уровня нижние уровни должны будут использовать другие границы).

1 голос
/ 12 февраля 2010

Почему бы просто не рассматривать изображение как 25-разрядное целое число? 32-битное int может работать. Например, буква «I» может рассматриваться как целое число 14815374 в десятичном виде, поскольку ее двоичное выражение - 0111000100001000010001110. Для вас удобно сравнивать два изображения с операцией «==» как два целых числа.

1 голос
/ 12 февраля 2010

У меня нет алгоритма для предоставления вам ключевых функций, но вот некоторые вещи, которые могут помочь.

Во-первых, я бы не стал слишком беспокоиться о поиске характерного пикселя для каждого символа, потому что в среднем проверка соответствия данного символа заданной полосе (5x5) двоичного изображения не должна занимать более 5-7 проверок, чтобы сказать, что нет совпадения. Зачем? Вероятность. Для 7 двоичных пикселей существует 2 ** 7 = 128 различных возможностей. Это означает, что есть вероятность 1/128 <1% совпадения символов даже до 7 пикселей. Просто убедитесь, что вы остановили сравнения, когда обнаружите несоответствие. </p>

Во-вторых, если вы не хотите создавать хеш-таблицу, вы можете рассмотреть возможность использования trie для хранения всех данных вашего персонажа. Он будет использовать меньше памяти, и вы будете проверять все символы одновременно. Поиск не будет таким быстрым, как в хэш-таблице, но вам также не придется преобразовывать строку. В каждом узле дерева может быть только 2 потомка. Например, если у вас есть два символа 2x2 (назовем их A и B):

A   B
01  00
10  11

У вас есть только один потомок на первом узле - только слева (ветвь 0). Переходим к следующему узлу. У этого есть два потомка, левая (0) ветвь ведет к остальной части B, и правая (1) ветвь ведет к остальной части A. Вы получаете картину. Дайте мне знать, если эта часть не ясна.

0 голосов
/ 20 мая 2010

Я иду по тому же пути, пытаясь изобрести алгоритм, который даст мне минимальное количество тестов, которые я могу использовать, чтобы сопоставить изображение с тем, которое я видел ранее. Мое приложение - OCR, но в ограниченной области распознавания изображения из фиксированного набора изображений как можно быстрее.

Мое основное предположение (которое, я думаю, совпадает с вашим или было таким же) заключается в том, что если мы можем идентифицировать один уникальный пиксель (где пиксель определяется как точка на изображении плюс цвет), то мы нашли идеальный (самый быстрый) тест для этого изображения. В вашем случае вы хотите найти буквы.

Если мы не можем найти один такой пиксель, то мы (неохотно) ищем два пикселя, которые в комбинации являются уникальными. Или три. И так далее, пока у нас не будет минимального теста для каждого из изображений.

Я должен отметить, что у меня есть сильное чувство, что в моем конкретном домене я смогу найти такие уникальные пиксели. Это может быть не так для вашего приложения, где у вас, похоже, много «перекрытий».

После рассмотрения комментариев в этого другого вопроса (где я только начинаю понимать проблему) и комментариев здесь, я думаю, я мог бы придумать работоспособный алгоритм.

Вот что у меня так далеко. Метод, который я описываю ниже, написан в аннотации, но в моем приложении каждый «тест» представляет собой пиксель, идентифицируемый точкой плюс цвет, а «результат» представляет идентичность изображения. Идентификация этих изображений - моя конечная цель.

Рассмотрим следующие тесты, пронумерованные от 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,

  1. Найдите тест Tr, который дает желаемый результат R и наименьшее количество нежелательных результатов (в идеале нет других)
  2. Если тест дает результат R и ничего больше мы не закончили. Мы можем сопоставить R, где Tr истинно.
  3. За каждый нежелательный результат X в тесте Tr;
    • (a) Найдите самый короткий тест Tn, который дает R, но не X. Если мы найдем такой тест, то сможем сопоставить R, где (T && Tn)
    • (b) Если тест не соответствует условию (а), найдите кратчайший тест Tx, который включает X, но не включает R. (Такой тест исключит 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 с

A <- T4 && T1

для B

Чтобы найти «B», мы быстро находим тест T2, который является самым коротким тестом для «B», и, поскольку он дает только результат «B», мы закончили.

B <- T2

Для C

Чтобы найти 'C', мы начнем с T1 и T3. Поскольку результаты этих тестов одинаково короткие, мы произвольно выбираем T1 в качестве отправной точки.

Теперь согласно (3а) нам нужно найти тест, который включает «С», но не «А». Поскольку ни один тест не удовлетворяет этому условию, мы не можем использовать T1 в качестве первого теста. Т3 имеет ту же проблему.

Будучи неспособным найти тест, удовлетворяющий (3a), мы теперь ищем тест, удовлетворяющий условию (3b). Мы ищем тест, который дает «А», но не «С». Мы можем видеть, что тест T4 удовлетворяет этому условию, поэтому мы можем проверить на C с

C <- T1 && ¬T4

Для D

Чтобы найти D, мы начнем с T4. T4 включает нежелательный результат A. Нет других тестов, которые дают результат D, но не A, поэтому мы ищем тест, который дает A, но не D. Тест T1 удовлетворяет этому условию, поэтому мы можем проверить для D с

D <= T4 && ¬T1

Эти результаты хороши, но я не думаю, что я достаточно отладил этот алгоритм, чтобы иметь 100% уверенность. Я собираюсь подумать об этом немного больше и, возможно, написать несколько тестов, чтобы увидеть, как это работает. К сожалению, алгоритм достаточно сложен, поэтому его реализация займет более нескольких минут. Может пройти несколько дней, прежде чем я сделаю вывод.

Обновление

Я обнаружил, что оптимально одновременно искать тесты, которые удовлетворяют (a) ИЛИ (b), а не искать (a) и затем (b). Если мы сначала ищем (а), мы можем получить длинный список тестов, когда мы могли бы получить более короткий список, разрешив некоторые (б) тесты.

0 голосов
/ 15 февраля 2010

Хорошо, я разобрался с решением.

Вы просто используете поиск по глубине для каждого отдельного пикселя с любой другой комбинацией пикселей, пока не найдете набор уникальных характеристик буквы. Выполняя поиск в глубину, убедитесь, что вы не начинаете с 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.CharsWithS SimilarProperites.IsAlwaysWhite ()" или "IsAlwaysBlack ()", вы можете создать составную карту на каждой итерации очереди:

  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 символов.

...