sobota, 6 marca 2010

Praca z Bitmapami w WPF

Dzisiaj trochę informacji, jak w WPF zabrać się za obróbkę zdjęć i ogólne aspekty związane z kontrolką Image.

1. Wczytywanie obrazka do kontrolki Image:
Zadanie to można wykonać na kilka sposobów: strumień, pobranie z żądanej lokalizacji sieciowej lub ścieżka. Najprostszą i najczęściej stosowaną opcją jest wczytanie ze ścieżki:


                        Image image = new Image();
                        BitmapImage bitmapImage = new BitmapImage(new Uri("c:\obraz.png"));
                        image.Source = bitmapImage;

2. Zapisywanie zawartości kontrolki Image do pliku.
Tutaj niestety bez pomocy strumienia się nie obejdzie. 
                SaveFileDialog saveFileDialog = new SaveFileDialog();
                saveFileDialog.Filter = "Pliki BMP | *.bmp | Pliki PNG | *.png";
                if (saveFileDialog.ShowDialog(this) == true)
                {
                    FileStream saveStream = new FileStream(saveFileDialog.FileName, FileMode.OpenOrCreate);
                    BmpBitmapEncoder encoder = new BmpBitmapEncoder();
                    encoder.Frames.Add(BitmapFrame.Create(Image.Image));
                    encoder.Save(saveStream);
                    saveStream.Close();
                }

Kod trochę bardziej skomplikowany od tego znanego w WindowsForms, ale działa na takiej samej zasadzie :) Oczywiście jeżeli chcemy zapisać obrazek w innym formacie, możemy skorzystać z innego enkodera.

3. Dopasowanie rozmiarów obiektu Image do wczytywanego obrazka

                    Bitmap = new BitmapImage(new Uri(openFileDialog.FileName));

                    Image.Width = Bitmap.Width;
                    Image.Height = Bitmap.Height;

4. Konwersja bitmap
W WinForms istniała dziecinnie prosta możliwość zmiany wartości pojedyńczych pixeli na naszym obrazku.
Dla zobrazowania tego co chcemy zrobić przypuśćmy scenariusz w którym chcielibyśmy zmienić skalę kolorów na szare. W WinForms można by to zrobić w taki sposób:

            Stopwatch clock = new Stopwatch();
            clock.Start();
            Bitmap img = new Bitmap(Image.FromFile(@"c:\o.png"));
            int rows = img.Width;
            int cols = img.Height;
            Color c;
            int b;
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < cols; j++)
                {
                    c = img.GetPixel(i, j);
                    b = (c.R + c.G + c.B) / 3;
                    img.SetPixel(i, j, Color.FromArgb(b, b, b));
                }
            }
            pictureBox1.Image = img;
            clock.Stop();
            MessageBox.Show(clock.ElapsedMilliseconds.ToString());

Jak widać dołożyłem do kodu Stopwatch aby sprawdzić ile trwa konwersja do skali szarości w tym rozwiązaniu. Podczas konwersji wykorzystuje bitmapę o wielkości 512 x 512:

Jak widać średni czas konwersji takiego imaga zajmuje > 600 ms. Nie jest to zadowalający czas. Należy też wziąć pod uwagę fakt, że zdjęcia w większości przypadków są większej rozdzielczości. Nasz sposób sprawia, że wraz ze zwiększaniem rozdzielczości będzie rósł czas konwersji. Dzieje się tak z powodu powolnego działania funkcji GetPixel i SetPixel. Istnieje kilka rozwiązań tego problemu. Można skorzystać z kodu unsafe lub bardzo rozbudowanego mechanizmu oferowanego przez klasę ColorMatrix.

W WPF niestety (albo na szczęście) nie istnieją metody GetPixel i SetPixel.Aby zmienić wartość pojedynczego pixela należy użyć klasy WriteableBitmap, która posiada dwie metody: CopyPixels i WritePixels. Pierwsza z nich kopiuje wartości pixeli z Bitmapy do tablicy a druga nadpisuje wartości pixeli w Bitmapie.Zwrócić należy tutaj na słowo wartości pixeli. Otóż każdy pixel jest reprezentowany przez 3 składowe RGB i A (alpha - przeźroczystość). Czyli dla przykładu pierwszy pixel będzie zapisany na pierwszych 4 pozycjach naszej tablicy, drugi na kolejnych 4 itd.
Zobaczmy na przykład:

            BitmapImage image = new BitmapImage(new Uri(@"C:\Users\Public\Pictures\Sample Pictures\Desert.jpg"));
            width = (int)image.Width;
            height = (int)image.Height;
            _bitmap = new WriteableBitmap(image);
            stride = width * ((this._bitmap.Format.BitsPerPixel + 7) / 8);
            int arraySize = stride * height;
            byte[] pixels = new byte[arraySize];
            _bitmap.CopyPixels(pixels, stride, 0);
            int color = 0;
            int j = 0;
            for (int i = 0; i < pixels.Length / 4; ++i)
            {
                color = (pixels[j] + pixels[j + 1] + pixels[j + 2]) / 3;
                pixels[j] = (byte)color;
                pixels[j + 1] = (byte)color;
                pixels[j + 2] = (byte)color;
                pixels[j + 3] = 255;
                j += 4;
            }

            Int32Rect rect = new Int32Rect(0, 0, width, height);
            _bitmap.WritePixels(rect, pixels, stride, 0);
            Image.Source = _bitmap;

Kod może trochę bardziej rozbudowany od znanego z WinForms ale za to działający znacznie szybciej. O ile szybciej? O tym przekonamy się podczas tego testu:
Jak widać czas wynosi około 42 ms co jest dużo lepszym rezultatem. Kolejne czasy na poziomie 9 - 13 ms są wynikiem używania przez WPF Intelligent Redrawing (więcej o tym narzędziu można poczytać na stronach msdn).
 Jeżeli mowa o zamianie na skalę szarości nie można pominąć tego co sam framework oferuje. Dzięki klasie FormatConvertedBitmap mamy możliwość konwersji obrazka na dowolną skalę. Przykład:



            BitmapImage image = new BitmapImage(new Uri(@"C:\Users\Public\Pictures\Sample Pictures\Desert.jpg"));
            FormatConvertedBitmap format = new FormatConvertedBitmap();
            format.BeginInit();
            format.Source = image;
            format.DestinationFormat = PixelFormats.Gray32Float;
            format.EndInit();
            Image.Source = format;

Kod jak widać bardzo prosty, ciekawe jak z wydajnością?:

Wydajność całkiem niezła, czas konwersji zmniejszył się ponad 4 krotnie. A kolejne przemalowania nie zabierają praktycznie żadnego czasu (jedynie kiedy badamy takty procesora można wychwycić jakieś zmiany). 
Oczywiście naszą metodę można przyśpieszyć poprzez użycie wskaźników w trybie unsafe.

Brak komentarzy:

Prześlij komentarz