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);}elsecanvas->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.