Nelle puntate precedenti del presente corso di elaborazione delle immagini in Java abbiamo visto come si caricano le immagini in un applet oppure in una application, abbiamo visto come funziona il MediaTracker, che cosa é il modello Producer/Consumer fino alla definizione dei ColorModel. Quest'ultima cosa ci ha permesso di dire come si potrebbe caricare una immagine di formato diverso da quelle previste dal JDK cioè i formati JPG e GIF. Avete mai provato a caricare una immagine GIF89a cioè una immagine animata? Provate, vedrete che si ha effettivamente l'animazione. Possiamo quindi dire che le immagini GIF sono intese nel senso generale. In questa puntata mettiamo in pratica quelle che sono state le nozioni date la volta scorsa relativamente al procedimento della lettura di immagini di formato diverso da quelli sopra citati. Riassumiamo qui i punti che avevo elencato la volta scorsa:

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.

La prima cosa che bisogna conoscere per leggere le informazioni riguardo ad una immagine è lo standard dell'immagine stessa. Quelle che considereremo qui sono le immagini in formato BMP secondo Microsoft. Esiste, infatti, anche un altro tipo di immagini BMP secondo OS2 che differiscono comunque di poco dalle prime. Le informazioni che andremo a leggere dallo stream saranno relative alle dimensioni, alla palette ed ai pixel che costituiscono l'immagine stessa.

Gli algoritmi di questo tipo sono tutti pressapoco simili. L'unico problema che si verifica in questi casi, riguarda la compressione dei dati. Le immagini di standard GIF, per esempio, utilizzano un procedimento di compressione LZW (che abbiamo accennato nella seconda puntata del corso) il quale non è semplicissimo da realizzare anche se ovviamente possibile. Le immagini JPG utilizzano un procedimento di compressione con perdita basato su una proprietà del nostro occhio di essere sensibile di piu' alle variazioni di luminosità che di colore, utilizzando una minore risoluzione di crominanza. Lo standard BMP, invece, utilizza un metodo di compressione più semplice detto RLE (Run-Length Encoding). Questo metodo di compressione non fa altro che rimpiazzare un certo numero consecutivo di colori uguali con il numero delle ripetizioni del colore ed il colore stesso. Se, per esempio, c è un colore e nello stream esso si presenta 5 volte, esso sarà scritto come 5c. Il coefficiente di compressione è sicuramente inferiore a quello degli altri metodi di compressione. La cosa dipende anche, ovviamente, dal tipo di immagine. Se un'immagine ha una palette di 8 colori, ed ha dimensioni rilevanti, vi sarà molta probabilità di avere molti pixel uguali consecutivamente per cui con il metodo RLE avremo un alto rapporto di compressione. Appunto per la loro relativamente bassa compressione, i file in formato BMP non sono molto utilizzati nel Web.

Vediamo ora come è fatto lo standard BMP secondo la versione Microsoft. Esso è costituito di quattro parti che esamineremo una ad una.

1.Intestazione:

Essa rappresenta la prima parte del file. All'inizio presenta due caratteri che devono essere i caratteri BM. Se i primi due caratteri incontrati non sono questi, allora il formato non è quello esatto, e la elaborazione dello stream dovrà terminare. Segue un valore a 4 byte che contiene la lunghezza del file BMP in byte. Si hanno poi due valori a 16 bit che contengono informazioni riservate, ed altri ultimi 4 byte che contengono l'offset relativo a dove sono contenute le informazioni relative ai bit del bitmap. Questa informazione, come vedremo, è molto importante. numero di byte Descrizione 2 Devono contenere i caratteri BM 4 Indica la lunghezza totale del file in byte 2 byte riservati 2 byte riservati 4 offset relativo all'inizio della mappa dei bit

2. Informazioni relative all'immagine :In questa parte abbiamo memorizzate tutte le informazioni che riguardano le immagini, a partire dalle loro dimensioni. Riassumiamo qui di seguito la configurazione di questa parte di file. numero di byte Descrizione 4 Dimensione in byte di questa regione 4 Larghezza dell'immagine in pixel 4 Altezza dell'immagine in pixel 2 valore a 1 (bitplanes) 2 Numero di bit per ogni pixel 4 Tipo di compressione utilizzata 4 Se è stata utilizzata la compressione contiene il numero di byte nel bitmap 4 Numero di pixel orizzontali per metro 4 Numero di pixel verticali per metro 4 Numero di colori attualmente utilizzati 4 Numero di colori fondamentali Le informazioni relative al numero di pixel per metro sono utili nel caso di ridimensionamento delle immagini, mentre il numero di colori fondamentali serve nel caso si debba ridurre il numero dei colori stessi. Comunque questi ultimi valori ci interessano poco.

3.Tabella dei Colori

Si ha, quindi, la tabella dei colori i quali sono memorizzati in un formato che si chiama RGBQUAD che consiste di 4 byte. Un valore RGBQUAD consiste in 8 bit per la componente blue, 8 bit per la componente verde, 8 bit per la componente rossa, e si conclude con 8 bit di 0. Ora dobbiamo, però, fare una considerazione importante. Tutti i numeri nel bitmap sono memorizzati in una notazione detta little-endian. Questo significa che il primo byte nel file rappresenta quello finale nella rappresentazione di un numero. Se dovessimo scivere il numero esadecimale 0x2468 in formato little-endian dovremmo mettere nel file prima 0x68 e poi 0x24. Java, però, usa la notazione inversa, detta big-endian.Quindi se in Java vogliamo scrivere il precedente numero scriveremmo prima 0x24 e poi 0x68. Nella lettura dei valori nella notazione RGBQUAD dobbiamo, quindi, stare attenti. Supponiamo che un valore RGBQUAD sia costituito nel seguente modo: 0xff, 0x32,0x28 e 0x00, se lo leggiamo in little-endian e lo trasformiamo in big-endian esso diventa il colore 0x2832ff. Il numero dei colori nella tabella dei colori è dato dal numero di colori utilizzati descritti nella parte precedente del file. Dobbiamo stare attenti al caso di un bitmap di 24 bit. In questo caso non c'è, infatti, tabella dei colori in quanto le informazioni del colore corrente sono memorizzate nei bit relativi ai pixel. Il tipo di compressione è importante per sapere se si è utilizzata una compressione e quale. Se il relativo campo è 0 allora non è stata utilizzata compressione, se il valore è 1 o 2 la compressione utilizzata è stata rispettivamente la RLE8 o la RLE4. Il valore si riferisce al numero di bit utilizzati nella rappresentazione dei pixel. Come detto prima il metodo RLE consiste di sue informazioni, una relativa ad un codice di 2 byte e la seconda contiene le informazioni del pixel. Il codice può contenere, per esempio, il numero di volte che il pixel è ripetuto su di una riga di immagine. Se il primo byte del codice è 0, il secondo può indicare varie cose. Potrebbe indicare il fatto di saltare alla riga successiva, potrebbe fornire le coordinate in cui il pixel è presente, ecc. Una cosa da considerare è il fatto che le immagini sono memorizzate all'incontrario, cioè da destra a sinistra e dal basso verso l'alto.

4. I dati relativi ai pixel

Ultima parte è quella relativa ai pixel stessi.

Una volta dette le specifiche relative ai file BMP proseguiamo con il punto 2. In questo punto dovremmo acquisire lo stream dal file immagine. La cosa è molto semplice in quanto se siamo in un applet otterremo lo stream da un oggetto URL, mentre se siamo in locale potremmo ottenere un FileInputStream da un File. La cosa che vorrei sottolineare è dovuta al fatto che il risultato del nostro lavoro non sarà la creazione dell'immagine, ma la creazione di un ImageProducer per l'immagine. Questo in quanto, una volta creato l'ImageProducer, il gioco è fatto ed offre una maggiore libertà di movimenti. Il terzo punto consiste nella lettura dei valori per la creazione dell'oggetto ColorModel.

Prima, però dobbiamo creare un metodo che risolva il problema della diversità di memorizzazione accennata sopra. utilizzeremo, quindi, un opportuno metodo al riguardo. Vediamo ora il listato del programma che chiamiamo BMPReader commentandolo opportunamente:

 

 

 

 

 

import
java.awt.*; // Ci serve per loggeto Image

import
java.awt.image.*; // Ci serve per l'elaborazione delle immagini

import
java.io.*; // Ci serve per gli stream

 

 

 

 

public
class CaricaBMP

{

// Definiamo
delle costanti relative ai modi in cui le informazioni sini

// meorizzate

 

 

   
public static final int C_RGB = 0; // Assenza di compressione

   
public static final int C_RLE8 = 1; // Compressione RLE8

   
public static final int C_RLE4 = 2; // Compressione RLE4

 

 

// La presente
classe vuole fornire un metodo statico che fornisca un

// ImageProducer
dall'InputStream

 

 

   
public static ImageProducer getBMPImage(InputStream stream)

   
throws IOException

   
{

// Creiamo
un DataInputStream in quanto dispone di metodi più utili per

// la lettura
dei dati

 

 

       
DataInputStream in = new DataInputStream(stream);

 

 

// Come
prima cosa dobbiamo verificare che il formato sia effettivamente

// un formato
BMP cioè i primi due caratteri devono essere 'B' e 'M'

 

 

       
if (in.read() != 'B') { // Leggiamo il primo byte

           
throw new IOException("Not a .BMP file"); // Se non è 'B'->exception

       
}

       
if (in.read() != 'M') { // Leggiamo il secondo byte

           
throw new IOException("Not a .BMP file");// Se non è 'M'->exception

       
}

 

 

// Dopo
i due caratteri leggiamo il valore intero a 4 byte che contiene la

// lunghezza
del file in byte

 

 

       
int fileSize = sistemaInt(in.readInt());

 

 

// Ora
ci sono le due coppie di byte riservate. Le leggiamo a vuoto per andare

// avanti
con le altre informazioni

 

 

       
in.readUnsignedShort(); // Primi due byte riservati

       
in.readUnsignedShort(); // Secondi due byte riservati

 

 

// Leggiamo
i 4 byte relativi all'offset per raggiungere la bitmap

 

 

       
int bitmapOffset = sistemaInt(in.readInt());

// Iniziamo
qui a leggere le  informazioni relativi al secondo blocco

// Leggiamo
la dimensione in byte di questa regione

 

 

       
int bitmapInfoSize = sistemaInt(in.readInt());

 

 

// Qui
leggiamo le dimensioni della immagine che sono contenute,

// ciascuna,
in 4 byte.

 

 

       
int width = sistemaInt(in.readInt()); // Leggiamo la larghezza

       
int height = sistemaInt(in.readInt()); // Leggiamo l'altezza

 

 

// Saltiamo
i bit relativi al bitplanes

 

 

       
in.readUnsignedShort();

 

 

// Leggiamo
il numero di bit per pixel

 

 

       
int bitCount = sistemaShort(in.readUnsignedShort());

 

 

// Leggiamo
il tipo di compressione utilizzato

 

 

       
int compressionType = sistemaInt(in.readInt());

 

 

// Leggiamo
il numero di byte nel bitmap

 

 

       
int imageSize = sistemaInt(in.readInt());

 

 

 

 

// Come
detto prima il numero di pixel per metro nelle due dimensioni

// non
ci interessano per cui le saltiamo

 

 

       
in.readInt();

       
in.readInt();

 

 

// Leggiamo
il numero di colori utilizzati

 

 

       
int colorsUsed = sistemaInt(in.readInt());

 

 

// Leggiamo
il numero di colori che dovrebbero essere tenuti nel

// caso
di diminuzione di definizione

 

 

       
int colorsImportant = sistemaInt(in.readInt());

 

 

// Controlliamo
che il numero di colori utilizzati non sia specificato li

// calcoliamo
in base al numero di bit per pixel. Questo lo facciamo

// moltiplicando
per due tante volte quanti sono i pixel.

 

 

       
if (colorsUsed == 0) colorsUsed = 1 << bitCount;

 

 

// In base
al numero di colori creiamo la tabella degli stessi. Questa

// la mettiamo
in un vettore di interi di dimensione pari al

// numero
dei colori stessi.

 

 

       
int colorTable[] = new int[colorsUsed];

// Leggiamo
ora la tabella dei colori. La funzione sistemaInt permette di

// risolvere
il problema della diversità di notazione tra little-endian

// e big-endian

       
for (int i=0; i < colorsUsed; i++) {

           
colorTable[i] = (sistemaInt(in.readInt()) & 0xffffff) + 0xff000000;

       
}

 

 

// Creiamo
il vettore dei pixel che conterranno l'immagine. Notiamo che

// la dimensione
è data dal prodotto delle dimensioni

 

 

       
int pixels[] = new int[width * height];

 

 

// In base
al tipo di compressione utilizzata leggiamo il valore corrispondente

 

 

       
if (compressionType == C_RGB) { // Se non c'è compressione

           
if (bitCount == 24) { // se l'immagine è a 24 bit

   
// Utilizziamo l'opportuno metodo

               
leggiRGB24(width, height, pixels, in);

           
} else { // Altrimenti leggiamo con il leggiRGB

               
leggiRGB(width, height, colorTable, bitCount,

                   
pixels, in);

           
}

// Se il
metodo di compressione è diverso utilizziamo un metodo opportuno

 

 

       
} else if (compressionType == C_RLE8) {

           
leggiRLE(width, height, colorTable, bitCount,

               
pixels, in, imageSize, 8);

       
} else if (compressionType == C_RLE4) {

           
leggiRLE(width, height, colorTable, bitCount,

               
pixels, in, imageSize, 4);

       
}

 

 

// Una
volta creato il vettore dei pixel e conosciute le dimensioni

// creiamo
l'image producer attraverso il MemoryImageSource e lo

// ritorniamo
come risultato.

 

 

       
return new MemoryImageSource(width, height, pixels, 0,

           
width);

   
}

Come potete osservare il procedimento è molto semplice. A questo punto dobbiamo solamente mettere i vari metodi di lettura delle informazioni dei pixel in base al tipo di compressione.

 

// nel
caso di 24 bit non c'è la tabella dei colori e ciascun pixel

// è
rappresentato da coppie di 3 byte. Ricordiamo che l'immagine

// è
memorizzata all'incontrario.

 

 

   
protected static void leggiRGB24(int width, int height, int pixels[],

       
DataInputStream in)

   
throws IOException

   
{

       
for (int h = height-1; h >= 0; h--) {

   
int pos = h * width;

           
for (int w = 0; w < width; w++) {

 

 

// Leggiamo
i byte relativi alle varie componenti

int red
= in.read();

int green
= in.read();

int blue
= in.read();

               
pixels[pos++] = 0xff000000 + (red << 16) +

(green
<< 8) + blue;

           
}

       
}

   
}

 

 

   
protected static void leggiRGB(int width, int height, int colorTable[],

       
int bitCount, int pixels[], DataInputStream in)

   
throws IOException

   
{

       
int pixelsPerByte = 8 / bitCount;

       
int bitMask = (1 << bitCount) - 1;

       
int bitShifts[] = new int[pixelsPerByte];

       
for (int i=0; i < pixelsPerByte; i++) {

           
bitShifts[i] = 8 - ((i+1) * bitCount);

       
}

       
int whichBit = 0;

       
int currByte = in.read();

       
for (int h=height-1; h >= 0; h--) {

           
int pos = h * width;

           
for (int w=0; w < width; w++) {

               
pixels[pos] = colorTable[

                   
(currByte >> bitShifts[whichBit]) &

                   
bitMask];

pos++;

               
whichBit++;

 

 

               
if (whichBit >= pixelsPerByte) {

                   
whichBit = 0;

                   
currByte = in.read();

               
}

           
}

       
}

   
}

 

 

 

 

// leggiRLE
reads run-length encoded data in either RLE4 or RLE8 format.

// Questo
metodo legge in corrispondenza del metodo di compressione utilizzato

 

 

 

 

   
protected static void leggiRLE(int width, int height, int colorTable[],

       
int bitCount, int pixels[], DataInputStream in,

       
int imageSize, int pixelSize)

   
throws IOException

   
{

       
int x = 0;

       
int y = height-1;

 

 

       
for (int i=0; i < imageSize; i++) {

           
int byte1 = in.read();

           
int byte2 = in.read();

           
i += 2;

           
if (byte1 == 0) {

               
if (byte2 == 0) {

                   
x = 0;

                   
y--;

               
} else if (byte2 == 1) {

                   
return;

               
} else if (byte2 == 2) {

                   
int xoff = (char) sistemaShort(

                       
in.readUnsignedShort());

                   
i+= 2;

                   
int yoff = (char) sistemaShort(

                       
in.readUnsignedShort());

                   
i+= 2;

                   
x += xoff;

                   
y -= yoff;

               
} else {

                   
int whichBit = 0;

                   
int currByte = in.read();

                   
i++;

                   
for (int j=0; j < byte2; j++) {

                       
if (pixelSize == 4) {

                           
if (whichBit == 0) {

                               
pixels[y*width+x] = colorTable[(currByte >> 4)

                                   
& 0xf];

                           
} else {

                               
pixels[y*width+x] = colorTable[currByte & 0xf];

                               
currByte = in.read();

                               
i++;

                           
}

                       
} else {

                           
pixels[y*width+x] = colorTable[currByte];

                           
currByte = in.read();

                           
i++;

                       
}

                       
x++;

                       
if (x >= width) {

                           
x = 0;

                           
y--;

                       
}

                   
}

                   
if ((byte2 & 1) == 1) {

                       
in.read();

                       
i++;

                   
}

               
}

           
} else {

               
for (int j=0; j < byte1; j++) {

                  
if (pixelSize == 4) {

                      
if ((j & 1) == 0) {

                          
pixels[y*width+x] = colorTable[(byte2 >> 4) & 0xf];

                      
} else {

                          
pixels[y*width+x+1] = colorTable[byte2 & 0xf];

                      
}

                  
} else {

                      
pixels[y*width+x+1] = colorTable[byte2];

                  
}

                  
x++;

                  
if (x >= width) {

                      
x = 0;

                      
y--;

                  
}

               
}

           
}

       
}

   
}

Qui di seguito abbiamo i metodi che permettono di leggere i dati in formato little-endian.

protected
static int sistemaShort(int i) {

       
return ((i >> 8) & 0xff) + ((i << 8) & 0xff00);

       
}

 

 

protected
static int sistemaInt(int i) {

       
return ((i & 0xff) << 24) +

               
((i& 0xff00) << 8) +

               
((i & 0xff0000) >> 8) +

               
((i >>24) & 0xff);

       
}

Come potete vedere i concetti alla base di questo procedimento sono pochi e relativi solo alla creazione del ColorModel e dell'ImageProducer. Poi quello che bisogna fare riguarda solo la lettura di dati dallo stream una volta saputo quello che dobbiamo leggere conoscendo le specifiche di un determinato formato. Per esercizio provate a creare un applet che legge una immagine di tipo BMP e magari provate con qualche altro formato. Potremmo creare, qui a MB, un insieme di classi per la lettura dei più comuni formati, e non solo. Il prossimo mese faremo qualcosa di forse più divertente. Vedremo come si realizzano dei filtri di immagini creando dei filtri divertenti.