Пролог
По ходу своей трудовой деятельности получил задачу придумать и реализовать систему учета рекламной информации. Учет заключался в проверке наличия нужной информации на нужном рекламном щите. Щит и полиграфия пронумерованы.
В качестве исходной информации для системы предлагалось использовать фото. После
Собственно на этом постановка задачи заканчивается и начинается повествование о реализации.
Задача решается в три действия:
- Нахождение нужного прямоугольника на изображении.
- Распознавание текста.
- Проверка правильности распознавания.
Действие первое — поисковое
Чтобы найти нужный прямоугольник на картинке проще всего найти все куски, которые можно назвать прямоугольниками, а затем по определенным параметрам отфильтровать их. Для поиска прямоугольников на изображении был использован немного допиленный стандартный пример из OpenCV — squares.cpp, из которого взята функция поиска прямоугольников.
Процедура поиска фигур достаточно примитивная и при наличии на входе сложной картинки с множеством цветовых границ и переходов выдает кучу прямоугольников, из которых еще до процедуры распознавания, нужно повыкидывать ненужное.
Ненужное фильтруется нескольким критериям:
1. Соотношение ширины и высоты.
В программе стоит критерий отсечки (r.width < 5*r.height), его можно усовершенствовать и использовать более точно условие условие с дельтой.
Тут главное, чтобы фотограф не проявлял фантазию и не снимал объект, повернув камеру на 90o (сфотографируй меня с ногами).
2. Убрать приблизительно одинаковые фигуры.
Еще один момент: перед фильтрацией спрямляем прямоугольники, так как рука у фотографа может дрогнуть и искомый прямоугольник может иметь на фотографии не горизонтально вертикальные границы.
Далее делается нарезка в файл всех собранных прямоугольников.
Опытным путем было установлено, что утилита распознавания лучше отрабатывает картинки черно белого формата, для чего перед записью в файл вызывается метод cvAdaptiveThreshold. Размер блока в процедуре преобразования подбирался эксперементальным путем.
<source lang="cpp"> #include "cv.h" #include "highgui.h" #include <iostream> #include <math.h> #include <string.h> #include <stdio.h> using namespace cv; using namespace std; typedef vector<Point> polygon; typedef vector<polygon> polygonList; ... //Сравнение для фильтрации схожих фигур bool compareRect(const CvRect &r1, const CvRect &r2) { if (!r1.width || !r1.height) return false; if ((float)abs(r1.width- r2.width)/(float)r1.width > 0.05) return false; if ((float)abs(r1.height - r2.height)/(float)r1.height > 0.05) return false; if ((float)abs(r1.x - r2.x)/(float)r1.width > 0.02) return false; if ((float)abs(r1.y - r2.y)/(float)r1.height > 0.02) return false; return true; } //Спрямляем прямоугольник CvRect getRect(const polygon& poly) { CvPoint p1 = cvPoint(10000,10000); CvPoint p2 = cvPoint(-10000,-10000); for (size_t i=0; i < poly.size(); i++) { const Point p = poly[i]; if (p1.x > p.x) p1.x = p.x; if (p1.y > p.y) p1.y = p.y; if (p2.x < p.x) p2.x = p.x; if (p2.y < p.y) p2.y = p.y; } return cvRect(p1.x,p1.y,p2.x-p1.x,p2.y-p1.y); } int main(int argc, char** argv) { if(argc <= 3) { cout << "Wrong Param Count: " << argc << endl; cout << "Usage: findrect infile extension outfolder" << endl; return 1; } char *fileIn = argv[1]; char *fileExt = argv[2]; char *dirOut = argv[3]; char fileOut[128]; polygonList squares; IplImage *Img = cvLoadImage(fileIn,1); Mat image(Img); if(image.empty()) { cout << "Couldn't load " << fileIn << endl; return 1; } findSquares(image, squares); vector<CvRect> rectList; int p = 0; int adaptive_method = CV_ADAPTIVE_THRESH_GAUSSIAN_C; int threshold_type = CV_THRESH_BINARY; int block_size = 65; double offset = 10; for (int j=0; j<squares.size(); j++) { //спрямляем прямоугольник CvRect r = getRect(squares[j]); if (r.width < 5*r.height) continue; //не добавляем похожие по размерам bool doContinue = false; for (int k=0; k<rectList.size(); k++) if (compareRect(r, rectList[k])) { doContinue = true; break; } if (doContinue) continue; rectList.push_back(r); //копируем нужный участок с исходника cvSetImageROI(Img, r); IplImage *dst = cvCreateImage(cvSize(r.width, r.height), Img->depth, Img->nChannels); IplImage *gray = cvCreateImage(cvSize(r.width, r.height), 8, 1); IplImage *bw = cvCreateImage(cvSize(r.width, r.height), 8, 1); cvCopy(Img, dst, NULL); cvResetImageROI(Img); //выводим информацию о файле, она будет нужна для последующей обработки в php sprintf(fileOut,"%s/%d.%s",dirOut, p, fileExt); cout << fileOut << endl; p++; //преобразуем в черно-белый cvCvtColor(dst,gray,CV_RGB2GRAY); cvAdaptiveThreshold(gray, bw, 255, adaptive_method,threshold_type,block_size,offset); cvSaveImage(fileOut, bw); cvReleaseImage(&dst); cvReleaseImage(&gray); cvReleaseImage(&bw); } return 0; }
Действие второе — распознавательное
На вход утилитке распознавания поступает как нормальный контент так и мусор.




Как и было заявлено ранее, для распознавания используем утилиту от Google — tesseract.
Можно было использовать и другие средства для распознавания, тестировалось также cuniform.
Но tesseract был выбран по причине того, что по нему много информации и была понятная инструкция по его тренировке на свой набор символов.
Тренировка на свой алфавит была сделана с несколькими целями:
- Словарь для распознавания цифр — должен состоять из 10 символов, не нужны буквы и другие символы. Короткий набор вероятность ошибки.
- В принципе, на 1-м можно было и остановиться — у tesseract есть режим распознавания только цифр. Можно было бы использовать его и не заморачиваться созданием своего словаря.
Но результаты тестирования подвигли еще к одной идее и причина в следующем: обычные шрифты (входящие в стандартный набор), имеют символы цифр с точки зрения OCR похожие друг на друга: цифра «7» при определенных условиях похожа на «1», цифра «3» на «8», и т.д.
Поэтому и было принято решение использовать шрифт, в котором символ цифр не будут похожи друг на друга. В качестве подсказки для поиска шрифта было название оного — «OCR A Std». Этот шрифт как раз и использован на приведенных выше вырезках.
Таким образом, имеем еще один фактор для снижения вероятности ошибки.
В итоге для tesseract был создан словарь из 10 символов данного шрифта, его и видно на вырезках выше.
Инструкцию по тренингу утилиты приводить не буду, процесс не творческий, механический, в сети инструкций много.
Действие третье — собирательное
Работа системы тестировалась под Ubuntu. Запуск утилит нарезки и распознавания выполняется php.
Здесь же осуществляется окончательная проверка распознанных данных методом контрольной суммы.
Используется алгоритм crc-8.
$imagesout = '/home/toor/www/out'; $findrect = '/home/toor/OCR/OpenCV-2.2.0/samples/cpp/findrect'; $uploaddir = '/home/toor/www/uploads/'; $rectdir = '/home/toor/www/out/'; $tesseract = '/home/toor/OCR/tesseract-3.00/api/tesseract'; ... if (isset($_FILES['userfile']['tmp_name'])) { $uploadfile = $uploaddir. $_FILES['userfile']['name']; if (!move_uploaded_file($_FILES['userfile']['tmp_name'], $uploaddir . $_FILES['userfile']['name'])) { echo "Есть ошибки!"; exit(1); } echo "Файл {$_FILES['userfile']['name']} успешно загружен!"; $cmd = "$findrect $uploadfile tif $imagesout"; exec($cmd, $output); echo count($output)." фрагментов"; $datas = array(); foreach($output as $k => $f) { $recognized = "$rectdir$k.txt"; $cmd = "$tesseract $f $rectdir$k -l nums.ocr"; exec($cmd); if (!file_exists($recognized)) continue; echo "file: $recognized"; $data = file_get_contents($recognized); $data = preg_replace('/\D/','',$data); $data = trim($data); if (!strlen($data)) continue; if (!array_key_exists($data,$datas)) $datas[$data] = 1; else $datas[$data]++; } foreach ($datas as $d => $v) { if ($r = crc_check($d, NUMBER_LEN_1, NUMBER_LEN_CRC_1)) { echo 'Найден номер: '.$r; } if ($r = crc_check($d, NUMBER_LEN_2, NUMBER_LEN_CRC_2)) { echo 'Найден номер: '.$r; } } }
В целом в тестовом режиме система показала себя достаточно неплохо.
Отрабатываются картинки с самых простых телефонов как эта
и до нескольких мегабайт c цифровых фотоаппаратов.
Ссылки
Tesseract
OpenCV
OCR A Std Шрифт
