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ą:
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
iSetPixel
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
SKBitmap
Pixels
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 IntPtr
GetPixels
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 ptr
element . 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 SetPixels
zastę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ą SetPixels
polecenia .
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 SetPixels
elementu .
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:
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 basePtr
elementu . 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 PaintSurface
SKCanvasView
obsługi mapy bitowej jest rozciągnięta, aby wypełnić obszar wyświetlania:
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:
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.Rgba8888
SKColorType.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: