1. Objetivo

Este projeto visa entendermos melhor como podemos utilizar o histograma para detectar variações de cor (suas intensidades) numa imagem, e desse modo aprendermos como funciona um dos princípios da detecção de movimentos.

2. Detector de Movimentos

Atualmente, devido à insegurança, muitas casas possuem câmeras com detectores de movimentos instalados. Aprender seu funcionamento é dever do programador que trabalha com processamento digital de imagens.

2.1. Utilizando o Histograma

A detecção de movimentos pode ser feita com base nos histogramas. Para relembrar, o histograma nos diz a frequência de cada cor em uma imagem, ou seja, a quantidade de vezes que determinado pixel (ou componente dele) se repete. Mas o que isso quer dizer?

Isso quer dizer que podemos verificar se houve a introdução de algum objeto na cena observando a diferença entre dois histogramas consecutivos. Se essa diferença extrapolar um limiar pré-estabelecido, podemos dizer que houve movimento captado pela câmera. Utilizamos esse limiar para evitarmos que a cãmera detecte movimento caso haja somente uma pequena variação de luminosidade.

2.2. Um Efeito Desejado

Um efeito secundário, que apesar de não ser o foco principal é altamente desejado, é de que evitamos detectar movimentos devido a objetos que se movem dentro da cena, a menos que sejam movimentos bruscos ou que envolvam aumento da dimensão aparente do objeto na cena. Como sabemos, o histograma calcula somente a frequência de ocorrência, e não a posição, e isso nos diz que caso mudemos a posição de um objeto mas mantivermos a frequência geral, não teremos movimento detectado. Isso evita que o detector de movimento dispare com o movimento de um lençol dentro da cena, por exemplo.

2.3. Prevendo Mudanças de Luminosidade

Uma solução para a mudança de luminosidade ambiente é recalcularmos o histograma de comparação em intervalos fixos, por exemplo, a cada 10 minutos. Isso será mostrado em uma seção seguinte.

2.4. Comparando os Canais de Cores

Para uma matriz em grayscale, podemos comparar as imagens diretamente, ou seja, podemos calcular diretamente o seu histograma. Para imagens em RGB ou em outro sistema de cor, devemos calcular o histograma para as componentes separadamente, pois não temos como calcular um histograma das três componentes juntas (um valor para isso seria, no mínimo, estranho, além de indefinido). Em contrapartida, basta que calculemos o histograma de apenas uma das componentes do pixel, pois caso haja variação em uma das componentes, dificilmente não haverá nas outras duas.

Basta imaginarmos o fundo da cena contendo as 3 componentes; caso introduzamos algo totalmente verde (0 azul e 0 vermelho), apesar do histograma do verde ter aumentado, os de azul e vermelho diminuirão, em decorrência do fundo da cena ter sido "tapado". Dificilmente teremos somente cores puras em uma região extensa da imagem.

3. O Código

O código é mostrado abaixo, e a explicação está logo em seguida.

motiondetector.cpp
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(int argc, char** argv){
  Mat image;
  int width, height;
  VideoCapture cap;
  vector<Mat> planes;
  Mat histR_antigo, histR_novo;
  int nbins = 64;
  float range[] = {0, 256};
  const float *histrange = { range };
  bool uniform = true;
  bool acummulate = false;
  double D;
  int counter(0);
  cap.open(0);
  char key;

  if(!cap.isOpened()){
    cout << "cameras indisponiveis";
    return -1;
  }

  width  = cap.get(CV_CAP_PROP_FRAME_WIDTH);
  height = cap.get(CV_CAP_PROP_FRAME_HEIGHT);

  cout << "largura = " << width << endl;
  cout << "altura  = " << height << endl;

  cap >> image;
  split (image, planes);
  calcHist(&planes[0], 1, 0, Mat(), histR_antigo, 1, &nbins, &histrange, uniform, acummulate);

  while(1){
    cap >> image;
    split (image, planes);
    calcHist(&planes[0], 1, 0, Mat(), histR_novo, 1, &nbins, &histrange, uniform, acummulate);
    D = norm(histR_novo,histR_antigo,NORM_L2);

    if(D>5000){
    	calcHist(&planes[0], 1, 0, Mat(), histR_antigo, 1, &nbins, &histrange, uniform, acummulate);
    	cout<<"ALARME ATIVADO\n";
    }
    counter++;
    if(counter>18000){
    	calcHist(&planes[0], 1, 0, Mat(), histR_antigo, 1, &nbins, &histrange, uniform, acummulate);
    }

    //cout<<D<<endl;

    imshow("image", image);
    key = waitKey(30);
    if(key==105) imwrite("in.png",image);
    else if(key==111) imwrite("out.png",image);
    else if(key>=0) break;
  }
  return 0;
}

Utilizaremos, nesse programa, conceitos abordados em programas anteriores, e que portanto não serão abordados aqui. Também utilizamos como base o programa para visualização de histogramas do professor Agostinho, disponível aqui.

3.1. As variáveis Utilizadas

Mat image;
int width, height;
VideoCapture cap;
vector<Mat> planes;
Mat histR_antigo, histR_novo;
int nbins = 64;
float range[] = {0, 256};
const float *histrange = { range };
bool uniform = true;
bool acummulate = false;
double D;
int counter(0);

Utilizamos a matriz, da classe cv::Mat, image, como imagem de entrada. Temos o vetor de matrizes planes, que servirá para separarmos as componentes de cor da imagem, de modo similar ao programa da equalização de histogramas. As matrizes histR_antigo e histR_novo servem para armazenar um valor de comparação e o valor comparado, respectivamente, do histograma do canal Red da imagem, como vimos, só precisamos fazer a comparação em um dos canais de cores.

Os inteiros width e height servem para nos dizer as dimensões da imagem, enquanto que a classe VideoCapture nos permite fazer a captura de vídeo, assim como no programa anterior. O ponto flutuante double D é a norma da diferença entre os vetores de histogramas ("D" de diferença), e o inteiro counter é um contador que nos permitirá atualizar o histograma de comparação após um tempo definido.

Por último, as variáveis nbins, range, histrange, acummulate e uniform são parâmetro necessários à utilização da função cv::calcHist, cuja documentação pode ser encontrada aqui. As variáveis booleanas uniform e acummulate nos indicam se queremos, respectivamente, o histograma uniforme (os elementos variam em valores constantes, por exemplo, de 1 em 1 ou 32 em 32) e o histograma acumulado (somando os elementos anteriores). nbins nos indica o número de subdivisões do histograma.

calcHist(&planes[0], 1, 0, Mat(), histR_novo, 1, &nbins, &histrange, uniform, acummulate);

3.2. Calculando os Histogramas e Sua Diferença

Calculamos, inicialmente, um histograma assim que abrimos a câmera, utilizando a função calcHist(). Após isso, iniciamos a captura de vídeo continuamente (while(1)), e calculamos continuamente o histograma a cada captura. Para obtermos um parâmetro para comparação, utlizamos a função norm(vetor1,vetor2,NORM_L2); essa função, quando é passado o parâmetro NORM_L2 e dois vetores, nos retorna o módulo do vetor diferença entre esses dois vetores, de modo que:

\$\text{Retorno} = sqrt{\sum_{1}^{n}(x_i - y_i)^2\$

onde x e y são os vetores passados como parâmetro e n é a dimensão dos vetores. No nosso caso, passamos como parâmetro os vetores histR_antigoe histR_novo.

D = norm(histR_novo,histR_antigo,NORM_L2);

Estabelecemos, em seguida, um limiar; se a norma da diferença entre os histogramas for maior que 5000 if(D>5000), então há movimento. Esse valor foi estabelecido com um teste prático: printamos na tela o valor da norma quando não há movimento e quando há movimento. Verificamos que 5000 é um valor suficiente. Quando o valor do módulo da diferença ultrapassa 5000, então atualizamos o valor do histograma de comparação, para que movimento não seja continuamente detectado:

if(D>5000){
    calcHist(&planes[0], 1, 0, Mat(), histR_antigo, 1, &nbins, &histrange, uniform, acummulate);
   	cout<<"ALARME ATIVADO\n";
}

3.3. Prevendo Mudanças de Iluminação Ambiente

Se considerarmos um tempo fixo para a captura da imagem (como 30 quadros por segundo), podemos estabelecer um contador counter que quando atingir um determinado valor atualize o valor do histograma de comparação. Desse modo, caso desejemos 10 min:

\$\text{valor do contador} = \text{quadros por segundo}*\text{quantidade de segundos} = 30*600 = 1800\$

Quando o contador atingir 18000 o valor de comparação será atualizado. Não podemos escolher um valor muito baixo pois isso pode acarretar em perda de detecção de movimento. Muito dificilmente um movimento será perdido se utilizarmos um valor de 10 min (apenas se o movimento durasse 30 milissegundos a cada 10 minutos!)

4. A Saída do Programa

Primeiramente, temos a captura da tela sem movimento. Quando um objeto é inserido na cena, movimento é detectado e é mostrada uma mensagem de aviso no terminal:

in
Figura 1. Fundo da Imagem
out
Figura 2. Objeto Inserido
Terminal
Figura 3. Saída no terminal