Bitmapa o bezpośrednim dostępie do pikseli w C#

05.03.2009 @ 16:15:30 by Rafał Kozik | C# grafika

Kiedy w C# chcemy zmodyfikować zawartość bitmapy, możemy zrobić to na kilka sposobów. Pierwszym jest użycie metod GetPixel i SetPixel. Niestety ich wydajność pozostawia wiele do życzenia.

W większości tutoriali opisujących przetwarzanie bitmap, używa się metody Lock, aby dostać się do danych pikseli. Niestety metoda ta narzuca użycie unsafe w naszym kodzie, a poza tym brzydko wygląda.

Okazuje się, że jest jeszcze trzecia metoda, która jest rzadko opisywana -- stworzyć bitmapę na wcześniej przydzielonym obszarze pamięci. Dzięki temu mamy od razu dostęp do jej danych, bez potrzeby wcześniejszego jej blokowania. Taką bitmapę można używać normalnie do rysowania, można też utworzyć powiązany z nią Graphics, żeby po niej rysować. Ważne jest, żebyśmy zablokowali pamięć, której będzie używała taka bitmapa, żeby Garbage Collector jej nie przemieścił.

W dalszej części znajduje się przykładowy kod, który robi prostą animację na takiej bitmapie o bezpośrednim dostępie.


Obrazek


Skompilowaną wersję można pobrać tutaj.
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

using System.Drawing;
using System.Drawing.Imaging;

namespace DirectBitmap
{
    public class TestForm : Form
    {
        static void Main()
        {
            Application.Run(new TestForm());
        }

        Timer timer;
        DirectBitmap directBitmap;
        int nFrame = 0;

        public TestForm()
        {
            Text = "Direct bitmap test";
            ClientSize = new Size(256, 256);
            DoubleBuffered = true;            

            Paint += new PaintEventHandler(TestForm_Paint);
            FormClosed += new FormClosedEventHandler(TestForm_FormClosed);

            timer = new Timer();
            timer.Interval = 1000 / 30;
            timer.Enabled = true;
            timer.Tick += new EventHandler(timer_Tick);

            directBitmap = new DirectBitmap(256, 256);
        }

        void TestForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            // dobrze jest po sobie sprzątnąć, nie wiadomo kiedy GC to zrobi
            directBitmap.Dispose();
        }

        void Animate()
        {
            double X, Y;

            int r = (int)(192 + Math.Sin(nFrame / 3.0) * 64);
            int g = (int)(192 + Math.Sin(nFrame / 5.0) * 64);
            int b = (int)(192 + Math.Sin(nFrame / 7.0) * 64);

            for (int y = 0; y < 256; y++)
                for (int x = 0; x < 256; x++)
                {
                    X = Math.Sin((x + Math.Sin((y + nFrame) / 16.0) * 8) / 8);
                    Y = Math.Cos((y + Math.Cos((x + nFrame) / 16.0) * 8) / 8);

                    double s = (X + Y + 2) * 0.25;

                    directBitmap[x, y, 0] = (byte)(s * r);
                    directBitmap[x, y, 1] = (byte)(s * g);
                    directBitmap[x, y, 2] = (byte)(s * b);
                }
        }

        void timer_Tick(object sender, EventArgs e)
        {
            Animate();
            Invalidate();
            nFrame++;
        }

        void TestForm_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(directBitmap.Bitmap, 0, 0);
        }
    }

    class DirectBitmap : IDisposable
    {
        byte[] data;
        GCHandle dataHandle;
        Bitmap bitmap;
        int width;
        int height;

        public DirectBitmap(int width, int height)
        {
            data = new byte[width * height * 3];
            dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);

            // rozmiar wiersza bitmapy w bajtach
            int stride = width * 3;

            bitmap = new Bitmap(width, height, stride, 
                PixelFormat.Format24bppRgb, dataHandle.AddrOfPinnedObject());

            this.width = width;
            this.height = height;
        }

        void FreeHandle()
        {
            if (data == null) return;

            dataHandle.Free();
            data = null;
        }

        ~DirectBitmap()
        {
            FreeHandle();
        }

        public void Dispose()
        {
            GC.SuppressFinalize(this);
            FreeHandle();
        }

        public byte this[int x, int y, int component]
        {
            get
            {
                int index = (x + y * width) * 3 + component;
                return data[index];
            }

            set
            {
                int index = (x + y * width) * 3 + component;
                data[index] = value;
            }
        }

        public Bitmap Bitmap
        {
            get
            {
                return bitmap;
            }
        }

        public byte[] Data
        {
            get
            {
                return data;
            }
        }
    }
}

Komentarze

2009-03-08 @ 22:56:58

Po co unsafe? Jest metoda Bitmap.LockBits (http://msdn.microsoft.com/en-us/library/5ey6h79d.aspx) która zwraca nam kawałek pamięci. Działa to dosyć sprawnie. :)
2009-03-08 @ 23:08:37

Wiem, że jest taka metoda, ale dostajesz tylko IntPtr. Próba rzutowania na wskaźnik bez unsafe kończy się takim ładnym błędem:

Pointers and fixed size buffers may only be used in an unsafe context


Chyba, że jest jeszcze coś o czym nie wiem ;) (no chyba, że chcemy kopiować te dane za pomocą Marshal)
MSM
2010-03-11 @ 16:30:51

Sposób jest ŚWIETNY, po prostu powala na kolana ;). Tylko jest jakiś sposób żeby w wielkości bitmapy móc używać liczb innych niż wielokrotności dwójki? :/
2010-04-11 @ 19:13:46

Zapomniałem o tym wspomnieć. Wartość parametru stride przekazywana do konstruktora Bitmap powinna być podzielna przez 4. Tak więc przy tworzenia bitmapy potrzeba czegoś w stylu:
stride = width * 3;
if (stride % 4 != 0) stride += 4 - (stride % 4);
data = new byte[stride * height];


Potrzeba też zmienić indekser, żeby odpowiednio pobierał i ustawiał dane:

index = 3 * x + y * stride + component;
Komentowanie zostało tymczasowo wyłączone.