Neste laboratório, implementaremos o rastreamento de objetos de vídeo usando OpenCV.
Você deve concluir o curso "Construindo o Sistema Solar com C++" antes de aprender este projeto.
Tópicos a Aprender
Fundamentos de C++
Fundamentos de g++
Representação de imagem
Aplicação OpenCV
Algoritmos Meanshift & Camshift
Resultados Finais
Este experimento implementará um programa que pode rastrear os planetas em um sistema solar. (Na imagem a seguir, selecionamos Júpiter da órbita amarela, e você pode ver que o objeto rastreado foi marcado por uma elipse vermelha):
Antes de escrever este projeto, você deve concluir nosso curso "Construindo o Sistema Solar com C++".
Criar um Arquivo de Vídeo
No ambiente LabEx, não suportamos o ambiente da câmera. Portanto, precisamos criar um arquivo de vídeo para o nosso projeto.
Após a instalação, podemos encontrar o software de gravação no menu de aplicativos:
Em seguida, você pode executar o programa do sistema solar ./solarsystem e usar o RecordMyDesktop para gravar a tela da área de trabalho (10~30s serão suficientes) e salvá-lo em ~/Code/camshift com o nome video:
Quando você quiser terminar a gravação, pode clicar no botão Parar no canto inferior direito. Então, você obterá um arquivo video.ogv:
Fundamentos de Imagens Digitais
OpenCV é uma biblioteca de visão computacional de código aberto e multiplataforma. Diferente da renderização de imagem do OpenGL, o OpenCV implementa muitos algoritmos comuns para processamento de imagem e visão computacional. Antes de aprender OpenCV, precisamos entender alguns conceitos básicos de imagens e vídeos no computador.
Primeiramente, devemos entender como a imagem é representada no computador. Existem duas formas comuns de armazenar imagens: uma é o mapa vetorial e a outra é o mapa de pixels.
No mapa vetorial, as imagens são definidas matematicamente como uma série de pontos conectados por linhas. O elemento gráfico em um arquivo de mapa vetorial é chamado de objeto. Cada objeto é uma entidade autônoma, que possui propriedades como cor, forma, contorno, tamanho e posição na tela.
A forma mais comum é o mapa de pixels. Por exemplo, o tamanho de uma imagem é frequentemente 1024*768. Isso significa que a imagem tem 1024 pixels na direção horizontal e 768 pixels na direção vertical.
Pixel é a unidade básica do mapa de pixels. Normalmente, um pixel é uma mistura de três cores primárias (vermelho, verde e azul). Como a natureza do computador é o reconhecimento de números, em circunstâncias normais, representamos uma cor primária em termos de brilho de 0 a 255. Em outras palavras, para cores primárias vermelhas, 0 significa o mais escuro, ou seja, preto, e 255 significa o mais brilhante, ou seja, vermelho puro.
Assim, um pixel pode ser representado como uma trinca (R,G,B), de modo que branco pode ser representado como (255,255,255), e preto é (0,0,0). Então, chamamos essa imagem de imagem no espaço de cores RGB. R, G e B se tornam os três canais da imagem; e existem muitos outros espaços de cores além do espaço de cores RGB, como HSV, YCrCb e assim por diante.
Assim como o pixel é para o mapa de pixels, a imagem é a unidade básica do vídeo. Um vídeo consiste em uma série de imagens, nas quais chamamos cada uma das imagens de quadro (frame). E o que geralmente chamamos de taxa de quadros de vídeo significa que este vídeo contém tantas imagens de quadros por segundo. Por exemplo, se a taxa de quadros for 25, então este vídeo reproduzirá 25 quadros por segundo.
Se houver 1000 milissegundos em 1 segundo e digamos que a taxa de quadros seja rate, então o intervalo de tempo entre as imagens dos quadros é 1000/rate.
Histograma de Cores da Imagem
Um histograma de cores é uma ferramenta para descrever uma imagem. É semelhante a um histograma normal, exceto que o histograma de cores precisa ser calculado a partir de uma determinada imagem.
Se uma imagem estiver em um espaço de cores RGB, então podemos contar o número de ocorrências de cada cor no canal R. Assim, podemos obter um array de 256 comprimentos (tabela de consulta de probabilidade de cores). Divida todos os valores simultaneamente pelo número total de pixels (largura vezes altura) na imagem e converta a sequência resultante em um histograma. O resultado é um histograma de cores do canal R. De maneira semelhante, você pode ter os histogramas no canal G e no canal B.
Retroprojeção do Histograma
Foi comprovado que no espaço de cores RGB, o histograma é sensível às mudanças na iluminação da luz. Para reduzir o impacto dessa mudança no efeito de rastreamento, o histograma precisa ser retroprojetado (back-projected). Este processo é dividido em três etapas:
Primeiramente, convertemos a imagem do espaço RGB para o espaço HSV.
Em seguida, calculamos o histograma do canal H.
O valor de cada pixel na imagem é substituído pela probabilidade correspondente na tabela de consulta de probabilidade de cores para obter um mapa de distribuição de probabilidade de cores.
Este processo é chamado de retroprojeção (back projection) e o mapa de distribuição de probabilidade de cores é uma imagem em tons de cinza.
Assumimos que você já conhece a sintaxe básica de C++. Você sabe que quase todos os programas usarão o arquivo de cabeçalho #include <iostream> e using namespace std; ou std::cout. OpenCV também tem seu próprio namespace.
Para usar o OpenCV, só precisamos incluir o seguinte arquivo de cabeçalho:
#include <opencv2/opencv.hpp>
Então:
using namespace cv;
para habilitar o namespace OpenCV (ou usar diretamente o prefixo cv:: para todas as APIs).
Esta é sua primeira vez usando OpenCV e você pode não estar familiarizado com as interfaces do OpenCV, portanto, recomendamos usar o prefixo cv:: para aprender as APIs do OpenCV.
Vamos escrever nosso primeiro programa para ler nosso vídeo gravado:
//
// main.cpp
//
#include <opencv2/opencv.hpp> // Arquivo de cabeçalho do OpenCV
int main() {
// cria um objeto de captura de vídeo
// OpenCV oferece o objeto VideoCapture e
// trata a leitura de vídeo de um arquivo da mesma forma que a leitura de uma câmera.
// quando o parâmetro de entrada é um caminho de arquivo, ele lerá um arquivo de vídeo;
// se for um número identificador da câmera (geralmente é 0),
// ele lerá a câmera
cv::VideoCapture video("video.ogv"); // lendo do arquivo
// cv::VideoCapture video(0); // lendo da câmera
// contêiner para o quadro de imagem de leitura, objeto Mat no OpenCV
// A classe chave no OpenCV é Mat, que significa Matrix (Matriz)
// OpenCV usa matrizes para descrever imagens
cv::Mat frame;
while(true) {
// escreve dados de vídeo para o quadro, >> é sobrescrito pelo OpenCV
video >> frame;
// quando não houver quadro, interrompe o loop
if(frame.empty()) break;
// visualiza o quadro atual
cv::imshow("test", frame);
// a taxa de quadros de vídeo é 15, então precisamos esperar 1000/15 para reproduzir suavemente
// waitKey(int delay) é uma função de espera no OpenCV
// neste ponto, o programa esperará `delay` milissegundos para entrada do teclado
int key = cv::waitKey(1000/15);
// interrompe o loop ao clicar no botão ECS no teclado
if (key == 27) break;
}
// libera a memória
cv::destroyAllWindows();
video.release();
return 0;
}
Coloque este arquivo main.cpp na mesma pasta que video.ogv em ~/Code/camshift e compile o programa:
g++ main.cpp `pkg-config opencv --libs --cflags opencv` -o main
Quando executamos o programa, podemos ver o vídeo sendo reproduzido:
./main
Nota
Você pode observar o seguinte erro:
libdc1394 error: Failed to initialize libdc1394
Este é um bug do OpenCV e não influencia nossa execução.
Se você deseja eliminar o problema, pode executar o seguinte código antes de executar o programa:
sudo ln /dev/null /dev/raw1394
Algoritmos Meanshift e Camshift
Meanshift
Camshift
Para definir o evento de retorno de chamada do mouse para selecionar o alvo rastreado
Para ler a imagem do fluxo de vídeo
Para implementar o Camshift
Meanshift
Os algoritmos Meanshift e Camshift são dois algoritmos clássicos para rastreamento de objetos. Camshift é baseado em Meanshift. Sua interpretação matemática é complexa, mas a ideia básica é relativamente simples. Portanto, omitimos esses fatos matemáticos e primeiro apresentamos o algoritmo Meanshift.
Supondo que haja um conjunto de pontos vermelhos na tela, o círculo azul (janela) deve ser movido para os pontos onde há a região mais densa (ou onde os pontos são em maior número):
Como mostrado na imagem acima, marque o círculo azul como C1 e o centro do círculo como C1_o. Mas o baricentro deste círculo é C1_r, marcado como um círculo sólido azul.
Quando C1_o e C1_r não se sobrepõem, mova o círculo C1 para o centro do círculo C1_r repetidamente. Eventualmente, ele permanecerá no círculo de maior densidade C2.
Para processamento de imagem, geralmente usamos o histograma retroprojetado da imagem. Quando o alvo rastreado se move, é claro que esse processo de movimento pode ser refletido pelo histograma retroprojetado. Portanto, o algoritmo Meanshift eventualmente moverá nossa janela selecionada para a posição do alvo em movimento. (O algoritmo provou a convergência no final.)
Camshift
Após a descrição anterior, vimos que o algoritmo Meanshift sempre rastreia um tamanho de janela fixo, o que não está de acordo com nossas necessidades, porque em um vídeo, o objeto alvo não precisa ser grande.
Então, Camshift foi criado para melhorar esse problema. Isso também pode ser visto do "Continuously Adaptive Meanshift" de Camshift.
Sua ideia básica é: Primeiro, aplique o algoritmo Meanshift. Uma vez que os resultados do Meanshift convergem, Camshift atualiza o tamanho da janela, calcula uma elipse direcional para corresponder à janela e, em seguida, aplica a elipse como uma nova janela para aplicar o algoritmo Meanshift.
OpenCV fornece uma interface genérica para o algoritmo Camshift:
O primeiro parâmetro, probImage, é a retroprojeção do histograma do alvo. O segundo parâmetro, window, é a janela de busca do algoritmo Camshift. O terceiro parâmetro, criteria, é a condição para o fim (terminação) do algoritmo.
Análise
Após entender a ideia básica do algoritmo Camshift, podemos analisar que a implementação deste código é dividida principalmente em várias etapas:
Definir o evento de retorno de chamada do mouse (mouse callback event) para selecionar o alvo rastreado.
Ler a imagem do fluxo de vídeo.
Implementar o processo Camshift.
A seguir, continuamos a modificar o código em main.cpp.
Para Selecionar Objeto Rastreável por Função de Callback do Mouse
OpenCV é diferente do OpenGL. Existem cinco parâmetros especificados para a função de retorno de chamada do mouse. Os três primeiros são os que mais precisamos: Através do valor de event, podemos obter o evento de pressionar o botão esquerdo do mouse (CV_EVENT_LBUTTONDOWN), o evento de soltar o botão esquerdo do mouse (CV_EVENT_LBUTTONUP) e assim por diante:
bool selectObject = false; // usado para indicar se o objeto foi selecionado ou não
int trackObject = 0; // 1 significa que há um objeto rastreado, 0 significa que não há objeto e -1 significa que a propriedade Camshift não foi calculada
cv::Rect selection; // salva a região selecionada pelo mouse
cv::Mat image; // armazena a imagem do vídeo
// Função de retorno de chamada do mouse do OpenCV:
// void onMouse(int event, int x, int y, int flag, void *param)
// o quarto parâmetro `flag` representa o estado adicional,
// param significa parâmetro do usuário, não precisamos deles, então, sem nomes.
void onMouse( int event, int x, int y, int, void* ) {
static cv::Point origin;
if(selectObject) {
// determinando a altura e largura selecionadas e a posição do canto superior esquerdo
selection.x = MIN(x, origin.x);
selection.y = MIN(y, origin.y);
selection.width = std::abs(x - origin.x);
selection.height = std::abs(y - origin.y);
// & é sobrescrito por cv::Rect
// significa a interseção de duas regiões,
// o principal objetivo aqui é processar a região fora da região selecionada
selection &= cv::Rect(0, 0, image.cols, image.rows);
}
switch(event) {
// processando o botão esquerdo pressionado
case CV_EVENT_LBUTTONDOWN:
origin = cv::Point(x, y);
selection = cv::Rect(x, y, 0, 0);
selectObject = true;
break;
// processando o botão esquerdo liberado
case CV_EVENT_LBUTTONUP:
selectObject = false;
if( selection.width > 0 && selection.height > 0 )
trackObject = -1; // o objeto rastreado ainda não calculou a propriedade Camshift
break;
}
}
Para Ler Imagens de Streaming de Vídeo
Implementamos a estrutura para ler o streaming de vídeo. Vamos escrever mais detalhes:
int main() {
cv::VideoCapture video("video.ogv");
cv::namedWindow("CamShift at LabEx");
// 1. register mouse event callback
cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);
cv::Mat frame;
// 2. read image from video
while(true) {
video >> frame;
if(frame.empty()) break;
// write image from frame to global variable image for cache
frame.copyTo(image);
// draw ractangle if selecting object
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi);
}
imshow("CamShift at LabEx", image);
int key = cv::waitKey(1000/15.0);
if(key == 27) break;
}
// release allocated memory
cv::destroyAllWindows();
video.release();
return 0;
}
Nota:
ROI (Região de Interesse): Em processamento de imagem, qualquer região a ser processada pode ser uma região de interesse, ou seja, ROI.
Implementar Camshift com OpenCV
O histograma retroprojetado para calcular o alvo rastreado precisa usar a função cvtColor, que pode converter a imagem original do espaço de cores RGB para o espaço de cores HSV. Calcular o histograma deve ser feito após selecionar o alvo inicial, portanto:
int main() {
cv::VideoCapture video("video.ogv");
cv::namedWindow("CamShift at LabEx");
cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);
cv::Mat frame;
cv::Mat hsv, hue, mask, hist, backproj;
cv::Rect trackWindow; // janela de rastreamento
int hsize = 16; // para o histograma
float hranges[] = {0,180}; // para o histograma
const float* phranges = hranges; // para o histograma
while(true) {
video >> frame;
if(frame.empty()) break;
frame.copyTo(image);
// trasferir para o espaço HSV
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// processando quando há um objeto
if(trackObject) {
// processando apenas H: 0~180, S: 30~256, V: 10~256, filtrar os outros e copiar o restante para a máscara
cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 10), mask);
// separar o canal h do hsv
int ch[] = {0, 0};
hue.create(hsv.size(), hsv.depth());
cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);
// extração de propriedade se o objeto de rastreamento não foi calculado
if( trackObject < 0 ) {
// configurar o canal h e a máscara ROI
cv::Mat roi(hue, selection), maskroi(mask, selection);
// calcular o histograma ROI
calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
// normalização do histograma
normalize(hist, hist, 0, 255, CV_MINMAX);
// definindo o objeto de rastreamento 设置追踪的窗口
trackWindow = selection;
// marcar que o objeto de rastreamento foi calculado
trackObject = 1;
}
// retroprojetar o histograma
calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
// buscar a região comum
backproj &= mask;
// chamar o algoritmo Camshift
cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
// processando a região é muito pequena para desenhar
if( trackWindow.area() <= 1 ) {
int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
}
// desenhar a área de rastreamento
ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );
}
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi);
}
imshow("CamShift at LabEx", image);
int key = cv::waitKey(1000/15.0);
if(key == 27) break;
}
cv::destroyAllWindows();
video.release();
return 0;
}
Resumo
O seguinte mostra tudo o que escrevemos neste projeto:
#include <opencv2/opencv.hpp>
bool selectObject = false; // usado para indicar se um objeto foi selecionado ou não
int trackObject = 0; // 1 significa que há um objeto sendo rastreado, 0 significa que não há objeto, e -1 significa que a propriedade Camshift não foi calculada
cv::Rect selection; // salva a região selecionada pelo mouse
cv::Mat image; // armazena a imagem do vídeo
// Função de callback do mouse do OpenCV:
// void onMouse(int event, int x, int y, int flag, void *param)
// o quarto parâmetro `flag` representa o estado adicional,
// param significa parâmetro do usuário, não precisamos deles, então, sem nomes.
void onMouse( int event, int x, int y, int, void* ) {
static cv::Point origin;
if(selectObject) {
// determinando a altura e largura selecionadas e a posição do canto superior esquerdo
selection.x = MIN(x, origin.x);
selection.y = MIN(y, origin.y);
selection.width = std::abs(x - origin.x);
selection.height = std::abs(y - origin.y);
// & é sobrescrito por cv::Rect
// significa a interseção de duas regiões,
// o principal objetivo aqui é processar a região fora da região selecionada
selection &= cv::Rect(0, 0, image.cols, image.rows);
}
switch(event) {
// processando o botão esquerdo pressionado
case CV_EVENT_LBUTTONDOWN:
origin = cv::Point(x, y);
selection = cv::Rect(x, y, 0, 0);
selectObject = true;
break;
// processando o botão esquerdo liberado
case CV_EVENT_LBUTTONUP:
selectObject = false;
if( selection.width > 0 && selection.height > 0 )
trackObject = -1; // o objeto de rastreamento não calculou a propriedade Camshift
break;
}
}
int main( int argc, const char** argv ) {
cv::VideoCapture video("video.ogv");
cv::namedWindow("CamShift at LabEx");
cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);
cv::Mat frame, hsv, hue, mask, hist, backproj;
cv::Rect trackWindow; // janela de rastreamento
int hsize = 16; // para o histograma
float hranges[] = {0,180}; // para o histograma
const float* phranges = hranges; // para o histograma
while(true) {
video >> frame;
if(frame.empty()) break;
frame.copyTo(image);
// transferir para o espaço HSV
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// processando quando há um objeto
if(trackObject) {
// processando apenas H: 0~180, S: 30~256, V: 10~256, filtrar os outros e copiar o restante para a máscara
cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 256), mask);
// separar o canal h do hsv
int ch[] = {0, 0};
hue.create(hsv.size(), hsv.depth());
cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);
// extração de propriedade se o objeto de rastreamento não foi calculado
if( trackObject < 0 ) {
// configurar o canal h e a máscara ROI
cv::Mat roi(hue, selection), maskroi(mask, selection);
// calcular o histograma ROI
calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
// normalização do histograma
normalize(hist, hist, 0, 255, CV_MINMAX);
// definindo o objeto de rastreamento
trackWindow = selection;
// marcar que o objeto de rastreamento foi calculado
trackObject = 1;
}
// retroprojetar o histograma
calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
// buscar a região comum
backproj &= mask;
// chamar o algoritmo Camshift
cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
// processando a região é muito pequena para desenhar
if( trackWindow.area() <= 1 ) {
int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
}
// desenhar a área de rastreamento
ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );
}
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi);
}
imshow("CamShift at LabEx", image);
int key = cv::waitKey(1000/15.0);
if(key == 27) break;
}
cv::destroyAllWindows();
video.release();
return 0;
}
Vamos recompilar main.cpp:
g++ main.cpp $(pkg-config opencv --libs --cflags opencv) -o main
e executá-lo:
./main
Agora, podemos selecionar o objeto no programa, e o rastreamento está em andamento:
Na imagem acima, selecionamos Júpiter e a janela de rastreamento é uma elipse vermelha.