Столкнулся я с этим устройством много лет назад, когда по долгу службы собирал в сети некоторые данные. Сотнями гигабайт с просторов всемирной и глобальной добывал я адреса и телефоны, имена и должности, сферы деятельности и прочую потенциально полезную для компании информацию. Что с ней дальше делала машина компании мне не сообщалось, да и я, в общем-то, не очень уж и интересовался. Знаю лишь, что фильтровалась она особым способом, да складировалось в железных сундуках серверной и периодически использовалась в благих, безусловно, целях. Работа была не пыльная и была бы она скучна, как сольная карьера Влада Сташевского, если бы не одно замечание, вернее сказать, особенность — сервисы, то есть справочники, которые так любезно предоставляли мне информацию: иногда они жадничали и вредничали, словно красивые девочки. Блокировали мой 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). Текст распознавала она отлично, как заправский полиграф. Из минусов — в контексте одного процесса могла работать только одна процедура распознания, т.е. просто распаралеливаться в несколько потоков она не хотела.
Автор: nenuacho