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:

Ukázky bitů pixelů

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 metody SetPixel 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

SKBitmapPixels 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 rowcol 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 IntPtrGetPixels 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 ptrodkazuje . 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 ToPointeruint 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í:

Bitmapa přechodu

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ě SKCanvasViewje rastrový obrázek roztažen tak, aby vyplnil oblast zobrazení:

Duha sinus

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í:

Úprava barvy

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:

Snímek obrazovky ukazuje plakátový obrázek opice toy na dvou mobilních zařízeních a v okně plochy.