Udostępnij za pośrednictwem


Uzyskiwanie dostępu do bitów pikseli mapy bitowej SkiaSharp

Jak pokazano w artykule Zapisywanie map bitowych SkiaSharp do plików, mapy bitowe są zwykle przechowywane w plikach w formacie skompresowanym, takim jak JPEG lub PNG. W constrast mapa bitowa SkiaSharp przechowywana w pamięci nie jest kompresowana. Jest on przechowywany jako sekwencyjny szereg pikseli. Ten nieskompresowany format ułatwia transfer map bitowych na powierzchnię wyświetlania.

Blok pamięci zajmowany przez mapę bitową SkiaSharp jest zorganizowany w bardzo prosty sposób: zaczyna się od pierwszego wiersza pikseli, od lewego do prawej, a następnie kontynuuje drugi wiersz. W przypadku map bitowych w pełnym kolorze każdy piksel składa się z czterech bajtów, co oznacza, że łączna ilość miejsca na pamięć wymagana przez mapę bitową jest czterokrotnie krotnie częścią jego szerokości i wysokości.

W tym artykule opisano, jak aplikacja może uzyskać dostęp do tych pikseli, bezpośrednio przez uzyskanie dostępu do bloku pamięci pikseli mapy bitowej lub pośrednio. W niektórych przypadkach program może chcieć przeanalizować piksele obrazu i utworzyć histogram pewnego rodzaju. Częściej aplikacje mogą tworzyć unikatowe obrazy przez algorytmicznie tworzące piksele tworzące mapę bitową:

Przykłady bitów pikseli

Techniki

SkiaSharp udostępnia kilka technik uzyskiwania dostępu do bitów pikseli mapy bitowej. Który z nich wybierasz jest zwykle kompromisem między wygodą kodowania (co jest związane z konserwacją i łatwością debugowania) i wydajnością. W większości przypadków użyjesz jednej z następujących metod i właściwości SKBitmap do uzyskiwania dostępu do pikseli mapy bitowej:

  • Metody GetPixel i SetPixel umożliwiają uzyskanie lub ustawienie koloru pojedynczego piksela.
  • Właściwość Pixels uzyskuje tablicę kolorów pikseli dla całej mapy bitowej lub ustawia tablicę kolorów.
  • GetPixels Zwraca adres pamięci pikseli używanej przez mapę bitową.
  • SetPixels Zastępuje adres pamięci pikseli używanej przez mapę bitową.

Dwie pierwsze techniki można traktować jako "wysoki poziom", a drugie jako "niski poziom". Istnieją inne metody i właściwości, których można użyć, ale są one najcenniejsze.

Aby zobaczyć różnice wydajności między tymi technikami, przykładowa aplikacja zawiera stronę o nazwie Mapa bitowa gradientu, która tworzy mapę bitową z pikselami, które łączą czerwone i niebieskie odcienie w celu utworzenia gradientu. Program tworzy osiem różnych kopii tej mapy bitowej, wszystkie przy użyciu różnych technik ustawiania pikseli mapy bitowej. Każda z tych ośmiu map bitowych jest tworzona w oddzielnej metodzie, która ustawia również krótki opis tekstu techniki i oblicza czas wymagany do ustawienia wszystkich pikseli. Każda metoda przechodzi przez logikę ustawienia pikseli 100 razy, aby uzyskać lepsze oszacowanie wydajności.

Metoda SetPixel

Jeśli wystarczy ustawić lub uzyskać kilka pojedynczych pikseli, SetPixel metody i GetPixel są idealne. Dla każdej z tych dwóch metod należy określić kolumnę całkowitą i wiersz. Niezależnie od formatu pikseli te dwie metody umożliwiają uzyskanie lub ustawienie piksela SKColor jako wartości:

bitmap.SetPixel(col, row, color);

SKColor color = bitmap.GetPixel(col, row);

col Argument musi wahać się od 0 do jednej mniejszej niż Width właściwość mapy bitowej i row waha się od 0 do jednej mniejszej Height niż właściwość .

Oto metoda w mapie bitowej gradientu, która ustawia zawartość mapy bitowej przy użyciu SetPixel metody . Mapa bitowa jest 256 o 256 pikseli, a for pętle są zakodowane na stałe z zakresem wartości:

public class GradientBitmapPage : ContentPage
{
    const int REPS = 100;

    Stopwatch stopwatch = new Stopwatch();
    ···
    SKBitmap FillBitmapSetPixel(out string description, out int milliseconds)
    {
        description = "SetPixel";
        SKBitmap bitmap = new SKBitmap(256, 256);

        stopwatch.Restart();

        for (int rep = 0; rep < REPS; rep++)
            for (int row = 0; row < 256; row++)
                for (int col = 0; col < 256; col++)
                {
                    bitmap.SetPixel(col, row, new SKColor((byte)col, 0, (byte)row));
                }

        milliseconds = (int)stopwatch.ElapsedMilliseconds;
        return bitmap;
    }
    ···
}

Kolor ustawiony dla każdego piksela ma czerwony składnik równy kolumnie mapy bitowej, a niebieski składnik jest równy wierszowi. Wynikowa mapa bitowa jest czarna w lewym górnym rogu, czerwona w prawym górnym rogu, niebieska w lewym dolnym rogu i magenta w prawym dolnym rogu, z gradientami gdzie indziej.

Metoda SetPixel jest wywoływana 65 536 razy i niezależnie od tego, jak wydajna może być ta metoda, zazwyczaj nie jest dobrym pomysłem, aby wiele wywołań interfejsu API było dostępnych, jeśli jest dostępna alternatywa. Na szczęście istnieje kilka alternatyw.

Właściwość Pixel

SKBitmapPixels definiuje właściwość zwracającą tablicę SKColor wartości dla całej mapy bitowej. Można również Pixels ustawić tablicę wartości kolorów dla mapy bitowej:

SKColor[] pixels = bitmap.Pixels;

bitmap.Pixels = pixels;

Piksele są rozmieszczane w tablicy, zaczynając od pierwszego wiersza, od lewej do prawej, a następnie drugiego wiersza itd. Całkowita liczba kolorów w tablicy jest równa iloczynowi szerokości i wysokości mapy bitowej.

Mimo że ta właściwość wydaje się wydajna, należy pamiętać, że piksele są kopiowane z mapy bitowej do tablicy, a z tablicy z powrotem do mapy bitowej, a piksele są konwertowane z i na SKColor wartości.

Oto metoda w GradientBitmapPage klasie, która ustawia mapę bitową przy użyciu Pixels właściwości . Metoda przydziela tablicę SKColor wymaganego rozmiaru, ale mogła użyć Pixels właściwości do utworzenia tej tablicy:

SKBitmap FillBitmapPixelsProp(out string description, out int milliseconds)
{
    description = "Pixels property";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    SKColor[] pixels = new SKColor[256 * 256];

    for (int rep = 0; rep < REPS; rep++)
        for (int row = 0; row < 256; row++)
            for (int col = 0; col < 256; col++)
            {
                pixels[256 * row + col] = new SKColor((byte)col, 0, (byte)row);
            }

    bitmap.Pixels = pixels;

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Zwróć uwagę, że indeks tablicy pixels musi być obliczany na podstawie row zmiennych i col . Wiersz jest mnożony przez liczbę pikseli w każdym wierszu (w tym przypadku 256), a następnie dodawana jest kolumna.

SKBitmap Definiuje również podobną Bytes właściwość, która zwraca tablicę bajtów dla całej mapy bitowej, ale jest bardziej uciążliwa dla map bitowych pełnokolorowych.

Wskaźnik GetPixels

Potencjalnie najbardziej zaawansowaną techniką uzyskiwania dostępu do pikseli mapy bitowej jest GetPixels, nie należy mylić z GetPixel metodą lub właściwością Pixels . Natychmiast zauważysz różnicę w GetPixels tym, że zwraca coś, co nie jest bardzo powszechne w programowaniu w języku C#:

IntPtr pixelsAddr = bitmap.GetPixels();

Typ platformy .NET IntPtr reprezentuje wskaźnik. Jest wywoływana IntPtr , ponieważ jest to długość liczby całkowitej na natywnym procesorze maszyny, na której jest uruchamiany program, zazwyczaj 32 bity lub 64 bity długości. Zwracany IntPtrGetPixels jest adres rzeczywistego bloku pamięci używanego przez obiekt mapy bitowej do przechowywania jego pikseli.

Typ wskaźnika języka C# można przekonwertować IntPtr na przy użyciu ToPointer metody . Składnia wskaźnika C# jest taka sama jak C i C++:

byte* ptr = (byte*)pixelsAddr.ToPointer();

Zmienna ptr jest wskaźnikiem bajtów typu. Ta ptr zmienna umożliwia dostęp do poszczególnych bajtów pamięci, które są używane do przechowywania pikseli mapy bitowej. Kod podobny do tego służy do odczytywania bajtu z tej pamięci lub zapisu bajtu w pamięci:

byte pixelComponent = *ptr;

*ptr = pixelComponent;

W tym kontekście gwiazdka jest operatorem pośrednim języka C# i służy do odwołowania się do zawartości pamięci wskazywanej przez ptrelement . ptr Początkowo wskazuje pierwszy bajt pierwszego piksela pierwszego wiersza mapy bitowej, ale można wykonać operację arytmetyczną na ptr zmiennej, aby przenieść ją do innych lokalizacji w mapie bitowej.

Jedną z wad jest to, że tej zmiennej można używać ptr tylko w bloku kodu oznaczonym unsafe słowem kluczowym. Ponadto zestaw musi być oflagowany jako zezwalający na niebezpieczne bloki. Odbywa się to we właściwościach projektu.

Używanie wskaźników w języku C# jest bardzo wydajne, ale także bardzo niebezpieczne. Należy zachować ostrożność, aby nie uzyskiwać dostępu do pamięci poza tym, do czego ma odwoływać się wskaźnik. Dlatego użycie wskaźnika jest skojarzone ze słowem "niebezpiecznym".

Oto metoda w GradientBitmapPage klasie, która używa GetPixels metody . Zwróć uwagę na unsafe blok, który obejmuje cały kod przy użyciu wskaźnika bajtów:

SKBitmap FillBitmapBytePtr(out string description, out int milliseconds)
{
    description = "GetPixels byte ptr";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    IntPtr pixelsAddr = bitmap.GetPixels();

    unsafe
    {
        for (int rep = 0; rep < REPS; rep++)
        {
            byte* ptr = (byte*)pixelsAddr.ToPointer();

            for (int row = 0; row < 256; row++)
                for (int col = 0; col < 256; col++)
                {
                    *ptr++ = (byte)(col);   // red
                    *ptr++ = 0;             // green
                    *ptr++ = (byte)(row);   // blue
                    *ptr++ = 0xFF;          // alpha
                }
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Gdy zmienna ptr zostanie po raz pierwszy uzyskana z ToPointer metody, wskazuje pierwszy bajt lewego piksela pierwszego wiersza mapy bitowej. Pętle for dla row elementu i col są konfigurowane w taki sposób, aby ptr można je było zwiększać za pomocą ++ operatora po ustawieniu każdego bajtu każdego piksela. Dla pozostałych 99 pętli przez piksele ptr należy ustawić z powrotem na początek mapy bitowej.

Każdy piksel to cztery bajty pamięci, więc każdy bajt musi być ustawiony oddzielnie. W tym kodzie przyjęto założenie, że bajty znajdują się w kolejności czerwonej, zielonej, niebieskiej i alfa, która jest zgodna z typem SKColorType.Rgba8888 koloru. Można pamiętać, że jest to domyślny typ koloru dla systemów iOS i Android, ale nie dla platforma uniwersalna systemu Windows. Domyślnie platforma UWP tworzy mapy bitowe o typie SKColorType.Bgra8888 koloru. Z tego powodu spodziewaj się, że na tej platformie będą widoczne różne wyniki.

Istnieje możliwość rzutowania wartości zwracanej z ToPointer do uint wskaźnika, a nie byte wskaźnika. Umożliwia to dostęp do całego piksela w jednej instrukcji. ++ Zastosowanie operatora do tego wskaźnika zwiększa go o cztery bajty, aby wskazać następny piksel:

public class GradientBitmapPage : ContentPage
{
    ···
    SKBitmap FillBitmapUintPtr(out string description, out int milliseconds)
    {
        description = "GetPixels uint ptr";
        SKBitmap bitmap = new SKBitmap(256, 256);

        stopwatch.Restart();

        IntPtr pixelsAddr = bitmap.GetPixels();

        unsafe
        {
            for (int rep = 0; rep < REPS; rep++)
            {
                uint* ptr = (uint*)pixelsAddr.ToPointer();

                for (int row = 0; row < 256; row++)
                    for (int col = 0; col < 256; col++)
                    {
                        *ptr++ = MakePixel((byte)col, 0, (byte)row, 0xFF);
                    }
            }
        }

        milliseconds = (int)stopwatch.ElapsedMilliseconds;
        return bitmap;
    }
    ···
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    uint MakePixel(byte red, byte green, byte blue, byte alpha) =>
            (uint)((alpha << 24) | (blue << 16) | (green << 8) | red);
    ···
}

Piksel jest ustawiany przy użyciu MakePixel metody , która konstruuje piksel całkowity z czerwonych, zielonych, niebieskich i alfa składników. Pamiętaj, że SKColorType.Rgba8888 format ma kolejność bajtów pikseli w następujący sposób:

RR GG BB AA

Jednak liczba całkowita odpowiadająca tym bajtom to:

AABBGGRR

Najmniej znaczący bajt liczby całkowitej jest przechowywany jako pierwszy zgodnie z architekturą mało endianu. Ta MakePixel metoda nie będzie działać poprawnie w przypadku map bitowych o typie Bgra8888 koloru.

Metoda MakePixel jest oflagowana za pomocą MethodImplOptions.AggressiveInlining opcji , aby zachęcić kompilator do uniknięcia utworzenia oddzielnej metody, ale zamiast tego skompilować kod, w którym jest wywoływana metoda. Powinno to poprawić wydajność.

Co ciekawe, SKColor struktura definiuje jawną konwersję z SKColor na niepodpisaną liczbę całkowitą, co oznacza, że SKColor można utworzyć wartość, a konwersja, która uint ma być używana zamiast MakePixel:

SKBitmap FillBitmapUintPtrColor(out string description, out int milliseconds)
{
    description = "GetPixels SKColor";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    IntPtr pixelsAddr = bitmap.GetPixels();

    unsafe
    {
        for (int rep = 0; rep < REPS; rep++)
        {
            uint* ptr = (uint*)pixelsAddr.ToPointer();

            for (int row = 0; row < 256; row++)
                for (int col = 0; col < 256; col++)
                {
                    *ptr++ = (uint)new SKColor((byte)col, 0, (byte)row);
                }
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Jedynym pytaniem jest to: Czy format liczby całkowitej SKColor wartości w kolejności SKColorType.Rgba8888 typu koloru, czy SKColorType.Bgra8888 typ koloru, czy jest to coś innego całkowicie? Wkrótce zostanie ujawniona odpowiedź na to pytanie.

SetPixels, metoda

SKBitmap Definiuje również metodę o nazwie SetPixels, która jest wywoływana w następujący sposób:

bitmap.SetPixels(intPtr);

Pamiętaj, że GetPixels uzyskuje IntPtr odwołanie do bloku pamięci używanego przez mapę bitową do przechowywania jego pikseli. Wywołanie SetPixelszastępuje ten blok pamięci blokiem pamięci, do którego odwołuje się IntPtr określony jako SetPixels argument. Mapa bitowa zwalnia następnie blok pamięci, którego używał wcześniej. Następnym razem, gdy GetPixels zostanie wywołana, uzyska blok pamięci ustawiony za pomocą SetPixelspolecenia .

Na początku wydaje się, że SetPixels nie daje więcej mocy i wydajności niż GetPixels podczas bycia mniej wygodnym. Uzyskasz GetPixels blok pamięci mapy bitowej i uzyskasz do niego dostęp. Przydzielisz SetPixels i uzyskaj dostęp do pamięci, a następnie ustawisz go jako blok pamięci mapy bitowej.

Jednak użycie SetPixels oferuje odrębną przewagę składniową: umożliwia dostęp do bitów pikseli mapy bitowej przy użyciu tablicy. Poniżej przedstawiono metodę, która GradientBitmapPage demonstruje tę technikę. Metoda najpierw definiuje wielowymiarową tablicę bajtów odpowiadającą bajtom pikseli mapy bitowej. Pierwszy wymiar to wiersz, drugi wymiar to kolumna, a trzeci wymiar jest skorelowany z czterema składnikami każdego piksela:

SKBitmap FillBitmapByteBuffer(out string description, out int milliseconds)
{
    description = "SetPixels byte buffer";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    byte[,,] buffer = new byte[256, 256, 4];

    for (int rep = 0; rep < REPS; rep++)
        for (int row = 0; row < 256; row++)
            for (int col = 0; col < 256; col++)
            {
                buffer[row, col, 0] = (byte)col;   // red
                buffer[row, col, 1] = 0;           // green
                buffer[row, col, 2] = (byte)row;   // blue
                buffer[row, col, 3] = 0xFF;        // alpha
            }

    unsafe
    {
        fixed (byte* ptr = buffer)
        {
            bitmap.SetPixels((IntPtr)ptr);
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Następnie po wypełnieniu tablicy pikselami unsafe blok i fixed instrukcja jest używana do uzyskania wskaźnika bajtów wskazującego tę tablicę. Wskaźnik bajtu można następnie rzutować do elementu IntPtr w celu przekazania do SetPixelselementu .

Utworzona tablica nie musi być tablicą bajtów. Może to być tablica całkowita zawierająca tylko dwa wymiary dla wiersza i kolumny:

SKBitmap FillBitmapUintBuffer(out string description, out int milliseconds)
{
    description = "SetPixels uint buffer";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    uint[,] buffer = new uint[256, 256];

    for (int rep = 0; rep < REPS; rep++)
        for (int row = 0; row < 256; row++)
            for (int col = 0; col < 256; col++)
            {
                buffer[row, col] = MakePixel((byte)col, 0, (byte)row, 0xFF);
            }

    unsafe
    {
        fixed (uint* ptr = buffer)
        {
            bitmap.SetPixels((IntPtr)ptr);
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Metoda MakePixel jest ponownie używana do łączenia składników kolorów w 32-bitowy piksel.

Po prostu dla kompletności oto ten sam kod, ale z wartością rzutowaną SKColor na niepodpisaną liczbę całkowitą:

SKBitmap FillBitmapUintBufferColor(out string description, out int milliseconds)
{
    description = "SetPixels SKColor";
    SKBitmap bitmap = new SKBitmap(256, 256);

    stopwatch.Restart();

    uint[,] buffer = new uint[256, 256];

    for (int rep = 0; rep < REPS; rep++)
        for (int row = 0; row < 256; row++)
            for (int col = 0; col < 256; col++)
            {
                buffer[row, col] = (uint)new SKColor((byte)col, 0, (byte)row);
            }

    unsafe
    {
        fixed (uint* ptr = buffer)
        {
            bitmap.SetPixels((IntPtr)ptr);
        }
    }

    milliseconds = (int)stopwatch.ElapsedMilliseconds;
    return bitmap;
}

Porównywanie technik

Konstruktor strony Kolor gradientu wywołuje wszystkie osiem metod przedstawionych powyżej i zapisuje wyniki:

public class GradientBitmapPage : ContentPage
{
    ···
    string[] descriptions = new string[8];
    SKBitmap[] bitmaps = new SKBitmap[8];
    int[] elapsedTimes = new int[8];

    SKCanvasView canvasView;

    public GradientBitmapPage ()
    {
        Title = "Gradient Bitmap";

        bitmaps[0] = FillBitmapSetPixel(out descriptions[0], out elapsedTimes[0]);
        bitmaps[1] = FillBitmapPixelsProp(out descriptions[1], out elapsedTimes[1]);
        bitmaps[2] = FillBitmapBytePtr(out descriptions[2], out elapsedTimes[2]);
        bitmaps[4] = FillBitmapUintPtr(out descriptions[4], out elapsedTimes[4]);
        bitmaps[6] = FillBitmapUintPtrColor(out descriptions[6], out elapsedTimes[6]);
        bitmaps[3] = FillBitmapByteBuffer(out descriptions[3], out elapsedTimes[3]);
        bitmaps[5] = FillBitmapUintBuffer(out descriptions[5], out elapsedTimes[5]);
        bitmaps[7] = FillBitmapUintBufferColor(out descriptions[7], out elapsedTimes[7]);

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }
    ···
}

Konstruktor kończy, tworząc obiekt SKCanvasView , aby wyświetlić wynikowe mapy bitowe. Procedura PaintSurface obsługi dzieli powierzchnię na osiem prostokątów i wywołuje Display każdą z nich:

public class GradientBitmapPage : ContentPage
{
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        int width = info.Width;
        int height = info.Height;

        canvas.Clear();

        Display(canvas, 0, new SKRect(0, 0, width / 2, height / 4));
        Display(canvas, 1, new SKRect(width / 2, 0, width, height / 4));
        Display(canvas, 2, new SKRect(0, height / 4, width / 2, 2 * height / 4));
        Display(canvas, 3, new SKRect(width / 2, height / 4, width, 2 * height / 4));
        Display(canvas, 4, new SKRect(0, 2 * height / 4, width / 2, 3 * height / 4));
        Display(canvas, 5, new SKRect(width / 2, 2 * height / 4, width, 3 * height / 4));
        Display(canvas, 6, new SKRect(0, 3 * height / 4, width / 2, height));
        Display(canvas, 7, new SKRect(width / 2, 3 * height / 4, width, height));
    }

    void Display(SKCanvas canvas, int index, SKRect rect)
    {
        string text = String.Format("{0}: {1:F1} msec", descriptions[index],
                                    (double)elapsedTimes[index] / REPS);

        SKRect bounds = new SKRect();

        using (SKPaint textPaint = new SKPaint())
        {
            textPaint.TextSize = (float)(12 * canvasView.CanvasSize.Width / canvasView.Width);
            textPaint.TextAlign = SKTextAlign.Center;
            textPaint.MeasureText("Tly", ref bounds);

            canvas.DrawText(text, new SKPoint(rect.MidX, rect.Bottom - bounds.Bottom), textPaint);
            rect.Bottom -= bounds.Height;
            canvas.DrawBitmap(bitmaps[index], rect, BitmapStretch.Uniform);
        }
    }
}

Aby umożliwić kompilatorowi optymalizację kodu, ta strona została uruchomiona w trybie wydania . Oto ta strona uruchomiona na symulatorze i Telefon 8 na komputerze MacBook Pro, telefonie z systemem Android Nexus 5 i urządzeniu Surface Pro 3 z systemem Windows 10. Ze względu na różnice sprzętowe unikaj porównywania czasów wydajności między urządzeniami, ale zamiast tego przyjrzyj się względnym czasom na każdym urządzeniu:

Mapa bitowa gradientowa

Oto tabela, która konsoliduje czasy wykonywania w milisekundach:

interfejs API Typ danych iOS Android Platforma UWP
Setpixel 3.17 10.77 3.49
Piksele 0.32 1.23 0,07
GetPixels byte 0,09 0,24 0.10
uint 0,06 0,26 0.05
SkColor 0.29 0,99 0,07
SetPixels byte 1.33 6.78 0,11
uint 0,14 0.69 0,06
SkColor 0,35 1.93 0.10

Zgodnie z oczekiwaniami wywołanie SetPixel 65 536 razy jest najmniej wydajnym sposobem ustawiania pikseli mapy bitowej. SKColor Wypełnianie tablicy i ustawianie Pixels właściwości jest znacznie lepsze, a nawet porównuje się z niektórymi technikami GetPixels i SetPixels . Praca z wartościami uint pikseli jest zazwyczaj szybsza niż ustawianie oddzielnych byte składników i konwertowanie SKColor wartości na niepodpisaną liczbę całkowitą powoduje dodanie pewnych obciążeń do procesu.

Warto również porównać różne gradienty: pierwsze wiersze każdej platformy są takie same i pokazują gradient zgodnie z oczekiwaniami. Oznacza to, że SetPixel metoda i Pixels właściwość poprawnie tworzą piksele na podstawie kolorów niezależnie od bazowego formatu pikseli.

Dwa następne wiersze zrzutów ekranu systemu iOS i Android są również takie same, co potwierdza, że mała MakePixel metoda jest poprawnie zdefiniowana dla domyślnego Rgba8888 formatu pikseli dla tych platform.

Dolny wiersz zrzutów ekranu systemu iOS i Android jest do tyłu, co oznacza, że niepodpisane liczby całkowite uzyskane przez rzutowanie SKColor wartości znajdują się w postaci:

AARRGGBB

Bajty są w następującej kolejności:

BB GG RR AA

Jest to Bgra8888 kolejność, a nie Rgba8888 kolejność. Format Brga8888 jest domyślny dla platformy uniwersalnej systemu Windows, dlatego gradienty w ostatnim wierszu tego zrzutu ekranu są takie same jak w pierwszym wierszu. Jednak dwa środkowe wiersze są nieprawidłowe, ponieważ kod tworzący te mapy bitowe przyjął Rgba8888 kolejność.

Jeśli chcesz użyć tego samego kodu do uzyskiwania dostępu do bitów pikseli na każdej platformie, możesz jawnie utworzyć SKBitmap obiekt przy użyciu Rgba8888 formatu lub Bgra8888 . Jeśli chcesz rzutować SKColor wartości na piksele mapy bitowej, użyj polecenia Bgra8888.

Dostęp losowy pikseli

Metody FillBitmapBytePtr i FillBitmapUintPtr na stronie Mapa bitowa gradientu skorzystały z for pętli zaprojektowanych w celu sekwencyjnego wypełnienia mapy bitowej, od górnego wiersza do dolnego wiersza i w każdym wierszu od lewej do prawej. Piksel można ustawić za pomocą tej samej instrukcji, która zwiększa wskaźnik.

Czasami konieczne jest uzyskanie dostępu do pikseli losowo, a nie sekwencyjnie. Jeśli używasz GetPixels podejścia, musisz obliczyć wskaźniki na podstawie wiersza i kolumny. Jest to pokazane na stronie Rainbow Sine , która tworzy mapę bitową pokazującą tęczę w postaci jednego cyklu krzywej sinusu.

Kolory tęczy są najłatwiejsze do utworzenia przy użyciu modelu kolorów HSL (odcienie, nasycenie, jasność). Metoda SKColor.FromHsl tworzy SKColor wartość przy użyciu wartości odcieni, które wahają się od 0 do 360 (na przykład kąty okręgu, ale przechodząc od czerwonego, przez zielony i niebieski, a z powrotem na czerwony) oraz nasycenie i jasność wartości od 0 do 100. W przypadku kolorów tęczy nasycenie powinno być ustawione na maksymalnie 100, a jasność do połowy punktu 50.

Rainbow Sine tworzy ten obraz poprzez pętlę wierszy mapy bitowej, a następnie pętlę przez 360 wartości odcieni. Z każdej wartości odcienia oblicza kolumnę mapy bitowej, która jest również oparta na wartości sinusu:

public class RainbowSinePage : ContentPage
{
    SKBitmap bitmap;

    public RainbowSinePage()
    {
        Title = "Rainbow Sine";

        bitmap = new SKBitmap(360 * 3, 1024, SKColorType.Bgra8888, SKAlphaType.Unpremul);

        unsafe
        {
            // Pointer to first pixel of bitmap
            uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();

            // Loop through the rows
            for (int row = 0; row < bitmap.Height; row++)
            {
                // Calculate the sine curve angle and the sine value
                double angle = 2 * Math.PI * row / bitmap.Height;
                double sine = Math.Sin(angle);

                // Loop through the hues
                for (int hue = 0; hue < 360; hue++)
                {
                    // Calculate the column
                    int col = (int)(360 + 360 * sine + hue);

                    // Calculate the address
                    uint* ptr = basePtr + bitmap.Width * row + col;

                    // Store the color value
                    *ptr = (uint)SKColor.FromHsl(hue, 100, 50);
                }
            }
        }

        // Create the SKCanvasView
        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(bitmap, info.Rect);
    }
}

Zwróć uwagę, że konstruktor tworzy mapę bitową na SKColorType.Bgra8888 podstawie formatu:

bitmap = new SKBitmap(360 * 3, 1024, SKColorType.Bgra8888, SKAlphaType.Unpremul);

Dzięki temu program może używać konwersji SKColor wartości na uint piksele bez obaw. Chociaż nie odgrywa roli w tym konkretnym programie, zawsze, gdy używasz SKColor konwersji do ustawiania pikseli, należy również określić SKAlphaType.Unpremul , ponieważ SKColor nie premultiply jego składników kolorów według wartości alfa.

Konstruktor następnie używa GetPixels metody , aby uzyskać wskaźnik do pierwszego piksela mapy bitowej:

uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();

W przypadku dowolnego określonego wiersza i kolumny należy dodać wartość przesunięcia do basePtrelementu . To przesunięcie to czas, w których wiersz ma szerokość mapy bitowej oraz kolumnę:

uint* ptr = basePtr + bitmap.Width * row + col;

Wartość SKColor jest przechowywana w pamięci przy użyciu tego wskaźnika:

*ptr = (uint)SKColor.FromHsl(hue, 100, 50);

W procedurze PaintSurfaceSKCanvasViewobsługi mapy bitowej jest rozciągnięta, aby wypełnić obszar wyświetlania:

Sinus tęczy

Od jednej mapy bitowej do innej

Bardzo wiele zadań przetwarzania obrazów obejmuje modyfikowanie pikseli, ponieważ są przesyłane z jednej mapy bitowej do innej. Ta technika jest pokazana na stronie Korekta kolorów. Strona ładuje jeden z zasobów mapy bitowej, a następnie umożliwia modyfikowanie obrazu przy użyciu trzech Slider widoków:

Korekta koloru

Dla każdego koloru pikseli pierwszy Slider dodaje wartość z zakresu od 0 do 360 do odcienia, ale następnie używa operatora modulo, aby zachować wynik z zakresu od 0 do 360, skutecznie przesuwając kolory wzdłuż spektrum (jak pokazuje zrzut ekranu platformy UWP). Slider Drugi pozwala wybrać współczynnik mnożenia z zakresu od 0,5 do 2, aby zastosować do nasycenia, a trzeci Slider robi to samo dla jasności, jak pokazano na zrzucie ekranu systemu Android.

Program obsługuje dwie mapy bitowe, oryginalną mapę bitową źródłową o nazwie srcBitmap i dostosowaną docelową mapę bitową o nazwie dstBitmap. Za każdym razem, gdy element zostanie przeniesiony, program oblicza wszystkie nowe piksele w elemSlider.dstBitmap Oczywiście użytkownicy będą eksperymentować, przenosząc Slider widoki bardzo szybko, aby uzyskać najlepszą wydajność, którą można zarządzać. Obejmuje to metodę GetPixels zarówno źródłowych, jak i docelowych map bitowych.

Strona Korekta koloru nie kontroluje formatu kolorów źródłowych i docelowych map bitowych. Zamiast tego zawiera nieco inną logikę i SKColorType.Rgba8888SKColorType.Bgra8888 formaty. Źródło i lokalizacja docelowa mogą mieć różne formaty, a program nadal będzie działać.

Oto program z wyjątkiem kluczowej TransferPixels metody, która transferuje piksele tworzą źródło do miejsca docelowego. Konstruktor ustawia dstBitmap wartość równą srcBitmap. Program PaintSurface obsługi wyświetla następujące informacje dstBitmap:

public partial class ColorAdjustmentPage : ContentPage
{
    SKBitmap srcBitmap =
        BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
                                            "SkiaSharpFormsDemos.Media.Banana.jpg");
    SKBitmap dstBitmap;

    public ColorAdjustmentPage()
    {
        InitializeComponent();

        dstBitmap = new SKBitmap(srcBitmap.Width, srcBitmap.Height);
        OnSliderValueChanged(null, null);
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        float hueAdjust = (float)hueSlider.Value;
        hueLabel.Text = $"Hue Adjustment: {hueAdjust:F0}";

        float saturationAdjust = (float)Math.Pow(2, saturationSlider.Value);
        saturationLabel.Text = $"Saturation Adjustment: {saturationAdjust:F2}";

        float luminosityAdjust = (float)Math.Pow(2, luminositySlider.Value);
        luminosityLabel.Text = $"Luminosity Adjustment: {luminosityAdjust:F2}";

        TransferPixels(hueAdjust, saturationAdjust, luminosityAdjust);
        canvasView.InvalidateSurface();
    }
    ···
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(dstBitmap, info.Rect, BitmapStretch.Uniform);
    }
}

Procedura ValueChanged obsługi widoków Slider oblicza wartości korekty i wywołuje metodę TransferPixels.

Cała TransferPixels metoda jest oznaczona jako unsafe. Zaczyna się od uzyskania wskaźników bajtów do bitów pikseli obu map bitowych, a następnie przechodzi przez wszystkie wiersze i kolumny. Z źródłowej mapy bitowej metoda uzyskuje cztery bajty dla każdego piksela. Mogą one znajdować się w Rgba8888 kolejności lub Bgra8888 . Sprawdzanie typu koloru umożliwia SKColor utworzenie wartości. Składniki HSL są następnie wyodrębniane, dostosowywane i używane do ponownego utworzenia SKColor wartości. W zależności od tego, czy docelowa mapa bitowa to Rgba8888 , czy Bgra8888, bajty są przechowywane w docelowym bitmp:

public partial class ColorAdjustmentPage : ContentPage
{
    ···
    unsafe void TransferPixels(float hueAdjust, float saturationAdjust, float luminosityAdjust)
    {
        byte* srcPtr = (byte*)srcBitmap.GetPixels().ToPointer();
        byte* dstPtr = (byte*)dstBitmap.GetPixels().ToPointer();

        int width = srcBitmap.Width;       // same for both bitmaps
        int height = srcBitmap.Height;

        SKColorType typeOrg = srcBitmap.ColorType;
        SKColorType typeAdj = dstBitmap.ColorType;

        for (int row = 0; row < height; row++)
        {
            for (int col = 0; col < width; col++)
            {
                // Get color from original bitmap
                byte byte1 = *srcPtr++;         // red or blue
                byte byte2 = *srcPtr++;         // green
                byte byte3 = *srcPtr++;         // blue or red
                byte byte4 = *srcPtr++;         // alpha

                SKColor color = new SKColor();

                if (typeOrg == SKColorType.Rgba8888)
                {
                    color = new SKColor(byte1, byte2, byte3, byte4);
                }
                else if (typeOrg == SKColorType.Bgra8888)
                {
                    color = new SKColor(byte3, byte2, byte1, byte4);
                }

                // Get HSL components
                color.ToHsl(out float hue, out float saturation, out float luminosity);

                // Adjust HSL components based on adjustments
                hue = (hue + hueAdjust) % 360;
                saturation = Math.Max(0, Math.Min(100, saturationAdjust * saturation));
                luminosity = Math.Max(0, Math.Min(100, luminosityAdjust * luminosity));

                // Recreate color from HSL components
                color = SKColor.FromHsl(hue, saturation, luminosity);

                // Store the bytes in the adjusted bitmap
                if (typeAdj == SKColorType.Rgba8888)
                {
                    *dstPtr++ = color.Red;
                    *dstPtr++ = color.Green;
                    *dstPtr++ = color.Blue;
                    *dstPtr++ = color.Alpha;
                }
                else if (typeAdj == SKColorType.Bgra8888)
                {
                    *dstPtr++ = color.Blue;
                    *dstPtr++ = color.Green;
                    *dstPtr++ = color.Red;
                    *dstPtr++ = color.Alpha;
                }
            }
        }
    }
    ···
}

Jest prawdopodobne, że wydajność tej metody może być jeszcze większa, tworząc oddzielne metody dla różnych kombinacji kolorów źródłowych i docelowych map bitowych i unikając sprawdzania typu dla każdego piksela. Inną opcją jest posiadanie wielu for pętli dla zmiennej col na podstawie typu koloru.

Posteryzacja

Innym typowym zadaniem, które polega na uzyskiwaniu dostępu do bitów pikseli, jest plakatyzacja. Liczba, jeśli kolory zakodowane w pikselach mapy bitowej są zmniejszane, tak aby wynik przypominał ręcznie rysowany plakat przy użyciu ograniczonej palety kolorów.

Strona Plakatize wykonuje ten proces na jednym z obrazów małp:

public class PosterizePage : ContentPage
{
    SKBitmap bitmap =
        BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
                                            "SkiaSharpFormsDemos.Media.Banana.jpg");
    public PosterizePage()
    {
        Title = "Posterize";

        unsafe
        {
            uint* ptr = (uint*)bitmap.GetPixels().ToPointer();
            int pixelCount = bitmap.Width * bitmap.Height;

            for (int i = 0; i < pixelCount; i++)
            {
                *ptr++ &= 0xE0E0E0FF;
            }
        }

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform;
    }
}

Kod w konstruktorze uzyskuje dostęp do każdego piksela, wykonuje bitową operację AND z wartością 0xE0E0E0FF, a następnie zapisuje wynik z powrotem w mapie bitowej. Wartości 0xE0E0E0FF utrzymuje wysokie 3 bity każdego składnika koloru i ustawia niższe 5 bitów na 0. Zamiast 224 lub 16 777 216 kolorów mapa bitowa jest zmniejszana do 29 lub 512 kolorów:

Zrzut ekranu przedstawia obraz małpy toy na dwóch urządzeniach przenośnych i w oknie pulpitu.