JAI: Java Advanced Imaging

Propaganda
JAI: Java Advanced Imaging
Rafael Santos1
Resumo: Apesar da popularização de pacotes de software para processamento de
imagens, em muitos casos é necessário implementar novas operações ou algoritmos
usando uma linguagem de programação alto nível. Java é uma excelente alternativa
por ser portátil entre sistemas operacionais, por ser relativamente simples e em especial por já ter mais de uma API (Application Programming Interface) para representação, processamento e entrada e saída de imagens. Uma das APIs é a Java Advanced
Imaging (JAI), que provê métodos para representação e processamento de imagens de
grande porte.
Este tutorial apresenta os conceitos básicos de representação e processamento de imagens usando Java e a API JAI, com ênfase em exemplos simples.
Abstract:
In spite of the popularization of image processing software, for some
tasks one must implement new operations or algorithms using a high-level programming language. Java is an excellent alternative since it is portable, relatively simple
and since it already have more than one API (Application Programming Interface)
for image representation, processing and input/output. One of those APIs is the Java
Advanced Imaging API, which provides methods for representation and processing of
large images.
This tutorial presents the basic concepts on image representation and processing using
Java and the JAI API, with an emphasis on simple examples.
1
Introdução
Técnicas de processamento de imagens digitais existem há quase noventa anos, com
um crescimento explosivo em aplicações nos últimos quarenta anos[3]. Algoritmos para diversos tipos de processamento já são parte integrante de pacotes de software e até de sistemas
embarcados, mas frequentes avanços tecnológicos e novas áreas de aplicação demandam o
estudo e criação de novos algoritmos e soluções.
Alguns pacotes de software para processamento de imagens permitem a implementação de novos algoritmos, geralmente usando uma linguagem de alto nível e a API da própria
aplicação, mas frequentemente existe um custo na aquisição do pacote e/ou na aquisição do
1 Laboratório
Associado de Computação e Matemática Aplicada
Instituto Nacional de Pesquisas Espaciais
Av. dos Astronautas, 1758 – Jardim da Granja – CEP 12227-010
São José dos Campos – SP – Brasil
[email protected]
JAI: Java Advanced Imaging
módulo de desenvolvimento de novos módulos, e possíveis restrições na disponibilização
destes novos módulos para outros usuários.
Uma alternativa interessante para o desenvolvimento de rotinas de processamento de
imagens é a linguagem Java2 , em especial quando usada em conjunto com a API (Application
Programming Interface) JAI (Java Advanced Imaging)3 . A linguagem e API são portáteis
entre sistemas operacionais, podem ser obtidas gratuitamente e contém operadores e classes para representação, processamento e entrada e saída de imagens flexíveis e poderosas,
permitindo também a criação de novos operadores.
Neste tutorial veremos alguns conceitos de representação de imagens digitais, seu
processamento, armazenamento e visualização usando Java e a API JAI. Este tutorial é de
caráter introdutório e prático, com ênfase em exemplos simples que podem facilmente ser
modificados e adaptados. Informações sobre a arquitetura das APIs mencionadas não serão
apresentadas, e alguns conceitos serão apresentados de forma simplificada para que o leitor
possa colocá-los em prática o mais rápido possível. Assume-se que o leitor tenha conhecimentos básicos de programação em Java ou em outra linguagem moderna, e que tenha noções
de processamento de imagens e matemática.
O tutorial é organizado da seguinte forma: cada seção apresenta uma tarefa genérica de
representação, processamento, visualização ou entrada e saída de imagens, alguns conceitos
teóricos (se necessário) e exemplos de código que ilustram como realizar a tarefa. Alguns
exemplos requerem somente APIs que fazem parte da distribuição básica de Java (AWT,
Abstract Window Toolkit, Swing, ImageIO), enquanto outros são específicos da API JAI.
Algumas listagens deste tutorial não foram reproduzidas integralmente – nestas, somente as linhas de código relevantes ao exemplo foram incluidas. Todas as listagens completas podem ser copiadas do site do autor (http://www.lac.inpe.br/∼rafael.santos) ou de [7].
2
Representando imagens digitais
O primeiro passo necessário para implementar algoritmos de processamento de imagens é entender o que é uma imagem digital e como a mesma é representada na memória.
Uma imagem digital pode ser considerada como uma matriz regular bidimensional de elementos chamados pixels. Para a grande maioria das aplicações consideramos que os pixels
são quadrados, isto é, tem a mesma largura e altura; que a matriz é regular, sem buracos
ou descontinuidades; e que a coordenada superior esquerda da matriz é (0, 0). Consideramos também que um pixel pode representar um ou mais valores (correspondendo ao número
da bandas na imagem) e que são todos do mesmo tipo básico. Valores dos pixels podem
2 http://java.sun.com/javase/downloads/index.jsp
3 http://java.sun.com/javase/technologies/desktop/media/jai/
2
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
ser interpretados através de diversos sistemas de cores, ou mesmo ser somente índices para
tabelas de cores. Outras variações na representação de imagens são possíveis mas não as
consideraremos neste tutorial.
Para exemplificar, uma imagem de câmera digital é uma matriz de dimensões correspondentes à resolução da câmera, com três bandas que representam a intensidade dos
componentes vermelho, verde e azul, ou seja, com três valores (normalmente entre 0 e 255)
associados a cada pixel. Uma imagem digital correspondente a um modelo de elevação de
terreno, usada em várias aplicações de sensoriamento remoto, é uma matriz regular onde
cada pixel corresponde a um único valor, frequentemente de ponto flutuante, podendo até
representar valores negativos, correspondentes à altura do terreno naquela posição.
Imagens digitais podem ser armazenadas em dispositivos ou transmitidas por diversos
meios – quando isto é feito, a estrutura que compõe uma imagem é codificada usando algum
formato específico para armazenamento ou transmissão e decodificada para leitura para a
memória. Existem diversos tipos de formatos de armazenamento de imagens digitais, entre
eles, TIFF (Tagged Image File Format), PNG (Portable Network Graphics), JPEG (Joint
Photographic Experts Group) e outros.
O formato de uma imagem armazenada em dispositivos não deve ser confundido com
o formato de imagens na memória – os primeiros sempre dependem da estrutura do codificador e do algoritmo usado pelo mesmo, enquanto os últimos são baseados nas estruturas
descritas na próxima subseção. O desenvolvedor só deve se preocupar em entender as limitações dos codificadores e decodificadores. Por exemplo, o formato JPEG causa perda de
precisão na representação dos pixels, e o formato TIFF permite armazenamento de imagens
com pixels com mais de quatro bandas e/ou com valores de ponto flutuante.
2.1
Representação de imagens digitais em Java
De uma forma geral uma imagem digital é representada em Java usando-se uma instância de classe que representa uma imagem. Esta classe, por sua vez, contém instâncias de
duas classes (ou herdeiras) que são [5]:
• Uma instância de Raster, que contém os valores dos pixels da imagem. Um raster,
por sua vez, é representado por uma instância de DataBuffer, que contém os valores
dos pixels e de SampleModel, que indica como estes valores são organizados na
memória.
• Uma instância de ColorModel, que indica como os valores dos pixels serão interpretados para renderização da imagem em dispositivos. Esta instância pode conter uma
referência a instância de ColorSpace que representa alguns dos tipos básicos de
espaços de cores.
RITA • Volume – • Número – • —-
3
JAI: Java Advanced Imaging
Tanto a API básica de representação de imagens em Java quanto a API JAI contém
métodos para criar e manipular imagens usando instâncias destas classes. Várias combinações de classes e parâmetros são possíveis, permitindo a representação de uma enorme gama
de tipos de imagens digitais.
Um ponto que gera alguma confusão é a existência de várias classes e interfaces, tanto
em Java quanto na API JAI, que podem ser usadas para representar imagens. Algumas destas
classes e interfaces são descritas a seguir.
• BufferedImage é uma classe padrão de Java (isto é, não faz parte da API JAI), que
pode ser usada para a criação de imagens simples, que possivelmente atendem a maior
parte das necessidades de processamento de imagens.
• RenderedImage é uma interface padrão de Java, implementada pela classe BufferedImage, que define que métodos devem ser implementados por classes que contenham dados na forma de Rasters.
• PlanarImage é uma classe da API JAI que também implementa RenderedImage
mas que pode representar imagens de forma bem mais complexa do que BufferedImage, por exemplo, incluindo a capacidade de representar imagens como nós em um
grafo, possibilitando, por exemplo, que uma imagem seja definida em função de outras,
mas permitindo somente a criação e leitura (mas não modificação!) de seus pixels.
• TiledImage é uma subclasse mais flexível de PlanarImage que permite acesso
direto a seus pixels e tiles. Um tile ou ladrilho é um segmento retangular da imagem
que pode ser processado independente dos outros segmentos, possibilitando o processamento parcial de imagens, o que é muito útil para o processamento de imagens de
grande porte.
Existem métodos de conversão de instâncias de imagens de um tipo para outro. De
forma simples, para criar imagens na memória usamos BufferedImage para imagens simples e TiledImage para imagens com características mais complexas. Usaremos instâncias
de PlanarImage para representar nós em grafos de processamento com os operadores da
API JAI, como será mostrado posteriormente.
Como primeiro exemplo de criação de imagens na memória, vejamos como criar uma
imagem colorida em Java usando a classe BufferedImage (ou seja, sem usar a API JAI).
Instâncias desta classe podem ser criadas de forma simples, ocultando a complexidade interna
da classe. Com a instância criada podemos obter uma instância de WritableRaster e
manipular os pixels da imagem através desta instância. O trecho de código mostrado na
Listagem 1 mostra como isto pode ser feito.
Listagem 1. Criando uma imagem RGB (sem usar a API JAI)
public static void main(String[] args) throws IOException
12
4
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
int width = 256;
int height = 256;
BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
WritableRaster raster = image.getRaster();
int[] cor1 = new int[]{255,0,0};
int[] cor2 = new int[]{0,0,255};
int cont=0;
for(int h=0;h<height;h++)
for(int w=0;w<width;w++)
{
if ((((w/32)+(h/32)) % 2) == 0) raster.setPixel(w,h,cor1);
else raster.setPixel(w,h,cor2);
}
ImageIO.write(image,"PNG",new File("checkerboard.png"));
}
O código mostrado na Listagem 1 cria uma imagem com duas cores básicas formando
um padrão de tabuleiro de xadrez. A imagem terá três bandas, usando o sistema de cores
RGB, ou seja, cada pixel terá um componente vermelho, um verde e um azul. A imagem será
armazenada no formato PNG em um arquivo local.
A criação de uma imagem usando a API JAI é consideravelmente mais complexa, mas
esta complexidade permite enorme flexibilidade nos formatos e estruturas internas. Os passos
são os seguintes:
1. Criar os dados da imagem em um array unidimensional (o mapeamento das coordenadas da imagem deve ser feito pelo programador).
2. Com este array, criar uma instância de classe que herda de DataBuffer do tipo
adequado.
3. Criar uma instância de SampleModel do tipo e dimensões adequadas. A classe
RasterFactory contém métodos-fábrica que facilitam este passo.
4. Com a instância de SampleModel criar um ColorModel compatível.
5. Usar as instâncias de DataBuffer e SampleModel para criar uma instância de
WritableRaster. Esta instância, depois de criada, pode ser também usada para
manipular os pixels desta imagem.
6. Usar as instâncias de SampleModel e ColorModel para criar uma instância de
TiledImage.
7. Associar o Raster à instância de TiledImage.
Ao fim destes passos a instância de TiledImage pode ser manipulada, visualizada
ou armazenada. Como exemplo, vejamos o trecho de código na Listagem 2, que cria uma
imagem com pixels de ponto flutuante. A imagem é armazenada no formato TIFF, que permite o armazenamento de pixels de ponto flutuante.
RITA • Volume – • Número – • —-
5
JAI: Java Advanced Imaging
Listagem 2. Criando uma imagem com pixels de ponto flutuante (usando JAI)
public static void main(String[] args) throws IOException
{
int width = 256;
int height = 256;
float[] imageData = new float[width*height];
int count = 0;
for(int h=0;h<height;h++)
for(int w=0;w<width;w++)
imageData[count++] = 20f*w*h;
DataBufferFloat dbuffer = new DataBufferFloat(imageData,width*height);
SampleModel sampleModel =
RasterFactory.createBandedSampleModel(DataBuffer.TYPE_FLOAT,width,height,1);
ColorModel colorModel = PlanarImage.createColorModel(sampleModel);
WritableRaster raster =
RasterFactory.createWritableRaster(sampleModel,dbuffer,new Point(0,0));
TiledImage tiledImage = new TiledImage(0,0,width,height,0,0,sampleModel,colorModel);
tiledImage.setData(raster);
JAI.create("filestore",tiledImage,"floatpattern.tif","TIFF");
}
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
3
Entrada e Saída
Os exemplos mostrados até agora ilustram como imagens podem ser criadas na memória, mas a grande maioria das aplicações de processamento de imagens assume que as
imagens já existem em um dispositivo, e que devem ser carregadas na memória para processamento ou exibição, podendo opcionalmente ser armazenadas novamente (com as modificações) nos dispositivos.
Na seção 2 vimos que o formato de armazenamento das imagens em disco é diferente
do formato de armazenamento na memória. Para armazenar e recuperar imagens em dispositivos devemos usar codificadores e decodificadores para transformar de um formato para o
outro. Estes codificadores e decodificadores são implementados em classes de Java em duas
APIs: a API ImageIO e em métodos da API JAI. As duas APIs se complementam, pois nem
todos os formatos são suportados pelas duas.
A classe ImageIO contém métodos estáticos para codificar e decodificar imagens
(instâncias de BufferedImage) de e para arquivos e streams e para descobrir quais codificadores e decodificadores estão disponíveis para uso em uma determinada instalação da
máquina virtual Java. O método getImageReadersByFormatName recebe um nome
de um formato como parâmetro ("tiff", "jpg", etc) e retorna um Iterator de instâncias de
ImageReaders que podem ser usados para decodificar as imagens. O método getImageWritersByFormatName também recebe um nome de formato e retorna um Iterator
de instâncias de ImageWriters que podem ser usados para codificar as imagens. Para saber quais codificadores e decodificadores podem ser usados basta percorrer este Iterator
6
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
procurando uma instância adequada.
O armazenamento de uma imagem em arquivo ou stream pode ser feito usando o
método estático ImageIO.write que, na sua forma mais usada, recebe três parâmetros: a
instância de classe que implementa RenderedImage, uma string com o formato no qual
a imagem será codificada e uma instância da classe File representando o arquivo onde a
imagem será armazenada. Este método retorna o booleano false se não houver codificador
apropriado para a imagem. Um exemplo deste método pode ser visto na última linha da
Listagem 1.
Uma imagem pode ser decodificada de um arquivo ou stream com uma chamada ao
método estático ImageIO.read que na sua forma mais usada recebe um único argumento:
uma instância da classe File representando o arquivo onde a imagem está armazenada.
Este método retorna uma instância de BufferedImage ou null caso não seja possível
decodificar a imagem. A Listagem 3 mostra uma classe que lê uma imagem de um arquivo
em disco e mostra as suas dimensões.
Listagem 3. Decodificando uma imagem com o método ImageIO.read
public static void main(String[] args) throws IOException
{
File f = new File(args[0]);
BufferedImage image = ImageIO.read(f);
System.out.println("Dimensões: "+image.getWidth()+"x"+image.getHeight()+" pixels");
}
11
12
13
14
15
16
A codificação (gravação) e decodificação (leitura) de imagens podem ser feita também
usando um método estático da API JAI. Este método é o método genérico de chamada de
operadores da API, executado com o parâmetro filestore, que indica que a imagem será
armazenada em um arquivo local. O método é o JAI.create, e recebe quatro parâmetros:
o primeiro é uma string contendo a operação (filestore), o segundo é a instância de classe
que implementa RenderedImage, o terceiro é o nome do arquivo para armazenamento e o
quarto é o formato de codificação a ser usado. Um exemplo de chamada deste operador pode
ser visto na última linha da Listagem 2. Para leitura de imagens usando a API JAI podemos
usar o método estático JAI.create com o primeiro parâmetro igual à string fileload e
o segundo contendo o nome do arquivo a ser lido. Se a imagem for decodificada com sucesso
será armazenada em uma instância de classe que herda de PlanarImage.
4
Acesso a valores de pixels
Para a implementação de alguns algoritmos específicos de processamento de imagens,
é necessário obter os valores dos pixels em determinadas posições ou regiões. A forma mais
simples de fazer isto é usando uma imagem já na memória (instância de BufferedImage
RITA • Volume – • Número – • —-
7
JAI: Java Advanced Imaging
ou PlanarImage, por exemplo), obtendo o Raster associado a ela e usando um dos
métodos getSample ou getPixel. Estes métodos recuperam um valor de um pixel ou
todos os valores (array) associado a um pixel, respectivamente.
Um exemplo é dado pelo trecho de código na Listagem 4, que varre todos os pixels de
uma imagem contando os que são exatamente brancos.
Listagem 4. Recuperando valores de pixels (sem usar JAI)
public static void main(String[] args) throws IOException
{
File f = new File(args[0]);
BufferedImage imagem = ImageIO.read(f);
Raster raster = imagem.getRaster();
int[] pixel = new int[3];
int brancos = 0;
for(int h=0;h<imagem.getHeight();h++)
for(int w=0;w<imagem.getWidth();w++)
{
raster.getPixel(w,h,pixel);
if ((pixel[0] == 255) && (pixel[1] == 255) && (pixel[2] == 255)) brancos++;
}
System.out.println(brancos+" pixels brancos");
}
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Valores de pixels também podem ser obtidos diretamente de uma instância de RenderedImage com o método getRGB, que retorna os bytes correspondentes aos valores R, G,
B e A (transparência) compactados em um valor inteiro.
A API JAI permite também a recuperação de Rasters de instâncias de PlanarImage, mas também provê uma forma alternativa de obter os valores dos pixels. A classe
RandomIter permite a criação de um mecanismo de iteração associado a uma imagem para
recuperação dos valores, como mostrado no exemplo da Listagem 5, que faz basicamente o
mesmo que a classe na Listagem 4 usando iteradores.
Listagem 5. Recuperando valores de pixels (usando JAI)
public static void main(String[] args) throws IOException
{
File f = new File(args[0]);
BufferedImage imagem = ImageIO.read(f);
RandomIter iterator = RandomIterFactory.create(imagem,null);
int[] pixel = new int[3];
int brancos = 0;
for(int h=0;h<imagem.getHeight();h++)
for(int w=0;w<imagem.getWidth();w++)
{
iterator.getPixel(w,h,pixel);
if ((pixel[0] == 255) && (pixel[1] == 255) && (pixel[2] == 255)) brancos++;
}
System.out.println(brancos+" pixels brancos");
}
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
8
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
A API JAI provê também iteradores sequenciais através das classes RectIter e
RookIter. Versões que permitem a modificação dos valores dos pixels são implementadas
pelas classes WritableRandomIter, WritableRectIter e WritableRookIter.
Pixels também podem ser modificados em instâncias de TiledImage através do método
setSample, e em instâncias da classe WritableRaster através dos métodos setSample, setPixel, setSamples e setPixels.
5
Display de imagens
Exibição de imagens em um monitor é uma função indispensável de um sistema de
processamento de imagens. Aplicações que exibem imagens podem ser facilmente escritas
usando Java, com ou sem a API JAI.
Podemos usar classes da API Swing para exibir instâncias de BufferedImages
através da criação de uma instância da classe JLabel que deve receber no seu construtor,
como argumento uma instância de ImageIcon que por sua vez recebe como argumento
uma instância de BufferedImage. A instância de JLabel pode ser adicionada à interface
gráfica de uma aplicação envolvida em uma instância de JScrollPane para conter barras
de rolagem que são necessárias para imagens com áreas maiores do que a disponível na
interface gráfica.
A técnica é exemplificada na classe mostrada na Listagem 6. A Figura 1 mostra a
interface criada pela aplicação na Listagem 6.
Listagem 6. Exibindo uma instância de BufferedImage (sem usar JAI)
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws IOException
{
BufferedImage image = ImageIO.read(new File(args[0]));
JFrame frame = new JFrame("Display Image: "+args[0]);
ImageIcon icon = new ImageIcon(image);
JLabel imageLabel = new JLabel(icon);
frame.getContentPane().add(new JScrollPane(imageLabel));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(600,300);
frame.setVisible(true);
}
A exibição de imagens também pode ser feita usando uma classe específica da API
JAI: DisplayJAI. Esta classe representa um componente gráfico que pode ser adicionado à
qualquer interface gráfica de aplicações em Java. Apesar desta classe não ser parte oficial da
API ela é provida pela implementação da Sun Microsystems, e além de ser mais flexível do
que a combinação ImageIcon/JLabel ela permite a visualização de imagens de grande
porte, contanto que sejam ladrilhadas (tiled).
RITA • Volume – • Número – • —-
9
JAI: Java Advanced Imaging
Figura 1. Interface gráfica da classe DisplaySemJAI
O trecho de código na Listagem 7 mostra como usar uma instância de DisplayJAI
para exibir uma instância de BufferedImage. A interface com o usuário é semelhante à
mostrada na Figura 1.
Listagem 7. Exibindo uma instância de BufferedImage (usando JAI)
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws IOException
{
BufferedImage image = ImageIO.read(new File(args[0]));
JFrame frame = new JFrame("Display Image: "+args[0]);
DisplayJAI display = new DisplayJAI(image);
frame.getContentPane().add(new JScrollPane(display));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(600,300);
frame.setVisible(true);
}
A classe DisplayJAI pode ser usada como base para outras finalidades mais específicas. Consideremos, por exemplo, o problema de visualizar imagens cujos pixels não
são valores inteiros, ou cujos pixels representem mais do que três valores. É relativamente
simples criar uma classe herdeira de DisplayJAI que, a partir de uma destas imagens
especiais, crie uma imagem substituta para visualização.
Para o caso de imagens cujos pixels podem assumir valores diferentes dos entre 0 e
255, podemos normalizar os valores subtraindo de cada pixel o valor Vmin e multiplicando
o resultado por 255/(Vmax − Vmin ), onde Vmin e Vmax são respectivamente os menores
e maiores valores da imagem original. A imagem normalizada pode ser convertida para
o tipo adequado (byte) e exibida normalmente. Os valores extremos das imagens podem
ser obtidos com operadores estatísticos da API JAI. Esta técnica é demonstrada na classe
DisplaySurrogateImage, mostrada na Listagem 8.
10
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
Listagem 8. Componente para criação e exibição de uma imagem substituta (usando JAI)
1
package wvc.display;
2
3
4
import java.awt.image.DataBuffer;
import java.awt.image.renderable.ParameterBlock;
5
6
7
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
8
9
import com.sun.media.jai.widget.DisplayJAI;;
10
11
12
public class DisplaySurrogateImage extends DisplayJAI
{
13
14
15
protected PlanarImage surrogateImage;
protected int width,height;
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public DisplaySurrogateImage(PlanarImage image)
{
width = image.getWidth();
height = image.getHeight();
// Recuperamos valores extremos da imagem.
ParameterBlock pbMaxMin = new ParameterBlock();
pbMaxMin.addSource(image);
PlanarImage extrema = JAI.create("extrema", pbMaxMin);
double[] allMins = (double[])extrema.getProperty("minimum");
double[] allMaxs = (double[])extrema.getProperty("maximum");
double minValue = allMins[0];
double maxValue = allMaxs[0];
for(int v=1;v<allMins.length;v++)
{
if (allMins[v] < minValue) minValue = allMins[v];
if (allMaxs[v] > maxValue) maxValue = allMaxs[v];
}
// Reescalamos os níveis de cinza da imagem.
double[] subtract
= new double[1]; subtract[0]
= minValue;
double[] multiplyBy = new double[1]; multiplyBy[0] = 255./(maxValue-minValue);
ParameterBlock pbSub = new ParameterBlock();
pbSub.addSource(image);
pbSub.add(subtract);
surrogateImage = (PlanarImage)JAI.create("subtractconst",pbSub);
ParameterBlock pbMult = new ParameterBlock();
pbMult.addSource(surrogateImage);
pbMult.add(multiplyBy);
surrogateImage = (PlanarImage)JAI.create("multiplyconst",pbMult);
// Convertemos para bytes.
ParameterBlock pbConvert = new ParameterBlock();
pbConvert.addSource(surrogateImage);
pbConvert.add(DataBuffer.TYPE_BYTE);
surrogateImage = JAI.create("format", pbConvert);
// Usamos esta imagem para display.
set(surrogateImage);
}
53
54
}
Um exemplo de uso da classe DisplaySurrogateImage pode ser visto na Lista-
RITA • Volume – • Número – • —-
11
JAI: Java Advanced Imaging
gem 9. Este exemplo é usado para mostrar uma imagem substituta correspondente à imagem
com pixels de ponto flutuante gerada pela classe da Listagem 2. A interface gráfica da aplicação é mostrada na Figura 2.
Listagem 9. Aplicação que demonstra a classe DisplaySurrogateImage
10
11
12
13
14
15
16
17
18
public static void main(String[] args)
{
PlanarImage image = JAI.create("fileload", args[0]);
JFrame frame = new JFrame("Mostrando "+args[0]);
frame.getContentPane().add(new JScrollPane(new DisplaySurrogateImage(image)));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
Figura 2. Interface gráfica da classe DemonstraDisplaySurrogateImage
Um outro exemplo de extensão da classe DisplayJAI é mostrado a seguir. Podemos
usar duas instâncias de DisplayJAI para exibir duas imagens com janelamento sincronizado – se as áreas imagens forem maiores do que a área disponível na interface gráfica elas
aparecerão dentro de JScrollPanes, e quando uma das barras de rolagem de uma imagem
for reposicionada a outra imagem também será reposicionada para mostrar a região correspondente à primeira. Esta técnica é implementada através da criação de um novo componente
gráfico, que herda de JPanel. A classe é mostrada na Listagem 10.
Listagem 10. Exibindo duas instâncias de DisplayJAI de forma sincronizada
1
package wvc.display;
2
3
4
5
6
import
import
import
import
java.awt.GridLayout;
java.awt.event.AdjustmentEvent;
java.awt.event.AdjustmentListener;
java.awt.image.RenderedImage;
7
12
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
8
9
import javax.swing.JPanel;
import javax.swing.JScrollPane;
10
11
import com.sun.media.jai.widget.DisplayJAI;
12
13
14
15
16
17
18
public class DisplayTwoSynchronizedImages extends JPanel implements AdjustmentListener
{
protected DisplayJAI dj1;
protected DisplayJAI dj2;
protected JScrollPane jsp1;
protected JScrollPane jsp2;
19
public DisplayTwoSynchronizedImages(RenderedImage im1, RenderedImage im2)
{
super();
// Cria componente com duas imagens com JScrollPanes
setLayout(new GridLayout(1,2));
dj1 = new DisplayJAI(im1);
dj2 = new DisplayJAI(im2);
jsp1 = new JScrollPane(dj1);
jsp2 = new JScrollPane(dj2);
add(jsp1);
add(jsp2);
// Registra listeners para os scroll bars do JScrollPanes
jsp1.getHorizontalScrollBar().addAdjustmentListener(this);
jsp1.getVerticalScrollBar().addAdjustmentListener(this);
jsp2.getHorizontalScrollBar().addAdjustmentListener(this);
jsp2.getVerticalScrollBar().addAdjustmentListener(this);
}
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void adjustmentValueChanged(AdjustmentEvent e)
{
if (e.getSource() == jsp1.getHorizontalScrollBar())
jsp2.getHorizontalScrollBar().setValue(e.getValue());
if (e.getSource() == jsp1.getVerticalScrollBar())
jsp2.getVerticalScrollBar().setValue(e.getValue());
if (e.getSource() == jsp2.getHorizontalScrollBar())
jsp1.getHorizontalScrollBar().setValue(e.getValue());
if (e.getSource() == jsp2.getVerticalScrollBar())
jsp1.getVerticalScrollBar().setValue(e.getValue());
}
38
39
40
41
42
43
44
45
46
47
48
49
}
50
A classe DisplayTwoSynchronizedImages (Listagem 10) será bastante utilizada na seção seguinte para mostrar, lado a lado, algumas imagens originais e depois de
processadas com operadores da API JAI.
6
Operadores da API Java Advanced Imaging
Já vimos que a API JAI provê um método estático JAI.create que recebe um
número variável de argumentos e que executa algum tipo de operação em instâncias de classes
que representam imagens, que são passadas como argumentos ou que são retornadas deste
RITA • Volume – • Número – • —-
13
JAI: Java Advanced Imaging
método. O operador filestore foi mostrado na Listagem 2 e o operador fileload na
Listagem 9. Alguns outros operadores podem ser vistos na Listagem 8, e seu uso será descrito
com detalhes nesta seção.
Como diferentes operadores podem ter diferentes números, tipos de parâmetros e até
número de imagens para processamento, é necessário ter uma forma simples de executálos através do método JAI.create, sem ter inúmeras versões do mesmo. Isto é feito
usando-se instâncias da classe ParameterBlock para armazenar as fontes (imagens) e
os parâmetros necessários a cada tipo de operador, e chamando o método JAI.create
com esta instância de ParameterBlock. Como cada operador espera um determinado
número e tipo de parâmetros, os mesmos devem ser armazenados na ordem e com os tipos
esperados na instância de ParameterBlock (exceto para operadores simples, com poucos
parâmetros, embora o uso também seja possível). Exemplos específicos serão mostrados para
os diversos operadores.
Para simplificar a apresentação dos operadores, os mesmos foram divididos em categorias. Para cada categoria alguns dos operadores mais usados serão exemplificados.
6.1
Operadores que modificam cores ou níveis de cinza
Alguns operadores podem ser usados para manipular os valores dos pixels das imagens
de forma comum e independente para cada pixel. Um operador bem simples que implementa
esta conceito é o operador invert, que inverte os valores de cada pixel em uma imagem. Se
os pixels tiverem valores com sinais, o operador simplesmente inverte os sinais, caso contrário
subtrai os valores dos pixels do máximo valor representável na imagem. O uso deste operador
é realmente simples, e como somente o nome do operador e a imagem de origem são necessários como parâmetros, não precisamos usar uma instância de ParameterBlock. Um
trecho de código que carrega e inverte os pixels de uma imagem é mostrado na Listagem 11.
Listagem 11. Invertendo uma imagem (usando JAI)
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args)
{
PlanarImage input = JAI.create("fileload", args[0]);
PlanarImage output = JAI.create("invert", input);
JFrame frame = new JFrame();
frame.setTitle("Invert image "+args[0]);
frame.getContentPane().add(new DisplayTwoSynchronizedImages(input,output));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
Outro operador que implementa uma função bastante usada em processamento de
imagens é o binarize, que recebe como parâmetros uma imagem e um valor limiar e
14
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
binariza todos os pixels da imagem, ou seja, cria uma imagem com valores 1 se o valor do
pixel original for maior que o limiar ou 0 caso contrário. Caso a imagem tenha mais de uma
banda, estas serão binarizadas independentemente.
Um exemplo de binarização pode ser visto no trecho de código da Listagem 12.
Como precisamos passar como parâmetros a imagem original e o valor limiar, usaremos
uma instância de ParameterBlock para adicionar a imagem original (usando o método
addSource) e o valor limiar (com o método add). A classe também mostra como usar uma
instância de DisplayTwoSynchronizedImages (Listagem 10) para exibir a imagem
original e binarizada.
Listagem 12. Binarizando uma imagem (usando JAI)
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args)
{
PlanarImage imagem = JAI.create("fileload", args[0]);
ParameterBlock pb = new ParameterBlock();
pb.addSource(imagem);
pb.add(127.0);
PlanarImage binarizada = JAI.create("binarize", pb);
JFrame frame = new JFrame("Imagem binarizada");
frame.add(new DisplayTwoSynchronizedImages(imagem,binarizada));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
Figura 3. Interface gráfica da classe Binariza
Um terceiro exemplo de operador que modificam cores é mostrado a seguir. Imagens
coloridas são representadas em um sistema de coordenadas de cores que considera três valores por pixel, cada um correspondente à intensidade nas bandas vermelho, verde e azul
(RGB), devido à forma com que a maioria dos dispositivos de captura e exibição de imagens
RITA • Volume – • Número – • —-
15
JAI: Java Advanced Imaging
funcionam.
Outros sistemas de cores são baseados em outras características: um destes é o sistema
IHS (Intensity, Hue e Saturation; ou intensidade, croma e saturação) que representa cores
de forma mais perceptual: intensidade indica o quanto uma cor é clara (com branco tendo
alta intensidade e preto baixa intensidade); croma representa a cor base (vermelho, amarelo,
verde, etc.) e saturação representando a quantidade de pigmentação da cor (vermelho sendo
bem saturado e rosa pouco saturado, por exemplo). Algumas operações de processamento
de imagens baseadas no sistema IHS são reforço das cores, melhoria na clareza da imagem
e melhoria na qualidade de imagens de sensoriamento remoto através da mistura de imagens
de diferentes resoluções [2].
É possível fazer operações em imagens, convertendo os pixels entre os sistemas RGB
e IHS para obter diversos efeitos, usando os operadores de JAI. Para exemplificar, faremos a
manipulação de uma imagem colorida, convertendo-a para o espaço de cores IHS, separando
a banda H, criando bandas constantes para I e S, compondo novamente a imagem IHS e
convertendo-a para RGB, obtendo assim uma imagem com cores fortemente evidenciadas.
Os passos são ilustrados pelo código na Listagem 13.
Listagem 13. Manipulando uma imagem no espaço de cores IHS
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public static void main(String[] args)
{
PlanarImage imagem = JAI.create("fileload", args[0]);
// Converte para o modelo de cores IHS.
IHSColorSpace ihs = IHSColorSpace.getInstance();
ColorModel modeloIHS =
new ComponentColorModel(ihs,
new int []{8,8,8},
false,false,
Transparency.OPAQUE,
DataBuffer.TYPE_BYTE) ;
ParameterBlock pb = new ParameterBlock();
pb.addSource(imagem);
pb.add(modeloIHS);
RenderedImage imagemIHS = JAI.create("colorconvert", pb);
// Extraímos as bandas I, H e S.
RenderedImage[] bandas = new RenderedImage[3];
for(int band=0;band<3;band++)
{
pb = new ParameterBlock();
pb.addSource(imagemIHS);
pb.add(new int[]{band});
bandas[band] = JAI.create("bandselect",pb);
}
// Criamos bandas constantes para as bandas I e S.
pb = new ParameterBlock();
pb.add((float)imagem.getWidth());
pb.add((float)imagem.getHeight());
pb.add(new Byte[]{(byte)255});
RenderedImage novaIntensidade = JAI.create("constant",pb);
pb = new ParameterBlock();
pb.add((float)imagem.getWidth());
16
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
pb.add((float)imagem.getHeight());
pb.add(new Byte[]{(byte)255});
RenderedImage novaSaturação = JAI.create("constant",pb);
// Juntamos as bandas H e as I e S constantes.
// Devemos passar um RenderingHint que indica que o modelo de cor IHS será usado.
ImageLayout imageLayout = new ImageLayout();
imageLayout.setColorModel(modeloIHS);
imageLayout.setSampleModel(imagemIHS.getSampleModel());
RenderingHints rendHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT,imageLayout);
pb = new ParameterBlock();
pb.addSource(novaIntensidade);
pb.addSource(bandas[1]);
pb.addSource(novaSaturação);
RenderedImage imagemIHSModificada = JAI.create("bandmerge", pb, rendHints);
// Convertemos de volta para RGB.
pb = new ParameterBlock();
pb.addSource(imagemIHSModificada);
pb.add(imagem.getColorModel()); // Imagem original era em RGB!
RenderedImage imagemFinal = JAI.create("colorconvert", pb);
JFrame frame = new JFrame("Modificação via IHS");
frame.add(new DisplayTwoSynchronizedImages(imagem,imagemFinal));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
A classe na Listagem 13 usa os seguintes operadores de JAI:
• fileload, para decodificar a imagem;
• colorconvert, para converter a imagem do espaço de cores RGB para IHS e viceversa;
• bandselect, para separar a imagem IHS nas três bandas componentes;
• constant, para criar imagens com as mesmas dimensões das originais mas com
somente uma banda de valor constante; e
• bandmerge, para juntar as três bandas em uma nova imagem IHS.
A parte realmente mais complexa do código na Listagem 13 é a que faz a unificação
das bandas I, H e S – para que a unificação seja feita corretamente devemos informar o
operador bandmerge de que a imagem a ser composta deve estar no sistema de cores IHS,
o que é feito entre as linhas 58 e 61 da Listagem.
O resultado da conversão e manipulação é uma imagem com cores fortemente realçadas, que é mostrada ao lado da imagem original. Um exemplo de aplicação deste conjunto de
operadores é mostrado na Figura 4.
6.2
Operadores em regiões
Outras funções usados frequentemente em processamento de imagens (e implementados como operadores pela API JAI) envolvem o cálculo de um valor baseado em uma vizi-
RITA • Volume – • Número – • —-
17
JAI: Java Advanced Imaging
Figura 4. Interface gráfica da classe RGBtoIHS
nhança local de pixels – em outras palavras, alguns operadores calcularão o valor de um pixel
em uma imagem de saída usando os valores dos pixels da imagem de entrada próximos às
coordenadas daquele pixel.
Um dos operadores mais simples desta categoria é o que filtra uma imagem suavizando
os valores de seus pixels. O valor de um pixel na imagem de saída é calculado tirando-se a
média dos valores dos pixels na vizinhança do pixel correspondente na imagem de entrada.
Para calcular este valor precisamos de uma definição de região de vizinhança, que inclui o
peso (multiplicador) que deve ser dado a cada pixel na vizinhança, com isto podemos usar o
operador convolve, como exemplificado pelo trecho de código na Listagem 14.
Listagem 14. Suavizando os pixels de uma imagem
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args)
{
PlanarImage imagem = JAI.create("fileload", args[0]);
float[] kernelMatrix = { 1f/25f, 1f/25f, 1f/25f, 1f/25f, 1f/25f,
1f/25f, 1f/25f, 1f/25f, 1f/25f, 1f/25f,
1f/25f, 1f/25f, 1f/25f, 1f/25f, 1f/25f,
1f/25f, 1f/25f, 1f/25f, 1f/25f, 1f/25f,
1f/25f, 1f/25f, 1f/25f, 1f/25f, 1f/25f};
KernelJAI kernel = new KernelJAI(5,5,kernelMatrix);
PlanarImage bordas = JAI.create("convolve",imagem,kernel);
JFrame frame = new JFrame("Suavização da imagem");
frame.add(new DisplayTwoSynchronizedImages(imagem,bordas));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
18
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
27
}
O código na Listagem 14 declara uma vizinhança de 5 × 5 pixels, todos com o mesmo
peso para multiplicação (1/25) e usa esta vizinhança para suavizar a imagem de entrada. A
vizinhança é representada por uma instância da classe KernelJAI, que é criada com um
array de valores de ponto flutuante. A imagem original e a filtrada são mostradas em uma
interface gráfica. Um resultado de aplicação é mostrado na Figura 5.
Figura 5. Interface gráfica da classe Suaviza
Outra operação que envolve vizinhanças é a identificação de bordas locais. Vários
operadores existem para este tipo de operação [3, 4], sendo que um dos mais populares é o
operador Sobel, que requer a convolução da imagem com um filtro com valores específicos
para deteção de bordas horizontais e verticais. O código na Listagem 15 mostra como aplicar
o operador Sobel horizontal em uma imagem usando o operador convolve da API JAI.
Listagem 15. Detetando bordas horizontais em uma imagem
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args)
{
PlanarImage imagem = JAI.create("fileload", args[0]);
float[] kernelMatrix = { -1, -2, -1,
0, 0, 0,
1, 2, 1 };
KernelJAI kernel = new KernelJAI(3,3,kernelMatrix);
PlanarImage bordas = JAI.create("convolve",imagem,kernel);
JFrame frame = new JFrame("Bordas horizontais");
frame.add(new DisplayTwoSynchronizedImages(imagem,bordas));
frame.pack();
RITA • Volume – • Número – • —-
19
JAI: Java Advanced Imaging
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
23
24
25
A deteção de bordas horizontais pode ser vista na Figura 6, que mostra a imagem
original e a com as bordas detectadas lado a lado.
Figura 6. Interface gráfica da classe Borda
Ainda outros operadores que calculam valores de pixels usando regiões na imagem de
entrada são relacionados com morfologia matemática [1]. Dois dos operadores mais comuns
de morfologia matemática são dilatação e erosão: estes operadores usam uma região chamada
elemento estrutural que pode ser definida como uma vizinhança. A erosão de uma imagem
em níveis de cinza causa a expansão ou crescimento de regiões da imagem; enquanto sua
dilatação causa a remoção de pequenas regiões da imagem.
As duas operações básicas são implementadas pela API JAI usando os operadores
dilate e erode. O trecho de código da Listagem 16 demonstra o uso do operador
dilate com uma máscara semelhante à usada como vizinhança em exemplos anteriores.
Listagem 16. Dilatação de uma imagem
14
15
16
17
18
public static void main(String[] args)
{
PlanarImage imagem = JAI.create("fileload", args[0]);
float[] estrutura = {
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
1,
1,
1,
0,
20
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
0,
1,
1,
1,
1,
1,
0,
0,
1,
1,
1,
1,
1,
0,
0,
1,
1,
1,
1,
1,
0,
0,
1,
1,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0};
KernelJAI kernel = new KernelJAI(7,7,estrutura);
ParameterBlock p = new ParameterBlock();
p.addSource(imagem);
p.add(kernel);
PlanarImage dilatada = JAI.create("dilate",p);
JFrame frame = new JFrame("Imagem dilatada");
frame.add(new DisplayTwoSynchronizedImages(imagem,dilatada));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
A Figura 7 mostra a imagem original e a dilatada, lado a lado. Podemos observar que
somente regiões que poderiam conter o elemento estrutural foram preservadas.
Figura 7. Interface gráfica da classe Dilata
O mesmo código pode ser adaptado para fazer a erosão da imagem com o mesmo
operador, conforme mostrado no trecho de código da Listagem 17.
Listagem 17. Erosão de uma imagem
26
27
28
p.addSource(imagem);
p.add(kernel);
PlanarImage erodida = JAI.create("erode",p);
O resultado da execução do código completo da Listagem 17 está na Figura 8, com a
imagem original e a erodida, mostradas lado a lado.
RITA • Volume – • Número – • —-
21
JAI: Java Advanced Imaging
Figura 8. Interface gráfica da classe Erode
6.3
Operadores geométricos
Algumas operações servem para modificar a geometria (escala, posição, orientação,
etc.) dos pixels das imagens. A API JAI também provê operadores para várias destas operações. Um operador básico usado em tarefas de processamento de imagens de documentos é o
que permite rotacionar os pixels de uma imagem em torno de uma determinada coordenada.
Este operador (rotate) recebe como parâmetros a imagem de entrada, o ângulo de rotação
em radianos e a coordenada do ponto pivô da rotação. Outro parãmetro deste operador serve
para determinar como os valores dos pixels serão calculados, já que um pixel na imagem
de saída pode não ter correspondente direto em um pixel na imagem de entrada, sendo frequentemente calculado através de interpolações. O último parâmetro determina que tipo de
interpolação será usada neste cálculo.
A classe na Listagem 18 gira os pixels de uma imagem em torno do ponto central
da mesma, interpolando os valores com interpolação bilinear. O resultado, com a imagem
original e a rotacionada, é mostrado na Figura 9.
Listagem 18. Rotação de uma imagem em torno de um ponto
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args)
{
PlanarImage imagem = JAI.create("fileload",args[0]);
float angle = (float)Math.toRadians(10);
// Usamos o centro da imagem para rotação
float centerX = imagem.getWidth()/2f;
float centerY = imagem.getHeight()/2f;
ParameterBlock pb = new ParameterBlock();
pb.addSource(imagem);
pb.add(centerX);
22
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
24
25
26
27
28
29
30
31
32
33
pb.add(centerY);
pb.add(angle);
pb.add(new InterpolationBilinear());
PlanarImage rotacionada = JAI.create("rotate", pb);
JFrame frame = new JFrame("Imagem rotacionada");
frame.add(new DisplayTwoSynchronizedImages(imagem,rotacionada));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
Figura 9. Interface gráfica da classe Rotaciona
Outra operação comum é a de modificar as dimensões de uma imagem, mudando a
quantidade dos pixels da mesma. Pixels na imagem de saída devem ser calculados interpolando os valores correspondentes dos pixels da imagem de entrada. O operador scale
permite a modificação das escalas da imagem de forma independente (vertical e horizontal),
além de permitir uma translação opcional. O operador é demonstrado no trecho de código
mostrado na Listagem 19.
Listagem 19. Mudando a escala de uma imagem
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args)
{
PlanarImage imagem = JAI.create("fileload",args[0]);
float scale = 0.3f;
ParameterBlock pb = new ParameterBlock();
pb.addSource(imagem);
pb.add(scale);
pb.add(scale);
pb.add(0.0F);
pb.add(0.0F);
pb.add(new InterpolationNearest());
PlanarImage reescalada = JAI.create("scale", pb);
JFrame frame = new JFrame("Imagem reescalada");
frame.add(new DisplayTwoSynchronizedImages(imagem,reescalada));
RITA • Volume – • Número – • —-
23
JAI: Java Advanced Imaging
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
28
29
30
31
No exemplo mostrado na Listagem 19 usamos a mesma escala horizontal e vertical não
foi feita nenhuma translação nos pixels da imagem (parâmetros 0.0F usados nas linhas 22
e 23). O resultado da aplicação pode ser visto na Figura 10.
Figura 10. Interface gráfica da classe Escala
6.4
Operadores estatísticos
Alguns operadores não criam, como resultado, imagens filtradas ou modificadas, e sim
são usados para obter valores e medidas das imagens. A API JAI provê alguns operadores
estatísticos que podem ser usados para medir atributos das imagens. Um destes operadores
é o histogram, que calcula um histograma de uma imagem (contendo a distribuição dos
valores dos pixels em diversos intervalos ou bins) e que armazena este histograma em uma
pseudo-imagem de saída, como um atributo especial.
O trecho de código mostrado na Listagem 20 mostra como calcular dois histogramas
da mesma imagem de entrada. O primeiro histograma usa 256 intervalos ou bins, e o segundo,
somente 32. Os histogramas são exibidos usando uma classe que implementa um componente
gráfico, não mostrada neste tutorial (veja [7] para o código-fonte desta classe).
Listagem 20. Calculando histogramas de uma imagem
15
16
public static void main(String[] args)
{
24
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
PlanarImage image = JAI.create("fileload", args[0]);
// Primeiro histograma com 256 bins.
ParameterBlock pb1 = new ParameterBlock();
pb1.addSource(image);
pb1.add(null);
pb1.add(1); pb1.add(1);
pb1.add(new int[]{256});
pb1.add(new double[]{0}); pb1.add(new double[]{256});
PlanarImage dummyImage1 = JAI.create("histogram", pb1);
Histogram histo1 = (Histogram)dummyImage1.getProperty("histogram");
// Segundo histograma com 32 bins.
ParameterBlock pb2 = new ParameterBlock();
pb2.addSource(image);
pb2.add(null);
pb2.add(1); pb2.add(1);
pb2.add(new int[]{32});
pb2.add(new double[]{0}); pb2.add(new double[]{256});
PlanarImage dummyImage2 = JAI.create("histogram", pb2);
Histogram histo2 = (Histogram)dummyImage2.getProperty("histogram");
// Exibimos os histogramas usando um componente específico.
JFrame f = new JFrame("Histogramas");
DisplayHistogram dh1 = new DisplayHistogram(histo1,"256 bins");
dh1.setBinWidth(2); dh1.setHeight(160); dh1.setIndexMultiplier(1);
DisplayHistogram dh2 = new DisplayHistogram(histo2,"32 bins");
dh2.setBinWidth(16); dh2.setHeight(160); dh2.setIndexMultiplier(8);
dh2.setSkipIndexes(2);
f.getContentPane().setLayout(new GridLayout(2,1));
f.getContentPane().add(dh1);
f.getContentPane().add(dh2);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.pack();
f.setVisible(true);
Para executar o operador histogram precisamos passar como parâmetros a imagem
original, uma área de interesse (ou null para usar toda a imagem), as taxas de amostragem
horizontal e vertical (podemos assim evitar amostrar todos os pixels na imagem), o número
de intervalos ou bins para cada banda da imagem e dois valores limiares inferior e superior.
Pixels na imagem com valores abaixo do inferior ou acima do superior não serão considerados
para o cálculo do histograma. O histograma pode ser recuperado através da propriedade
histogram da pseudo-imagem de saída, que retorna uma instância da classe Histogram
com os valores dos histogramas.
A Figura 11 mostra os dois histogramas calculados com o código da Listagem 20. A
imagem usada é a mesma imagem mostrada na parte esquerda da Figura 10.
Outro operador estatístico da API JAI foi demonstrado na classe na Listagem 8: o
operador extrema calcula os valores mínimos e máximos em cada banda de uma imagem,
e na sua forma mais simples recebe como parâmetro a imagem original, armazenando vetores
de valores na pseudo-imagem de saída nas propriedades minimum e maximum, que podem
ser recuperadas como arrays de valores do tipo double com os valores mínimos e máximos
de cada banda.
RITA • Volume – • Número – • —-
25
JAI: Java Advanced Imaging
Figura 11. Interface gráfica da classe Histograma
6.5
Outros operadores
Existem mais de cem operadores já existentes na API JAI, que cobrem uma gama
razoável de operações comuns de processamento de imagens. Além dos operadores vistos
em exemplos específicos temos operadores para modificação dos valores dos pixels usando
constantes ou outras imagens como addconst, subtractconst (usado na Listagem 8),
multiplyconst (também usado na Listagem 8), divideconst, dividebyconst,
add subtract, multiply e divide e operadores que implementam operações booleanas entre imagens como or, and e xor.
Outros operadores geométricos são border, crop, shear, translate, transpose, border e warp. Outros operadores que permitem a modificação das cores das
imagens são lookup e colorquantizer. As bandas de uma imagem também podem
ser manipuladas com o operador bandcombine. Alguns operadores implementam funções
relacionadas com o domínio de frequência: DCT, IDCT, DFT e IDFT.
Outros operadores em regiões são relacionados com filtros, como, por exemplo, maxfilter, medianfilter e boxfilter.
26
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
O uso de alguns destes operadores, com código completo e comentado (em inglês) é
exemplificado em [6] e [7].
7
Criando novos operadores
A API JAI permite também a criação de novos operadores e o registro dos mesmos
junto à classe JAI para que possam ser chamados pelo mesmo mecanismo usado no método
create, provendo ao programador uma interface única para a execução de operações. Os
passos para a criação de um operador são os seguintes:
1. Criar uma classe contendo operador que herde de uma das seguintes classes: SourcelessOpImage para operadores que não precisem de imagens originais para calcular imagens de saída; PointOpImage para operadores que calculem os valores dos
pixels da imagem de saída baseado nos valores dos pixels correspondentes na imagem
de entrada; AreaOpImage para operadores que precisem calcular valores de pixels
considerando pequenas áreas retangulares na imagem original; GeometricOpImage
para operadores que manipulem toda a imagem de entrada geometricamente ou StatisticsOpImage para operadores que calculam valores estatísticos sobre a imagem
de entrada.
2. Criar uma classe que implementa RenderedImageFactory que conterá um método que criará uma imagem a partir dos parâmetros do operador;
3. Criar uma classe que implementa OperationDescriptor ou herda de OperationDescriptorImpl que descreve os parâmetros e valores default dos operadores do mesmo.
4. Registrar o novo operador junto às instâncias estáticas de OperationRegistry e
RIFRegistry. O registro pode ser feito por um método estático na própria classe
que implementa OperationDescriptor.
7.1
Exemplo 1: um operador pontual
Como primeiro exemplo consideremos um segmentador por limiar ligeiramente diferente do implementado pelo operador binarize, que chamaremos de segmenta3: neste
operador usaremos dois limiares t1 e t2 , de forma que um pixel p na imagem de saída terá
seu valor igual a 0 se o pixel na entrada tiver valor menor do que t1 ; 255 se tiver valor maior
que t2 e 127 nos outros casos.
A classe principal para implementação deste operador atua sobre os pontos das imagens de entrada e saída, portando deve herdar de PointOpImage. A classe completa é
mostrada na Listagem 21.
RITA • Volume – • Número – • —-
27
JAI: Java Advanced Imaging
Listagem 21. Classe que implementa o operador segmenta3
1
package wvc.operadores.segmenta;
2
3
4
5
6
import
import
import
import
java.awt.RenderingHints;
java.awt.image.Raster;
java.awt.image.RenderedImage;
java.awt.image.WritableRaster;
7
8
9
import javax.media.jai.ImageLayout;
import javax.media.jai.PointOpImage;
10
11
12
13
14
public class Segmenta3OpImage extends PointOpImage
{
private RenderedImage source;
private int threshold1,threshold2;
15
16
17
18
19
20
21
22
23
public Segmenta3OpImage(RenderedImage source,int th1,int th2,
ImageLayout layout,RenderingHints hints, boolean b)
{
super(source,layout,hints,b);
this.source = source;
this.threshold1 = th1;
this.threshold2 = th2;
}
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public Raster computeTile(int x,int y)
{
Raster r = source.getTile(x,y);
int minX = r.getMinX();
int minY = r.getMinY();
int width = r.getWidth();
int height = r.getHeight();
// Criamos um WritableRaster da região sendo considerada.
WritableRaster wr = r.createCompatibleWritableRaster(minX,minY,width,height);
for(int l=0;l<r.getHeight();l++)
for(int c=0;c<r.getWidth();c++)
for(int b=0;b<r.getNumBands();b++)
{
int p = r.getSample(c+minX,l+minY,b);
if (p < threshold1) p = 0;
else if (p > threshold2) p = 255;
else p = 127;
wr.setSample(c+minX,l+minY,b,p);
}
return wr;
}
46
47
}
A classe mostrada na Listagem 21 tem somente dois métodos: o construtor, que inicializa atributos da classe (uma referência à imagem original e os dois valores limiares)
e o método computeTile, que será usado quando for necessário calcular uma região
(tile) da imagem de saída. Este método varre o Raster da imagem original, criando um
WritableRaster correspondente na imagem de saída e modificando os valores dos pixels no WritableRaster dependendo das comparações dos valores dos pixels da imagem
28
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
original com os limiares. O exemplo é simples o bastante para ser facilmente reproduzido.
Além da classe que implementa o operador, temos que definir uma classe que criará
a imagem propriamente dita; esta classe deve implementar a interface RenderedImageFactory que define um método create que retorna uma RenderedImage com a imagem resultante. Esta classe é mostrada na Listagem 22.
Listagem 22. RenderedImageFactory para o operador segmenta3
1
package wvc.operadores.segmenta;
2
3
4
5
6
import
import
import
import
java.awt.RenderingHints;
java.awt.image.RenderedImage;
java.awt.image.renderable.ParameterBlock;
java.awt.image.renderable.RenderedImageFactory;
7
8
import javax.media.jai.ImageLayout;
9
10
11
12
13
14
15
16
17
18
19
20
public class Segmenta3RIF implements RenderedImageFactory
{
public RenderedImage create(ParameterBlock paramBlock, RenderingHints hints)
{
RenderedImage source = paramBlock.getRenderedSource(0);
int threshold1 = paramBlock.getIntParameter(0);
int threshold2 = paramBlock.getIntParameter(1);
ImageLayout layout = new ImageLayout(source);
return new Segmenta3OpImage(source,threshold1,threshold2,layout,hints,false);
}
}
Finalmente precisamos de uma classe que descreva o operador, e que implemente
OperationDescriptor ou herde de OperationDescriptorImpl. A classe para
nosso exemplo é mostrada na Listagem 23.
Listagem 23. Classe que descreve o operador segmenta3
1
package wvc.operadores.segmenta;
2
3
4
5
6
7
import
import
import
import
import
javax.media.jai.JAI;
javax.media.jai.OperationDescriptorImpl;
javax.media.jai.OperationRegistry;
javax.media.jai.registry.RIFRegistry;
javax.media.jai.util.Range;
8
9
10
11
12
13
14
15
16
17
18
19
public class Segmenta3Descriptor extends OperationDescriptorImpl
{
private static final String opName = "segmenta3";
private static final String vendorName = "Hypothetical Image Processing Lab";
private static final String[][] resources =
{
{"GlobalName",
opName},
{"LocalName",
opName},
{"Vendor",
vendorName},
{"Description", "A simple three-level image segmentation operator"},
{"DocURL",
"http://www.lac.inpe.br/~rafael.santos"},
RITA • Volume – • Número – • —-
29
JAI: Java Advanced Imaging
{"Version",
{"arg0Desc",
{"arg1Desc",
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
"1.0"},
"First Threshold Value"},
"Second Threshold Value"}
};
private static final String[] supportedModes = {"rendered"};
private static final String[] paramNames = {"1st threshold","2nd threshold"};
private static final Class[] paramClasses = {Integer.class, Integer.class};
private static final Object[] paramDefaults = { new Integer(85), new Integer(170) };
private static final Range[] validParamValues =
{
new Range(Integer.class, Integer.MIN_VALUE, Integer.MAX_VALUE),
new Range(Integer.class, Integer.MIN_VALUE, Integer.MAX_VALUE)
};
private static final int numSources = 1;
private static boolean registered = false;
35
36
37
38
39
40
public Segmenta3Descriptor()
{
super(resources,supportedModes,numSources,paramNames,
paramClasses,paramDefaults,validParamValues);
}
41
42
43
44
45
46
47
48
49
50
51
52
53
public static void register()
{
if (!registered)
{
OperationRegistry op = JAI.getDefaultInstance().getOperationRegistry();
Segmenta3Descriptor desc = new Segmenta3Descriptor();
op.registerDescriptor(desc);
Segmenta3RIF rif = new Segmenta3RIF();
RIFRegistry.register(op,opName,vendorName,rif);
registered = true;
}
}
54
55
}
A classe na Listagem 23 contém algumas string estáticas descrevendo o operador e fornecedor, um mapa de indicadores (strings) e campos declarando o tipo de operador, os tipos
de parâmetros, valores default, etc. O construtor desta classe simplesmente chama o construtor da classe ancestral com estes campos. O método estático register registra o descritor
do operador na instância global de OperationRegistry e o RenderedImageFactory para o operador na instância global de RIFRegistry.
Com estas classes prontas podemos usar o operador em qualquer aplicação. Um exemplo do operador pode ser visto no trecho de código da Listagem 24. É importante observar
que devemos registrar o operador antes de seu uso, conforme mostrado na linha 15.
Listagem 24. Exemplo de uso do operador segmenta3
13
14
15
16
public static void main(String[] args)
{
Segmenta3Descriptor.register();
PlanarImage imagem = JAI.create("fileload", args[0]);
30
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
ParameterBlock p = new ParameterBlock();
p.addSource(imagem);
p.add(new Integer(120));
p.add(new Integer(200));
PlanarImage resultado = JAI.create("segmenta3",p);
JFrame frame = new JFrame("Imagem binarizada");
frame.add(new DisplayTwoSynchronizedImages(imagem,resultado));
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
17
18
19
20
21
22
23
24
25
26
27
A Figura 12 mostra uma imagem original e uma resultante da execução do operador
segmenta3 lado a lado.
Figura 12. Interface gráfica da classe DemonstraSegmenta3
7.2
Exemplo 2: um operador estatístico
Como segundo exemplo consideremos um operador estatístico simples (contapixels) que conte o número de pixels semelhantes a uma determinada cor no espaço RGB,
onde o critério de semelhança é implementado como a distância Euclideana entre o pixel da
imagem e o valor referência – se a distância for menor ou igual a um valor de tolerância
consideramos o pixel como sendo igual àquela cor e incrementamos um contador.
A classe que implementa este operador só precisa dos parâmetros do operador e da
imagem de entrada, portando deve herdar de StatisticsOpImage. A classe completa é
mostrada na Listagem 25.
RITA • Volume – • Número – • —-
31
JAI: Java Advanced Imaging
Listagem 25. Classe que implementa o operador contapixels
1
package wvc.operadores.contapixels;
2
3
4
5
import java.awt.Color;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
6
7
import javax.media.jai.StatisticsOpImage;
8
9
10
11
12
13
public class ContaPixelsOpImage extends StatisticsOpImage
{
private Color target;
private Float tolerance;
private Long count;
14
15
16
17
18
19
20
21
public ContaPixelsOpImage(RenderedImage source,Color target,Float tolerance)
{
super(source,null,source.getMinX(),source.getMinY(),1,1);
this.target = target;
this.tolerance = tolerance;
count = null;
}
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected void accumulateStatistics(String name, Raster raster, Object stats)
{
if (count == null) count = new Long(0);
int r,g,b;
for(int l=0;l<raster.getHeight();l++)
for(int c=0;c<raster.getWidth();c++)
{
int x = raster.getMinX()+c;
int y = raster.getMinY()+l;
r = raster.getSample(x,y,0);
g = raster.getSample(x,y,1);
b = raster.getSample(x,y,2);
float dist = (target.getRed()-r)*(target.getRed()-r)+
(target.getGreen()-g)*(target.getGreen()-g)+
(target.getBlue()-b)*(target.getBlue()-b);
if (dist<=tolerance*tolerance) count++;
}
}
41
42
43
44
45
46
protected Object createStatistics(String arg0)
{
if (count == null) count = new Long(0);
return count;
}
47
48
49
50
51
protected String[] getStatisticsNames()
{
return new String[]{"count"};
}
52
53
54
55
56
57
public Object getProperty(String name)
{
if (count == null) super.getProperty(name);
return count;
}
58
32
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
59
}
O método principal da classe mostrada na Listagem 25 é o accumulateStatistics, que acumula as estatísticas de um determinado atributo para um Raster. Como este
operador somente calcula o atributo count, o nome do atributo é ignorado pelos métodos da
classe. Outros métodos retornam o nome do atributo sendo calculado (getStatisticsNames) e seu valor (getProperty).
Também precisamos declarar uma classe que criará a pseudo-imagem resultante do
operador (esta pseudo-imagem não terá pixels mas sim propriedades estatísticas calculadas).
Esta classe também deve implementar a interface RenderedImageFactory de forma
semelhante à mostrada na Listagem 22. Esta classe é mostrada na Listagem 26.
Listagem 26. RenderedImageFactory para o operador contapixels
1
package wvc.operadores.contapixels;
2
3
4
5
6
7
import
import
import
import
import
java.awt.Color;
java.awt.RenderingHints;
java.awt.image.RenderedImage;
java.awt.image.renderable.ParameterBlock;
java.awt.image.renderable.RenderedImageFactory;
8
9
10
11
12
13
14
15
16
17
18
public class ContaPixelsRIF implements RenderedImageFactory
{
public RenderedImage create(ParameterBlock paramBlock, RenderingHints hints)
{
RenderedImage source = paramBlock.getRenderedSource(0);
Color target = (Color)paramBlock.getObjectParameter(0);
Float tolerance = (Float)paramBlock.getObjectParameter(1);
return new ContaPixelsOpImage(source,target,tolerance);
}
}
Finalmente a descrição do operador e seu registro é feita na classe ContaPixelsDescriptor, mostrada na Listagem 27.
Listagem 27. Classe que descreve o operador contapixels
1
package wvc.operadores.contapixels;
2
3
import java.awt.Color;
4
5
6
7
8
import
import
import
import
javax.media.jai.JAI;
javax.media.jai.OperationDescriptorImpl;
javax.media.jai.OperationRegistry;
javax.media.jai.registry.RIFRegistry;
9
10
11
12
public class ContaPixelsDescriptor extends OperationDescriptorImpl
{
private static final String opName = "contapixels";
RITA • Volume – • Número – • —-
33
JAI: Java Advanced Imaging
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static final String vendorName = "Hypothetical Image Processing Lab";
private static final String[][] attributes =
{
{"GlobalName",
opName},
{"LocalName",
opName},
{"Vendor",
vendorName},
{"Description", "A simple RGB pixel counting operator"},
{"DocURL",
"http://www.lac.inpe.br/~rafael.santos"},
{"Version",
"1.0"},
{"arg0Desc",
"Target value (RGB color used for similarity)"},
{"arg1Desc",
"Tolerance value"},
};
private static final String[] modes = {"rendered"};
private static final int numSources = 1;
private static final String[] paramNames = {attributes[6][0],attributes[7][0]};
private static final Class[] paramClasses = {Color.class,Float.class};
private static final Object[] paramDefaults = { new Color(0,0,0),new Float(0) };
private static boolean registered = false;
31
32
33
34
35
public ContaPixelsDescriptor()
{
super(attributes,modes,numSources,paramNames,paramClasses,paramDefaults,null);
}
36
37
38
39
40
41
42
43
44
45
46
47
48
public static void register()
{
if (!registered)
{
OperationRegistry op = JAI.getDefaultInstance().getOperationRegistry();
ContaPixelsDescriptor desc = new ContaPixelsDescriptor();
op.registerDescriptor(desc);
ContaPixelsRIF rif = new ContaPixelsRIF();
RIFRegistry.register(op,opName,vendorName,rif);
registered = true;
}
}
49
50
}
Podemos demonstrar o uso do operador com uma aplicação simples, que recebe os
parâmetros da linha de comando e que registra o operador antes de seu uso. O trecho principal
desta classe é mostrada na Listagem 28.
Listagem 28. Exemplo de uso do operador contapixels
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args)
{
ContaPixelsDescriptor.register();
PlanarImage input = JAI.create("fileload", args[0]);
int r = Integer.parseInt(args[1]);
int g = Integer.parseInt(args[2]);
int b = Integer.parseInt(args[3]);
float t = Float.parseFloat(args[4]);
Color color = new Color(r,g,b);
ParameterBlock p = new ParameterBlock();
p.addSource(input);
p.add(color);
34
RITA • Volume – • Número – • —-
JAI: Java Advanced Imaging
p.add(t);
PlanarImage output = JAI.create("contapixels",p);
Long count = (Long)output.getProperty("count");
System.out.println("Existem "+count+" pixels com cores semelhantes a "+color);
}
23
24
25
26
27
8
Conclusões e comentários
Neste tutorial vimos como implementar operações básicas de processamento de imagens usando as API AWT/Swing e Java Advanced Imaging, com exemplos simples que podem ser modificados para resolver problemas semelhantes.
A API JAI ainda provê muitos outros métodos, operadores e facilidades não cobertos
pelo tutorial. O leitor interessado pode encontrar mais informações na documentação da API
e nos sites [6] e [7].
Referências
[1] Edward R. Dougherty. An Introduction to Morphological Image Processing. SPIE Optical Engineering Press, 1992.
[2] GeoSage.
Image fusion and pan-sharpening:
the big picture, 2008.
http://www.geosage.com/highview/imagefusion.html. Verificado em 30 de Julho de
2008.
[3] Rafael C. Gonzalez and Paul Wintz. Digital Image Processing. Addison-Wesley, 2nd
edition, 1987.
[4] Martin D. Levine. Vision in Man and Machine. McGraw-Hill, 1985.
[5] L. H. Rodrigues. Building Imaging Applications with Java Technology. Addison-Wesley,
2001.
[6] Rafael Santos. Java Advanced Imaging Stuff (repositório de exemplos on-line), 2008.
https://jaistuff.dev.java.net. Verificado em 30 de Julho de 2008.
[7] Rafael Santos.
Java Image Processing Cookbook (livro on-line), 2008.
http://www.lac.inpe.br/∼rafael.santos/JIPCookbook/index.jsp. Verificado em 30 de
Julho de 2008.
RITA • Volume – • Número – • —-
35
Download