FOX Toolkit + OpenCV - Vídeo
Autor: Edgar Diniz | Publicado: 2010-01-15T15:12:00-03:00

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&lt;FXEvent*&gt;(ptr);  FXDCWindow dc(canvas, ev); if (frame) {   dc.drawImage(image, 0, 0); }
 else {   dc.setForeground(FXRGB(0, 0, 0));
   dc.fillRectangle(0, 0, canvas-&gt;getWidth(), canvas-&gt;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-&gt;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-&gt;create(); dummy = cvCreateImageHeader(cvSize(width, height), IPL_DEPTH_8U, 4);   cvSetData(dummy, image-&gt;getData(), CV_AUTO_STEP);Criando as imagens. Nada de especial, já falei sobre isso.

  getApp()-&gt;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-&gt;render();
   canvas-&gt;update();
   getApp()-&gt;addTimeout(this, ID_FRAME, freq); }
 else   canvas-&gt;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-&gt;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.