Přístup k bitům rastrového obrázku SkiaSharp
Jak jste viděli v článku Ukládání rastrových obrázků SkiaSharp do souborů, rastrové obrázky jsou obecně uloženy v souborech v komprimovaném formátu, jako je JPEG nebo PNG. V constrast, SkiaSharp rastrový obrázek uložený v paměti není komprimován. Uloží se jako sekvenční řada pixelů. Tento nekomprimovaný formát usnadňuje přenos rastrových obrázků na plochu zobrazení.
Blok paměti obsazený rastrovým obrázkem SkiaSharp je uspořádaný velmi jednoduchým způsobem: Začíná prvním řádkem pixelů, zleva doprava a pokračuje druhým řádkem. U plnobarevných rastrových obrázků se každý pixel skládá ze čtyř bajtů, což znamená, že celkový prostor paměti vyžadovaný rastrovým obrázkem je čtyřikrát součin jeho šířky a výšky.
Tento článek popisuje, jak může aplikace získat přístup k těmto pixelům, a to buď přímo přístupem k bloku paměti pixelového obrázku, nebo nepřímo. V některých případech může program chtít analyzovat pixely obrázku a sestavit histogram určitého druhu. Častěji můžou aplikace vytvářet jedinečné obrázky algoritmicky vytvořením pixelů, které tvoří rastrový obrázek:
Techniky
SkiaSharp poskytuje několik technik pro přístup k bitům rastrového obrázku. Který z nich si zvolíte, je obvykle kompromisem mezi usnadněním kódování (které souvisí s údržbou a snadným laděním) a výkonem. Ve většiněpřípadůch SKBitmap
- Tyto
GetPixel
metodySetPixel
umožňují získat nebo nastavit barvu jednoho pixelu. - Vlastnost
Pixels
získá matici barev pixelů pro celý rastrový obrázek nebo nastaví pole barev. GetPixels
vrátí adresu paměti pixelu, kterou rastrový obrázek používá.SetPixels
nahradí adresu paměti pixelu používanou bitmapou.
První dvě techniky si můžete představit jako "vysokou úroveň" a druhá dvě jako "nízká úroveň". Existují některé další metody a vlastnosti, které můžete použít, ale jsou to nejcennější.
Aby bylo možné zobrazit rozdíly mezi výkonem mezi těmito technikami, ukázková aplikace obsahuje stránku s názvem Gradient Bitmap , která vytvoří rastrový obrázek s pixely, které kombinují červené a modré odstíny a vytvoří přechod. Program vytvoří osm různých kopií tohoto rastrového obrázku, a to vše pomocí různých technik pro nastavení rastrových pixelů. Každý z těchto osmi rastrových obrázků je vytvořen v samostatné metodě, která také nastaví stručný textový popis techniky a vypočítá čas potřebný k nastavení všech pixelů. Každá metoda prochází logikou nastavení pixelů 100krát, aby získala lepší odhad výkonu.
Metoda SetPixel
Pokud potřebujete nastavit nebo získat pouze několik jednotlivých pixelů, SetPixel
jsou tyto metody GetPixel
ideální. Pro každou z těchto dvou metod zadáte celočíselné sloupce a řádek. Bez ohledu na formát pixelu vám tyto dvě metody umožňují získat nebo nastavit pixel jako SKColor
hodnotu:
bitmap.SetPixel(col, row, color);
SKColor color = bitmap.GetPixel(col, row);
Argument col
musí být v rozsahu od 0 do jedné menší než Width
vlastnost rastrového obrázku a row
rozsah od 0 do jedné menší než Height
vlastnost.
Zde je metoda v Gradient Bitmap , která nastavuje obsah rastrového obrázku SetPixel
pomocí metody. Rastrový obrázek je 256 × 256 pixelů a for
smyčky jsou pevně zakódované s rozsahem hodnot:
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;
}
···
}
Sada barev pro každý pixel má červenou komponentu, která se rovná rastrovém sloupci, a modrá komponenta rovna řádku. Výsledný rastrový obrázek je v levém horním rohu černý, červený v pravém horním rohu, modrý v levém dolním rohu a purpurový v pravém dolním rohu s přechody jinde.
Tato SetPixel
metoda se nazývá 65 536krát a bez ohledu na to, jak efektivní může být tato metoda, obecně není vhodné, aby se v případě, že je k dispozici alternativní řešení, mnoho volání rozhraní API. Naštěstí existuje několik alternativ.
Vlastnost Pixels
SKBitmap
Pixels
definuje vlastnost, která vrací pole SKColor
hodnot pro celý rastrový obrázek. Můžete také použít Pixels
k nastavení pole barevných hodnot pro rastrový obrázek:
SKColor[] pixels = bitmap.Pixels;
bitmap.Pixels = pixels;
Pixely jsou uspořádány v poli počínaje prvním řádkem, zleva doprava, pak druhý řádek atd. Celkový počet barev v poli se rovná součinu šířky a výšky rastrového obrázku.
I když se zdá, že tato vlastnost je efektivní, mějte na paměti, že pixely se kopírují z rastrového obrázku do matice a z pole zpět do rastrového obrázku a pixely se převedou z a na SKColor
hodnoty.
Zde je metoda ve GradientBitmapPage
třídě, která nastavuje bitmapu pomocí Pixels
vlastnosti. Metoda přidělí SKColor
pole požadované velikosti, ale mohla použít Pixels
vlastnost k vytvoření tohoto pole:
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;
}
Všimněte si, že index pixels
pole je potřeba vypočítat z row
col
proměnných. Řádek se vynásobí počtem pixelů v každém řádku (v tomto případě 256) a pak se přidá sloupec.
SKBitmap
definuje také podobnou Bytes
vlastnost, která vrací bajtové pole pro celý rastrový obrázek, ale je pro plnobarevnější rastrové obrázky.
Ukazatel GetPixels
Potenciálně nejúčinnější technika pro přístup k rastrovým pixelům je GetPixels
, není zaměňovat s metodou GetPixel
nebo Pixels
vlastností. Okamžitě si všimnete rozdílu v GetPixels
tom, že v programování v jazyce C# vrací něco, co není velmi běžné:
IntPtr pixelsAddr = bitmap.GetPixels();
Typ .NET IntPtr
představuje ukazatel. Volá se IntPtr
, protože se jedná o délku celého čísla v nativním procesoru počítače, na kterém je program spuštěn, obecně 32 bitů nebo 64 bitů. Tato IntPtr
GetPixels
hodnota je adresa skutečného bloku paměti, který rastrový objekt používá k uložení jeho pixelů.
Pomocí metody můžete převést na IntPtr
typ ToPointer
ukazatele jazyka C#. Syntaxe ukazatele jazyka C# je stejná jako syntaxe C a C++:
byte* ptr = (byte*)pixelsAddr.ToPointer();
Proměnná ptr
je typu bajtového ukazatele. Tato ptr
proměnná umožňuje přístup k jednotlivým bajtům paměti, které se používají k uložení pixelů rastrového obrázku. Tento kód slouží ke čtení bajtu z této paměti nebo k zápisu bajtu do paměti:
byte pixelComponent = *ptr;
*ptr = pixelComponent;
V tomto kontextu je hvězdička operátorem nepřímých odkazů jazyka C# a slouží k odkazování na obsah paměti, na kterou ptr
odkazuje . Zpočátku ptr
odkazuje na první bajt prvního pixelu prvního řádku rastrového obrázku, ale u proměnné můžete provést aritmetika ptr
, která se přesune do jiných umístění v rastrovém obrázku.
Jednou z nevýhod je, že tuto ptr
proměnnou můžete použít pouze v bloku kódu označeném klíčovým slovem unsafe
. Kromě toho musí být sestavení označeno příznakem jako povolení nebezpečných bloků. To se provádí ve vlastnostech projektu.
Použití ukazatelů v jazyce C# je velmi výkonné, ale také velmi nebezpečné. Musíte být opatrní, že nemáte přístup k paměti nad rámec toho, co má ukazatel odkazovat. Proto je použití ukazatele přidružené ke slovu "nebezpečné".
Tady je metoda ve GradientBitmapPage
třídě, která metodu GetPixels
používá. unsafe
Všimněte si bloku, který zahrnuje veškerý kód pomocí ukazatele bajtu:
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;
}
ptr
Při prvním získání proměnné z ToPointer
metody odkazuje na první bajt levého pixelu prvního řádku rastrového obrázku. Smyčky for
pro row
a col
jsou nastaveny tak, aby ptr
bylo možné zvýšit pomocí operátoru ++
po nastavení každého bajtu každého pixelu. U ostatních 99 smyček přes pixely ptr
musí být nastaven zpět na začátek rastrového obrázku.
Každý pixel je čtyři bajty paměti, takže každý bajt musí být nastaven samostatně. Kód zde předpokládá, že bajty jsou v pořadí červené, zelené, modré a alfa, což je konzistentní s typem SKColorType.Rgba8888
barvy. Možná si vzpomenete, že se jedná o výchozí typ barvy pro iOS a Android, ale ne pro Univerzální platforma Windows. Ve výchozím nastavení vytváří UPW rastrové obrázky s typem SKColorType.Bgra8888
barvy. Z tohoto důvodu očekáváme, že na této platformě uvidíte několik různých výsledků.
Hodnotu vrácenou ToPointer
uint
z ukazatele je možné přetypovat místo byte
ukazatele. To umožňuje přístup k celému pixelu v jednom příkazu. Použití operátoru ++
na tento ukazatel zvýší o čtyři bajty, aby ukazoval na další pixel:
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);
···
}
Pixel je nastaven pomocí MakePixel
metody, která vytváří celočíselné pixely z červených, zelených, modrých a alfa komponent. Mějte na paměti, že SKColorType.Rgba8888
formát má řazení bajtů pixelů takto:
RR GG BB AA
Celé číslo odpovídající těmto bajtům je ale:
AABBGGRR
Nejméně významný bajt celého čísla je uložen jako první v souladu s architekturou little-endian. Tato MakePixel
metoda nebude správně fungovat u rastrových obrázků s typem Bgra8888
barvy.
Metoda MakePixel
je označena příznakem s MethodImplOptions.AggressiveInlining
možností povzbuzovat kompilátor, aby se zabránilo vytvoření samostatné metody, ale místo toho zkompilovat kód, kde je volána metoda. Tím by se měl zvýšit výkon.
Zajímavé je, že SKColor
struktura definuje explicitní převod z SKColor
na celé číslo bez znaménka, což znamená, že SKColor
hodnotu lze vytvořit a převod na uint
lze použít místo 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;
}
Jedinou otázkou je: Jedná se o celočíselnou hodnotu SKColor
v pořadí typu SKColorType.Rgba8888
barvy, nebo SKColorType.Bgra8888
o typ barvy nebo o něco jiného? Odpověď na tuto otázku bude brzy odhalena.
SetPixels – metoda
SKBitmap
definuje také metodu s názvem SetPixels
, kterou voláte takto:
bitmap.SetPixels(intPtr);
Vzpomeňte si, že GetPixels
získá IntPtr
odkaz na blok paměti, který rastrový obrázek používá k uložení jeho pixelů. Volání SetPixels
nahradí tento blok paměti blokem paměti, na který IntPtr
odkazuje zadaný argumentSetPixels
. Rastrový obrázek pak uvolní blok paměti, který používal dříve. Při příštím GetPixels
zavolání získá blok paměti nastavený na SetPixels
.
Zpočátku to vypadá, jako kdybyste SetPixels
neměli větší výkon a výkon, než GetPixels
kdybyste byli méně pohodlní. Když GetPixels
získáte rastrový blok paměti a budete k němu přistupovat. Když SetPixels
přidělíte a získáte přístup k nějaké paměti, a pak ji nastavte jako rastrový blok paměti.
Použití SetPixels
ale nabízí jedinečnou syntaktickou výhodu: Umožňuje přístup k bitům rastrových pixelů pomocí pole. Tady je metoda, GradientBitmapPage
která ukazuje tuto techniku. Metoda nejprve definuje vícerozměrné bajtové pole odpovídající bajtům pixelů rastrového obrázku. První dimenze je řádek, druhá dimenze je sloupec a třetí dimenze korresonduje na čtyři součásti každého pixelu:
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;
}
Potom po vyplnění pole pixely se k získání bajtového ukazatele, který odkazuje na toto pole, unsafe
použije blok a fixed
příkaz. Tento bajtový ukazatel pak lze přetypovat na předání IntPtr
do SetPixels
.
Pole, které vytvoříte, nemusí být bajtové pole. Může se jednat o celočíselnou matici s pouze dvěma dimenzemi pro řádek a sloupec:
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
se znovu používá ke kombinování barevných komponent do 32bitového pixelu.
Pro úplnost je stejný kód, ale s hodnotou přetypovanou SKColor
na celé číslo bez znaménka:
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;
}
Porovnání technik
Konstruktor stránky Barva přechodu volá všechny osm z výše uvedených metod a uloží výsledky:
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 končí vytvořením objektu SKCanvasView
pro zobrazení výsledných rastrových obrázků. Obslužná PaintSurface
rutina rozdělí svoji plochu na osm obdélníků a volání Display
, která se mají zobrazit:
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 kompilátor mohl optimalizovat kód, byla tato stránka spuštěna v režimu vydání . Zde je tato stránka spuštěná na simulátoru i Telefon 8 na MacBooku Pro, telefonu Nexus 5 s Androidem a zařízení Surface Pro 3 s Windows 10. Vzhledem k rozdílům v hardwaru se vyhněte porovnávání časů výkonu mezi zařízeními, ale místo toho se podívejte na relativní časy na každém zařízení:
Tady je tabulka, která konsoliduje doby provádění v milisekundách:
rozhraní API | Datový typ | iOS | Android | UWP |
---|---|---|---|---|
SetPixel | 3.17 | 10.77 | 3.49 | |
Pixely | 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 |
Podle očekávání je volání SetPixel
65 536krát nejméně efektivní způsob, jak nastavit pixely rastrového obrázku. SKColor
Vyplnění pole a nastavení Pixels
vlastnosti je mnohem lepší, a dokonce i porovnat příznivě s některými GetPixels
z těchto a SetPixels
technik. Práce s uint
hodnotami pixelů je obecně rychlejší než nastavení samostatných byte
komponent a převod SKColor
hodnoty na celé číslo bez znaménka zvyšuje režii procesu.
Je také zajímavé porovnat různé přechody: Horní řádky každé platformy jsou stejné a ukazují přechod tak, jak to bylo zamýšleno. To znamená, že SetPixel
metoda a Pixels
vlastnost správně vytvářejí pixely z barev bez ohledu na formát podkladových pixelů.
Další dva řádky snímků obrazovky s iOSem a Androidem jsou také stejné, což potvrzuje, že malá MakePixel
metoda je správně definovaná pro výchozí Rgba8888
formát pixelů pro tyto platformy.
Dolní řádek snímků obrazovky s iOSem a Androidem je zpět, což znamená, že celé číslo bez znaménka získané přetypováním SKColor
hodnoty je ve tvaru:
AARRGGBB
Bajty jsou v pořadí:
BB GG RR AA
Toto je Bgra8888
řazení, nikoli Rgba8888
řazení. Formát Brga8888
je výchozí pro univerzální platformu Windows, což je důvod, proč přechody na posledním řádku tohoto snímku obrazovky jsou stejné jako první řádek. Střední dva řádky jsou ale nesprávné, protože kód, který vytváří tyto rastrové obrázky, Rgba8888
předpokládá řazení.
Pokud chcete použít stejný kód pro přístup k pixelům na jednotlivých platformách, můžete explicitně vytvořit SKBitmap
pomocí formátu Rgba8888
nebo Bgra8888
formátu. Chcete-li přetypovat SKColor
hodnoty na rastrové pixely, použijte Bgra8888
.
Náhodný přístup k pixelům
Ze FillBitmapBytePtr
smyček navržených k postupnému vyplnění rastrového obrázku, od horního řádku po dolní řádek a v každém řádku zleva doprava jsou výhody for
a FillBitmapUintPtr
metody na stránce Gradient Bitmap. Pixel lze nastavit pomocí stejného příkazu, který inkrementoval ukazatel.
Někdy je potřeba místo sekvenčně přistupovat k pixelům náhodně. Pokud používáte GetPixels
přístup, budete muset vypočítat ukazatele na základě řádku a sloupce. To je znázorněno na stránce Rainbow Sine , která vytvoří rastrový obrázek zobrazující duhu ve formě jednoho cyklu sinusové křivky.
Barvy duhy se nejsnadněji vytvářejí pomocí barevného modelu HSL (odstín, sytost, světelnost). Metoda SKColor.FromHsl
vytvoří SKColor
hodnotu pomocí hodnot odstínu, které jsou v rozsahu od 0 do 360 (například úhly kruhu, ale prochází červenou, zelenou a modrou a zpět na červenou) a sytost a světelnost od 0 do 100. Pro barvy duhy by sytost měla být nastavena na maximálně 100 a světelnost na střed bodu 50.
Duha Sine vytvoří tento obrázek tak, že prochází řádky rastrového obrázku a pak prochází 360 hodnot odstínů. Z každé hodnoty odstínu vypočítá rastrový sloupec, který je založen také na sinusové hodnotě:
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);
}
}
Všimněte si, že konstruktor vytvoří bitmapu SKColorType.Bgra8888
na základě formátu:
bitmap = new SKBitmap(360 * 3, 1024, SKColorType.Bgra8888, SKAlphaType.Unpremul);
To umožňuje programu používat převod SKColor
hodnot na uint
pixely bez obav. I když v tomto konkrétním programu nehraje roli, při každém použití převodu SKColor
na nastavení pixelů byste měli také určit SKAlphaType.Unpremul
, protože SKColor
není předem jeho barevné součásti alfa hodnota.
Konstruktor pak použije metodu GetPixels
k získání ukazatele na první pixel rastrového obrázku:
uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();
U každého konkrétního řádku a sloupce musí být hodnota posunu přidána do basePtr
. Tento posun je čas řádku, kdy je rastrová mapa šířka a sloupec:
uint* ptr = basePtr + bitmap.Width * row + col;
Hodnota SKColor
je uložena v paměti pomocí tohoto ukazatele:
*ptr = (uint)SKColor.FromHsl(hue, 100, 50);
PaintSurface
V obslužné rutině SKCanvasView
je rastrový obrázek roztažen tak, aby vyplnil oblast zobrazení:
Z jednoho rastrového obrázku do druhého
Velmi mnoho úloh zpracování obrázků zahrnuje úpravy pixelů při jejich přenosu z jednoho rastrového obrázku do druhého. Tato technika je ukázaná na stránce Úpravy barev. Stránka načte jeden z rastrových prostředků a pak umožňuje upravit obrázek pomocí tří Slider
zobrazení:
Pro každou barvu pixelů přidá první Slider
hodnotu od 0 do 360 k odstínu, ale pak pomocí operátoru modulo zachová výsledek mezi 0 a 360 a efektivně posune barvy podél spektra (jak ukazuje snímek obrazovky UPW). Druhý Slider
umožňuje vybrat multiplikativní faktor mezi 0,5 a 2, který se použije na sytost, a třetí Slider
provede totéž pro světelnost, jak je znázorněno na snímku obrazovky s Androidem.
Program udržuje dvě bitmapy, původní zdrojový rastrový obrázek pojmenovaný srcBitmap
a upravený cílový rastr s názvem dstBitmap
. Pokaždé, když Slider
je přesunut, program vypočítá všechny nové pixely v dstBitmap
. Uživatelé samozřejmě budou experimentovat přesunutím Slider
zobrazení velmi rychle, takže chcete mít nejlepší výkon, který můžete spravovat. To zahrnuje metodu GetPixels
pro zdrojové i cílové rastrové obrázky.
Stránka Nastavení barev neřídí formát barvy zdrojového a cílového rastrového obrázku. Místo toho obsahuje mírně odlišnou logiku pro SKColorType.Rgba8888
a SKColorType.Bgra8888
formáty. Zdroj a cíl můžou být různé formáty a program bude i nadále fungovat.
Tady je program s výjimkou klíčové TransferPixels
metody, která přenáší pixely tvoří zdroj do cíle. Konstruktor nastaví dstBitmap
hodnotu rovnající se srcBitmap
. Obslužná rutina PaintSurface
zobrazí 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);
}
}
Obslužná rutina ValueChanged
pro Slider
zobrazení vypočítá hodnoty úpravy a volání TransferPixels
.
Celá TransferPixels
metoda je označena jako unsafe
. Začíná získáním bajtů ukazatelů na pixelové bity obou rastrových obrázků a pak prochází všechny řádky a sloupce. Ze zdrojového rastrového obrázku metoda získá čtyři bajty pro každý pixel. Ty můžou být buď v pořadí Rgba8888
, nebo Bgra8888
v pořadí. Kontrola typu barvy umožňuje SKColor
vytvoření hodnoty. Komponenty HSL se pak extrahují, upraví a použijí k opětovnému vytvoření SKColor
hodnoty. V závislosti na tom, zda je Rgba8888
cílový rastrový obrázek nebo Bgra8888
, jsou bajty uloženy v cílové 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;
}
}
}
}
···
}
Je pravděpodobné, že výkon této metody může být ještě lepší vytvořením samostatných metod pro různé kombinace barevných typů zdrojových a cílových rastrových obrázků a vyhnout se kontrole typu pro každý pixel. Další možností je mít více for
smyček pro col
proměnnou na základě typu barvy.
Plakátování
Další běžnou úlohou, která zahrnuje přístup k bitům pixelů, je plakátování. Číslo, pokud jsou barvy zakódované v pixelech rastrového obrázku zmenšeny tak, aby výsledek připomínal ručně kreslený plakát pomocí omezené barevné palety.
Stránka Posterize provádí tento proces na jednom z obrázků opice:
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;
}
}
Kód v konstruktoru přistupuje ke každému pixelu, provede bitové operace AND s hodnotou 0xE0E0E0FF a potom uloží výsledek zpět v rastrovém obrázku. Hodnoty 0xE0E0E0FF zachová vysoké 3 bity každé barevné komponenty a nastaví nižší 5 bitů na 0. Místo 224 nebo 16 777 216 barev se rastrový obrázek zmenší na 29 nebo 512 barev: