Для примера я создал на популярной площадке, которая отображает номера в виде картинок, объявление.

Вот сам номер:

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

Каждый символ выделяет то, что он не сливается с фоном, а каждый идентичный символ нарисован одним и тем же образом. Для начала уберем прозрачность:
Отрезаем лишнее:
Метод GetCorner особо описывать не буду. Вкратце, он попиксельно сравнивает цвета и возвращает верхнюю левую или нижнюю правую точки, обрамляющего полезную область, прямоугольника.
Далее разбираем полученную картинку на символы и добавляем их в коллекцию. Я использовал алгоритм, который каждую итерацию отщипывает по символу слева:
Ключевых момента здесь 2:
1. stringPattern представляет собой сроку «8929520-51-488926959-74-93», каждый символ которой соответствует графическому представлению символа.
2. Сущность, которая описывает символ:
Теперь, вернувшись к номеру в объявлении, остается только лишь сколотить аналогичную коллекцию (с одним отличием: символьное представление для каждого элемента будет занимать пробел) и сравнить каждый элемент со словарем.
В результате мы имеем коллекцию символов, для которой железяка подобрала и правильно проставила все цифры.

Распознавание букв алфавита, к примеру, буквы «й», несколько сложнее, за счет того, что они имеют составные части и требуют более сложного алгоритма нахождения обрамляющего прямоугольника, но сам алгоритм сравнения при этом будет тот же.
P.S. Что касается сторонних библиотек, в то время я находил их несколько, среди которых (впрочем, названия прочих я не помню) выбрал для своих целей библиотеку MODI от Microsoft (она входила в состав MS Office). Текст распознавала она отлично. Из минусов — в контексте одного процесса могла работать только одна процедура распознания, т.е. просто распаралеливаться в несколько потоков она не хотела.

Вот сам номер:

Прежде всего мне нужен будет словарь всех символов, которые могут встретится в подобных картинках, поэтому начну не с этого телефона, а с обучения. Для этого я нашёл на том же сайте объявлений 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. Сущность, которая описывает символ:
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). Текст распознавала она отлично. Из минусов — в контексте одного процесса могла работать только одна процедура распознания, т.е. просто распаралеливаться в несколько потоков она не хотела.
