Miłosz Orzeł

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

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

Komentarze (5) -

  • Janusz Gwóźdź

    2011-11-12 12:32:43 | Odpowiedź

    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.

    • morzel

      2011-11-12 16:06:25 | Odpowiedź

      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.

      • Janusz Gwóźdź

        2011-11-12 17:29:59 | Odpowiedź

        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ę.

        • morzel

          2011-11-12 17:47:28 | Odpowiedź

          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

  • Janusz Gwóźdź

    2011-11-13 22:07:36 | Odpowiedź

    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.

Dodaj komentarz

Loading