Accesso ottimizzato alle Bitmap
Categories: .NET, C, Source Code
Tags:
Se bisogna fare elaborazione delle immagini in c#, il metodo più immediato per un programmatore di accedere ai pixel di una Bitmap è quello di utilizzare i metodi SetPixel() e GetPixel() forniti dal .NET. Purtroppo il metodo più veloce per il programmatore, spesso non corrisponde al metodo più veloce per il programma: considerando che queste due funzioni sono MOLTO lente, se il vostro scopo è quello di analizzare tutti i pixel di un immagine o, ancora peggio, di fare elaborazione di tante immagini in sequenza, è fortemente sconsigliato utilizzarle. Per fortuna il c# è un linguaggio molto flessibile, che permette di fare cose ad altissimo livello ma che comunque ci lascia sempre la possibilità di scendere a basso livello quando è necessario.
In questo articolo per semplicità tratterò soltanto le immagini a 8bpp (8 bit per pixel), ovvero a 256 colori.
Per riuscire a lavorare con i dati di un immagine in maniera veloce la soluzione è solo una: utilizzare il puntatore ai dati dell’immagine! Lavorare con i puntatori ad immagini è un pò come lavorare con i puntatori a char (le stringhe in c++): Il puntatore punterà al primo indirizzo di memoria dei dati della bitmap, dove sarà contenuto il primo pixel. Incrementando il puntatore andremmo ottenendo via via i valori degli altri pixel.

Uno dei grandi vantaggi di utilizzare puntatori quando si hanno strutture dati molto grandi sta nel fatto che il processore non è costretto a fare operazioni matematiche per leggere il prossimo elemento. Mi spiego meglio con un esempio:
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
byte b = bmp.GetPixel(x,y);
}
}
Il metodo GetPixel per ritornare il pixel di posizione x,y prenderà il puntatore all’immagine e sommerà (width * y) + x.
Essendo la nostra istruzione all’interno di un doppio ciclo for in cui scorriamo tutti i pixel dell’immagine, se la nostra immagine è per esempio di 1024*768 pixel, il precedente esempio farà più di 3 milioni di operazioni inutili tra moltiplicazioni e somme per calcolare ogni volta l’offset all’interno della bitmap. Diventa ben chiaro che non stiamo più parlando di numerini, ma stiamo parlando di cifre in grado di mettere in ginocchio anche i processori più moderni, e quindi bisogna sempre avere un occhio di riguardo all’ottimizzazione se si sta lavorando sulle immagini.
Un’altro fattore da tenere in considerazione è lo stride di un immagine, ovvero il numero di byte occupati da una riga di pixel. E’ importante tenere conto della stride perchè questo non equivale per forza al numero di pixel in larghezza dell’immagine, neanche con le immagini con un byte per pixel: il numero di byte di ogni riga infatti è arrotondato a gruppi di 4 byte, quindi se per esempio la larghezza dell’immagine è 30 pixel, la stride di ogni riga sarà di 32 pixel, e quindi per ogni riga avremmo 2 byte non utilizzati! Se non teniamo conto di questo valore, durante la lettura della bitmap, per ogni riga letta andremo “sfasando” sempre più rispetto l’immagine originale!
Bitmap bmp = new Bitmap("Dzamir.bmp");
E’ la bitmap di cui vogliamo analizzare i pixel.
Attraverso il codice:
BitmapData bmpData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); System.IntPtr bmpScan0 = bmpData.Scan0;
Accediamo al puntatore dell’immagine. Il metodo LockBits() serve per accedere ai dati dell’immagine, e ci ritorna un BitmapData, mentre il metodo Scan0 ritorna appunto il puntatore alla prima riga di scansione dell’immagine.
Adesso è necessario iniziare un blocco di codice unsafe, per avvisare al c# che stiamo lavorando con i puntatori.
unsafe
{
// inserire qui il codice pericoloso
}
All’interno del blocco di codice unsafe scriviamo:
byte * bmpPtr = (byte *)(void *)bmpScan0; int nOffset = stride - bData.Width;
Nella variabile nOffset abbiamo salvato la differenza tra lo stride e il numero dei pixel nella riga, in modo da sommare questo valore
alla fine della lettura di ogni riga ed evitare problemi con gli stride. Adesso possiamo quindi inserire il doppio ciclo for per ciclare tutti i pixel dell’immagine:
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// Accedo al pixel e incremento il puntatore
byte pixelXY = *(bmpPtr++);
}
bmpPtr += nOffset;
}
Come si vede, adesso il codice risulta più elegante e anche molto più ottimizzato! Ovviamente in questo caso non fa assolutamente niente, ma questo pezzo di codice può benissimo essere utilizzato come stub da cui creare altre funzioni per l’elaborazione delle immagini digitali.
Alla fine di tutto non bisogna scordarsi comunque di chiamare il metodo:
bmp.UnlockBits(bData);
Per sbloccare l’oggetto Bitmap dalla memoria di sistema.
Se sei interessato a questo post, potresti anche provare a leggere:
-
No related posts
13 Feb 2007 dzamir
Ciao complimenti per il sito ben fatto e per gli articoli. Anche io ho un blog realizzato con wordpress sulla programmazione ( http://www.loige.altervista.org ). Ho avuto il tuo stesso problema col codice. Poi l’ ho risolto installando un plugin che permette di formattare e colorare il codice in base al linguaggio di programmazione che si sta utilizzando. Sul mio sito nella sezione “guida” trovi qualche informazione su come usarlo. il plugin lo trovi qui : http://www.regex.ru/ (il sito è pero’ in russo e spesso è anche offline ) se non riesci a trovare il file prova a cercare geshi syntax colorer sui siti di plugin per wordpress. in alternativa contattami via e-mail che te lo invio!
ciao ciao ed in bocca al lupo per il blog 
Grazie per la “dritta” che mi ha permesso di uscire da una scomoda empasse.
Volevo segnalare due bachi presenti nel codice proposto.
1)
L’Offset deve essere calcolato come int nOffset = bData.stride - (bData.Width * numerodibytedelformato): infatti la singola “riga” è il numero di pixel in larghezza per il umero di byte che compongono il pixel (se a 24 bit sono 3 byte, se a 32 bit sono 4 byte, …)
2)
L’Offset è da applicare sulla singola riga, quindi il codice corretto prevede un ciclo esterno su Y e un ciclo interno su X:
for (int y = 0; y
1)
Infatti il post è stato scritto esplicitamente per le immagini a 8 bpp. Volevo scrivere un altro post più generico ma poi non ho avuto il tempo…
2)
Hai ragione… comunque alla fine funziona lo stesso, semplicemente i nomi delle variabili x e y erano invertite.
Grazie per le correzioni!
per caso c’e un buchetto anche dentro al ciclo for?
byte pixelXY = bmpPtr++;
non dovrebbe essere
byte * pixelXY = bmpPtr++;
cioè come quell di prima ma con * prima di pixelXY?
@muccio:
Grazie per la segnalazione dell’errore!
Il codice che c’era prima nell’articolo era errato, ma la soluzione corretta non è quella che dici tu, in cui prendi il puntatore al pixel, bensì:
byte pixelXY = *(bmpPtr++);
In questo modo si prende il valore del pixel corrente.
ciao!
sto lavorando con le immagini a 8 bit, e devo fare il cambio di tonalita di colore(chiarire o scurire) una porzione
( o tutta l’immagine) riporto il codice che uso ma non fa esattamente quello che desidero; questo codice funziona
con le bitmap e 24bit per pixel ma non con quelle a 8bit per pixel
int width = bitmap.Width;
int height = bitmap.Height;
//BitmapData bmpData = this.bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
BitmapData bmpData = this.bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, this.bitmap.PixelFormat );
unsafe
{
double newColor, color, color1;
byte* newPixel = (byte*)(void*)bmpData.Scan0;//ora newPixel punta al primo px dell’immagine
//newPixel += ((intYPartenza*1) * width) + intXPartenza*1;//sposto il puntatore sul punto in cui l’utente vuole efettuare il cambimento di tonalita
int scostamento = bytesPerPixel(bitmap.PixelFormat);//ottengo lo scostamento di cui mi devo spostare in base al formato dell’immagine (24bbp , 8bpp….)
int intOffset = bmpData.Stride - ( bmpData.Width * scostamento );
int posizionePuntatore = (intYPartenza * scostamento * width) + (intXPartenza * scostamento) + (intYPartenza * intOffset);
newPixel += posizionePuntatore;
for (int i = 0; i 255)//controllo per il range di valori assegnabili
newColor = 255;
else
{
if (newColor
@muccio: mi sa che wordpress ti ha troncato il codice, puoi provare a mandarmelo via mail (trovi l’email nella pagina info).