MokaByte Numero 07 - Aprile 1997
Foto
Elaborazione delle immagini
 
di
Massimo Carli
Puntata numero 5

 





Dopo la puntata relativa alla lettura di una immagine di formato diverso da quelli JPG e GIF (ricordiamo che si possono leggere anche quelli in formato GIF89a, le famose GIF animate), vediamo ora di realizzare dei filtri grafici in Java.


Prima cosa vediamo quale potrebbe essere l'utilità di tali filtri. Come sappiamo uno degli aspetti più importanti nel caso di applet è il tempo che essi impiegano ad essere scaricati dal browser. Il tempo aumenta nel caso in cui si debbano trasferire anche delle immagini. La regola è quella, quindi, di limitare il più possibile tali tempi. La Sun ci è venuta incontro creando la possibilità di costruire files Jar che sappiamo comprimere sia le classi che le immagini ed il suono in un solo file. Questo ha il vantaggio di permettere il trasferimento degli oggetti in una unica sessione oltre al fatto di comprimere gli stessi secondo le specifiche classiche dello ZIP. Non per questo dobbiamo approfittarne e riempire gli applet di immagini da scaricare, specialmente se le immagini scaricabili sono legate tra loro da somiglianze di vario tipo. Si cerca, quindi, di elaborare le immagini presenti nel browser piuttosto che scaricarne di nuove. Un esempio può essere quello di un bottone. Se ci siamo costruiti un pulsante con una immagine (attraverso un Canvas ecc.) vogliamo che l'immagine dia l'impressione del fatto che il pulsante stesso sia premuto oppure no. Per fare questo basterà rendere l'immagine un po' più scura. Questo sarà possibile attraverso un filtro piuttosto che scaricando una nuova immagine. Ovviamente anche qui si ha a che fare con quello che si chiama trade-off cioè con il fatto che se ci si guadagna da una parte ci si perde dall'altra. Lo svantaggio, in questo caso, sta nel fatto che si da un numero maggiore di elaborazioni all'applet ed il risultato non sarà bello come nel caso in cui si costruiscano le varie fasi del pulsante una ad una.

L'uso di filtri non è comunque consigliato solo nei casi precedentemente descritti. Ci sono applicazioni in cui l'uso di filtri di immagini è l'unica soluzione. Per esempio nel caso in cui si vogliano creare vari effetti applicando un filtro più volte ad una immagine. Sarebbe assurdo pensare al caricamento di 100 immagini attraverso la rete.

Qualcuno di voi potrebbe dire che non è necessario creare dei filtri per elaborare delle immagini. Per esempio, basterà gestire opportunamente le classi incontrate nelle puntate precedenti: MemoryImageSource o PixelGrabber. La cosa potrebbe anche essere vera, ma dobbiamo sempre ricordare che Java è un linguaggio Object Oriented.
Uno dei fondamentali vantaggi dell'Object Orienting è il riutilizzo del software. Si potrebbe creare un applet che data un'immagine ne fa quello che vuole trasformandola in vettore di pixel e poi ricreandone un'altra. Questa è cosa semplice. Ma cosa succede se si volessero fare diverse elaborazioni dell'immagine. Bisognerebbe creare vari metodi della classe ciascuno per ogni elaborazione. Fin qui tutto bene, ma cosa succede se dobbiamo creare un altro applet che utilizzi gli stessi effetti? Basterà fare un cut&paste dei metodi, ma la cosa non è certo delle migliori quando si ha a che fare con un linguaggio OO. Si potrebbe creare una classe di utilità con tutte le varie operazioni. Va beh. Ma cosa succede se dobbiamo aggiungere un nuovo effetto? Dobbiamo aggiungere un nuovo metodo e rimettere mano alla classe di utilità. Se si ha ache fare con l'OO deve essere limitato al massimo il "rimettere mano". Se l'oggetto, poi, è serializzabile e lo avete salvato avreste addirittura problemi di versione e non riuscireste più a ripritinarlo se non con una accorta gestione del versioning. La soluzione naturale è quella di creare una classe per ogni effetto o filtro. Se dobbiamo creare un nuovo effetto creeremo una nuova classe che fungerà, come vedremo, da filtro. Gli effetti, o filtri, precedenti non si toccano più. Cosa fondamentale, l'uso dei filtri, come vedremo, è cosa semplicissima.

Cosa è un filtro in Java
Nel package java.awt.image esistono alcune classi che permetono la creazione di filtri. Prima di vedere tali classi vediamo come, dato un filtro, si crea l'immagine filtrata. Il procedimento è simile a quello utilizzato per la classe MemoryImageSource. Sappiamo che essa è una classe ImageProducer utilizzata poi nella createImage per la creazione dell'immagine stessa. Nel caso dei filtri esiste la classe FilteredImageSource che funziona in modo analogo alla MemoryImageSource. Anche FilteredImageSource è infatti un ImageProducer. Supponiamo di avere una immagine img da filtrare.

Image img = getImage("esempio.gif");

ImageFilter filtro = new RGBImageFilter();

      FilteredImageSource fis = new FilteredImageSource(img.getSource(),filtro);

      Image fil_img = createImage(fis);
 
 
 

Nella prima riga prendiamo l'immagine da elaborare. Nella seconda creiamo un oggetto filtro che sarà appunto il filtro che andremo ad applicare. Dal filtro e dal Producer dell'immagine ottenuto con il metodo getSource(), creiamo un ImageProducer per l'immagine filtrata che poi andremo a creare.
Come si può notare il procedimento di creazione dell'immagine filtrata è molto semplice e non modifica l'immagine iniziale.

Una volta visto come, dato il filtro, si crei la versione filtrata di un'immagine, vediamo come si creano e gestiscono i filtri. Come prima cosa notiamo che la classe FilteredImageSource vuole come secondo parametro un oggetto ImageFilter. Tutti i filtri, in Java,estendono la classe ImageFilter che implementa l'interfaccia ImageConsumer e ci risparmia di dover definire metodi che non ci interessano come per esempio quello del settaggio delle proprietà setProperties(). Oltre ai metodi previsti dalla ImageConsumer sono presenti un metodo clone() per l'interfaccia Cloneable ed i metodi getFilteredInstance() e resendTopDownLeftRight(ImageProducer).
Il funzionamento di ogni oggetto ImageFilter è legato al fatto che esso deve fungere sia da ImageProducer che da ImageConsumer. Esso deve essere ImageConsumerImageProducer per l'immagine filtrata.
Ricordiamo che il procedimento nelle chiamate all'ImageConsumerImageConsumer.

Inizialmente l'ImageConsumer acquisisce le dimensioni dell'immagine sorgente attraverso la chiamata, da parte dell'ImageProducer, del metodo setDimension(). Dopo questo, l'ImageConsumer si aspetta l'insieme delle informazioni relative ai pixel. E' possibile, però, definire anche quale sarà il modello di colore utilizzato cioè il significato dei valori che saranno forniti come dati relativi ai pixel. Questo potrà essere fatto dalla setColorModel() se il ColorModel no è quello di default. E' possibile poi assegnare delle proprietà all'immagine attraverso il metodo setProperties() che permette di associare una Hahtable all'immagine stessa. Un metodo importante è setHints(). Esso specifica, infatti, come i pixel saranno forniti dall'ImageProducer all'ImageConsumer. Esistono varie possibilità definite nell'interfaccia ImageConsumer e date da costanti ed espresse da un insieme di flag. Esse sono le seguenti:

1   RANDOMPIXELORDER    : Non si danno informazioni riguardo al metodo di fornire i pixel.

2   TOPDOWNLEFTRIGHT    : Si forniscono i pixel andando dall'alto al basso e da sx verso dx

4   COMPLETESCANLINES   : Si forniscono i pixel dando una riga alla volta

8   SINGLEPASS          : Si forniscono i pixel dell'immagine tutti in una volta.

16 SINGLEFRAME          : L'immagine è costituita da un unico frame statico


Una volta specificato il metodo con cui l'ImageProducer fornisce le informazioni relative ai pixel si avranno una o più chiamate al metodo setPixels() che fornirà le informazioni relative ai pixel stessi. Esistono due versioni di questo metodo a seconda che si vogliano fornire i dati sotto forma di vettori di byte o di int. Notiamo che in ogni chiamata compare il ColorModel utilizzato per cui quello fornito con il metodo setColorModel() serve colamente come indicazione generale per cui non si hanno molti vantaggi utilizzandolo.
Una volta che tutti i pixel sono stati forniti l'ImageProducer chiamerà il metodo imageComplete() dell'ImageConsumer fornendo informazioni sull'esito dell'elaborazione.

Da quello detto si deduce che per la creazione di un filtro basterà ridefinire alcuni dei metodi appena descritti elaborando, di volta in volta, i pixel che vengono forniti dall'ImageProducer. Questo sarà fatto nei metodi setPixels in quanto è qui che i dati relativi ai pixel sono forniti.
 

I filtri di Java Prima di creare alcuni esempi vediamo quelli che sono i filtri forniti con il JDK. Essi sono rappresentati dalle seguenti classi:

RBGImageFilter : Questa è una classe astratta e come tale può essere solo estesa. Essa prevede la creazione di tutti i metodi per la realizzazione di filtro supponendo che il modello utilizzato sia il modello di default corrispondente alla rappresentazione RGB dei pixel. Per creare un filtro nel modello RGB basterà estendere questa classe e definire solamente il metodo filterRGB(int x, int y, int rgb). Questo metodo permette, infatti, di convertire un pixel nella posizione (x,y) nel valore indicato dal parametro rgb. Il filtro non farà altro che percorrere ad uno ad uno i pixel forniti dall'ImageProducer e convertirli con la regola specificata nel metodo filterRGB().
 

CropImageFilter : Questa rappresenta un filtro vero e proprio nel senso che non ha bisogno di essere estesa. Se utilizziamo questo filtro possiamo ottenere una immagine da un'altra prendendone una parte. Infatti possiamo specificare il rettangolo da estrarre.
 

AverageScaleFilter e ReplicateScaleFilter : Queste due classi sono state introdotte nella versione 1.1 del JDK. Esse hanno lo scopo fornire due algoritmi per creare delle immagini scalate rispetto a quelle originali. In precedenza, infatti, le immagini si potevano rappresentare nelle volute dimensioni attraverso il metodo drawImage() della classe Graphics specificando le dimensioni. Questo non era, però, molto efficente in quanto non dava la possibilità di ottenere un oggetto distinto rappresentativo dell'immagine scalata e perchè ciascuna versione scalata di una immagine era memorizzata in una cache. Se l'immagine doveva essere modificata nelle dimensioni un numero elevato di volte si aveva una occupazione di memoria elevata. Questo doveva avere il vantaggio di una veloce visualizzazione. Ora che i calcolatori sono molto potenti, questo non ha più bisogno di esistere per cui si possono creare versioni scalate di una immagine anche onthefly senza problemi. Per ovviare a questi problemi sono allora state introdotti i due filtri citati oltre ad un nuovo metodo della classe Image createScaledImage().
 

Qualche semplice esempio Una volta che abbiamo visto come si utilizzano i filtri, creiamone alcuni molto semplici rimandando al mese prossimo la creazione di filtri più complicati ma sicuramente più belli. Come primi esempi utilizziamo i filtri già disponibili con il JDK RGBImageFilter ed CropImageFilter.

Iniziamo con utilizzare un filtro RGBImageFilter. Come sappiamo questa classe non puo' essere istanziata perchè classe astratta in quanto il metodo filterRGB è un metodo abstract. Per creare un filtro bisognerà, quindi, estendere la classe RGBImageFilter e definire il metodo filterRGB. Nel nostro esempio realizziamo un filtro che estrae una delle componenti RGB dall'immagine. Per fare questo creiamo la classe ComponentFilter. Essa ha un unico costruttore che permette di decidere quale componente filtrare. Nel nostro caso le componenti RGB sono assegnate rispettivamente ai valori 0, 1 e 2 del parametro tipo. La classe ComponentFilter un listato molto semplice ComponentFilter.java
Come si può notare la nostra classe estende la classe RGBImageFilter e ridefinisce il metodo filterRGB testando il tipo di filtro e sovrapponendo una opportuna maschera a ciascun valore rgb.
Nel seguente applet ( listato AppletComponentFilter.java) si può osservare il risultato.

Proviamo ora ad utilizzare il filtro CropImageFilter. In questo caso la classe non è astratta e rappresenta un vero e proprio filtro che permette di estrarre una immagine da un altra specificandone l'area. Facciamo un semplice applet (listato AppletCropFilter.java ) che estrae 4 parti della nostra immagine e le posiziona (nel senso di stampa e non di immagine) nelle posizioni opposte diagonalmente. Se avessimo voluto realizzare questa operazione attraverso un filtro avremmo dovuto creare un filtro estendendo la ImageFilter avendo controllo su tutti i pixel dell'immagine nel suo insieme. Vedremo come nella prossima puntata.

Conclusioni
In questa puntata bbiamo visto come si realizza un filtro,abbiamo esaminato le classi fornite nel JDK ed abbiamo realizzato due piccoli esempi di filtraggio. Il prossimo mese vedremo come realizzare filtri più complessi che considerino l'intera immagine e non i singoli pixel.
 
 
 
 

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it