Делаем детектор движения, или OpenCV — это просто

от автора

Надо оправдывать название компании — заняться хоть чем-то, что связано с видео. По предыдущему топику можно понять, что мы не только чайник делаем, но и пилим «умное освещение» для умного дома. На этой недели я был занят тем, что ковырял OpenCV — это набор алгоритмов и библиотек для работы с компьютерным зрением. Поиск обьектов на изображениях, распознание символов и все такое прочее.

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

Сразу говорю: в статье может встретиться страшнейший быдлокод, который может вас напугать или лишить сна до конца жизни.

Если вам еще не страшно — то добро пожаловать дальше.

Введение

Собственно, в чем состояла идея. Хотелось полностью избавится от ручного включения света. У нас есть квартира, и есть люди, которые по ней перемещаются. Людям нужен свет. Всем остальным предметам в квартире свет не нужен. Предметы не двигаются, а люди двигаются(если человек не двигается — он или умер, или спит. Мертвым и спящим свет тоже не нужен). Соответственно, надо освещать только те места в квартире, где наблюдается какое-то движение. Движение прекратилось — можно через полчаса-час выключить свет.
Как определять движение?

О сенсорах

Можно определять вот такими детекторами:

Называют они PIR — Пассивный Инфракрасный Сенсор. Или не пассивный, а пироэлектрический. Короче, в основе его лежит, по сути, единичный пиксель тепловизора — та самая ячейка, которая выдает сигнал, если на нее попадает дальний ик.

Простая схема после нее выдает импульс только если сигнал резко меняется — так что на горячий чайник он сигналить не будет, а вот на перемещающийся теплый объект — будет.
Такие детекторы устанавливают в 99% сигнализаций, и вы их все думаю, видели — это те штуки, которые висят под потолком:

Еще такие же штуки, но с обвязкой посложнее стоят в бесконтактных термометрах — тех, которые меряют температуру за пару секунд на лбу или в ухе.

И в пирометрах, тех же термометрах, но с бОльши диапазоном:

Хотя я что-то отвлекся. Такие сенсоры, конечно, штука хорошая. Но у них есть минус — он показывает движение во всем обьеме наблюдения, не уточняя где оно произошло — близко, далеко. А у меня большая комната. И хочется включать свет только в той части, где работает человек. Можно было, конечно поставить штук 5 таких сенсоров, но я отказался от этой идеи — если можно обойтись одной камерой примерно за такую же сумму, зачем ставить кучу сенсоров?

Ну и OpenCV хотелось поковырять, не без этого, да. Так что я нашел в закромах камеру, взял одноплатник(CubieBoard2 на A20) и поехало.

Установка

Естественно, для использования OpenCV сначала надо поставить. В большинстве современных систем(я говорю про *nix) она ставится одной командой типа apt-get install opencv. Но мы же пойдем простым путем, да? Да и например в системе для одноплатника, которую я использую ее нету.
Исчерпывающее руководство по установке можно найти вот тут, поэтому я не буду очень подробно останавливаться на ней.
Ставим cmake и GTK(вот его я как раз со спокойной совестью поставил apt-get install cmake libgtk2.0-dev).
Идем на офсайт и скачиваем последнюю версию. А вот если мы полезем на SourceForge по ссылке из руководства на Robocraft, то скачаем не последнюю версию(2.4.6.1), а 2.4.6, в которой абсолютно неожиданно не работает прием изображения с камеры через v4l2. Я этого не знал, поэтому 4 дня пытался заставить работать эту версию. Хоть бы написали где-то.
Дальше — стандартно:

tar -xjf OpenCV-*.tar.bz2 && cd OpenCV-* && cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local ./ && make && make install 

Можно собрать примеры, которые идут в комплекте:

cd samples/c/ && chmod +x build_all.sh && ./build_all.sh  

Собственно, большая часть моего кода взята из примера под названием motempl — это как раз и есть программа, реализующая функционал определения движения в кадре. Выглядит это вот так:

Допилка

Работает, но как это применить для включения света? Он показывает движение на экране, но нам-то надо, чтобы об этом узнал контроллер, который у нас управляет освещением. И желательно, чтобы он узнал не координаты точки, а место, в котором надо включить свет.

Для начала, немного поймем, как же эта штука работает. Чтобы показать видео с камеры в окошке, многое не требуется:

#include <cv.h> #include <highgui.h> #include <stdlib.h> #include <stdio.h>  int main(int argc, char* argv[]) {         CvCapture* capture = cvCaptureFromCAM(0);// Создаем обьект CvCapture(внутреннее название для обьекта, в который кладутся кадры с камеры),  который называется capture. Сразу подключаем его к камере функцией cvCaptureFromCAM, 0 в параметрах которой означает, что видео надо брать с первой подвернувшейся камеры.         IplImage* image = cvQueryFrame( capture ); // Создаем обьект типа изображение(имя image) и кладем туда текущий кадр с камеры         cvNamedWindow("image window", 1); //Создаем окно с названием image window         for(;;) //запускаем в бесконечном цикле         {                 image = cvQueryFrame( capture ); //получаем очередной кадр с камеры и записываем его в image                 cvShowImage("image window", image);//Показываем в созданном окне(image window) кадр с камеры, который мы получили в предыдущем пункте                 cvWaitKey(10); //ждем 10 мс нажатия кнопки. Тут оно без надобности, но без этого окно не создается. Я не против, если кто-то, более понимающий в этом, объяснит такое поведение.         } } 

Эту программу можно скопировать в файл test.c и собрать его вот так:

gcc -ggdb `pkg-config --cflags opencv` -o `basename test.c .c` test.c `pkg-config --libs opencv` 

Опять же, честно говоря, я не совсем понимаю, что именно делает эта команда. Ну собирает. А почему именно такая?

Оно запустится, и покажет вам видео с камеры. Но из него даже не получится выйти — программа застряла в бесконечном цикле и только Ctrl+C прервет ее бессмысленную жизнь. Добавим обработчик кнопок:

char c = cvWaitKey(10); //Ждем нажатия кнопки и записываем нажатую кнопку в переменную с. if (c == 113 || c == 81) //Проверяем, какая кнопка нажата. 113 и 81 - это коды кнопки "q" - в английской и русской раскладках.  { cvReleaseCapture( &capture ); //корретно освобождаем память и уничтожаем созданные обьекты. cvDestroyWindow("capture"); //я тебя породил, я тебя и убью! return 0;  //выходит из программы.  } 

И счетчик FPS:

CvFont font; //создаем структуру "шрифт" cvInitFont(&font, CV_FONT_HERSHEY_COMPLEX_SMALL, 1.0, 1.0, 1,1,8); //Инициализуем ее параметрами - название шрифта, размеры, сглаживание struct timeval tv0; //Что-то связаннное с временем.  int fps=0; int fps_sec=0;  char fps_text[2]; int now_sec=0;//Создаем переменные ...  gettimeofday(&tv0,0); //Получаем текущее время now_sec=tv0.tv_sec; //Получаем из него секунды if (fps_sec == now_sec) //Сравниваем, совпадает ли текущая секунда с той, в которой вы считаем фпс { fps++; //если совпадает, то прибавляем еще один кадр(это все крутится в цикле, который рисует кадры.) } else  { fps_sec=now_sec; //если не совпадает, то обнуляем секунду snprintf(fps_text,254,"%d",fps); //формируем текстовую строку с FPS fps=0; // обнуляем счетчик }  cvPutText(image, fps_text, cvPoint(5, 20), &font, CV_RGB(255,255,255));//выводим в текущий кадр(image) в место с координатами 5х20, белым цветом, тем шрифтом, что мы задали ранее, переменную, в которой записан текущий фпс. 
Полный текст программы

#include "opencv2/video/tracking.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc_c.h" #include <time.h> #include <stdio.h> #include <ctype.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <errno.h>  int main(int argc, char** argv) {   IplImage* image = 0;   CvCapture* capture = 0;   struct timeval tv0;   int fps=0;   int fps_sec=0;   int now_sec=0;   char fps_text[2];   CvFont font;   cvInitFont(&font, CV_FONT_HERSHEY_COMPLEX_SMALL, 1.0, 1.0, 1,1,8);   capture = cvCaptureFromCAM(0);   cvNamedWindow( "Motion", 1 );     for(;;)     {       IplImage* image = cvQueryFrame( capture );       gettimeofday(&tv0,0);       now_sec=tv0.tv_sec;       if (fps_sec == now_sec)       {         fps++;       }       else        {         fps_sec=now_sec;         snprintf(fps_text,254,"%d",fps);          fps=0;       }       cvPutText(image, fps_text, cvPoint(5, 20), &font, CV_RGB(255,255,255));       cvShowImage( "Motion", image );       if( cvWaitKey(10) >= 0 )         break;     }     cvReleaseCapture( &capture );     cvReleaseImage(&image);     cvDestroyWindow( "Motion" );   return 0; }

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

int dig_key=0;//переменная, хранящее нажатую кнопку int region_coordinates[10][4]; //координаты регионов, в которых надо определять движение. ... char c = cvWaitKey(20); //Ждем нажатия кнопки и записываем нажатую кнопку в переменную с. if (c <=57 && c>= 48) //Проверяем, относится ли нажатая кнопка к цифрам { dig_key=c-48; //key "0123456789" //если относится, то записываем в переменную номер кнопки. }  cvSetMouseCallback( "Motion", myMouseCallback, (void*) image); //говорим, что нам надо выполнить подпрограмму myMouseCallback при событиях, связанных с мышью в окне Motion и с изображением image  if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] == 0 && region_coordinates[dig_key][3] == 0) //Рисуем прямоугольник. Если есть в переменной только одни координаты - рисуем точку по этим координатам. cvRectangle(image, cvPoint(region_coordinates[dig_key][0],region_coordinates[dig_key][1]), cvPoint(region_coordinates[dig_key][0]+1,region_coordinates[dig_key][1]+1), CV_RGB(0,0,255), 2, CV_AA, 0 );   if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] != 0 && region_coordinates[dig_key][3] != 0) //А если в переменной двое наборов координат - рисуем полностью прямоугольник. cvRectangle(image, cvPoint(region_coordinates[dig_key][0],region_coordinates[dig_key][1]), cvPoint(region_coordinates[dig_key][2],region_coordinates[dig_key][3]), CV_RGB(0,0,255), 2, CV_AA, 0 );   void myMouseCallback( int event, int x, int y, int flags, void* param) //описываем что нам надо будет делать при событиях, связанных с мышью {   IplImage* img = (IplImage*) param; //получаем картинку. Видимо, ему это надо для определение координат   switch( event ){ //вбираем действие в зависимости от событий   case CV_EVENT_MOUSEMOVE:     break; //ничего не делаем при движении мыши. А можно, например, кидать в консоль координаты под курсором: printf("%d x %d\n", x, y);    case CV_EVENT_LBUTTONDOWN: //при нажатии левой кнопки мыши     if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] == 0 && region_coordinates[dig_key][3] == 0) //если это второе нажатие(заполнена первая половина координат - х и у верхнего угла региона), то записываем в переменную вторую половину - х и у нижнего угла региона     {       region_coordinates[dig_key][2]=x; //dig_key - определяет, какой регион устанавливается сейчас. А меняется он нажатием цифровых кнопок.       region_coordinates[dig_key][3]=y;     }     if (region_coordinates[dig_key][0] == 0 && region_coordinates[dig_key][1] == 0)//если это первое нажатие(не заполнена первая половина координат ), то записываем в переменную первую половину.     {       region_coordinates[dig_key][0]=x;        region_coordinates[dig_key][1]=y;     }     break;   } } 

Вот как оно работает:

Регионы переключаются цифровыми кнопками.

Полный текст программы

 #include "opencv2/video/tracking.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc_c.h" #include <time.h> #include <stdio.h> #include <ctype.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <errno.h>  int dig_key=0; int region_coordinates[10][4]; void myMouseCallback( int event, int x, int y, int flags, void* param) {    IplImage* img = (IplImage*) param;    switch( event ){   case CV_EVENT_MOUSEMOVE:      //printf("%d x %d\n", x, y);     break;    case CV_EVENT_LBUTTONDOWN:     //printf("%d x %d\n", region_coordinates[dig_key][0], region_coordinates[dig_key][1]);       if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] == 0 && region_coordinates[dig_key][3] == 0)     {       region_coordinates[dig_key][2]=x;        region_coordinates[dig_key][3]=y;     }     if (region_coordinates[dig_key][0] == 0 && region_coordinates[dig_key][1] == 0)     {       region_coordinates[dig_key][0]=x;        region_coordinates[dig_key][1]=y;     }     break;    case CV_EVENT_RBUTTONDOWN:      break;   case CV_EVENT_LBUTTONUP:      break;   } }  int main(int argc, char** argv) {   IplImage* image = 0;   CvCapture* capture = 0;   struct timeval tv0;   int fps=0;   int fps_sec=0;   int now_sec=0;   char fps_text[2];   CvFont font;   cvInitFont(&font, CV_FONT_HERSHEY_COMPLEX_SMALL, 1.0, 1.0, 1,1,8);   capture = cvCaptureFromCAM(0);   cvNamedWindow( "Motion", 1 );   for(;;)   {     IplImage* image = cvQueryFrame( capture );      gettimeofday(&tv0,0);     now_sec=tv0.tv_sec;     if (fps_sec == now_sec)     {       fps++;     }     else      {       fps_sec=now_sec;       snprintf(fps_text,254,"%d",fps);        fps=0;     }     cvSetMouseCallback( "Motion", myMouseCallback, (void*) image);     if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] == 0 && region_coordinates[dig_key][3] == 0)       cvRectangle(image, cvPoint(region_coordinates[dig_key][0],region_coordinates[dig_key][1]), cvPoint(region_coordinates[dig_key][0]+1,region_coordinates[dig_key][1]+1), CV_RGB(0,0,255), 2, CV_AA, 0 );      if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] != 0 && region_coordinates[dig_key][3] != 0)       cvRectangle(image, cvPoint(region_coordinates[dig_key][0],region_coordinates[dig_key][1]), cvPoint(region_coordinates[dig_key][2],region_coordinates[dig_key][3]), CV_RGB(0,0,255), 2, CV_AA, 0 );     cvPutText(image, fps_text, cvPoint(5, 20), &font, CV_RGB(255,255,255));     cvShowImage( "Motion", image );      char c = cvWaitKey(20);     if (c <=57 && c>= 48)      {       dig_key=c-48; //key "0123456789"     }   }   cvReleaseCapture( &capture );   cvReleaseImage(&image);   cvDestroyWindow( "Motion" );   return 0; } 

Но не будем же мы каждый раз при запуске программы устанавливать регионы наблюдения вручную? Сделаем сохранение в файл.

FILE *settings_file;  FILE* fd = fopen("regions.bin", "rb");  //открываем файл.  "rb" - чтение бинарных данных if (fd == NULL)  {   printf("Error opening file for reading\n");  //если файл не нашли   FILE* fd = fopen("regions.bin", "wb"); //пытаемся создать   if (fd == NULL)    {     printf("Error opening file for writing\n");   }   else   {     fwrite(region_coordinates, 1, sizeof(region_coordinates), fd); //если получилось - записываем туда нулевые координаты     fclose(fd); //закрываем файл     printf("File created, please restart program\n");   }   return 0; }  size_t result = fread(region_coordinates, 1, sizeof(region_coordinates), fd); //читаем файл if (result != sizeof(region_coordinates)) //если прочитали количество байт не равное размеру массива printf("Error size file\n"); //вываливаем ошибку fclose(fd); //закрываем файл   FILE* fd = fopen("regions.bin", "wb"); //открываем файл.  "wb" - запись бинарных данных if (fd == NULL)  //если на нашли файл printf("Error opening file for writing\n"); //ругаемся fwrite(region_coordinates, 1, sizeof(region_coordinates), fd); //читаем файл в массив fclose(fd); //закрываем файл 

Привязываем эти функции, например на кнопки w и r, и при нажатии их сохраняем и открываем массив.

Осталась самая малость — собственно, определение в каком регионе произошло движение. Переносим наши наработки в исходник motempl.с, и находим куда нам можно вклиниться.
Вот функция, которая рисует круги на месте обнаружения движения:

cvCircle( dst, center, cvRound(magnitude*1.2), color, 3, CV_AA, 0 );

А координаты центра определяются вот так:

center = cvPoint( (comp_rect.x + comp_rect.width/2), (comp_rect.y + comp_rect.height/2) );

Вставляем в этот кусок свой код:

int i_mass;  //создаем переменную цикла for (i_mass = 0; i_mass <= 9; i_mass++) //перебираем все наши массивы в цикле, проверяя принадлежность точки к каждому из них. {   if( comp_rect.x + comp_rect.width/2 <= region_coordinates[i_mass][2] && comp_rect.x + comp_rect.width/2 >= region_coordinates[i_mass][0] && comp_rect.y + comp_rect.height/2 <= region_coordinates[i_mass][3] && comp_rect.y + comp_rect.height/2 >= region_coordinates[i_mass][1] ) //проверяем, принадлежит ли точка, в которой обнаружено движение нашему прямоугольнику-региону.    {     cvRectangle(dst, cvPoint(region_coordinates[i_mass][0],region_coordinates[i_mass][1]), cvPoint(region_coordinates[i_mass][2],region_coordinates[i_mass][3]), CV_RGB(0,0,255), 2, CV_AA, 0 ); //если текущая точка принадлежит региону, то рисуем этот регион синим прямоугольником, показывая, что в нем произошло срабатывание.     printf("Detect motion in region %d\n",i_mass); //и ругаемся в консоль с номером региона   } } 

Работает:

Осталось немного: направлять вывод не в консоль, а в UART, подключить к любому МК реле, которые будут управлять светом. Программа обнаруживает движение в регионе, отправляет номер региона контроллеру, а тот зажигает назначенную ему лампу. Но об этом — в следующей серии.

Исходник проекта я выложил на github, и буду не против, если кто-нибудь найдет время для исправления ошибок и улучшения программы:
github.com/vvzvlad/motion-sensor-opencv

Напоминаю, если вы не хотите пропустить эпопею с чайником и хотите увидеть все новые посты нашей компании, вы можете подписаться на imageна странице компании(кнопка «подписаться»)
И да, я опять писал пост в 5 утра, поэтому приму сообщения об ошибках. Но — в личку.

ссылка на оригинал статьи http://habrahabr.ru/company/avi/blog/200804/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *