Оптимизированный алгоритм черно-белых пикселей OCR
Я пишу простое решение OCR для конечного набора символов. То есть я точно знаю, как будут выглядеть все 26 букв в алфавите. Я использую C# и могу легко определить, должен ли данный пиксель рассматриваться как черный или белый.
я генерирую матрицу черно-белых пикселей для каждого символа. Так, например, буква I (заглавная i) может выглядеть следующим образом:
01110
00100
00100
00100
01110
Примечание: все пункты, которые я использую позже в этом post, предположим, что верхний левый пиксель (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 на меньшем изображении).
теперь, когда я даю ему 10px на 11px часть изображения для анализа с OCR, ему нужно будет запускать каждую букву (26) на каждом пикселе (10 * 11 = 110), что будет означать 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, значительно улучшением (например вместо его принимая 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 характер, то есть он должен быть "я" в этом случае, и мы нашли нашу уникальную характеристику.
вы также можете сделать то же самое для пикселей, которые будут белыми. Обратите внимание, что ведро (2,0) содержит все буквы, кроме "L", это означает, что его можно использовать в качестве теста белого пикселя. Аналогично, (2,4) не содержит "а".
ведра, которые содержат все буквы или ни одну из букв, могут быть немедленно отброшены, поскольку эти пиксели не могут помочь определить уникальную характеристику (например, 1,1; 4,0; 0,1; 4,4).
это становится сложнее, когда у вас нет теста на 1 пиксель для буквы, например, в случае " O " и "B". Давайте пройдем тест на "О"...
он содержится в следующих периодах:
// 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 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". Поэтому нам нужен простой способ отличить " а " от "О". Мы могли бы либо искать черное пиксельное ведро, содержащее "O", но не " A " (например, 2,4), либо белое пиксельное ведро, содержащее "О", но не " а " (например, 1,1). Любой из них может использоваться в сочетании с пикселем (3,2) для уникальной идентификации буквы " O " только с помощью 2 тестов.
это кажется простым алгоритмом, когда есть 5 символов, но как я могу это сделать, когда есть 26 букв и намного больше пикселей, перекрывающихся? Например, предположим, что после теста (3,2) пикселя он обнаружил 10 разных символов, содержащих пиксель (и это было наименьшее из всех ведер). Теперь мне нужно найдите отличия от 9 других символов вместо только 1 другого символа. Как бы я достиг своей цели получения наименьшего количества проверок, насколько это возможно, и обеспечения того, чтобы я не выполнял посторонние тесты?
7 ответов
У меня нет ответа, но вот некоторые границы вашего окончательного решения:
Если вы хотите прямо "использовать X пикселей в качестве ключа", то вам потребуется не менее ceiling(log2(number of characters))
пикселей. Вы не сможете разобрать буквы с меньшим количеством битов. В вашем случае попытка найти 5 пикселей эквивалентна поиску 5 пикселей, которые разделяют буквы на независимые разделы. Наверное, это не так просто.
вы также можете использовать (хехех идиота) предложение и построить дерево, основанное на частотах букв языка, который вы сканируете, похоже на кодирование Хаффмана. Что бы занимают больше места, чем 5 бит на букву, но, вероятно, будет меньше, если распределение власти-закон использования письмо. Я бы пошел с этим подходом, поскольку он позволяет искать определенный раздел для каждого узла, а не искать набор разделов.
вы можете создать дерево.
выберите пиксель и разделите буквы на два ведра, основываясь на том, что пиксель белый или черный. Затем выберите второй пиксель, разделите ведра на два ведра на основе этого пикселя и так далее.
вы можете попытаться оптимизировать глубину дерева, выбрав пиксели, которые дают ведра, которые примерно равны по размеру.
создание дерева является одноразовым шагом предварительной обработки. Вы не должны делать это несколько раз.
теперь, когда вы получаете алфавит, чтобы соответствовать, следуйте за деревом на основе набора пикселей / не установлен и получить письмо.
У меня нет алгоритма, чтобы дать вам ключевые функции, но вот некоторые вещи, которые могут помочь.
во-первых, я бы не слишком беспокоился о поиске характерного пикселя для каждого символа, потому что, в среднем, проверка соответствия данного символа заданной полосе (5x5) двоичного изображения не должна занимать более 5-7 проверок, чтобы сказать, что совпадения нет. Почему? Вероятность. Для 7 двоичных пикселей существует 2**7=128 различных возможностей. Те средства существует 1/128
во-вторых, если вы не хотите делать хэш-таблицу, вы можете использовать бор для хранения всех ваших символьных данных. Он будет использовать меньше памяти, и вы будете проверять всех персонажей сразу. Поиск не будет таким быстрым, как в хэш-таблице, но вам также не придется преобразовывать в строку. На каждый узел в дереве может иметь не более 2 потомков. Например, если у вас есть два символа 2x2 (назовем их A и B):
A B
01 00
10 11
у вас будет только один потомок на первом узле-только слева (ветка 0). Переходим к следующему узлу. У него есть два потомка, левая (0) ветвь ведет к остальной части B, а правая (1) ветвь ведет к остальной части A. Вы получаете картину. Дайте мне знать, если эта часть не ясна.
Почему бы просто не рассмотреть изображение как 25-битное целое число? 32-битный int может работать. Например, букву " I " можно рассматривать как целое число 14815374 в десятичном формате, так как ее двоичное выражение-0111000100001000010001110. Это удобство для вас, чтобы сравнить два изображения с операцией '= = ' как два целого числа.
одним из способов было бы определить пиксель, который черный примерно в половине букв и белый в другом наборе. Затем можно разделить буквы на две группы, используя один и тот же алгоритм на обеих половинах рекурсивно, пока вы не достигнете отдельных символов.
Если вы не можете найти один пиксель, который разбивает наборы на два, вам, возможно, придется перейти к группе из двух или более пикселей, но, надеюсь, с помощью одного пикселя должно быть достаточно хорошо.
найти пиксель, начните с массива целых чисел, такого же размера, как ваши буквы, инициализируйте все элементы до 0, а затем увеличьте элементы, если соответствующий пиксель в букве (скажем) черный. Те, которые вас интересуют, находятся в диапазоне (примерно) 10≤sum≤16 (для верхнего уровня нижние уровни должны использовать другие границы).
хорошо, я нашел решение.
вы просто используете глубину первого поиска на каждом пикселе с каждой другой комбинацией пикселей, пока не найдете набор уникальных характеристик буквы. При выполнении первого поиска глубины убедитесь, что вы не начинаете с 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; }
для каждой итерации, если я не мог найти уникальную характеристику (например, все другие буквы имеют этот пиксель как черный, но эта буква имеет его как белый... или наоборот) я добавляю все последующие пиксели в очередь, которая обрабатывается, создавая экземпляр этого вышеуказанного объекта с правильно заданными свойствами.
некоторый код psuedo:
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)
определить, если "узел.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 символов.
я иду по аналогичному пути, пытаясь изобрести алгоритм, который даст мне минимальное количество тестов, которые я могу использовать, чтобы соответствовать изображению, которое я видел ранее. Мое приложение OCR, но в ограниченной области распознавания изображения из фиксированного набора изображений как можно быстрее.
мое основное предположение (которое, я думаю, совпадает с вашим или было таким же) заключается в том, что если мы можем идентифицировать один уникальный пиксель (где пиксель определяется как точка внутри изображения плюс цвет) затем мы нашли идеальный (самый быстрый) тест для этого изображения. В вашем случае вы хотите найти письма.
если мы не можем найти один такой пиксель, то мы (неохотно) ищем два пикселя, которые в комбинации уникальны. Или три. И так далее, пока у нас не будет минимального теста для каждого из изображений.
я должен отметить, что у меня есть сильное чувство, что в моем конкретном домене я смогу найти такие уникальные пиксели. Это может быть не то же самое для вашего приложения, где вы кажетесь чтобы было много "перекрытий".
рассмотрев в комментарии это другой вопрос (где я только начинаю чувствовать проблему) и комментарии здесь я думаю, что я мог бы придумать работоспособный алгоритм.
вот что у меня есть до сих пор. Метод, который я описываю ниже, написан абстрактно, но в моем приложении каждый "тест" - это пиксель, идентифицированный точкой плюс цвет, а" результат " представляет идентичность изображения. Идентификация из этих образов - моя конечная цель.
рассмотрим следующие тесты, пронумерованные от T1 до T4.
- T1: A B C
- T2: B
- T3: A C D
- T4: A D
этот список тестов можно интерпретировать следующим образом:
- если тест T1 верно мы заключаем, что у нас есть результат A или B или С.
- если тест 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), а не T4 (не A и не D) дает желаемое результат.
и аналогично, D можно найти с комбинацией T4 и (не T1).
в резюме
A <- T4 && T1
B <- T2
C <- T3 && ¬T4
D <- T4 && ¬T1
(где <-
следует читать как "можно найти, если следующие тесты оцениваются как true")
интуиция и косоглазие в порядке, но мы, вероятно, не получим эти методы, встроенные в язык, по крайней мере, до C# 5.0, так что вот попытка формализации метода для реализации на меньших языках.
в найти результат R
,
- найти тест
Tr
что дает желаемый результатR
и наименьшее количество нежелательных результатов (в идеале никаких других) - если тест дает результат
R
и больше ничего мы не закончили. Мы можем соответствовать дляR
здесьTr
- это правда. - для каждого нежелательного результата
X
в тестеTr
;- (a) найти кратчайший тест
Tn
даетR
а неX
. Если мы найдем такое тест, который мы можем затем сопоставить дляR
здесь(T && Tn)
- (b) Если тест не соответствует условию (a), найдите самый короткий тест
Tx
, которая включаетX
но не включает в себяR
. (Такой тест исключил быX
в результате испытанийTr
). Затем мы можем проверитьR
здесь(T && ¬Tx)
- (a) найти кратчайший тест
теперь я постараюсь следовать этим правилам для каждого из желаемых результатов, A, B, C, D.
вот тесты снова для справки;
- T1: A B C
- T2: B
- T3: A C D
- T4: A D
На
согласно правилу (1) мы начинаем с T4, так как это самый простой тест, который дает результат A. Но он также дает результат "D", который является нежелательным результатом. Согласно правилу (3), мы можем использовать тест T1, так как он включает "A", но не включает "Д".
поэтому мы можем проверить на A с
A <- T4 && T1
Для B
чтобы найти "B", мы быстро находим тест T2, который является самым коротким тестом для "B", и поскольку он дает только результат "B", мы закончили.
B <- T2
Для C
чтобы найти "C", мы начинаем с T1 и T3. Поскольку результаты этих тестов одинаково короткие, мы произвольно выбираем T1 в качестве отправной точки.
теперь в соответствии с (3a) нам нужно найти тест, который включает "C", но не "A". Поскольку ни один тест не удовлетворяет этому условию, мы не можем использовать T1 в качестве первого теста. У T3 такая же проблема.
будучи не в состоянии найти тест, который удовлетворяет (3a) теперь мы ищем тест, который удовлетворяет условию (3b). Мы ищем тест, который дает "А", но не "с". Мы видим, что тест T4 удовлетворяет этому условию, поэтому мы можем проверить для C с
C <- T1 && ¬T4
Для D
чтобы найти D, мы начнем с T4. T4 включает нежелательный результат A. нет других тестов, которые дают результат D, но не A, поэтому мы ищем тест, который дает, но не D. тест T1 удовлетворяет этому условию, поэтому мы можем проверить для D с
D <= T4 && ¬T1
эти результаты хороши, но я не думаю, что я достаточно отладил этот алгоритм, чтобы иметь 100% уверенность. Я собираюсь подумать об этом немного больше и, возможно, закодировать некоторые тесты, чтобы увидеть, как он держится. К сожалению, алгоритм достаточно сложен, что займет не мало времени минуты на реализацию тщательно. Может пройти несколько дней, прежде чем я приду к какому-нибудь заключению.
обновление
я обнаружил, что оптимально одновременно искать тесты, удовлетворяющие (a) или (b), а не искать (a), а затем (b). Если мы сначала посмотрим на (А), мы можем получить длинный список тестов, когда мы могли бы получить более короткий список, разрешив некоторые (Б) тесты.