MokaByte Numero 04 - Gennaio 1997

 
Introduzione alla elaborazione 
delle immagini in Java
 
di
 Massimo Carli 
Terza puntata

 


 

 

Nella scorsa puntata abbiamo visto come si caricano e visualizzano delle immagini di formato standard JPG e GIF supportate da Java. Abbiamo poi visto come si può, attraverso la classe MediaTracker, attendere che un certo numero di immagini siano caricate. Oggi vediamo più in profondità, le interfacce del package java.awt.image solo accennate la volta scorsa. Vedremo poi, i concetti che stanno alla base per il caricamento e la visualizzazione di immagini in formato diverso da quelli standard di Java. La realizzazione di una classe per la lettura del formato BMP sarà realizzata il prossimo mese per non appesantire la presente puntata. Nel costruttore della classe MediaTracker (per classi di cui ho già parlato tralascierò il package di appartenenza visto che nell'AWT non esistono classi di package diversi con nomi uguali) appare un oggetto di tipo Component. La cosa importante non è nel fatto di essere un Component in quanto oggetto da inserire in un Container per realizzare GUI ecc, ma per il fatto che il Component implementa l'interfaccia ImageObserver. Infatti se andiamo a vedere la documentazione della classe Component, vediamo che esistono dei flag ed il metodo imageUpdate previsti dalla suddetta interfaccia. Ma andiamo per ordine. Quando si parla di immagini in Java si devono tenere presente due cose: la classe Image e il package java.awt.image. La prima rappresenta gli oggetti immagine e dispone di vari metodi di utilità per sapere, per esempio, le dimensioni dell'immagine stessa. Il package è, invece, responsabile di tutto il background che riguarda la creazione e l'utilizzo dell'immagine. Chiunque scarichi una pagina Web con delle immagini, nota che esse vengono visualizzate un po' alla volta, mano a mano che sono scaricate. Le fasi di scaricamento e di visualizzazione avvengono in maniera asincrona. Asincrona significa che le due cose avvengono senza un preciso legame temporale o sincronizzazione (se non quello che la visualizzazione avviene dopo il caricamento). In Java la sorgente ed il ricevitore di questa elaborazione asincrona dell'immagine sono le due interfacce ImageProducer e ImageObserver. L'interfaccia ImageProducer è responsabile della creazione, bit dopo bit, dell'immagine e della comunicazione della stessa attraverso un ImageObserver. Questo avviene proprio attraverso l'invocazione del metodo imageUpdate dell'interfaccia ImageObserver. Se carichiamo una immagine senza il MediaTracker si ha proprio il comportamento descritto. Consideriamo il seguente codice che supponiamo facente parte della init di un applet:

Non appena si ha la creazione dell'oggetto immagine (di tipo Image) visualizziamo le sue dimensioni. Se eseguiamo un applet che contiene queste poche righe noteremo che i valori visualizzati per le dimensioni sono entrambi -1. Questo perche' quando arriviamo alla stampa, l'immagine non è stata ancora caricata. Fino a qui niente di nuovo perche' lo avevamo già detto il mese scorso. Abbiamo detto che affinche' si abbia l'inizio ( da sottolineare "inizio" ) della fase di caricamento, dovevamo visualizzarla. Ma se mettiamo il metodo paint() con la visualizzazione dell'immagine si ha una visualizzazione graduale mentre l'immagine si forma. Per questo avevamo introdotto l'uso del MediaTracker per attendere di avere tutta l'immagine prima di visualizzarla. Sappiamo che un applet, risalendo la gerarchia delle classi, in fin fine è un Component (tralasciando ovviamente che sia un Object come ogni oggetto in Java). Se è un Component, implementa la ImageObserver per cui ha il metodo imageUpdate. Noi, allora, vogliamo ridefinire questo metodo la cui firma è:
public boolean imageUpdate(Image img, int flags, int x, int y,int w,int h);
Vediamo i parametri: img è ovviamente l'immagine in fase di creazione flagsè un intero i cui primi 8 bit meno significativi sono utilizzati come flag secondo il seguente ordine (gli altri sono a 0):
WIDTH E' settato a 1 quando il parametro relativo alla larghezza può essere letto con getWidth
HEIGHT E' settato a 1 quando il parametro relativo alla altezza può essere letto con getHeight
PROPERTIES E' settato a 1 quando le proprietà della immagine sono disponibili cioè si può usare getProperties()
SOMEBIT E' settato a 1 se sono disponibili dei pixel aggiuntivi per disegnare una immagine. I pixel sono quelli dentro il box rappresentato dai valori di x,y,w e h.
FRAMEBITS E' settato a 1 se l'immagine è completamente caricata e può essere visualizzata
ALLBITS E' settato a 1 se una immagine static è stata completamente caricata e può essere visualizzata
ERROR E' settato a 1 se si è verificato un errore che non è comunque catalogato.
ABORT E' a 1 se l'elaborazione dell'immagine è fallita x,y sono valori interi che indicano il pixel in "fabbricazione" in quel momento oppure il vertice superione sinistro del box di pixel attivi in quel momento h,w sono valori interi che rappresentano rispettivamente l'altezza e la larghezza della regione di pixel attiva in quel momento

Un lettore attento dovrebbe aver notato subito il flag ALLBITS e gli dovrebbe essere venuta alla mente la classe MediaTracker. Comunque procediamo lentamente. Abbiamo detto che vogliamo ridefinire la imageUpdate del nostro applet per vedere se questa viene eseguita o meno. Proviamo a ridefinirla nel modo descritto in questo applet

// Importiamo le classi che ci servono

import          java.net.URL;                   // Per gli URL

import          java.applet.Applet;             // Perchè è un applet

import          java.awt.*;                     // Oggetti grafici



public class provaUpdate extends Applet {

        private         Image   img;



        public void init(){

                // Carichiamo una immagine

                img = getImage(getCodeBase(),nomefile);

                System.out.println("INIZIO");   // Avvisiamo che siamo all'inizio

                // Visualizziamo le dimensioni

                System.out.println("Altezza immagine     = "+img.getHeight(this) );

                System.out.println("Larghezza immagine = "+img.getWidth(this) );                

        }// fine init

        

        // Per eliminare il flicker

        public void update(Graphics g){

                paint(g);

        }// fine update

        

        // La paint non fa altro che disegnare l'immagine

        public void paint(Graphics g){

                g.drawImage(img,0,0,this);      // Usiamo il noto metodo (this=ImageObserver)

        }// fine paint



        // Ora ridefiniamo la imageUpdate stampando lo stato dei suoi parametri

        // Se non è finita allora ristampiamo l'immagine

        public boolean imageUpdate(Image  img, int  flags,int  x, int  y, int  w, int  h){

                System.out.println("E' stata chiamata la Update con:");

                System.out.println("x="+x+" y="+y+" w="+w+" h="+h);

                repaint();

                return true;

        }// fine imageUpdate

}// fine provaUpdate
Questo applet mostra una cosa importante. Per osservare il suo funzionamento provate ad aprire la finestra relativa a Java di Netscape (menu Options e Java Console). Noterete che l'immagine viene visualizzata per righe ma la cosa più importante è quello che viene visualizzato nella Java console. Una cosa importante si può notare se facciamo il reload. L'applet è ancora vivo e mantiene in memoria la classe per cui l'immagine non sarà piu' ricreata e quindi le scritte relative alla imageUpdate saranno visualizzate una sola volta. La cosa che si voleva verificare era, però, se il metodo imageUpdate veniva chiamato oppure no. Con il precedente applet la risposta è scontata. Notiamo che quando viene fatta la getImage si ha l'attivazione di un qualcosa, che è l'imageProducer, che crea l'immagine ed informa l'imageObserver, l'applet in questo caso, dell'avvenuta variazione. Osserviamo alcune righe stampate:
        ..........

        E' stata chiamata la Update con:

        x=0 y=143 w=200 h=1

        E' stata chiamata la Update con:

        x=0 y=144 w=200 h=1

        E' stata chiamata la Update con:

        x=0 y=145 w=200 h=1

        E' stata chiamata la Update con:

        x=0 y=146 w=200 h=1

        ..........
Notiamo come x sia sempre a 0 mentre la y aumenta di 1. Inoltre, dopo la prima visualizzazione, il valore di w coincide con la larghezza dell'immagine mentre h vale sempre 1. Questo indica che l'imageProducer avvisa l'imageObserver dell'avvenuta variazione solo dopo il caricamento di ciascuna riga. Infatti, nel nostro applet si ha la visualizzazione della immagine riga dopo riga. Abbiamo, quindi, visto il legame che esiste tra un imageProducer associato ad una immagine ed un imageObserver. Da qui il passo verso il MediaTracker è molto breve. Supponiamo di cambiare il metodo imageUpdate sostituendo la istruzione repaint(); con la seguente:
        if (( flags & ALLBITS ) !=0)

                repaint();
Nella imageUpdate aggiungiamo un if.  Ricordo che il simbolo & (un solo &) rappresenta l'AND logico cioè bit a bit. Con la espressione flags & ALLBITS verifichiamo che il bit 5 sia a 1. Quando tale bit è a 1 significa che l'immagine è stata caricata da cui la visualizzazione da cui l'effetto del MediaTracker. Per verificare che il bit relativo ad un campo abbia un certo valore o meno basterà fare l'AND logico tra la variabile flags e una opportuna maschera. Queste maschere fanno parte dell'interfaccia imageObserver ed hanno i nomi relativi ai bit descritti in precedenza. Visto come si può verificare l'avvenuto caricamento di una immagine, il passaggio a più immagini ed all'associazione di ciascuna ad un numero, come fa il MediaTracker, è cosa semplice. Una domanda potrebbe sorgere a questo punto. Ma dove è l'interfaccia imageProducer? Nel caso delle immagini la imageProducer è creata automaticamente dal sistema. In generale, però, ogni classe per oggetti che devono creare una immagine devono implementare l'interfaccia imageProducer. Tutte le classi che si interessano di oggetti creati da una imageProducer devono implementare la imageConsumer. Ecco l'ultima delle tre interfaccie. Capisco che ora nel lettore ci possa essere un po' di confusione riguardo queste tre interfaccie per cui è meglio riassumere brevemente di cosa si tratta. L'oggetto della classe Image è solo piccola cosa riguardo alla gestione delle immagini in Java. Il tutto è basato sul funzionamento di tre interfacce. La prima deve essere implementata da ogni classe che vuole creare un'immagine. Essa è imageProducer. Essa si occupa della creazione dell'immagine e della comunicazione dell'avvenuta creazione o del verificarsi di errori. Queste informazioni le fornisce a tutti gli oggetti che si sono registrati ad essa. Questo significa che se io voglio sapere se una immagine è pronta o meno dico all'imageProducer di avvisarmi quando questo è avvenuto. Io devo implementare la imageConsumer e chiamare il metodo addConsumer dell'imageProducer per informarlo del fatto di voler essere avvisato. Ma come fa l'imageProducer ad avvisarmi? Per avvisarmi chiamerà miei opportuni metodi. Ma come fa a sapere quali metodi? Semplice. Sa che io devo implementare la imageConsumer per cui sono obbligato a realizzare certi metodi tra cui sicuramente quello chiamato per l'avviso. In particolare sarà il metodo imageComplete con un opportuno parametro che darà il come l'operazione è terminata. Per quello che riguarda l'imageObserver, essa serve solamente per tenere traccia del progredire della creazione dell'immagine. Ancora più in sintesi, l'imageProducer crea e dice all'imageObserver come sta andando, ma fornisce l'esito all'imageConsumer. Terra a terra, l'imageProducer fa,l'imageObserver guarda e l'imageConsumer utilizza. Un'altra domanda, a questo punto, potrebbe essere questa: ma se l'imageProducer crea l'immagine e l'imageObserver la utilizza ed attraverso la imageComplete posso sapere che l'immagine è pronta, perchè il MediaTracker utilizza la imageObserver? Semplicemente perchè fornisce le stesse informazioni relativamente alla terminazione del caricamento e prevede la definizione del solo metodo imageUpdate(). Essa è quindi più semplice. Per comprendere al meglio l'uso delle interfacce relative alla gestione delle immagini consiglio di leggere il presente articolo tenendo sempre sott'occhio la documentazione fornita con il JDK per avere sempre una visione completa dei metodi che esse implementano. Per coloro che vogliono sapere di più riguardo alla imageObserver consiglio di provare a stampare ogni flag ad ogni chiamata della imageUpdate in modo da verificarne il funzionamento. E' utile verificare se effettivamente i flag di WIDTHe HEIGHT vanno a 1 quando sono disponibili le dimensioni oppure, ancora meglio, se quando l'immagine è completamente caricata il bit di ALLBITS va effettivamente a

1. LETTURA DI IMMAGINI IN FORMATO DIVERSO DA JPG e GIF

Come annunciato nella introduzione della presente puntata, questo mese vedremo i concetti che stanno alla base della creazione di una immagine di formato diverso da quelli JPG e GIFprevisti da Java riservandoci, il mese prossimo, la realizzazione di una classe per il caricamento e la visualizzazione di immagini in formato BMP. Diciamo subito che quello che faremo il prossimo mese sarà solo una realizzazione pratica di quello che dirò tra breve. Le fasi di realizzazione saranno sempre le stesse indipendentemente dal formato. Come prima cosa dobbiamo dire che cosa è un modello di colore. Questo è descritto in Java dalla classe java.awt.image.ColorModel. Nella prima puntata abbiamo visto come una immagine sia solo un insieme di colori opportunamente posizionati. Nel nostro caso le immagini sono un insieme di pixel opportunamente disposti. Quello che differenzia una immagine da un'altra delle stesse dimensioni è la qualità o il colore dei pixel. Abbiamo già visto come tale colore viene rappresentato attraverso le scomposizioni RGB e HSBR. La rappresentazione di default per Java è quella RGB quindi un pixel viene rappresentato dalle componenti R,G e B più quella relativa alla trasparenza. Sappiamo, però, che un colore può essere rappresentato anche in altro modo. Per esempio attraverso un numero intero ottenuto dalla struttura binaria descritta nella prima puntata. Ebbene, il modo con cui si stabiliscono le caratteristiche di ogni pixel è descritto nel ColorModel utilizzato. Il costruttore di tale classe è il seguente:

        ColorModel ( int bits );
dove bits è il numero di bit che si decide di utilizzare per rappresentare il colore di un pixel. Esistono, poi, altri metodi che ci permettono di conoscere le componenti RGB ed alpha del colore rappresentato con il ColorModel, oppure il valore intero RGB oppure il numero stesso di bits per pixel:
        public abstract int getAlpha(int  pixel);   

        public abstract int getBlue(int  pixel);    

        public abstract int getGreen(int  pixel);   

        public int getPixelSize();  

        public abstract int getRed(int  pixel);     

        public int getRGB(int  pixel);     

        public static ColorModel getRGBdefault();  



L'ultimo metodo è statico (può essere invocato senza instanziare la classe) e permette di ottenere il modello di Default cioè quello per una descrizione dei colori in RGB. La classe ColorModel, comunque, è troppo generale per cui la Sun fornisce (nel JDK1.02 e nel JDK1.1) altre due classi che la estendono e che rappresentano altri due modi, abbastanza comuni, di rappresentare dei pixel. La prima di queste classi è la DirectColorModel la quale permette di gestire il caso in cui i pixel contengono le informazioni del colore direttamente in termini di componenti RGB. Questo significa che il valore associato ad un pixel, nella sua rappresentazione binaria, contiene le informazioni relative alle componenti RGB e che quindi bisogna estrarre. Per creare questo modello, conosciuto anche come "true color" , è sufficiente specificare il numero di bit utilizzati per la rappresentazione del colore e quali di questi bit sono relativi alla componente R, quali alla G e quali alla B. Esistono due costruttori di cui uno con la componente alpha ed uno senza. I costruttori sono i seguenti:
        public DirectColorModel(int  bits, int rmask, int gmask, int  bmask);

        public DirectColorModel(int  bits, int  rmask, int gmask, int  bmask, int  amask);
Notiamo come il parametro bits rappresenti il numero dei bit utilizzati nella descrizione e come vi siano poi dei parametri che fungono da maschere per i bit relativi alla descrizione della relativa componente. Possiamo, dal DirectColorModel, ottenere direttamente il modello di default ponendo i seguenti parametri:
        bits   = 32

        amask  = 0xff000000

        rmask  = 0x00ff0000

        gmask  = 0x0000ff00

        bmask  = 0x000000ff
Ricordiamo che la rappresentazione di un intero in Java avviene con 4 byte e che la rappresentazione esadecimale del byte costituito da tutti 1 è ff. Molto spesso il numero dei colori presenti in una immagine è abbastanza limitato per cui è vantaggioso considerare un vettore di colori e rappresentare il colore di un pixel con l'indice del colore nel vettore stesso. Questo è il caso del modello rappresentato dalla classe IndexColorModel . Essa dispone dei seguenti costruttori:
     public IndexColorModel(int  bits, int  size, byte  r[], byte  g[], 

                                        byte  b[]);

     public IndexColorModel(int  bits, int  size, byte  r[], byte  g[], 

                                        byte  b[], byte  a[]);     

     public IndexColorModel(int  bits, int  size, byte  r[], byte  g[], 

                                        byte  b[], int  trans);  

     public IndexColor(int  bits, int  size, byte cmap[], int  start, 

                                        boolean  hasalpha);

     public IndexColorModel(int bits, int size,byte cmap[], int  start, 

                                        boolean  hasalpha,  int  trans);



Come per le classi precedenti bisogna specificare il numero di bit necessari per la rappresentazione del colore ed i vettori relativi alle componenti dei colori presenti. Notiamo che è prevista anche la presenza del vettore delle componenti alpha. Con il parametro trans si specifica, invece, l'indice del colore che è da considerarsi trasparente. Il vettore cmap[] serve a mettere le componenti RGB ed alpha una di seguito all'altra come se i vettori fossero concatenati. A questo punto si rende necessario un parametro booleano hasalpha per sapere se nel vettore cmap[] sono presenti le componenti alpha oppure no. La cosa sarebbe difficile da sapere altrimenti. Il parametro start esprime un offset che indica quale colore è da considerarsi il primo. Se utilizziamo questo modello, non rappresenteremo più un colore con le sue componenti RGB o altro, ma solamente attraverso un valore che corrisponde all'indice nel vettore, o nei vettori, delle sue componenti. Questo modello è il più usato e sarà utilizzato anche nella lettura del formato BMP. Questo formato, come molti altri, dispone sempre da qualche parte nel file (di solito dopo una zona di intestazione, header) della mappa dei colori presenti (infatti si chiama Image Map). Poi l'immagine è rappresentata con una successione degli indici che identificano il colore nella mappa. Ovviamente questo metodo è conveniente qualora si abbia una bassa gamma di colori ed un gran numero di pixel di uguale colore e trasparenza. A questo punto sappiamo cosa è un color model ed abbiamo capito all'incirca cosa ci servirà. Supponiamo di conoscere le specifiche del file dell'immagine da visualizzare. Questo è fondamentale. Da qualche parte nel file, oltre alle informazioni generali dell'immagine quali le sue dimensioni, troveremo la mappa dei colori presenti (ripeto: che questo non è vero sempre ma lo è nella maggioranza dei casi). Una volta creata la mappa dei colori e creato il relativo oggetto ColorModel o altro da esso derivato, dobbiamo leggere le informazioni dei pixel. Conoscendo le specifiche del formato la cosa è semplice. Ma dopo cosa facciamo? Come facciamo dai pixel creare l'immagine? Come primo approccio la cosa più intuitiva è quella di mettere tutte le informazioni dei pixel in un vettore in maniera da disporne globalmente e da dare ad essi un certo ordine. E poi? Fortunatamente la Sun ha pensato anche a questo attraverso la classe MemoryImageSource. Questa classe è forse tra le piu' divertenti del package relativo alle immagini in quanto, guarda caso, da un vettore di pixel mi permette di avere ........che cosa? Se vi aspettavate un'immagine la risposta è no. La classe MemoryImageSource ci da molto di più. Essa è infatti (udite udite...) un ImageProducer. Da qui all'immagine il passo è corto. Per ottenere l'immagine relativa basterà scrivere :
        Image img = createImage(ImageProducer);
Il metodo createImage esiste sia nella classe Component sia in Toolkit per cui è abbastanza facile da raggiungere ed utilizzare. Ma ritorniamo al nostro caso. Eravamo rimasti con il vettore dei pixel dell'immagine, conosciamo le sue dimensioni ed abbiamo il suo ColorModel. Basterà creare un oggetto MemoryImageSource attraverso uno dei seguenti costruttori:
  public MemoryImageSource(int  w, int  h, ColorModel cm, byte  pix[], int  off,  int  scan);

  public MemoryImageSource(int  w, int  h, ColorModel cm, byte  pix[], int  off,  int  scan                                        Hashtable props);

  public MemoryImageSource(int  w, int  h, ColorModel cm, int  pix[], int  off,  int  scan);

  public MemoryImageSource(int  w, int  h, ColorModel cm, int  pix[], int  off, int scan, 

                                        Hashtable  props);

  public MemoryImageSource(int  w, int  h, int pix[],int off, int  scan);

  public MemoryImageSource(int  w, int  h, int pix[], int  off, int scan, Hashtable  props);
I vari parametri dovrebbero essere, a questo punto, comprensibili. Da sottolineare solo off che rappresenta l'indice del pixel da considerare il primo dell'immagine, scan che rappresenta il numero di pixel per riga che non è detto debba coincidere con la larghezza w dell'immagine, e props che rappresenta l'Hashtable che si può leggere con il metodo getProperty dell'oggetto Image per memorizzare informazioni aggiuntive. A questo punto l'immagine è fatta e la si può visualizzare o altro. Ovviamente, come nel caso del caricamento, l'immagine non è costruita subito ma inizia un processo di elaborazione che si svilupperà in maniera asincrona come già sottolineato in precedenza. Si potrebbe anche utilizzare il Mediatracker per aspettare che l'immagine si crei e poi visualizzarla. Proprio come nel caso del getImage. In sintesi, cosa dobbiamo fare per visualizzare una immagine di formato non previsto da java?

1) Disporre delle specifiche complete del formato per sapere cosa e dove andare a leggere.

2) Acquisire lo stream di input per i dati dell'immagine.

3) Elaborare i dati letti dallo stream per creare il ColorModel

4) Estrarre dallo stream i dati relativi ai pixel secondo il ColorModel utilizzato e metterli in un vettore

5) Usare il vettore dei pixel, il ColorModel ed altre informazioni presenti nell'header per creare un oggetto MemoryImageSource.

6) Creare l'immagine attraverso il metodo createImage. Il procedimento di creazione di un ImageProducer per creare immagini è, come vedremo, alla base anche del filtraggio di immagini. Il prossimo mese utilizzeremo questo procedimento per leggere un'immagine in formato BMP.
 
  

 

MokaByte rivista web su Java

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