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.
Puntatore a bitmap
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