Mostrando postagens com marcador OpenCV. Mostrar todas as postagens
Mostrando postagens com marcador OpenCV. Mostrar todas as postagens

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.

segunda-feira, 28 de dezembro de 2009

IplImage e FXImage: revisitado

Anteriormente, falei aqui como utilizar uma imagem do OpenCV (IplImage) e uma do FOX (FXImage), ambas compartilhando o vetor de pixels. Confira aqui.

Pois bem. Fazendo daquela maneira, há certos problemas: ambos objetos devem permanecer vivos, e o dono dos pixels tem que ser a IplImage.

Isto pode não ser desejado. Por exemplo, vou citar o caso que aconteceu comigo e tive que matar um pouco a cabeça para resolver.

Muitas imagens que eu exibo (com o FOX Toolkit) precisam antes passar por algum processamento (com o OpenCV). Depois disso, eu não preciso mais da IplImage, apenas dos pixels.

Entretanto, seguir os passos explicados no outro tópico levava a um problema seriíssimo, que será explicado no decorrer deste tópico.

Compartilhando pixels: antigamente

Conforme eu havia explicado, criava-se primeiro a IplImage e depois a FXImage, utilizando o vetor de pixels, mais ou menos assim:


IplImage *iplImage = cvCreateImage(cvSize(200, 200), IPL_DEPTH_8U, 4);

FXImage *fxImage = new FXImage(getApp(), iplImage->imageData,
    IMAGE_KEEP|IMAGE_OWNED|IMAGE_SHMP|IMAGE_SHMI,
    iplImage->width, iplImage->height);


Lembrando que era necessário um cast horroroso com reinterpret_cast para transformar os pixels da IplImage de char* para FXColor.

Com isso, eu tenho que minha IplImage "doou" seus pixels para a FXImage, que tomou conta deles. Assim, a IplImage poderia ser dispensada (na verdade, somente o cabeçalho da imagem; o OpenCV permite isso) que a FXImage, quando for liberada, libera também o vetor de pixels.

O problema

Isso causa um problema que eu demorei muito para descobrir. E, depois de descoberto, vê-se que é uma coisa tão simples... é sempre um detalhezinho que cega a gente.

O grande problema é que, como o vetor de pixels é alocado pela IplImage, ele é alocado usando malloc. A FXImage, quando tenta liberar, tenta com delete. Aí não dá certo...

A "solução"

Eu criava as imagens independentemente, cada uma com seu vetor de pixels. Depois de processada, eu fazia uma chamada a memcpy para copiar os pixels. Uma tristeza considerando o contexto.

Compartilhando pixels: a solução de fato

Depois de quase desistir da idéia, eu descobri (dia desses...) o problema. E logo cheguei a uma solução elegante (e óbvia).

Basta criar primeiro a FXImage, que vai apenas "emprestar" os pixels para a IplImage. Assim:


FXImage *fxImage = new FXImage(getApp(), 0,
    IMAGE_KEEP|IMAGE_OWNED|IMAGE_SHMP|IMAGE_SHMI,
    200, 200);


Percebam que o segundo parâmetro, que seria o vetor de pixels, é nulo. Como os pixels são de responsabilidade da FXImage, ela mesma os cria (isso é informado pela opção IMAGE_OWNED).

É necessário também manter os pixels no lado do cliente (opção IMAGE_KEEP). Sem ela, os pixels são liberados depois da imagem criada, impossibilitando o compartilhamento.

Pois bem, agora vamos "emprestar" os pixels da FXImage para a IplImage:


IplImage *iplImage = cvCreateImageHeader(cvSize(200, 200),
    IPL_DEPTH_8U, 4);
  cvSetData(iplImage, fxImage->getData(), CV_AUTOSTEP);


cvCreateImageHeader() aloca todas as informações de uma imagem, com exceção do vetor de pixels. Com isso, a IplImage, que é temporária, apenas pega emprestado os pixels para trabalhar sobre eles. Ao terminar o processamento, podemos liberar o cabeçalho:


cvReleaseImageHeader(&iplImage);


Uma grande vantagem dessa abordagem é que não precisa nem converter a imagem, conforme eu havia feito anteriormente. A imagem já é criada no formato da FXImage.

Conclusão

Conforme havia dito, é uma coisa tão simples e óbvia que fica invisível.

Espero que essa informação seja útil para quem for usar essas duas bibliotecas juntas.

Abraço e até a próxima.

quinta-feira, 16 de abril de 2009

Exibir imagem do OpenCV (IplImage)

Além do FOX Toolkit, eu utilizo a biblioteca OpenCV (Open Source Computer Vision Library), pois trabalho com Processamento de Imagens. Então surgiu a necessidade de juntar os dois.

O OpenCV até fornece alguns recursos de interface gráfica, mas são muito limitados: apenas janelas, trackbars e tratamento de eventos de mouse e teclado. A próxima figura mostra uma janela com uma trackbar.


Quebrando, então, a seqüência de tutoriais básicos, vou ensinar como exibir uma imagem do OpenCV (IplImage) em um aplicativo FOX.

Para isso, criarei um aplicativo que abre uma imagem pelo OpenCV, exibe e aplica o filtro da média. Não vou falar da criação da interface e do tratamento de eventos, apenas mostrar como se faz a "conversão" de IplImage para FXImage.

Na declaração da classe, eu apenas declaro os ponteiros para as imagens:

19 FXImageView *imageView;
20
21 FXImage *fxImage;
22 IplImage *iplImage;

No construtor, eu apenas atribuo nulo a ambas, já que o usuário é quem vai escolher a imagem.

32 fxImage = 0;
33 iplImage = 0;

A "conversão" é feita ao carregar a imagem:

52 long MainWindow::onCmdOpen(FXObject*, FXSelector, void*) {
53 FXFileDialog dialog(this, "Abrir imagem");
54
55 if (!dialog.execute(PLACEMENT_OWNER))
56 return 1;
57
58 if (fxImage)
59 delete fxImage;
60
61 if (iplImage)
62 cvReleaseImage(&iplImage);
63
64 FXString filename = dialog.getFilename();
65
66 IplImage *temp = cvLoadImage(filename.text());
67 iplImage = cvCreateImage(cvGetSize(temp), IPL_DEPTH_8U, 4);
68
69 cvCvtColor(temp, iplImage, CV_BGR2RGBA);
70
71 cvReleaseImage(&temp);
72
73 FXColor *colorData = reinterpret_cast<FXColor*> (iplImage->imageData);
74
75 fxImage = new FXImage(getApp(), colorData,
76 IMAGE_SHMP|IMAGE_SHMI|IMAGE_KEEP,
77 iplImage->width, iplImage->height);
78
79 fxImage->create();
80
81 imageView->setImage(fxImage);
82
83 return 1;
84 }

Aqui, eu crio uma imagem temporária só para carregar a imagem do arquivo. Neste caso, ele abre uma imagem colorida com três canais de 8 bits.

A representação interna do IplImage é um vetor de caracteres e os pixels são armazenados em BGR.

A representação interna do FXImage é um vetor de FXColor (equivalente a um inteiro sem sinal). Em cada inteiro são armazenados quatro canais (um byte para cada): RGBA.

Bom, aí já temos uma semelhança: ambos armazenam em um vetor. Isso que dizer que eu posso aproveitar o vetor de pixels do IplImage e jogar dentro do FXImage. Basta converter a representação interna.

Então, cria-se uma IplImage com quatro canais e profundidade de 8 bits sem sinal. Esta é a que foi declarada em nossa classe.

67 iplImage = cvCreateImage(cvGetSize(temp), IPL_DEPTH_8U, 4);

Basta então converter nossa imagem temporária de BGR para RGBA:

69 cvCvtColor(temp, iplImage, CV_BGR2RGBA);

Assim, já temos uma IplImage com representação interna parecida com a de FXImage. É preciso ainda converter de char* para FXColor*. Essa não é uma conversão padrão, portanto é preciso usar um operador especial para isso.

Eu acho até dolorido usá-lo, mas não vi outra alternativa.

73 FXColor *colorData = reinterpret_cast<FXColor*> (iplImage->imageData);

Com um vetor de pixels no formato interno do FXImage, é só criar nossa imagem:

75 fxImage = new FXImage(getApp(), colorData,
76 IMAGE_SHMP|IMAGE_SHMI|IMAGE_KEEP,
77 iplImage->width, iplImage->height);
78
79 fxImage->create();

Como a imagem foi criada depois do aplicativo, é preciso chamar create() manualmente.

Manipulando a imagem

Agora, temos um FXImage com o vetor de pixels compartilhado com o IplImage. Isso significa que qualquer operação executada sobre a imagem Ipl refletirá na imagem FX.

Isso é demonstrado aqui, onde eu aplico o filtro da média para suavizar a imagem:

88 long MainWindow::onCmdSmooth(FXObject*, FXSelector, void*) {
89 cvSmooth(iplImage, iplImage, CV_BLUR, 3, 3);
90
91 fxImage->render();
92
93 imageView->update();
94
95 return 1;
96 }

Os únicos detalhes é que é preciso chamar render() de FXImage e atualizar o imageView.

O render() é necessário porque há duas representações do FXImage: uma do lado do cliente, que neste caso é o vetor compartilhado entre as duas imagens, e o lado do servidor gráfico. render() atualiza o lado do servidor gráfico, de forma que seja exibida a imagem modificada.

E é preciso atualizar o imageView porque ele não detecta automaticamente alterações na imagem, então é preciso informar manualmente que ele deve se redesenhar.

Trabalhando com vídeos

O OpenCV, além de imagens estáticas, também trabalha com vídeos, que podem ser capturados de câmeras ou lidos de arquivos.

Cada quadro do vídeo é recuperado em um IplImage. Isso significa que dá para usar a técnica descrita acima. O problema é que a imagem que representa cada quadro não pode ser deletada nem modificada. Isso significa que para cada quadro do vídeo, é preciso fazer a conversão para uma outra imagem.

Seria mais simples se desse para modificar a imagem retornada, assim dava para compartilhar os pixels sem a necessidade de uma imagem extra.

Infelizmente, não tenho nenhum exemplo a mostrar com vídeos.

Resultado



Créditos

Imagem retirada do Eu Podia Tá Matando

---
Código-fonte deste tutorial.
Baixar vídeo.