Mostrando postagens com marcador tutoriais. Mostrar todas as postagens
Mostrando postagens com marcador tutoriais. 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.

terça-feira, 5 de maio de 2009

Botões: Texto e ícone

Neste tópico, falarei sobre adicionar um ícone ao botão, e mostrarei as posições relativas entre ícone e texto.

Carregando o ícone

Para carregar o ícone, basta proceder conforme explicado aqui.

Na declaração da classe:
30   FXIcon *searchIcon;

Em loadIcons():
82   searchIcon = iconSource.loadScaledIconFile("icons/search.gif", 32);

Lembrando que o ícone deve ser destruído manualmente, em destroyIcons():
89   delete searchIcon;

Com isso, basta passar o ícone como terceiro argumento do construtor do botão:
new FXButton(p, "text", searchIcon, ...


Posições relativas entre ícone e texto

A relação de posição entre ícone e menu é definida em FXLabel.h. FXButton é derivado direto de FXLabel. Isso significa que todas as opções deste valem para aquele também. Significa também que toda a discussão a seguir é válida para quando adicionarmos legendas (labels) na nossa interface.

Existem cinco maneiras de posicionar um ícone em relação ao texto, e cada uma delas possui dois sinônimos. As opções são:
enum {
ICON_UNDER_TEXT = 0,
ICON_AFTER_TEXT = 0x00080000,
ICON_BEFORE_TEXT = 0x00100000,
ICON_ABOVE_TEXT = 0x00200000,
ICON_BELOW_TEXT = 0x00400000,
TEXT_OVER_ICON = ICON_UNDER_TEXT,
TEXT_AFTER_ICON = ICON_BEFORE_TEXT,
TEXT_BEFORE_ICON = ICON_AFTER_TEXT,
TEXT_ABOVE_ICON = ICON_BELOW_TEXT,
TEXT_BELOW_ICON = ICON_ABOVE_TEXT
};

Obs.: existem também opções de posicionamento de texto e ícone em relação ao widget, usando opções de justificação, mas isso fica para outro tópico.


ICON_UNDER_TEXT
TEXT_OVER_ICON

Seu valor é 0, o que indica que esta é a opção "padrão". No fim deste tópico, falarei a respeito dessa questão.

Esta opção faz com que o ícone apareça por trás do texto. Exemplo:
25   new FXButton(matrix, "ICON_UNDER_TEXT", searchIcon, NULL, 0,
26 FRAME_RAISED|FRAME_THICK|ICON_UNDER_TEXT);
27
28 new FXButton(matrix, "TEXT_OVER_ICON", searchIcon, NULL, 0,
29 FRAME_RAISED|FRAME_THICK|TEXT_OVER_ICON);



ICON_AFTER_TEXT
TEXT_BEFORE_ICON

Desenha o ícone à direita do texto. Exemplo:
32   new FXButton(matrix, "ICON_AFTER_TEXT", searchIcon, NULL, 0,
33 FRAME_RAISED|FRAME_THICK|ICON_AFTER_TEXT);
34
35 new FXButton(matrix, "TEXT_BEFORE_ICON", searchIcon, NULL, 0,
36 FRAME_RAISED|FRAME_THICK|TEXT_BEFORE_ICON);


ICON_BEFORE_TEXT
TEXT_AFTER_ICON

Desenha o ícone à esquerda do texto. Exemplo:
39   new FXButton(matrix, "ICON_BEFORE_TEXT", searchIcon, NULL, 0,
40 FRAME_RAISED|FRAME_THICK|ICON_BEFORE_TEXT);
41
42 new FXButton(matrix, "TEXT_AFTER_ICON", searchIcon, NULL, 0,
43 FRAME_RAISED|FRAME_THICK|TEXT_AFTER_ICON);


ICON_ABOVE_TEXT
TEXT_BELOW_ICON

Desenha o ícone acima do texto. Exemplo:
46   new FXButton(matrix, "ICON_ABOVE_TEXT", searchIcon, NULL, 0,
47 FRAME_RAISED|FRAME_THICK|ICON_ABOVE_TEXT);
48
49 new FXButton(matrix, "TEXT_BELOW_ICON", searchIcon, NULL, 0,
50 FRAME_RAISED|FRAME_THICK|TEXT_BELOW_ICON);


ICON_BELOW_TEXT
TEXT_ABOVE_ICON

Desenha o ícone sob o texto. Exemplo:
53   new FXButton(matrix, "ICON_BELOW_TEXT", searchIcon, NULL, 0,
54 FRAME_RAISED|FRAME_THICK|ICON_BELOW_TEXT);
55
56 new FXButton(matrix, "TEXT_ABOVE_ICON", searchIcon, NULL, 0,
57 FRAME_RAISED|FRAME_THICK|TEXT_ABOVE_ICON);


Voltando à questão do valor "padrão"

ICON_UNDER_TEXT tem valor 0, o que o torna, teoricamente, a opção padrão.

Para entender essa questão, note que as demais opções podem ser divididas entre posicionamento vertical (ICON_ABOVE_TEXT e ICON_BELOW_TEXT) e posicionamento horizontal (ICON_BEFORE_TEXT e ICON_AFTER_TEXT).

Isso significa que eu posso combinar opções de posicionameto horizontal e vertical. Porém, se eu não especificar um ou outro, a opção padrão será utilizada. Neste caso, é melhor entender a opção padrão não como o nome diz (ícone debaixo (atrás?) do texto), mas que o ícone ficará centralizado naquele eixo que não foi especificado.

Voltando aos exemplos, perceba que ICON_AFTER_TEXT e ICON_BEFORE_TEXT deixam o ícone centralizado na vertical. Da mesma forma, ICON_ABOVE_TEXT e ICON_BELOW_TEXT deixam o ícone centralizado na horizontal. Ainda, a opção ICON_UNDER_TEXT deixa o ícone centralizado em ambos os eixos.

Então por que "padrão", entre aspas? Tem algum detalhe?
Sim. Este padrão é considerado neste caso, em que se omite o posicionamento em um dos eixos. Acontece que muitos widgets FOX têm uma opção com o sufixo _NORMAL, que reúnem as opções mais comuns, e são passadas como argumento padrão para o construtor.

Assim, para o FXButton, temos BUTTON_NORMAL, definido junto com outras opções:
BUTTON_NORMAL    = (FRAME_RAISED|FRAME_THICK |
JUSTIFY_NORMAL
|ICON_BEFORE_TEXT)

Na declaração do construtor:
FXButton(FXComposite* p, ..., FXuint opts=BUTTON_NORMAL, ...

Ou seja, para um botão, o padrão é exibir o ícone à esquerda do texto. Como não foi especificado o posicionamento na vertical, fica centralizado.

Espero que essa diferença tenha ficado clara. A distinção está na questão de existir uma opção padrão (que leva o valor 0 exatamente para que não seja necessário informá-la explicitamente) e existir um argumento padrão para o construtor de um widget, que reúne as opções mais comuns, que não necessariamente são as opções padrões.

E como fica se eu combinar posicionamento vertical e horizontal?

Veja:


Deixo como exercício.

Até a próxima.

---
Código-fonte para este tutorial.

segunda-feira, 4 de maio de 2009

Layout Managers: FXHorizontalFrame

Neste tópico, falarei sobre o gerenciador de layout FXHorizontalFrame. Aproveito também para mostrar algumas dicas de layout.


Descrição
O gerenciador de layout HorizontalFrame é utilizado para posicionar automaticamente as janelas filhas horizontalmente, da esquerda para a direita, ou da direita para a esquerda, dependendo das dicas de layout da janela.

Obs.: "janela" aqui refere-se a qualquer derivado de FXWindow.


Exemplo

Neste exemplo, temos um HorizontalFrame e dentro dele seis botões:
22   FXHorizontalFrame *frame = new FXHorizontalFrame(this,
23 LAYOUT_FILL_X|PACK_UNIFORM_WIDTH|PACK_UNIFORM_HEIGHT|FRAME_RAISED);

Aqui, é criado o frame. Ele se expandirá por todo o eixo X da janela (LAYOUT_FILL_X) e todos os seus filhos terão a mesma dimensão (PACK_UNIFORM_WIDTH | PACK_UNIFORM_HEIGHT). Também possui uma borda "levantada", que eu coloquei aqui apenas para destacar.

Em seguida, são criados os botões:
25   new FXButton(frame, "Botão 1");
26 new FXButton(frame, "Botão 2");
27 new FXButton(frame, "Botão com texto grande");
28 new FXButton(frame, "Botão em\nduas linhas");
29
30 new FXButton(frame, "Centro", NULL, NULL, 0,
31 BUTTON_NORMAL|LAYOUT_CENTER_X);
32
33 new FXButton(frame, "Botão à direita", NULL, NULL, 0,
34 BUTTON_NORMAL|LAYOUT_RIGHT);

Os dois primeiros não têm nada de peculiar. O terceiro possui o texto mais longo de todos e o quarto tem o texto quebrado em duas linhas. Isso vai influir no layout dos botões, conforme veremos mais adiante.

O quinto botão é posicionado no centro (LAYOUT_CENTER_X). Isso não fará que ele fique no centro da janela, mas no centro da área restante. O BUTTON_NORMAL serve para preservar as características originais de borda e texto do botão. O sexto botão fica alinhado à direita (LAYOUT_RIGHT).

Não há muito o que discutir além disso, só mostrar o resultado.


Resultado


Percebe-se que os botões estão todos juntos. Isso se deve ao fato de eu não ter especificado nenhum tamanho para a janela principal (na verdade, não especifiquei tamanho de ninguém...). Assim, ela assume o tamanho mínimo para que caibam todos os seus filhos. E assim cada filho dela também o fez.

Por isso o "Botão com texto grande" ficou mais longo e o "Botão em duas linhas" ficou com altura maior. Como o frame foi instruído a deixar todo mundo nas mesmas dimensões, todo mundo recebeu a largura do terceiro botão e a altura do quarto.

Aliás, esse é o principal motivo pelo qual eu não especifiquei um tamanho para a janela principal, como vinha fazendo nos tutoriais anteriores (nos quais eu criava uma janela 200x200). Especificar um tamanho corre o risco de não caber todo mundo, ou de ficar grande demais. Na verdade, é bem difícil acertar. É muito melhor deixar o próprio FOX tratar esses detalhes.

E quanto ao posicionamento dos dois últimos botões? Um centralizado e o outro à direita?

Pois bem, esse é o tamanho mínimo, que coube todo mundo, então fica todo mundo juntinho. Se eu aumentar a janela, eles assumem a posição correspondente:


Desta vez fica claro: o botão "Centro" ficou no meio do espaço disponível, e o "Botão à direita" ficou no canto onde realmente deveria estar.


Conclusão

Este tópico mostrou um dos gerenciadores de layout mais simples que o FOX Toolkit fornece. Mostrou também como agem algumas dicas de layout, bem como as dicas de empacotamento discutidas aqui e aqui.

Com o tempo, ficará bem mais claro o poder que os gerenciadores de layout oferecem.

Só como presente de despedida desse tópico, uma pergunta que não quer calar:
Se eu não definisse aquelas opções de empacotamento?

Apenas veja:


Eu poderia ainda instruir os botões a ficarem centralizados no eixo Y (LAYOUT_CENTER_Y), mas isso não ajudaria muito em termos estéticos. Vou deixar isso a cargo da curiosidade do leitor.

---
Código-fonte para este tutorial.

terça-feira, 28 de abril de 2009

Tratamento de Eventos: Introdução

Neste tutorial, coloquei um botão simples na janela. Esse botão não possuía ação nenhuma. Inútil.

Agora, vou adicionar um evento a este botão: fechar o aplicativo (embora o texto do botão seja "Hello FOX"...).

Para isso, basta informar quem é o alvo do evento e qual a mensagem:
22   new FXButton(this, "Hello, &FOX!", NULL, getApp(), FXApp::ID_QUIT,
23 BUTTON_NORMAL|LAYOUT_FILL);

Neste caso, o alvo do meu evento é o próprio aplicativo, que pode ser recuperado chamando getApp(), presente em todas as classes derivadas de FXId (na prática, quase todas as classes FOX são; veja aqui a hierarquia de FXId).

A bem da verdade, nem seria necessário chamar esse método, já que estamos no construtor da janela, cujo primeiro parâmetro é a aplicação; bastava eu passar o ponteiro. Utilizei essa chamada aqui apenas para ilustrar.

O próximo parâmetro é o identificador da mensagem. Por convenção, sempre começa com ID_. Aqui eu passei ID_QUIT, que pertence à classe FXApp.

É só isso. Assim, eu conectei o botão à aplicação. Quando o botão envia um evento, a aplicação responde. Basicamente, é assim que os objetos se comunicam no FOX Toolkit.

Mas o que acontece lá dentro?

Bom, existem alguns detalhes a respeito disso. Com este exemplo, só posso dizer que o botão envia para a aplicação uma mensagem do tipo ID_QUIT, e a aplicação responde com a ação correspondente, ou seja, terminar a aplicação.

Futuramente, entrarei em detalhes e explicarei como funciona o mecanismo de comunicação entre os objetos FOX.

Só um detalhe, para o qual eu apanhei no começo e levei um certo tempo para descobrir o que era: eu criei um botão para fechar o aplicativo, mas em vez de passar o aplicativo, eu passei a janela. Resultado: a janela fechava, mas o aplicativo continuava executando... aí eu tinha que matar o processo.

quinta-feira, 23 de abril de 2009

Botões: Botão simples

Retornando aos tutoriais básicos, falarei agora rapidamente sobre criar um botão na interface. Também os detalhes são diversos aqui, portanto este também é o primeiro tópico de uma série sobre os botões que o FOX Toolkit disponibiliza.

Neste primeiro tópico, criarei apenas um botão que se expande pela janela inteira e não está associado a nenhum evento (falarei mais tarde sobre tratamento de eventos).

Vamos lá, então.

FoxTutorialMainWindow.cpp

22   new FXButton(this, "Hello, &FOX!", NULL, NULL, 0,
23 BUTTON_NORMAL|LAYOUT_FILL);

Pronto, o botão está criado.

Discussão

FXButton é a classe FOX para um botão simples, que você pressiona, solta, e ele executa uma ação. Aqui eu instancio um novo objeto dessa classe. Novamente, sem a necessidade de atribuir a um ponteiro, pois a própria janela se encarregará de liberá-lo.

Os parâmetros que foram passados:
  • this: significa que o botão será colocado dentro da própria janela principal.
  • "Hello, &FOX!": texto que aparecerá no botão. O & significa a letra que servirá de mnemônico, no caso F. Assim, Alt+F é o atalho para pressionar o botão. Existem outros detalhes dentro do texto do botão, que serão discutidos mais adiante.
  • NULL: o ícone do botão. Aqui é criado um botão somente com texto, então fica nulo mesmo.
  • NULL: o alvo da mensagem. Como ele não lança nenhum evento, nulo.
  • 0: a mensagem que o botão envia. Novamente, ele não lança nenhum evento: zero.
  • BUTTON_NORMAL|LAYOUT_FILL: opções do botão. BUTTON_NORMAL significa um botão com borda grossa e elevada, com o texto centralizado e o ícone à esquerda. LAYOUT_FILL significa que ele será expandido para ocupar a janela inteira. Esses detalhes serão explicados em seu devido tempo.

As demais opções referem-se à posição, tamanho e espaçamentos do botão, e normalmente não é necessário informá-los, pois têm valores padrões.

Resultado


Lembrando que FXMainWindow é um gerenciador de layout, redimensionar a janela fará com que o botão seja automaticamente redimensionado.

---
Código-fonte para este tutorial.

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.