Elaborazione di immagini con Java 2D

L'elaborazione delle immagini è l'arte e la scienza della manipolazione delle immagini digitali. Sta con un piede saldamente in matematica e l'altro in estetica ed è un componente critico dei sistemi informatici grafici. Se ti sei mai preso la briga di creare le tue immagini per le pagine Web, apprezzerai senza dubbio l'importanza delle capacità di manipolazione delle immagini di Photoshop per ripulire le scansioni e chiarire immagini non ottimali.

Se hai eseguito un lavoro di elaborazione delle immagini in JDK 1.0 o 1.1, probabilmente ti ricorderai che era un po 'ottuso. Il vecchio modello di produttori e consumatori di dati di immagine è poco maneggevole per l'elaborazione delle immagini. Prima di JDK 1.2, l'elaborazione delle immagini coinvolgeva MemoryImageSources, se PixelGrabberaltri arcani simili. Java 2D, tuttavia, fornisce un modello più pulito e più facile da usare.

Questo mese esamineremo gli algoritmi alla base di diverse importanti operazioni di elaborazione delle immagini ( ops ) e mostreremo come possono essere implementate utilizzando Java 2D. Ti mostreremo anche come queste operazioni vengono utilizzate per influenzare l'aspetto dell'immagine.

Poiché l'elaborazione delle immagini è un'applicazione standalone veramente utile di Java 2D, abbiamo costruito l'esempio di questo mese, ImageDicer, per essere il più riutilizzabile possibile per le tue applicazioni. Questo singolo esempio dimostra tutte le tecniche di elaborazione delle immagini che tratteremo nella colonna di questo mese.

Notare che poco prima della pubblicazione di questo articolo, Sun ha rilasciato il kit di sviluppo Java 1.2 Beta 4. La Beta 4 sembra fornire prestazioni migliori per le nostre operazioni di elaborazione delle immagini di esempio, ma aggiunge anche alcuni nuovi bug che coinvolgono il controllo dei limiti di ConvolveOps. Questi problemi influenzano il rilevamento dei bordi e gli esempi di affilatura che utilizziamo nella nostra discussione.

Riteniamo che questi esempi siano preziosi, quindi invece di ometterli del tutto, siamo scesi a un compromesso: per assicurarci che venga eseguito, il codice di esempio riflette le modifiche della Beta 4, ma abbiamo mantenuto le cifre dell'esecuzione della 1.2 Beta 3 in modo da poter vedere le operazioni funziona correttamente.

Si spera che Sun risolva questi bug prima del rilascio finale di Java 1.2.

L'elaborazione delle immagini non è scienza missilistica

L'elaborazione delle immagini non deve essere difficile. In effetti, i concetti fondamentali sono davvero molto semplici. Un'immagine, dopotutto, è solo un rettangolo di pixel colorati. Elaborare un'immagine è semplicemente una questione di calcolare un nuovo colore per ogni pixel. Il nuovo colore di ogni pixel può essere basato sul colore del pixel esistente, sul colore dei pixel circostanti, su altri parametri o su una combinazione di questi elementi.

L'API 2D introduce un modello di elaborazione delle immagini semplice per aiutare gli sviluppatori a manipolare questi pixel dell'immagine. Questo modello si basa sulla java.awt.image.BufferedImageclasse e le operazioni di elaborazione delle immagini come convoluzione e soglia sono rappresentate dalle implementazioni java.awt.image.BufferedImageOpdell'interfaccia.

L'implementazione di queste operazioni è relativamente semplice. Supponiamo, ad esempio, di avere già l'immagine sorgente come BufferedImagechiamata source. L'esecuzione dell'operazione illustrata nella figura sopra richiederebbe solo poche righe di codice:

001 breve [] soglia = nuovo breve [256]; 002 per (int i = 0; i <256; i ++) 003 soglia [i] = (i <128)? (breve) 0: (breve) 255; 004 BufferedImageOp thresholdOp = 005 new LookupOp (new ShortLookupTable (0, threshold), null); 006 BufferedImage destinazione = thresholdOp.filter (source, null);

Questo è davvero tutto quello che c'è da fare. Ora diamo un'occhiata ai passaggi in modo più dettagliato:

  1. Istanziare l'operazione immagine di vostra scelta (righe 004 e 005). Qui abbiamo usato a LookupOp, che è una delle operazioni di immagine incluse nell'implementazione Java 2D. Come ogni altra operazione sull'immagine, implementa l' BufferedImageOpinterfaccia. Parleremo più avanti di questa operazione.

  2. Chiama il filter()metodo dell'operazione con l'immagine sorgente (riga 006). La sorgente viene elaborata e viene restituita l'immagine di destinazione.

Se hai già creato un BufferedImageche conterrà l'immagine di destinazione, puoi passarlo come secondo parametro a filter(). Se passi null, come abbiamo fatto nell'esempio sopra, BufferedImageviene creata una nuova destinazione .

L'API 2D include una manciata di queste operazioni di immagine integrate. Ne discuteremo tre in questa colonna: convoluzione, tabelle di ricerca e soglia. Fare riferimento alla documentazione Java 2D per informazioni sulle restanti operazioni disponibili nell'API 2D (risorse).

Convoluzione

Un'operazione di convoluzione consente di combinare i colori di un pixel di origine e dei suoi vicini per determinare il colore di un pixel di destinazione. Questa combinazione viene specificata utilizzando un kernel, un operatore lineare che determina la proporzione di ciascun colore del pixel di origine utilizzato per calcolare il colore del pixel di destinazione.

Pensa al kernel come a un modello che viene sovrapposto all'immagine per eseguire una convoluzione su un pixel alla volta. Poiché ogni pixel è contorto, il modello viene spostato al pixel successivo nell'immagine sorgente e il processo di convoluzione viene ripetuto. Una copia di origine dell'immagine viene utilizzata per i valori di input per la convoluzione e tutti i valori di output vengono salvati in una copia di destinazione dell'immagine. Una volta completata l'operazione di convoluzione, viene restituita l'immagine di destinazione.

Il centro del kernel può essere pensato come sovrapposto al pixel di origine che è contorto. Ad esempio, un'operazione di convoluzione che utilizza il seguente kernel non ha effetto su un'immagine: ogni pixel di destinazione ha lo stesso colore del pixel di origine corrispondente.

 0,0 0,0 0,0 0,0 1,0 0,0 0,0 0,0 0,0 

La regola cardinale per la creazione dei kernel è che gli elementi dovrebbero sommare tutti fino a 1 se si desidera preservare la luminosità dell'immagine.

Nell'API 2D, una convoluzione è rappresentata da un file java.awt.image.ConvolveOp. Puoi costruire un ConvolveOpusando un kernel, che è rappresentato da un'istanza di java.awt.image.Kernel. Il codice seguente costruisce un ConvolveOputilizzando il kernel presentato sopra.

001 float [] identityKernel = {002 0.0f, 0.0f, 0.0f, 003 0.0f, 1.0f, 0.0f, 004 0.0f, 0.0f, 0.0f 005}; 006 BufferedImageOp identity = 007 new ConvolveOp (new Kernel (3, 3, identityKernel));

L'operazione di convoluzione è utile per eseguire diverse operazioni comuni sulle immagini, di cui parleremo tra poco. Diversi kernel producono risultati radicalmente diversi.

Ora siamo pronti per illustrare alcuni kernel di elaborazione delle immagini e i loro effetti. La nostra immagine non modificata è Lady Agnew di Lochnaw, dipinta da John Singer Sargent nel 1892 e nel 1893.

Il codice seguente crea un ConvolveOpche combina quantità uguali di ogni pixel di origine e dei suoi vicini. Questa tecnica produce un effetto sfocato.

001 float ninth = 1.0f / 9.0f; 002 float [] blurKernel = {003 nono, nono, nono, 004 nono, nono, nono, 005 nono, nono, nono 006}; 007 BufferedImageOp blur = new ConvolveOp (new Kernel (3, 3, blurKernel));

Un altro kernel di convoluzione comune enfatizza i bordi dell'immagine. Questa operazione è comunemente chiamata rilevamento dei bordi. A differenza degli altri kernel presentati qui, i coefficienti di questo kernel non si sommano a 1.

001 float [] edgeKernel = {002 0.0f, -1.0f, 0.0f, 003 -1.0f, 4.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005}; 006 BufferedImageOp edge = new ConvolveOp (new Kernel (3, 3, edgeKernel));

Puoi vedere cosa fa questo kernel guardando i coefficienti nel kernel (righe 002-004). Pensa per un momento a come viene utilizzato il kernel di rilevamento dei bordi per operare in un'area interamente di un colore. Ogni pixel non avrà colore (nero) poiché il colore dei pixel circostanti annulla il colore del pixel di origine. I pixel luminosi circondati da pixel scuri rimarranno luminosi.

Nota quanto è più scura l'immagine elaborata rispetto all'originale. Ciò accade perché gli elementi del kernel di rilevamento dei bordi non si sommano a 1.

A simple variation on edge detection is the sharpening kernel. In this case, the source image is added into an edge detection kernel as follows:

 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 -1.0 4.0 -1.0 + 0.0 1.0 0.0 = -1.0 5.0 -1.0 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 

The sharpening kernel is actually only one possible kernel that sharpens images.

The choice of a 3 x 3 kernel is somewhat arbitrary. You can define kernels of any size, and presumably they don't even have to be square. In JDK 1.2 Beta 3 and 4, however, a non-square kernel produced an application crash, and a 5 x 5 kernel chewed up the image data in a most peculiar way. Unless you have a compelling reason to stray from 3 x 3 kernels, we don't recommend it.

You may also be wondering what happens at the edge of the image. As you know, the convolution operation takes a source pixel's neighbors into account, but source pixels at the edges of the image don't have neighbors on one side. The ConvolveOp class includes constants that specify what the behavior should be at the edges. The EDGE_ZERO_FILL constant specifies that the edges of the destination image are set to 0. The EDGE_NO_OP constant specifies that source pixels along the edge of the image are copied to the destination without being modified. If you don't specify an edge behavior when constructing a ConvolveOp, EDGE_ZERO_FILL is used.

The following example shows how you could create a sharpening operator that uses the EDGE_NO_OP rule (NO_OP is passed as a ConvolveOp parameter in line 008):

001 float[] sharpKernel = { 002 0.0f, -1.0f, 0.0f, 003 -1.0f, 5.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005 }; 006 BufferedImageOp sharpen = new ConvolveOp( 007 new Kernel(3, 3, sharpKernel), 008 ConvolveOp.EDGE_NO_OP, null); 

Lookup tables

Another versatile image operation involves using a lookup table. For this operation, source pixel colors are translated into destination pixels colors through the use of a table. A color, remember, is composed of red, green, and blue components. Each component has a value from 0 to 255. Three tables with 256 entries are sufficient to translate any source color to a destination color.

The java.awt.image.LookupOp and java.awt.image.LookupTable classes encapsulate this operation. You can define separate tables for each color component, or use one table for all three. Let's look at a simple example that inverts the colors of every component. All we need to do is create an array that represents the table (lines 001-003). Then we create a LookupTable from the array and a LookupOp from the LookupTable (lines 004-005).

001 short[] invert = new short[256]; 002 for (int i = 0; i < 256; i++) 003 invert[i] = (short)(255 - i); 004 BufferedImageOp invertOp = new LookupOp( 005 new ShortLookupTable(0, invert), null); 

LookupTable has two subclasses, ByteLookupTable and ShortLookupTable, that encapsulate byte and short arrays. If you create a LookupTable that doesn't have an entry for any input value, an exception will be thrown.

This operation creates an effect that looks like a color negative in conventional film. Also note that applying this operation twice will restore the original image; you're basically taking a negative of the negative.

What if you only wanted to affect one of the color components? Easy. You construct a LookupTable with separate tables for each of the red, green, and blue components. The following example shows how to create a LookupOp that only inverts the blue component of the color. As with the previous inversion operator, applying this operator twice restores the original image.

001 short[] invert = new short[256]; 002 short[] straight = new short[256]; 003 for (int i = 0; i < 256; i++) { 004 invert[i] = (short)(255 - i); 005 straight[i] = (short)i; 006 } 007 short[][] blueInvert = new short[][] { straight, straight, invert }; 008 BufferedImageOp blueInvertOp = 009 new LookupOp(new ShortLookupTable(0, blueInvert), null); 

Posterizing is another nice effect you can apply using a LookupOp. Posterizing involves reducing the number of colors used to display an image.

A LookupOp can achieve this effect by using a table that maps input values to a small set of output values. The following example shows how input values can be mapped to eight specific values.

001 short [] posterize = new short [256]; 002 per (int i = 0; i <256; i ++) 003 posterize [i] = (short) (i - (i% 32)); 004 BufferedImageOp posterizeOp = 005 new LookupOp (new ShortLookupTable (0, posterize), null);

Soglia

L'ultima operazione sull'immagine che esamineremo è la soglia. Il controllo della soglia rende più evidenti i cambiamenti di colore attraverso un "confine" o soglia determinato dal programmatore (simile a come le curve di livello su una mappa rendono più evidenti i confini dell'altitudine). Questa tecnica utilizza un valore di soglia specificato, un valore minimo e un valore massimo per controllare i valori dei componenti del colore per ogni pixel di un'immagine. Ai valori di colore al di sotto della soglia viene assegnato il valore minimo. Ai valori al di sopra della soglia viene assegnato il valore massimo.