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

в 4:02, , рубрики: opencv, Блог компании AVI, искусственный интеллект, Работа с видео, умный дом, метки: ,

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

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

Введение

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

О сенсорах

Можно определять вот такими детекторами:
Делаем детектор движения, или OpenCV — это просто
Называют они PIR — Пассивный Инфракрасный Сенсор. Или не пассивный, а пироэлектрический. Короче, в основе его лежит, по сути, единичный пиксель тепловизора — та самая ячейка, которая выдает сигнал, если на нее попадает дальний ик.
Делаем детектор движения, или OpenCV — это просто
Простая схема после нее выдает импульс только если сигнал резко меняется — так что на горячий чайник он сигналить не будет, а вот на перемещающийся теплый объект — будет.
Такие детекторы устанавливают в 99% сигнализаций, и вы их все думаю, видели — это те штуки, которые висят под потолком:
Делаем детектор движения, или OpenCV — это просто
Еще такие же штуки, но с обвязкой посложнее стоят в бесконтактных термометрах — тех, которые меряют температуру за пару секунд на лбу или в ухе.
Делаем детектор движения, или OpenCV — это просто
И в пирометрах, тех же термометрах, но с бОльши диапазоном:
Делаем детектор движения, или OpenCV — это просто
Хотя я что-то отвлекся. Такие сенсоры, конечно, штука хорошая. Но у них есть минус — он показывает движение во всем обьеме наблюдения, не уточняя где оно произошло — близко, далеко. А у меня большая комната. И хочется включать свет только в той части, где работает человек. Можно было, конечно поставить штук 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 — это как раз и есть программа, реализующая функционал определения движения в кадре. Выглядит это вот так:
Делаем детектор движения, или OpenCV — это просто

Допилка

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

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

#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 %dn", 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;
  }
}

Вот как оно работает:
Делаем детектор движения, или OpenCV — это просто
Регионы переключаются цифровыми кнопками.

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


#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 %dn", x, y);
    break;

  case CV_EVENT_LBUTTONDOWN:
    //printf("%d x %dn", 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 readingn");  //если файл не нашли
  FILE* fd = fopen("regions.bin", "wb"); //пытаемся создать
  if (fd == NULL) 
  {
    printf("Error opening file for writingn");
  }
  else
  {
    fwrite(region_coordinates, 1, sizeof(region_coordinates), fd); //если получилось - записываем туда нулевые координаты
    fclose(fd); //закрываем файл
    printf("File created, please restart programn");
  }
  return 0;
}

size_t result = fread(region_coordinates, 1, sizeof(region_coordinates), fd); //читаем файл
if (result != sizeof(region_coordinates)) //если прочитали количество байт не равное размеру массива
printf("Error size filen"); //вываливаем ошибку
fclose(fd); //закрываем файл


FILE* fd = fopen("regions.bin", "wb"); //открываем файл.  "wb" - запись бинарных данных
if (fd == NULL)  //если на нашли файл
printf("Error opening file for writingn"); //ругаемся
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 %dn",i_mass); //и ругаемся в консоль с номером региона
  }
}

Работает:

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

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

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

Автор: vvzvlad

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js