sexta-feira, 15 de janeiro de 2010

FOX Toolkit + OpenCV - Vídeo

Recentemente, uma amiga minha me pediu um exemplo de como exibir um vídeo carregado pelo OpenCV utilizando o FOX.

Então eu rapidamente fiz um programinha para ajudá-la: um micro-player de vídeo. Micro porque não faz nada além de exibir os quadros. Sem som (OpenCV não trabalha com som). Sem controles, nem nada. Abriu o vídeo e pronto.

Obs.: Este tópico, assim como os outros relacionados ao OpenCV, são direcionados àqueles que já tem experiência com o FOX Toolkit, especialmente este aqui por conter itens que ainda não tratei. Tratarei todos esses assuntos em tópicos futuros.

Bom, vamos lá.

FoxVideoPlayerMainWindow.h

Declaração dos objetos


FXImage *image;
Esta é a imagem que será desenhada no canvas (área de desenho), representando cada quadro do vídeo.

FXCanvas *canvas;
Esta é a área de desenho propriamente dita.

CvCapture *capture;
Uma estrutura sinistra que o OpenCV utiliza para guardar as informações do vídeo sendo capturado. Sinistra porque a documentação não revela a estrutura interna. Para conhecê-la, é preciso ir no fonte do OpenCV, coisa que eu ainda não quis fazer.

IplImage *frame;
Cada quadro do vídeo é uma IplImage. Precisa dizer mais?

double freq;
Frequência de atualização. É um vídeo, não é? Não pode ficar no mesmo quadro para sempre.

IplImage *dummy;
Essa é uma pseudo-imagem apenas para converter os quadros para a representação interna do FOX, conforme discutido aqui.

Eventos

Existem apenas três eventos: pintar o canvas, carregar um vídeo e pegar o próximo quadro. Assim:

public:
enum {
ID_CANVAS = FXMainWindow::ID_LAST,
ID_LOAD_VIDEO,
ID_FRAME,
};
/* Pintar o canvas */
long onPaintCanvas(FXObject*, FXSelector, void*);
/* Carregar o vídeo */
long onCmdLoadVideo(FXObject*, FXSelector, void*);
/* Pegar cada quadro do vídeo */
long onTimeoutFrame(FXObject*, FXSelector, void*);
E é isso. Agora, vamos implementar essa cambada.


FoxVideoPlayerMainWindow.cpp

Construtor da janela

FoxVideoPlayerMainWindow::FoxVideoPlayerMainWindow(FXApp *a)
: FXMainWindow(a, "FoxVideoPlayer", 0, 0, DECOR_ALL) {
FXToolBar *toolBar = new FXToolBar(this, this);
new FXButton(toolBar, "Abrir vídeo", 0, this, ID_LOAD_VIDEO,
BUTTON_NORMAL|BUTTON_TOOLBAR);
canvas = new FXCanvas(this, this, ID_CANVAS,
LAYOUT_FIX_WIDTH|LAYOUT_FIX_HEIGHT, 0, 0, 0, 0);
image = 0;
capture = 0;
frame = 0;
dummy = 0;
}
Aqui eu simplesmente crio uma barra de ferramentas com um botão para carregar um vídeo, crio o canvas e inicializo os demais objetos com nulo. Nada de mais.

Pintando o canvas

long FoxVideoPlayerMainWindow::onPaintCanvas(FXObject*, FXSelector, void *ptr) {
FXEvent *ev = static_cast<FXEvent*>(ptr);
FXDCWindow dc(canvas, ev);
if (frame) {
dc.drawImage(image, 0, 0);
}
else {
dc.setForeground(FXRGB(0, 0, 0));
dc.fillRectangle(0, 0, canvas->getWidth(), canvas->getHeight());
}
return 1;
}
Duas opções: se houver um quadro a desenhar, desenha. Senão, desenha um retângulo preto. Simples.

Carregando o vídeo

Essa função é um pouco grande, então vou quebrá-la em algumas partes.

long FoxVideoPlayerMainWindow::onCmdLoadVideo(FXObject*, FXSelector, void*) {
FXFileDialog dialog(this, "Abrir vídeo");
dialog.setDirectory(FXSystem::getHomeDirectory());
if (!dialog.execute(PLACEMENT_OWNER))
return 1;
FXString filename = dialog.getFilename();
Exibe uma caixa de diálogo para selecionar o vídeo.

capture = cvCaptureFromFile(filename.text());
if (!capture) {
FXMessageBox::error(this, MBOX_OK, "Erro", "Erro ao abrir o vídeo");
return 1;
}
Abre o vídeo e armazena na estrutura obscura. Um erro e a função cvCaptureFromFile() retorna nulo.

int width = (int)cvGetCaptureProperty(capture, CV_CAP_PROP_FRAME_WIDTH);
int height = (int)cvGetCaptureProperty(capture, CV_CAP_PROP_FRAME_HEIGHT);
freq = cvGetCaptureProperty(capture, CV_CAP_PROP_FPS);
freq = 1.0 / freq * 1000.0;
Eu preciso saber as dimensões do vídeo e o número de quadros por segundo. A função cvGetCaptureProperty() fornece diversas informações sobre o vídeo. As informações disponíveis são descritas na documentação do OpenCV.

Entretanto, é preciso fazer esse cálculo para saber, digamos assim, o tempo de permanência de cada quadro em milissegundos (o porquê mais para frente).

canvas->resize(width, height);
resize(getDefaultWidth(), getDefaultHeight());
Agora que eu sei o tamanho do vídeo, redimensiono o canvas. Depois dele a janela principal.

Explicando rapidamente o que acontece aqui: por padrão, a janela principal é configurada para ficar com o menor tamanho em que caibam todos os filhos. Como eu redimensionei um deles (depois de todos criados), as funções getDefaultWidth() e getDefaultHeight() retornam as dimensões necessárias para caber todo mundo novamente.

image = new FXImage(getApp(), 0, IMAGE_KEEP|IMAGE_OWNED|IMAGE_SHMP|IMAGE_SHMI,
width, height);
image->create();
dummy = cvCreateImageHeader(cvSize(width, height), IPL_DEPTH_8U, 4);
cvSetData(dummy, image->getData(), CV_AUTO_STEP);
Criando as imagens. Nada de especial, já falei sobre isso.

getApp()->addTimeout(this, ID_FRAME, 0);
return 1;
}
Bom, a última coisa a fazer aqui é disparar um timer para recuperar os quadros do vídeo e desenhá-los. Não queremos esperar, então o tempo é 0.

Quadro-a-quadro

long FoxVideoPlayerMainWindow::onTimeoutFrame(FXObject*, FXSelector, void*) {
if (!capture)
return 1;
frame = cvQueryFrame(capture);
if (frame) {
cvCvtColor(frame, dummy, CV_BGR2RGBA);
image->render();
canvas->update();
getApp()->addTimeout(this, ID_FRAME, freq);
}
else
canvas->update();
return 1;
}
Toda vez que essa função é chamada, o próximo quadro do vídeo é recuperado com cvQueryFrame(). Se não for nulo (ou seja, o vídeo ainda não acabou), os espaços de cor são convertidos com cvCvtColor(). Como dummy utiliza os pixels de image, é como se eu estivesse pintando duas imagens de uma vez só (mas, na verdade, é uma imagem só com duas representações).

Em seguida, vem uma operação importante. É algo que eu demorei muito para entender. Uma imagem (pelo menos no FOX Toolkit, não sei quando a outras bibliotecas de interface gráfica) tem duas representações, uma no lado do cliente outra do servidor. O lado do cliente é esse vetor de pixels que pode ser acessado por meio de image->getData(). O lado do servidor é o servidor gráfico (no meu caso, o X; não sei qual seria no caso do Uíndous ou do Macos). Quando o lado do cliente é alterado, é necessário transferir os pixels para o servidor com render(). Sem isso, seria exibida sempre a mesma imagem que foi criada, apenas um retângulo preto. Só por curiosidade, restore() faz o inverso, recupera os pixels do servidor e põe de volta no cliente.

Tendo a nova imagem no lado do servidor, basta atualizar o canvas. Depois, agenda a recuperação do próximo quadro, desta vez respeitando a frequência de quadros do vídeo.

Quando o vídeo termina, cvQueryFrame() returna nulo. Neste caso, não é mais necessário agendar o próximo quadro. Apenas atualiza o canvas, que será pintado de preto, apenas para não ficar exibindo o último quadro do vídeo.


Resultado

O resultado disso tudo pode ser observado neste vídeo que eu gravei:




Conclusão

Bem, isto foi apenas o básico necessário. Apenas exibi o vídeo, sem aplicar nenhum processamento. Isso, obviamente, deve ser feito depois de recuperar o quadro e antes de convertê-lo para RGBA.

Além disso, no exemplo o vídeo é carregado de um arquivo, mas o OpenCV também captura vídeo pela webcam.

Um abraço e até a próxima.