Делаем программу распознавания автомобильных номеров с IP камер на основе iANPR SDK, OpenCV и Windows Forms за 3 часа

Предисловие: в виду частых поисковых запросов именно по данной статье, снова публикуем ее в новом блоге. Помните, что здесь пример работы с iANPR 1.1 и только Си интерфейс OpenCV.

Бесплатное распознавание автомобильных номеров

Готовые системы распознавания автомобильных номеров могут быть достаточно дорогими, при этом качество распознавания не гарантируется на высоком уровне. Бывают случаи, когда, купив систему распознавания автомобильных номеров, пользователь обнаруживает, что она работает не достаточно достоверно. При использовании iANPR SDK (http://ianpr.org/) вы можете сами спроектировать систему распознавания за очень короткое время и убедиться в качественности работы. Кто может использовать iANPR SDK? В принципе любая компания, которой по тем или иным причинам желает использовать распознавание автомобильных номеров. Однако, среди них можно выделить 2 большие группы по целям использования:

  • для автоматизации въезда на территорию компании – нет нужды покупать дорогостоящее программное обеспечение, которое не удовлетворяет всем потребностям, когда можно купить лицензию iANPR RUS PRO LIMITED за 9950 рублей и сделать все, что нужно самостоятельно, если в компании есть программист;
  • для использования в собственной распространяемой системе видеонаблюдения – конечно, при разработке собственного программного продукта важно все модули разрабатывать самому, однако разработка надежной системы распознавания автомобильных номеров не является тривиальной задачей, и на неё требуются не только высококвалифицированные специалисты с высокой заработной платой, но и месяцы разработки-тестирования. В этом случае покупка лицензии iANPR RUS PRO FULL за 50000 рублей на начальном этапе позволит вам сразу же ввести функцию распознавания автомобильных номеров в вашу систему, что является сравнительно небольшими затратами.

В данной статье будет показано как, используя iANPR SDK, IP камеру, OpenCV и Windows Forms за 3 часа сделать простое приложение для распознавания автомобильных номеров. В конце разработки будет получено приложение следующего вида:

 

1

Что потребуется на компьютере:

После установки OpenCV необходимо в PATH переменной окружения Windows прописать путь до соответствующих dll – у меня это так:

C:\OpenCV246\opencv\build\x86\vc10\bin\

1. Создание и настройка проекта

 

Выбираем Файл -> Создать -> Проект

2

Visual C++ -> Приложение Windows Forms.

В результате создается исходный код:

3

Для начала отредактируем следующую форму:

4

Здесь есть место для вывода изображения с камеры, поле ввода- IP адреса и старта работы, поле вывода результатов распознавания.

Настройка путей до *.h и *.lib файлов:

5

Настройка подключаемых библиотек:

6

Естественно, что lib файл для iANPR должен быть уже в каталоге с вашим исходным кодом:

7

Однако, когда мы подключаем *.h файлы:

#include "opencv2/highgui/highgui_c.h"
#include "include\iANPR.h"

То обнаруживаем, что:

8

Чтобы исправить данные ошибки меняем настройки проекта на следующие:

9

После этого программа компилируется.

2. Подключение к IP камеры и вывод изображения

Создадим файлы camera.cpp и camera.h, в которых будем записывать все функции, работающие с изображением и распознаванием. Для начала заполним camera.cpp следующим кодом:

#include "stdafx.h"

#include "camera.h"

#include "opencv2/highgui/highgui_c.h"

#include "include\iANPR.h"

#include

#include

 

CvCapture* capture;

bool stop;

 

DWORD WINAPI ImageProcess(LPVOID)

{

       for (;;)

       {

             IplImage* frame = 0;

             frame = cvQueryFrame(capture);

             if (!frame)

                    break;

             Sleep(50); // небольшая задержка перед следующим кадром

             if (stop) break;

       }

 

       cvReleaseCapture(&capture);

       return 0;

}

 

void Start(char* IP)

{

       char* login = "admin";

       char* password = "111111";

       char buf[256];

 

       sprintf(buf, "http://%s:%s@%s/video.cgi?.mjpg", login, password, IP);

 

       capture = cvCaptureFromFile(buf);

 

       // Создать поток для вывода видео

       DWORD lpT;

       stop = false;

       CreateThread(NULL, 0, ImageProcess, NULL, 0, &lpT);

}

 

void Stop()

{

       stop = true;

 

}

Указатель capture будет хранить сведения о видеопотоке. Здесь для упрощения пользуемся глобальными переменными, что в реальном проекте недопустимо, но поскольку наша цель очень быстрое создание приложения, то мы ограничимся этим. stop указывает поступил ли сигнал на завершение потока.

Функция Start получает на вход IP адрес, которые далее модифицируется в строку:

http://логин:пароль@адрес/video.cgi?.mjpg

Логин и пароль для IP камеры пока записываем жестко. Эту строку передаем функции cvCaptureFromFile, инициализируя создание видеопотока. Сделаем получение изображения с камеры и его распознавание отдельным потоком, для чего с помощью функции CreateThread создаем новый поток, начинающийся в функции ImageProcess.

Функция ImageProcess сразу же входит в бесконечный цикл получения кадра с видеопотока, цикл будет прерываться, если значение stop станет true.

Функция Stop посылает сигнал об остановке, изменяя значение stop на true.

Файл camera.h будет следующим:

#pragma once

 

void Start(char* IP);

void Stop();

 

В файле формы Form1.h добавляем функцию, обрабатывающую нажатие кнопки «Старт»

private: System::Void button1_Click(System::Object^  sender, System::EventArgs^  e) {

       // Нажали кнопку "Старт"         

       pin_ptr<const wchar_t> wch = PtrToStringChars(textBox1->Text);

       int sizeInBytes = (textBox1->Text->Length + 1);

       char *ch = new char[sizeInBytes];

       WideCharToMultiByte(0, 0, wch, -1, ch, sizeInBytes, NULL, NULL);

       Start(ch);

       delete ch;

}

 

Здесь код предназначен для перевода из формата текстового поля в char*, а затем вызов функции Start.

Также добавляем обработку нажатия «Стоп»:

private: System::Void button2_Click(System::Object^  sender, System::EventArgs^  e) {

       Stop();

}

Если теперь скомпилировать и пройтись отладчиком, то видно, что видеопоток создается, а кадры получаются.

Следующее, что нужно сделать, это вывести изображение на форму. Для этого вставим в capture.cpp функцию, преобразующую IplImage* в HBITMAP.

#define WIDTHBYTES(bits) (((bits) + 31) / 32 * 4)

HBITMAP CreateRGBBitmap(IplImage* _Grab)

{

       char *App;

       LPBITMAPINFO lpbi = new BITMAPINFO;

       lpbi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

       lpbi->bmiHeader.biWidth = _Grab->width;

       lpbi->bmiHeader.biHeight = _Grab->height;

       lpbi->bmiHeader.biPlanes = 1;

       lpbi->bmiHeader.biBitCount = 24;

       lpbi->bmiHeader.biCompression = BI_RGB;

       lpbi->bmiHeader.biSizeImage = WIDTHBYTES((DWORD)_Grab->width * 8) * _Grab->height;

       lpbi->bmiHeader.biXPelsPerMeter = 0;

       lpbi->bmiHeader.biYPelsPerMeter = 0;

       lpbi->bmiHeader.biClrUsed = 0;

       lpbi->bmiHeader.biClrImportant = 0;

       void* pBits;

       HBITMAP hBitmap = CreateDIBSection(NULL, lpbi, DIB_RGB_COLORS, (void **)&pBits, NULL, 0);

       delete lpbi;

       if (hBitmap) App = (char*)pBits;

       long int length = 0;

       if (_Grab->nChannels == 1) // Серое или бинарное

       {

             length = _Grab->width*(_Grab->height);

             for (int i = 0; i<_Grab->height; i++)

             {

                    for (int j = 0; j<_Grab->width; j++)

                    {

                           App[_Grab->width * 3 * (_Grab->height - i - 1) + j * 3] = _Grab->imageData[_Grab->width*(i)+j];

                           App[_Grab->width * 3 * (_Grab->height - i - 1) + j * 3 + 1] = _Grab->imageData[_Grab->width*(i)+j];

                           App[_Grab->width * 3 * (_Grab->height - i - 1) + j * 3 + 2] = _Grab->imageData[_Grab->width*(i)+j];

                    }

             }

       }

       if (_Grab->nChannels == 3) // Цветное

       {

             for (int i = 0; i<_Grab->height; i++)

             {

                    // Копируем память

                    memcpy(App + _Grab->width * 3 * (_Grab->height - i - 1), _Grab->imageData + _Grab->width * 3 * i, _Grab->width * 3);

             }

       }

       return hBitmap;

}

 

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

Добавим 2 глобальные переменные в capture.cpp:

HBITMAP Bitmap = NULL;

CRITICAL_SECTION cs;

 

Одна для изображения, вторая для критической секции. Модернизируем ImageProcess следующим образом:

DWORD WINAPI ImageProcess(LPVOID)

{

       CvSize Size = cvSize(320, 240);

       IplImage* small_image = cvCreateImage(Size, 8, 3);

       for (;;)

       {

             IplImage* frame = 0;

             frame = cvQueryFrame(capture);

             if (!frame)

                    break;

             cvResize(frame, small_image);

             EnterCriticalSection(&cs);

             if (Bitmap != NULL) DeleteObject(Bitmap);

             Bitmap = CreateRGBBitmap(frame);

             LeaveCriticalSection(&cs);

             Sleep(50); // небольшая задержка перед следующим кадром

             if (stop) break;

       }

 

       cvReleaseCapture(&capture);

       cvReleaseImage(&small_image);

       DeleteCriticalSection(&cs);

       return 0;

}

 

В функции создаем небольшое изображение, которое будет хранить снимок с камеры. В цикле преобразование изображение, а работу с Bitmap возьмем в границы критической секции. В функции Start нужно добавить инициализацию критической секции:

InitializeCriticalSection(&cs);

Помимо этого в capture.cpp и h добавятся 2 функции:

HBITMAP LoadImage_()

{

       return Bitmap;

}

 

CRITICAL_SECTION* Section_()

{

       return &cs;

}

 

Одна для возвращения изображения, другая для критической секции.

В Form.h необходимо сделать следующие действия.

Ввести новую переменную private: int start_;, которая изначально инициализируется как false. Функции нажатия кнопок модернизируются:

private: System::Void button1_Click(System::Object^  sender, System::EventArgs^  e) {

       // Нажали кнопку "Старт"         

       if (!start_)

       {

             pin_ptr<const wchar_t> wch = PtrToStringChars(textBox1->Text);

             int sizeInBytes = (textBox1->Text->Length + 1);

             char *ch = new char[sizeInBytes];

             WideCharToMultiByte(0, 0, wch, -1, ch, sizeInBytes, NULL, NULL);

             Start(ch);

             start_ = true;

             delete ch;

       }

}

private: System::Void button2_Click(System::Object^  sender, System::EventArgs^  e) {

       if (start_)

       {

             start_ = false;

             Stop();

 

Также добавляем обработчик события закрытия формы:

private: System::Void Form1_FormClosed(System::Object^  sender, System::Windows::Forms::FormClosedEventArgs^  e) {

       start_ = false;

       Stop();

       Sleep(1000);

}

Вешаем на форму таймер, делаем задержку 100 милисекунд и обработчик таймера:

private: System::Void timer1_Tick(System::Object^  sender, System::EventArgs^  e) {

       // Срабатывание таймера                

       if (start_)

       {

             CRITICAL_SECTION* cs = Section_();

             EnterCriticalSection(cs);

             HBITMAP hBitmap = LoadImage_();

             if (hBitmap != NULL)

             {

                    Bitmap^ bmp = Bitmap::FromHbitmap((IntPtr)hBitmap);

                    pictureBox1->Image = bmp;

             }

             LeaveCriticalSection(cs);

       }

}

};

В обработчике, если процесс запущен, то внутри критической секции получается изображение из capture.cpp и передается на pictureBox1.

 

Компилируем все и запускаем. Немного ожидаем подключения к камере после нажатия, но после подключения все работает:

10

3. Распознавание автомобильных номеров

Для распознавания автомобильных номеров также добавляем глобальные переменные в capture.cpp:

// Для распознавания номеров

int all = 100;

CvRect Rects[100];

char** res;

 

Меняем функцию ImageProcess следующим образом:

DWORD WINAPI ImageProcess(LPVOID)

{

       ANPR_OPTIONS a;

       a.Detect_Mode = ANPR_DETECTCOMPLEXMODE;

       a.min_plate_size = 500;

       a.max_plate_size = 25000;

       a.max_text_size = 20;

       a.type_number = ANPR_RUSSIAN_BASE2;

       a.flags = 0;

 

       CvSize Size = cvSize(320, 240);

       IplImage* small_image = cvCreateImage(Size, 8, 3);

       IplImage* object = 0;

 

       char mem[100][20];

       int all_mem = 0;

 

       for (;;)

       {

             IplImage* frame = 0;

             frame = cvQueryFrame(capture);

             if (!frame)

                    break;

             if (!object)

             {

                    object = cvCreateImage(cvGetSize(frame), 8, 1);

                    cvZero(object);

                    object->origin = frame->origin;

             }

             cvCvtColor(frame, object, CV_BGR2GRAY);

             cvSaveImage("test.jpg", object);

             all = 100;

 

             int i1;

             __try

             {

                    i1 = anprPlate(object, a, &all, Rects, res);

             }

             __except (EXCEPTION_EXECUTE_HANDLER)

             {

                    ExitProcess(1);

             }

 

             if (i1 == 0)

             {

                    for (int j = 0; j < all; j++)

                    {

                           if (strlen(res[j]) >= 8)

                           {

                                  int k = 0;

                                  for (int j1 = 0; j1 < all_mem; j1++)

                                        if (strcmp(res[j], mem[j1]) == 0) k = 1; // Если несколько раз подряд один номер, то считается распознан правильно

                                  if (k == 0) continue;

                                  cvRectangle(frame, cvPoint(Rects[j].x, Rects[j].y), cvPoint(Rects[j].x + Rects[j].width,

                                        Rects[j].y + Rects[j].height), CV_RGB(255, 255, 0), 2);

                                  //if ( strlen( res[j] ) == 9 ) res[j][6] = '1';

                                  CvFont font;

                                  float aa = 0.001f*frame->width;

                                  cvInitFont(&font, CV_FONT_HERSHEY_SIMPLEX, aa,

                                        aa, 0, 1, 8);

                                  CvPoint pp2, pp1;

                                  pp2.x = Rects[j].x;

                                  pp2.y = Rects[j].y;

                                  pp1.x = Rects[j].x + 1;

                                  pp1.y = Rects[j].y + 1;

                                  cvPutText(frame, res[j], pp1, &font, CV_RGB(0, 0, 0));

                                  cvPutText(frame, res[j], pp2, &font, CV_RGB(0, 255, 0));

 

                           }

                    }

                    // Копирование в память

                    for (int j = 0; j < all; j++)

                    {

                           for (i1 = 0; i1 < strlen(res[j]); i1++)

                                  if (res[j][i1] == '?') {

                                        i1 = -1;

                                        break;

                                  }

                           if (i1 == -1) continue;

                           if (strlen(res[j]) >= 8)

                           {

                                  strcpy(mem[j], res[j]);

                           }

                    }

                    all_mem = all;

             }

 

             cvResize(frame, small_image);

 

             EnterCriticalSection(&cs);

             if (Bitmap != NULL) DeleteObject(Bitmap);

             Bitmap = CreateRGBBitmap(frame);

             LeaveCriticalSection(&cs);

             Sleep(2); // небольшая задержка перед следующим кадром

             if (stop) break;

       }

 

       cvReleaseCapture(&capture);

       cvReleaseImage(&small_image);

       cvReleaseImage(&object);

       DeleteCriticalSection(&cs);

 

       for (int j = 0; j<100; j++) delete[ res[j];

       delete[ res;

 

       return 0;

}

 

Здесь информация полностью взята из примера capture iANPR SDK.

Добавляем в Start выделение памяти для результатов распознавания:

res = new char*[all];

for (int j = 0; jnew char[20];

 

 

Компилируем, запускаем:

11

Все работает.

 4. «Причесывание» проекта

Здесь делается проект более презентабельным.

Во-первых, результаты нужно выдавать в виде лога в специальном текстовом блоке. Для этого в capture делаем глобальную строку char outbuf[256];, которая заполняется следующим образом в функции ImageProcess:

time_t rawtime;

struct tm * timeinfo;

time(&rawtime);

timeinfo = localtime(&rawtime);

sprintf(outbuf, "Time and data: %s - Number: %s\n", asctime(timeinfo), res[0]);

 

Если номер не будет распознан, то значение строки будет пустое. Также делаем функцию, которая будет это значение возвращать:

void GetResult(char* dst)

{

       strcpy_s(dst, 256, outbuf);

}

 

Вывод на форму в Form.h:

char* s = new char[256];

GetResult(s);

if (s[0] != 0) {

       String^ s2;

       s2 = gcnew String(s);

       textBox2->AppendText(s2);

}

delete[ s;

 

 

Результат:

12

Ну и последнее, это делаются поля для ввода логина и пароля. Изменяя можно посмотреть в исходном тексте.

Результат показан на первом слайде.

 

Заключение

Полный исходный текст программы вы можете найти здесь: http://intbusoft.com/download/IPtest.zip. Программа была написана только для примера, чтобы проиллюстрировать быстроту создания подобного приложения, поэтому присутствуют решения, которые не стоит использовать в реальном программном продукте. При тестировании библиотеке iANPR помните, что FREE версия работает очень медленно, поэтому работа с IP камерой будет с очень серьезными задержками, если вы не настроите камеру на передачу видео чаще 1 раза в 2 секунды. Поэтому для полноценного теста лучше использовать чтение из ранее записанного видеофайла. Платная версия в 32 раза быстрее бесплатной.