MokaByte Numero 06 - Marzo 1997
Introduzione alla elaborazione 
delle immagini in Java
 
di
Massimo Carli
Quarta puntata

 



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:

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.

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.
 
  
 

MokaByte rivista web su Java

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