Если есть готовые решения, нет смысла изобретать костыли и велосипеды. С особым цинизмом это утверждение доказали авторы криптолокера, который для своих целей пользовался CryptoAPI :). Справедливо оно и для решения нашей сегодняшней задачи — расшифровки капчи (с образовательными целями, разумеется). Вперед, запускаем Visual Studio!

 

Вступление

Весь процесс предстоящей работы можно условно поделить на несколько этапов:

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

В этом нам помогут:

  • AForgeNet — библиотеки компьютерного зрения и искусственного интеллекта;
  • Tesseract — программа для распознавания текстов;
  • Fanndotnetwrapper — обертка .NET нейросети FANN;
  • алгоритм поиска связанности CCLA от Omar Gameel Salem.

 

Исходник на dvd.xakep.ru

Не забудь скачать сабж, он пригодится тебе при прочтении этой статьи. Никакой малвари, никакого экстремизма — только чистая наука, только OCR-технологии, только хардкор!

 

Подготовительный этап

Запускаем Visual Studio и создаем новый оконный проект на языке C#. Откроем его в проводнике, для того чтобы скопировать туда требуемые файлы.

Начнем с www.aforgenet.com, заходим на сайт и скачиваем архив «AForge.NET Framework-2.2.5-(libs only).zip» по ссылке [Download Libraries Only]. Из него нам понадобятся только следующие библиотеки: AForge.dll, AForge.Imaging.dll, AForge.Imaging.Formats.dll и AForge.Math.dll, другие же можно удалить.

Далее идем на github.com/charlesw/tesseract читать инструкцию по установке Tesseract NuGet Package и языковым дата-файлам. Выяснили, что есть два пути установки пакета NuGet: через консоль или GUI. Проще всего второй вариант, для этого в Visual Studio нашего проекта переходим на вкладку «Сервис -> Диспетчер пакетов NuGet -> Управление пакетами NuGet для решения...». В открывшемся окне переходим на раздел «В сети -> nuget.org», в строке поиска пишем tesseract, и нужный нам пакет «A .Net wrapper for tesseract-ocr» будет первым и единственным в списке. Нажимаем «Установить» и ждем пару секунд, все произойдет автоматически — создадутся новые папки с требуемыми файлами и настройками в проекте. Замечу, что действует этот NuGet Packege только для текущего решения и ничего другого не затронет. В результате мы сможем использовать этот мощный инструмент путем добавления using Tesseract в нужный класс. Остались только готовые языковые пакеты (если таковые потребуются), они находятся тут. Берем только файлы версии 3.02! Копировать их следует в папку «Наш проект\bin\Debug\tessdata», например, у меня тут находятся eng.traineddata, equ.traineddata и другие.

Затем из ресурса code.google.com/p/fanndotnetwrapper/ достаем нейросеть в виде Fann.Net.dll и fanndoubleMT.dll. Кладем их рядом с библиотеками AForgeNet в папке самого проекта.

Последний ингредиент — CCLA лежит тут. Нажимаем «Download ZIP» и в скачанном архиве находим папку ConnectedComponentLabeling, откуда забираем весь проект, или в своем создаем новый класс и копируем в него содержимое из IConnectedComponentLabeling.cs, CCL.cs, Label.cs и Pixel.cs. Когда их код полностью окажется внутри одного класса (с небольшим допилом), ошибок возникать не должно.

Все готово? Тогда последнее. Устанавливаем ссылки на библиотеки AForgeNet и нейросети FANN (References -> Добавить ссылку) для текущего проекта, проверяем, чтобы студия не ругалась на ошибки. Накидываем кнопки, текстбоксы и другие чудеса интерфейса в Form1 на свой вкус, то же самое делаем и с Form2, показанной на картинках.

Да, небольшое дополнение: возможно, что в App.Config тебе придется добавить строку startup useLegacyV2RuntimeActivationPolicy="true", чтобы все это заработало на .NET выше второй версии.

 

Шаг первый. Скачать captcha

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

Итак, на первом этапе нам нужно в автоматическом режиме заполучить требуемое количество образцов. Создадим метод загрузки картинок в папку на рабочем столе с автоматическим нумерованием файлов и поддержкой прокси. Количество обработанных запросов будем выводить в прогрессбар через event.

Чтобы наша форма не подвисла, создадим новый поток Thread downloadImagesThread. И да, все потоки в этом приложении имеют атрибут IsBackground = true. Если пользователь производит загрузку картинок не в первый раз, то сначала проверяем наличие папки для сохранения картинок и их количество, чтобы нумеровать их далее в правильном порядке. Метод, который выполняет всю работу, имеет сигнатуру void DownloadRemoteImageFile(string getUrl, int num, ArrayList proxy, int timeout), где getUrl — адрес картинки; num — количество запросов к ней. Внутри него цикл for по числу num, а номер итерации передается событию if (NewEvent != null) NewEvent(i + 1), которое ловит наш основной класс и присваивает результат прогрессбару progressBar1.Invoke(new Action(() => { progressBar1.Value = indeX; })), расположенному внизу главного окна. Само сохранение картинки производится стандартно.

 

Шаг второй. Обработать изображение

Главная форма
Главная форма

Самый сложный этап, он индивидуален для каждого вида captcha. Мне очень нравятся библиотеки AForgenet, которые очень удобно и эффективно помогают реализовать некоторые фильтры (ColorFiltering, Dilatation, ConservativeSmoothing, CannyEdgeDetector и так далее), большинство кнопок на форме как раз используют этот функционал. Также присутствует возможность указать цвет кликом мыши на нужном участке картинки, сделано это через событие MouseDown на picturebox и передачей координат на картинку для извлечения цвета в pixelColor.

 

Шаг третий. Выделить символы

Здесь используются три готовых решения, расположенные в нижней правой части главной формы, в элементе Tabs. Первый — это Tesseract OCR с возможностью задания размера отступа для найденных символов. Второй — AForge.net, который принимает параметры максимум и минимум высоты и ширины объектов, которые надо выделить, плюс фильтрация. И третий, он же самый сильный, — OtherCCL принимает два параметра, задающих расстояние между пикселями, которые будут считаться соседями (одним символом). В случае если найденные символы слились, то есть пиксели в них расположены вплотную, надо обрабатывать это событие отдельно. Задать размер, который считается нормальным, и при его превышении разделить слившиеся точки в месте самого слабого соприкосновения и повторить проверку заново. Данная надстройка не была реализована, рассматриваемая мной captcha редко выдавала такой финт.

  • Tesseract использует Tesseract.Page page = OCRtesseractengine302.Process(img), далее на этой странице применяется using (var iter = page.GetIterator()). Находим для каждого if (iter.TryGetBoundingBox(PageIteratorLevel.Symbol, out symbolBounds)) значение bool и присваиваем Pix p = iter.GetImage(PageIteratorLevel.Symbol, paddinglevel, out c, out v) уже сам символ, если выражение вернуло true.
  • AForgeNet — поиск связанных частей для пользователя реализован проще, создаем BlobCounter bc = new BlobCounter(), затем даем ему картинку Engine.bc.ProcessImage(image) и на выходе ловим прямоугольники Rectangle[] rects = Engine.bc.GetObjectsRectangles(), которые в цикле foreach вырезаем через Crop crop = new AForge.Imaging.Filters.Crop(new Rectangle(rect.Location, rect.Size)) и сохраняем в массив картинок.
  • CCL1 ищет соседние пиксели в GetNeighboringLabels(Pixel pix), который циклами for (int i = pix.Position.Y - yYy; i <= pix.Position.Y + yYy && i < _height - yYy; i++) и for (int j = pix.Position.X - xXx; j <= pix.Position.X + xXx && j < _width - xXx; j++) проверяет условие if (i > -1 && j > -1 && _board[j, i] != 0) и в случае true выполняет neighboringLabels.Add(_board[j, i]).
 

Шаг четвертый. Сохранить символы

Символы
Символы

Удобство и лень! Кнопка RunINFilter открывает вторую форму, фильтры в которой применяются уже в автоматическом режиме. Их надо настроить заранее под каждый вид captcha отдельно. Задача формы — применить фильтры, разделить символы, сохранить указанные пользователем или автоматически найденные (Tesseract) связи «буква — картинка» в отдельную одноименную папку. Это означает, что в конце проделанной работы база данных для обучения создается автоматически и может быть использована как для Tesseract, так и для FANN. Но с одним условием: для нейросети все картинки должны быть одного размера, что в данной программе считается тоже одним из фильтров (Resize) и задается в правой части главного окна W/H. И не оставляй много пустого пространства на картинках, это собьет FANN с толку. Допустим, у нас есть 1000 картинок и на некоторых из них много пустого белого цвета; нейросеть будет считать, что это часть буквы, и все другие картинки, где тоже много белого цвета по краям, будут приравниваться к ней. Получим неправильный результат, и все придется начинать заново. Понятно, что буквы бывают разного размера, и, например, а по сравнению с f или даже W оставит белый участок сверху или снизу. Но никто не запрещает приводить их к одному размеру, заведомо искажать/сжимать для себя (а точнее, для нейросети).

Сохранение реализовано циклом по 24 элементам массива картинок, и если содержимое не равно null, то получаем textbox.text под этим элементом и сравниваем с пустой строкой. Если строка не пустая, то проверяем, существует ли папка с таким именем, помещая в него эту картинку. Когда строка пустая или содержит более одного символа, результат сохраняется в папку Garbage. Саму captcha с именем, совпадающим с правильным вводом текста, сохраняем в Images, для последующей возможности автоматизации проверки на процент корректного распознавания. Весь прогресс также отображается на progressBar1.

 

Шаг пятый. Обучение

Tesseract Training
Tesseract Training

Начнем с Tesseract, для его обучения требуется создать три файла test.arial.exp0.box, test.arial.exp0.tif, test.arial.exp0, где test — имя словаря; arial — имя шрифта; exp0 — номер файла; box — это текстовый файл, где указаны координаты каждого символа, tif — картинка, третий является копией первого. Для их создания предназначена кнопка GenPapers, которая использует следующий код:

List<GenPapers> genpaperlist = new List<GenPapers> { };
GenPapers tempgenpaper = new GenPapers();
string[] dirs = Directory.GetDirectories(Environment.GetEnvironmentVariable("userprofile") + "\\Desktop\\TESTDATA\\");
foreach (var item in dirs)
{
    string bb = item.Substring(item.LastIndexOf(@"\") + 1);
    if (bb == "Images" || bb == "Garbage") continue;
    genpaperlist.Add(new GenPapers { dir = item.ToString(), files = Directory.GetFiles(item.ToString(), "*.png") });
}
for (int i = 0; i < genpaperlist.Count; i++)
{
    tempgenpaper = (GenPapers)genpaperlist[i];
    using (Graphics g = Graphics.FromImage(BIGbit))
        {
            foreach (var item in tempgenpaper.files)
            {
                ... *Рисуем
            }
        }
}
...
public class GenPapers : List<string>
{
    public String dir { get; set; }
    public String[] files { get; set; }
}

Здесь используется класс GenPapers с двумя полями, первое — имя папки, второе — полный путь для всех картинок внутри этой папки. Далее производится поиск и наполнение объекта genpaperlist данными, после чего в цикле for начинаем работать с каждой директорией в отдельности, рисуя извлеченные данные на большом холсте и попутно записывая координаты для box-файла. Полученный результат требуется задать аргументами к установленному Tesseract в Program Files. Достаточно одного bat-файла, который проведет все действия в автоматическом режиме. Подробная инструкция по обучению находится по адресуcode.google.com/p/tesseract-ocr/wiki/TrainingTesseract3.

FANN Training
FANN Training

Для обучения нейросети FANN использована часть кода из Tesseract, отличие заключается в том, что мы создаем один текстовый файл train.tr, в котором первая строка — количество картинок, количество точек в каждой (ширина, умноженная на высоту) и количество выходов (букв, которые мы ищем). Сама картинка до всего этого проходит обязательную бинаризацию, для того чтобы выделить всего два состояния каждой точки (1 — черный, 0 — белый цвет), и сохраняется далее в этом же файле во всех следующих строках. Для удобства и возможности использовать разные заранее созданные обученные ann-файлы был создан дополнительный текстовый файл CONFIG.txt. Он состоит из одной строки и указывает количество точек и выходов с их значениями, случайно запустить проверку captcha на другом ann-файле не получится.

string a = File.ReadAllText(SaveFilesPath + "CONFIG.txt");
string[] b = a.Split(' ');
int SumPix = Convert.ToInt32(b[0]);
int Outpt = Convert.ToInt32(b[1].Length);
uint[] layers = { (uint)SumPix, (uint)layerS, (uint)Outpt };
net.CreateStandardArray(layers);
net.RandomizeWeights(-0.1, 0.1);
net.SetLearningRate(0.7f);
TrainingData data = new TrainingData();
data.ReadTrainFromFile(SaveFilesPath + "train.tr");
net.TrainOnData(data, 1000, 0, 0.001f);
net.Save(SaveFilesPath + "FANNLearning.ann");

Получаем конфиг, читаем параметры, число слоев (layers) по рекомендации Википедии задано равным 120, все остальное было выбрано случайным образом или подсмотрено в Сети. Скорость обучения зависит от мощности твоего железа и того, что написано выше. Например, i7-4702MQ при 6500 картинок одним ядром был занят минут 20–30.

 

Шаг шестой. Распознавание

Captcha
Captcha

В заключительном этапе реализовано два подхода, но используется тот, обучение которого было проведено. Tesseract 3.02 и FANN находятся в нижней левой части главного окна. Первый умеет искать по английскому (выбираем символы из выпадающего списка), русскому, Math и словарю пользователя. Поиск словаря происходит автоматически, и в подсказке tooltip высвечиваются все доступные. Второй распознает текст по кнопке FANNOCR и выводит в лог (левая часть окна) результат анализа для каждого выбранного символа. Очень удобно смотреть, почему нейронная сеть выбрала тот или иной выход. Рассмотрим, как это работает в случае нейронной сети.

private string OCR(Bitmap img)
{
    ...
    {
        int whx = img.Width * img.Height;
        if (SUMMPIX != whx) { /* Выводим ошибку, не сошлось количество пикселей */}
        double[] input = GetPix(img);
        double[] result = net.Run(input);
        if (tempanswer.Length != result.Length) { /* Выводим ошибку, разное количество выходов */}
        int maxN = FindMax(result);
        answer = Convert.ToString(tempanswer[maxN]);
        if (ToLogEvent != null)
            ToLogEvent(result, tempanswer, answer);
    ...
    }
}

Получили картинку от public-метода, где реализован net.CreateFromFile(SaveFilesPath + "FANNLearning.ann") и чтение конфиг файла, tempanswer — это переменная, равная b[1], в ней перечислены буквы, которые мы ищем. Сравниваем число пикселей, записываем их в массив и прогоняем через обученный ann, выискивая максимально высокий процент совпадения, затем направляем результат в событие, выбрав один выход и получив букву, закрепленную за ним.

 

Обсуждаем результаты

Мои результаты тестирования сильно зависели от количества и качества картинок для обучения, а в случае с нейросетью FANN — и от количества выходов тоже. В среднем captcha, поддавшаяся фильтрам, имела ~80% правильного распознавания, тут многое зависит от усидчивости и желания — чему научишь, то и получишь. Главное — это работает.

 

Заключение

Все описанное в статье можно применить для решения многих других задач. Например, мне при поиске информации для статьи повстречался подробный разбор распознавания образа автомобиля на стоянке. Включай фантазию и Visual Studio! 🙂

 

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии