На самом деле что-то в ней сделать — не такая сложная задача, даже для не-программиста. Вот я и расскажу, как.
Сразу говорю: в статье может встретиться страшнейший быдлокод, который может вас напугать или лишить сна до конца жизни.
Если вам еще не страшно — то добро пожаловать дальше.
Введение
Собственно, в чем состояла идея. Хотелось полностью избавится от ручного включения света. У нас есть квартира, и есть люди, которые по ней перемещаются. Людям нужен свет. Всем остальным предметам в квартире свет не нужен. Предметы не двигаются, а люди двигаются(если человек не двигается — он или умер, или спит. Мертвым и спящим свет тоже не нужен). Соответственно, надо освещать только те места в квартире, где наблюдается какое-то движение. Движение прекратилось — можно через полчаса-час выключить свет.
Как определять движение?
О сенсорах
Можно определять вот такими детекторами:
Называют они 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
Напоминаю, если вы не хотите пропустить эпопею с чайником и хотите увидеть все новые посты нашей компании, вы можете подписаться на на странице компании(кнопка «подписаться»)
И да, я опять писал пост в 5 утра, поэтому приму сообщения об ошибках. Но — в личку.
ссылка на оригинал статьи http://habrahabr.ru/company/avi/blog/200804/
Добавить комментарий