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

by Miłosz Orzeł 5. April 2011 23:28

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

Badanie różnicy kolorów

by Miłosz Orzeł 8. December 2008 00:28

W .NET Framework do opisu koloru wykorzystywana jest struktura Color. Jej najważniejsze właściwości to: R, G oraz B. Odpowiadają one za intensywność barw podstawowych (czerwonej, zielonej i niebieskiej).* Właściwości te przyjmują wartości z zakresu od 0 do 255. Trzy bajty użyte do opisu składowych dają możliwość stworzenia ponad 16 milionów barw (2^24 = 16 777 216)!

RGB(0, 0, 0) to kolor czarny, (255, 255, 255) to kolor biały, (255, 0, 0) to czerwony a np. (255, 255, 0) to żółty... Struktura Color posiada również właściowść A (kanał alfa), która jest wykorzystywana w grafice komputerowej do określania stopnia przeźroczystości. Wartość 255 oznacza pełną nieprzeźroczystość.

Do zbadania różnicy między kolorami można posłużyć się odległością euklidesową (dystans między punktami). Wyobraź sobie trójwymiarowy układ współrzędnych, których osie oznaczymy odpowiednio przez R, G oraz B (składową A pomijamy). Gdy umieścisz w takim układzie dwa punkty odpowiadające kolorom i zmierzysz odległość między nimi, otrzymasz informację o stopniu podobieństwa kolorów. Dzięki temu będziesz mógł np. wykryć na obrazie wszystkie miejsca mające odcień zieleni...

Color c1 =  Color.White;
Color c2 = Color.Black;

int odlegloscR = (c1.R - c2.R) * (c1.R - c2.R);
int odlegloscG = (c1.G - c2.G) * (c1.G - c2.G);
int odlegloscB = (c1.B - c2.B) * (c1.B - c2.B);

double roznicaKoloru = Math.Sqrt(odlegloscR + odlegloscG
 + odlegloscB);

W powyższym kodzie celowo nie korzystałem z funkcji Math.Pow. Jej zastosowanie przy takiej operacji jak podnoszenie do potęgi całkowitej to marnotrastwo CPU (gdy trzeba przebadać wiele punktów ma to znaczenie). Jeśli wyliczenie zmiennej roznicaKoloru ma słyżyć jedynie porównywaniu barw można by również zrezygnować z pierwiastkowania...

* Model RGB opisuje zjawisko syntezy addytywnej czyli mieszanie się swiatła emitowanego (np. przez monitor).

L-systemy. Tworzenie form roślinnych z wykorzystaniem System.Drawing.

by Miłosz Orzeł 1. June 2007 23:51

Artykuł w prosty sposób opisuje ciekawe zganienie algorytmu wzrostu. Czyni to na przykładzie tworzenia realistycznie wyglądających form roślinnych, rysowanych w oparciu o klasy z przestrzeni nazw System.Drawing...

Wersja: 1.1 (06.2007, poprawki 07.2007)
Poziom trudności: średniozaawansowany
Uwagi: Artykuł ten pisałem w ramach nauki .NET Framework jako pracę uczestniczącą w konkursie portalu CodeGuru.pl (tekst zajął pierwsze miejsce w czerwcu 2007 r.).

Artykuł w PDF (v1.1 - 07.2007):
http://morzel.net/download/lsystemy_art.pdf

Artykuł na CodeGuru.pl (v1.0 - 06.2007):
http://www.codeguru.pl/article-703.aspx

Załączony kod (C#, VS 2005):
http://morzel.net/download/lsystemy_src.zip

Po co?

Nie wyobrażam sobie pracy programisty bez setek stron, na których ludzie „mar- nując” swój wolny czas dzielą się tym czego udało im się dowiedzieć. Spróbuję zatem sam (w miarę swoich możliwości) dodać nieco pożytecznych informacji do zasobów Sieci... - o mnie

Language

Click here to see English version.