morzel.net

.net, js, html, arduino, java... no rants or clickbaits.

Dlaczego użycie GetPixel i SetPixel jest tak bardzo nieefektywne?

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:

Wyniki testów prędkości operacji na pikselach obrazu z użyciem metod GetPixel i SetPixel klasy Bitmap.

Ś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... ;)

* Testowałem na takim laptopie: HP Pavilion dv5, procesor AMD Turion X2 Dual-Core Mobile RM-70, 3 GB RAM, Vista Home Premium