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

Komentarze (5) -

Janusz Gw&#243;źdź
Janusz Gwóźdź Poland
11/12/2011 12:32:43 PM #

Witam!
W poście powyżej autor wspomniał o innych sposobach obsługi pixeli. Chciałbym prosić o przybilżenie tematu. Stosowanie SetPixel/ GetPixel do obsługi bitmap o rozmiarze większym niż 100x100 to istny koszmar.
Pozdrawiam i dziękuję za pomoc.

Reply

morzel
morzel Poland
11/12/2011 4:06:25 PM #

Gigantyczny zysk wydajnościowy przy dostępie do pojedynczych pikseli, można uzyskać stosując blok kodu unsafe i operacje wskaźnikowe na buforze pamięciowym, pozyskanym dzięki metodzie LockBits klasy Bitmap. W przypadku niemożliwości użycia kodu unsafe można poratować się klasą System.Runtime.InteropServices.Marshal...

Pytanie tylko czy na pewno potrzebujesz dostępu do pojedynczego piksela? Mnóstwo operacji, takich jak np. zmiana jasności można wykonać w prosty i szybki sposób dzięki tzw. macierzom koloru. Macierze takie są zaimplementowane w .NET: System.Drawing.Imaging.ColorMatrix.

Reply

Janusz Gw&#243;źdź
Janusz Gwóźdź Poland
11/12/2011 5:29:59 PM #

Dziękuję za szybką odpowiedź...
Zdecydowanie potrzebuję dostępu do pojedynczego piksela ponieważ to co potrzebuję zrobić z bitmapą to "wyłuskać" z każdego jej piksela zawartość składowej RGB, następnie przetransponować do postaci 16-to bitowej (RGB 565) i wysłać do mojego urządzenia aby tam go wyświetlić.

Jeszcze raz dziękuję za pomoc. Jestem bardziej elektronikiem niż programistą, a o takim cudzie jak blok UNSAFE pierwsze słyszę.

Reply

morzel
morzel Poland
11/12/2011 5:47:28 PM #

Generalnie automatyczne zarządzanie pamięcią to świetny wynalazek, ale czasem nawet w .NETcie warto pobawić się danymi w pamięci "na żywca"...

Tutaj jest kawałek kodu z mojej pracy dyplomowej, pokazujący pobieranie i zmianę składowych piksela:

private void Locked() {
   BitmapData imageData = image.LockBits(new Rectangle(0, 0, image.Width,
   image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
   int bytesForPixel = 3;
  
   unsafe {
      for (int x = 0; x < imageData.Width; x++) {
         for (int y = 0; y < imageData.Height; y++) {
            byte* row = (byte*)imageData.Scan0 + (y * imageData.Stride);

            int pixelB = row[x * bytesForPixel];
            int pixelG = row[x * bytesForPixel + 1];
            int pixelR = row[x * bytesForPixel + 2];

            row[x * bytesForPixel] = 0;
            row[x * bytesForPixel + 1] = 0;
            row[x * bytesForPixel + 2] = 0;
         }
      }
   }

   image.UnlockBits(imageData);
}

Powinien się przydać Smile

Reply

Janusz Gw&#243;źdź
Janusz Gwóźdź Poland
11/13/2011 10:07:36 PM #

Ja z racji tego, że korzystam z Visual Basica rozwiązałem problem nieco inaczej, ale idea pozostała taka jak w Twoim poście:

    Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click

        ' Create a new bitmap.
        Dim bmp As New Bitmap(PictureBox1.Image)

        ' Lock the bitmap's bits.  
        Dim rect As New Rectangle(0, 0, bmp.Width, bmp.Height)
        Dim bmpData As System.Drawing.Imaging.BitmapData = bmp.LockBits(rect, _
            Drawing.Imaging.ImageLockMode.ReadWrite, bmp.PixelFormat)

        ' Get the address of the first line.
        Dim ptr As IntPtr = bmpData.Scan0

        ' Declare an array to hold the bytes of the bitmap.
        ' This code is specific to a bitmap with 24 bits per pixels.
        Dim bytes As Integer = Math.Abs(bmpData.Stride) * bmp.Height
        Dim rgbValues(bytes - 1) As Byte

        Label1.Text = "Analiza"
        Me.Refresh()
        ' Copy the RGB values into the array.
        System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes)
        '0 - Blue    1 - Green     2 - Red
        For counter As Integer = 0 To rgbValues.Length - 1 Step 4
            ' Debug.Print(rgbValues(counter + 2) & " " & rgbValues(counter + 1) & " " & rgbValues(counter))
            Call konwertuj(rgbValues(counter + 2), rgbValues(counter + 1), rgbValues(counter))
            y = counter \ ((bmp.Width) * 4)
            x = ((counter / 4) - (y * bmp.Width))
            ProgressBar1.Value = y
        Next
        Label1.Text = "Zapis do pliku"
        Me.Refresh()
        Call zapis_do_pliku()

        bmp.UnlockBits(bmpData)
        Label1.Text = "gotowe"

    End Sub

Dziękuję za pomoc.. Prędkość którą uzyskałem jest dla mnie wystarczająca.

Reply

Dodaj komentarz




  Country flag
biuquote
  • Komentarz
  • Podgląd
Loading


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.