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