MokaByte
Numero 06 - Marzo 1997
|
|||
|
delle immagini in Java |
||
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:
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.
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.
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.
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:
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.
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);
}
Qui di seguito abbiamo i metodi che permettono di leggere i dati in formato little-endian.// 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--;
}
}
}
}
}
protected static int sistemaShort(int i) {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.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);
}
|
||
|
||
MokaByte ricerca
nuovi collaboratori
|
||
|