Optical Character Recognition силами .NET

Зіткнувся я з цим пристроєм багато років тому, коли з обов'язку служби збирав в мережі деякі дані. Сотнями гігабайт з просторів всесвітньої та глобальної здобував я адреси і телефони, імена та посади, сфери діяльності та іншу потенційно корисну інформацію для компанії. Що з нею далі робила машина компанії мені не повідомлялося, так і я, загалом-то, не дуже вже й цікавився. Знаю лише, що фильтровалась вона особливим способом, так складувалося у залізних скринях серверної і періодично використовувалася в благих, безумовно, цілях. Робота була не курна і була б вона нудна, як сольна кар'єра Влада Сташевського, якщо б не одне зауваження, вірніше сказати, особливість — сервіси, тобто довідники, які так люб'язно надавали мені інформацію: іноді вони скупилися і вредничали, немов красиві дівчата. Блокували мій IP, просили ввести їм капчу, деякі відверто підсовували неправдиву інформацію, але найцікавіші були ті, що не дозволяли дивитися їх текст в HTML, а кокетливо відображали його у вигляді намальованих на картинці символів. Ось вони, самі того не відаючи, і скрашували, шельмочки, мої сірі будні. І був у мене тоді особливий інтерес, навіть сказати, азарт — розпізнати той текст на зображенні без допомоги сторонніх бібліотек (про них я, може, скажу пізніше), а лише засобами прекрасного у всіх відносинах .NET. І тепер, багато років опісля, я хотів би, з вашого дозволу, перейнятися, що називається, ностальгією.

Для прикладу я створив на популярній майданчику оголошень, що любить, коли на її дані (номери телефонів) тільки дивляться і не чіпають, оголошення з неіснуючому номером телефону, щоб він пройшов шлях трансформації string в PNG і знову, але вже моєю волею, брудно звернений був string.



Ось сам номер:



Насамперед мені потрібен буде словник усіх символів, які можуть зустрітися в подібних картинках, тому почну не з цього телефону, а з навчання залізяки. Для цього я знайшов на тому ж сайті оголошень 2 телефону, які містили в собі всі можливі 10 цифр і склеїв їх в одне зображення:



Доки це не заплутані сучасні капчі, розпізнати які не в змозі не тільки програма, але і мозок людини, все досить просто. Кожен символ виділяє те, що він не зливається з фоном, а кожен ідентичний символ намальований одним і тим же чином. Для початку заберемо прозорість:

void RemoveAlphaChannel(Bitmap src)
{
for (int y = 0; y < src.Height; y++)
for (int x = 0; x < src.Width; x++)
{
var pxl = src.GetPixel(x, y);
if (pxl.A == 0) src.SetPixel(x, y, Color.FromArgb(255, 255, 255, 255));
}

}


Відрізаємо зайве:

private Bitmap CropImage(Bitmap sourceBitmap)
{
var upperLeft = GetCorner(sourceBitmap, true);
var lowerRight = GetCorner(sourceBitmap, false);
var width = lowerRight.X - upperLeft.X;
var height = lowerRight.Y - upperLeft.Y;

Bitmap target = new Bitmap(width, height);

using (Graphics g = Graphics.FromImage(target))
{
g.DrawImage(sourceBitmap, new Rectangle(0, 0, target.Width, target.Height), new Rectangle(ul, new Size(width, height)), GraphicsUnit.Pixel);
}

return target;
}

Метод GetCorner особливо описувати не буду. Коротко, він попіксельно порівнює кольору і повертає ліву верхню або нижню праву точки, що обрамляє корисну область, прямокутника.

Далі розбираємо отриману картинку на символи і додаємо їх в колекцію. Я використовував алгоритм, який кожну ітерацію відщипує, як кізка, по символу ліворуч. В процесі своєї плюгавой еволюції він кілька порос костыликами, але він досить нудний і робити з нього щось гарне я не хочу, думаю, і так все зрозуміло:


private void CropChars(Bitmap bitmapPattern, string stringPattern)
{
var croped = CropImage(bitmapPattern);

RemoveAlphaChannel(croped);

int cntr = 0;

for (int x = 0; x < croped.Width; x++)
{
for (int y = 0; y < croped.Height; y++)
{

if (
(y == croped.Height - 1 && x > 0)
|| (x == croped.Width - 1 && x > 0)
)
{
var rect = new Rectangle(0, 0, x, croped.Height);

//Дублі пропускаємо
if (_charInfoDictionary.FirstOrDefault(c => c.Char == stringPattern[cntr]) == null)
_charInfoDictionary.Add(new CharInfo(CropImage(croped, rect), stringPattern[cntr]));

++cntr;

if (croped.Width - x <= 1) return;

croped = CropImage(croped, new Rectangle(x, 0, croped.Width - x, croped.Height));
x = 0;
}

if (!IsEmptyPixel(croped.GetPixel(x, y)))
{
break;
}
}

}

}


Ключові моменти тут 2:

1. stringPattern являє собою терміну «8929520-51-488926959-74-93», символ якої відповідає графічному представленню символу.

2. Сутність, яка описує символ, я назвав CharInfo:

public class CharInfo
{

//Послідовність яркостей
public int[] _hsbSequence;

//Кількість областей, на які будуть поділені символи, для складання послідовності яскравостей (по горизонталі й вертикалі)
private const int XPoints = 4;
private const int YPoints = 4;

//Символьне уявлення сутності
public char Char { get; set; }

//Графічне представлення сутності
public Bitmap CharBitmap { get; private set; }

public CharInfo(Bitmap charBitmap, char letter)
{
Char = letter;

CharBitmap = charBitmap;

//Стискаємо наш символ у відповідності з кол-вом областей
Bitmap resized = new Bitmap(charBitmap, XPoints, YPoints);

_hsbSequence = new int[XPoints * YPoints];

int i = 0;

//Заповнюємо послідовність яркостями*10. Сама яскравість, це double від 0.0(чорне) до 1.0(біле)
for (int y = 0; y < YPoints; y++)
for (int x = 0; x < XPoints; x++)
_hsbSequence[i++] = (int)(resized.GetPixel(x, y).GetBrightness()*10);

}

/// <summary>
/// Метод порівняння з іншим символом, порівнює послідовності яркостей
/ / / < /summary>
/ / / < param name="charInfo"></param>
/ / / < returns>Кількість збігів</returns>
public int Compare(CharInfo charInfo)
{
int matches = 0;

for (int i = 0; i < _hsbSequence.Length; i++)
{
if (_hsbSequence[i] == charInfo._hsbSequence[i]) ++matches;
}

return matches;
}
}


Тепер, повернувшись до номеру в оголошенні, залишається лише сколотити аналогічну колекцію (з однією відмінністю: символьне подання для кожного елемента буде займати пробіл) і порівняти кожен елемент зі словником.

public IEnumerable<CharInfo> Recognize(Bitmap bitmap)
{
RemoveAlphaChannel(bitmap);

var charsToRecognize = CropChars(bitmap);

List<CharInfo> result = new List<CharInfo>();

foreach (var charInfo in charsToRecognize)
{
CharInfo closestChar = null;

int maxMatches = 0;

foreach (var dictItem in _charInfoDictionary)
{
var matches = dictItem.Compare(charInfo);

if (matches > maxMatches)
{
maxMatches = matches;
closestChar = dictItem;
}


}
result.Add(closestChar);

}
return result;
}



В результаті ми маємо колекцію символів, для якої вже утворена залізяка підібрала і правильно проставила всі цифри.

StringBuilder sb = new StringBuilder();
foreach (var charInfo in charsToRecognize)
sb.Append(charInfo.Char);

textBox1.Text = sb.ToString();



Розпізнавання літер алфавіту, наприклад, букви «ї», дещо складніше, за рахунок того, що вони мають складові частини і вимагають більш складного алгоритму знаходження обрамляє прямокутника, але сам алгоритм порівняння при цьому буде той же.

P.S. Що стосується сторонніх бібліотек, в той час я знаходив їх кілька, серед яких (втім, назви інших я не пам'ятаю) вибрав для своїх цілей бібліотеку MODI від Microsoft (вона входила до складу MS Office). Текст розпізнавала вона відмінно, як справдешній поліграф. З мінусів — в контексті одного процесу могла працювати тільки одна процедура розпізнавання, тобто просто распаралеливаться в кілька потоків вона не хотіла.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.