Klasa Bitmap dostarcza dwie proste metody: GetPixel i SetPixel służące odpowiednio do pobierania koloru punktu obrazu (jako struktury Color) oraz ustawienia punktu obrazu. Poniższy kod ilustruje sposób pobrania/ustawiania wszystkich pikseli bitmapy:
private void GetSetPixel(Bitmap image) {
for (int x = 0; x < image.Width; x++) {
for (int y = 0; y < image.Height; y++) {
Color pixel = image.GetPixel(x, y);
image.SetPixel(x, y, Color.Black);
}
}
}
Jak widać przeglądnięcie i modyfikacja pikseli jest niezwykle prosta. Niestety za prostotą kodu kryje się poważna pułapka wydajnościowa. O ile dla niewielkiej ilości odwołań do punktów obrazu, prędkość z jaką działają metody GetPixel i SetPixel jest zadowalająca, o tyle dla większych rozmiarów obrazu jest ona zdecydowanie za mała. Za dowód może posłużyć wykres z wynikami 10 testów*, które polegały na 10-krotnym wywołaniu w/w metody GetSetPixel na obrazach 100x100 i 1000x1000 pikseli:
Średni czas testu dla obrazu o wymiarach 100 na 100 pikseli wyniósł 543 milisekundy. Taka wydajność jest możliwa do zaakceptowania o ile przetwarzanie obrazu nie będzie wykonywane często. Problem wydajnościowy jest natomiast jasno widoczny przy próbie obsługi obrazu o rozmiarach 1000 na 1000 pikseli. Wykonanie testu zabiera w tym przypadku średnio ponad 41 sekund – ponad 4 sek. na jedno wywołanie GetSetPixel (sic!) .
Dlaczego tak wolno?
Niska wydajność spowodowana jest tym, że dostęp do piksela nie jest prostym odwołaniem do obszaru pamięci. Każde pobranie lub ustawienie koloru wiąże się z wywołaniem metody .NET Framework, będącej oprawą dla natywnej funkcji zawartej w bibliotece gdiplus.dll. Wywołanie to następuje za pomocą mechanizmu P/Invoke (Platform Invocation), który służy do komunikacji kodu zarządzanego z API niezarządzanym (API z poza .NET Framework). Tak więc np. dla bitmapy o rozmiarze 1000x1000 pikseli dojdzie do miliona wywołań metody GetPixel, która prócz walidacji parametrów korzysta z funkcji natywnej GdipBitmapGetPixel. Metoda z API GDI+ musi z kolei przed zwróceniem informacji o kolorze wykonać takie operację jak np. wyliczenie położenia bajtów odpowiedzialnych za opis szukanego piksela... Sytuacja analogiczna zachodzi w przypadku metody SetPixel.
Spójrz na poniższy kod metody Bitmap.GetPixel uzyskany dzięki .NET Reflector (System.Drawing.dll, .NET Framework 2.0):
public Color GetPixel(int x, int y) {
int argb = 0;
if ((x < 0) || (x >= base.Width)) {
throw new ArgumentOutOfRangeException(“x”, SR.GetString(“ValidRangeX”));
}
if ((y < 0) || (y >= base.Height)) {
throw new ArgumentOutOfRangeException(“y”, SR.GetString(“ValidRangeY”));
}
int status = SafeNativeMethods.Gdip.GdipBitmapGetPixel(new HandleRef(this, base.nativeImage), x, y, out argb);
if (status != 0) {
throw SafeNativeMethods.Gdip.StatusException(status);
}
return Color.FromArgb(argb);
}
A oto import funkcji z GDI+:
[DllImport(“gdiplus.dll”, CharSet=CharSet.Unicode, SetLastError=true,
ExactSpelling=true)]
internal static extern int GdipBitmapGetPixel(HandleRef bitmap, int x, int y, out int argb);
Teraz już wiesz dlaczego masowe użycie Get/SetPixel jest takie powolne. Na szczęście istnieją inne (dużo szybsze) sposoby obsługi pikseli z poziomu .NET. Przy pewnym wysiłku można napisać kod, który szybciej obsłuży obraz megapikselowy niż prymitywna metoda poradzi sobie z bitmapą 100x100!. Ale o tym, gdy znajdę nieco czasu... ;)
Aktualizacja 2013-11-07: Napisałem artykuł o szybkich operacjach pikselowych. Koniec z ślamazarnym Get
/SetPixel
:)
Aktualizacja 2018-01-08: Jeśli zależy Ci na naprawdę wydajnym przetwarzaniu obrazu powinieneś sięgnąć po wyspecjalizowaną bibliotekę, taką jak OpenCV. Kilka miesięcy temu napisałem serie postów "Detecting a Drone - OpenCV in .NET for Beginners (Emgu CV 3.2, Visual Studio 2017)", które pomogą Ci zacząć...
* Testowałem na takim laptopie: HP Pavilion dv5, procesor AMD Turion X2 Dual-Core Mobile RM-70, 3 GB RAM, Vista Home Premium