Condividi tramite


Accesso ai bit di pixel bitmap SkiaSharp

Come si è visto nell'articolo Salvataggio di bitmap SkiaSharp in file, le bitmap vengono in genere archiviate in file in un formato compresso, ad esempio JPEG o PNG. In constrast, una bitmap SkiaSharp archiviata in memoria non è compressa. Viene archiviata come una serie sequenziale di pixel. Questo formato non compresso facilita il trasferimento delle bitmap in una superficie di visualizzazione.

Il blocco di memoria occupato da una bitmap SkiaSharp è organizzato in modo molto semplice: inizia con la prima riga di pixel, da sinistra a destra e quindi continua con la seconda riga. Per le bitmap a colori completi, ogni pixel è costituito da quattro byte, il che significa che lo spazio di memoria totale richiesto dalla bitmap è quattro volte il prodotto della larghezza e dell'altezza.

Questo articolo descrive come un'applicazione può ottenere l'accesso a tali pixel, direttamente accedendo al blocco di memoria pixel della bitmap o indirettamente. In alcuni casi, un programma potrebbe voler analizzare i pixel di un'immagine e costruire un istogramma di qualche tipo. Più comunemente, le applicazioni possono costruire immagini univoche creando in modo algoritmico i pixel che costituiscono la bitmap:

Esempi di bit in pixel

Tecniche

SkiaSharp offre diverse tecniche per accedere ai bit di pixel di una bitmap. Quale scelta è in genere un compromesso tra la praticità del codice (correlata alla manutenzione e alla facilità di debug) e le prestazioni. Nella maggior parte dei casi, si userà uno dei metodi e delle proprietà seguenti di SKBitmap per accedere ai pixel della bitmap:

  • I GetPixel metodi e SetPixel consentono di ottenere o impostare il colore di un singolo pixel.
  • La Pixels proprietà ottiene una matrice di colori pixel per l'intera bitmap o imposta la matrice di colori.
  • GetPixels restituisce l'indirizzo della memoria pixel utilizzata dalla bitmap.
  • SetPixels sostituisce l'indirizzo della memoria pixel usata dalla bitmap.

È possibile considerare le prime due tecniche come "alto livello" e la seconda come "basso livello". Esistono altri metodi e proprietà che è possibile usare, ma questi sono i più importanti.

Per consentire di visualizzare le differenze di prestazioni tra queste tecniche, l'applicazione di esempio contiene una pagina denominata Bitmap sfumatura che crea una bitmap con pixel che combinano sfumature rosse e blu per creare una sfumatura. Il programma crea otto copie diverse di questa bitmap, tutte usando tecniche diverse per impostare i pixel bitmap. Ognuna di queste otto bitmap viene creata in un metodo separato che imposta anche una breve descrizione testuale della tecnica e calcola il tempo necessario per impostare tutti i pixel. Ogni metodo esegue un ciclo attraverso la logica di impostazione pixel 100 volte per ottenere una stima migliore delle prestazioni.

Metodo SetPixel

Se è sufficiente impostare o ottenere diversi pixel singoli, i SetPixel metodi e GetPixel sono ideali. Per ognuno di questi due metodi, specificare la colonna e la riga integer. Indipendentemente dal formato pixel, questi due metodi consentono di ottenere o impostare il pixel come SKColor valore:

bitmap.SetPixel(col, row, color);

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

L'argomento col deve essere compreso tra 0 e uno minore della Width proprietà della bitmap e deve essere compreso tra 0 e row uno minore della Height proprietà.

Ecco il metodo in Bitmap sfumatura che imposta il contenuto di una bitmap usando il SetPixel metodo . La bitmap è 256 di 256 pixel e i for cicli sono hardcoded con l'intervallo di valori:

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;
    }
    ···
}

Il set di colori per ogni pixel ha un componente rosso uguale alla colonna bitmap e un componente blu uguale alla riga. La bitmap risultante è nera in alto a sinistra, rossa in alto a destra, blu in basso a sinistra e magenta in basso a destra, con sfumature altrove.

Il SetPixel metodo viene chiamato 65.536 volte e indipendentemente dall'efficienza di questo metodo, in genere non è consigliabile effettuare molte chiamate API se è disponibile un'alternativa. Fortunatamente, ci sono diverse alternative.

Proprietà Pixel

SKBitmap definisce una proprietà che restituisce una Pixels matrice di SKColor valori per l'intera bitmap. È anche possibile usare Pixels per impostare una matrice di valori di colore per la bitmap:

SKColor[] pixels = bitmap.Pixels;

bitmap.Pixels = pixels;

I pixel sono disposti nella matrice a partire dalla prima riga, da sinistra a destra, quindi dalla seconda riga e così via. Il numero totale di colori nella matrice è uguale al prodotto della larghezza e dell'altezza della bitmap.

Anche se questa proprietà sembra essere efficiente, tenere presente che i pixel vengono copiati dalla bitmap nella matrice e dalla matrice di nuovo alla bitmap e i pixel vengono convertiti da e in SKColor valori.

Ecco il metodo nella GradientBitmapPage classe che imposta la bitmap usando la Pixels proprietà . Il metodo alloca una SKColor matrice delle dimensioni necessarie, ma potrebbe aver usato la Pixels proprietà per creare tale matrice:

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;
}

Si noti che l'indice della pixels matrice deve essere calcolato dalle row variabili e col . La riga viene moltiplicata per il numero di pixel in ogni riga (in questo caso 256) e quindi viene aggiunta la colonna.

SKBitmap definisce anche una proprietà simile Bytes , che restituisce una matrice di byte per l'intera bitmap, ma è più complessa per le bitmap a colori interi.

Puntatore GetPixels

Potenzialmente la tecnica più potente per accedere ai pixel bitmap è GetPixels, per non essere confusa con il GetPixel metodo o la Pixels proprietà . Si noterà immediatamente una differenza con GetPixels in quanto restituisce qualcosa di non molto comune nella programmazione C#:

IntPtr pixelsAddr = bitmap.GetPixels();

Il tipo .NET IntPtr rappresenta un puntatore. Viene chiamato IntPtr perché è la lunghezza di un numero intero nel processore nativo del computer in cui viene eseguito il programma, in genere 32 bit o 64 bit di lunghezza. L'oggetto IntPtr restituito GetPixels è l'indirizzo del blocco effettivo di memoria utilizzato dall'oggetto bitmap per archiviarne i pixel.

È possibile convertire l'oggetto IntPtr in un tipo di puntatore C# usando il ToPointer metodo . La sintassi del puntatore C# è identica a C e C++:

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

La ptr variabile è di tipo puntatore a byte. Questa ptr variabile consente di accedere ai singoli byte di memoria usati per archiviare i pixel della bitmap. Si usa codice simile al seguente per leggere un byte dalla memoria o scrivere un byte nella memoria:

byte pixelComponent = *ptr;

*ptr = pixelComponent;

In questo contesto, l'asterisco è l'operatore di riferimento indiretto C# e viene usato per fare riferimento al contenuto della memoria a ptrcui punta . Inizialmente, ptr punta al primo byte del primo pixel della prima riga della bitmap, ma puoi eseguire aritmetica sulla ptr variabile per spostarlo in altre posizioni all'interno della bitmap.

Uno svantaggio è che è possibile usare questa ptr variabile solo in un blocco di codice contrassegnato con la unsafe parola chiave . Inoltre, l'assembly deve essere contrassegnato come blocco non sicuro. Questa operazione viene eseguita nelle proprietà del progetto.

L'uso dei puntatori in C# è molto potente, ma anche molto pericoloso. È necessario prestare attenzione che non si accede alla memoria oltre a ciò che dovrebbe fare riferimento al puntatore. Questo è il motivo per cui l'uso del puntatore è associato alla parola "unsafe".

Ecco il metodo nella GradientBitmapPage classe che usa il GetPixels metodo . Si noti il unsafe blocco che include tutto il codice usando il puntatore a byte:

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;
}

Quando la ptr variabile viene ottenuta per la prima volta dal ToPointer metodo , punta al primo byte del pixel più a sinistra della prima riga della bitmap. I for cicli per row e col vengono configurati in modo che ptr possano essere incrementati con l'operatore dopo l'impostazione ++ di ogni byte di ogni pixel. Per gli altri 99 cicli tra i pixel, deve ptr essere impostato di nuovo all'inizio della bitmap.

Ogni pixel è costituito da quattro byte di memoria, quindi ogni byte deve essere impostato separatamente. Il codice qui presuppone che i byte siano nell'ordine rosso, verde, blu e alfa, che è coerente con il SKColorType.Rgba8888 tipo di colore. È possibile ricordare che si tratta del tipo di colore predefinito per iOS e Android, ma non per il piattaforma UWP (Universal Windows Platform). Per impostazione predefinita, la piattaforma UWP crea bitmap con il SKColorType.Bgra8888 tipo di colore. Per questo motivo, aspettatevi di vedere alcuni risultati diversi su tale piattaforma!

È possibile eseguire il cast del valore restituito da ToPointer a un uint puntatore anziché da un byte puntatore. In questo modo è possibile accedere a un intero pixel in un'unica istruzione. L'applicazione dell'operatore ++ a tale puntatore lo incrementa di quattro byte per puntare al pixel successivo:

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);
    ···
}

Il pixel viene impostato usando il MakePixel metodo , che costruisce un pixel intero da componenti rosso, verde, blu e alfa. Tenere presente che il SKColorType.Rgba8888 formato ha un ordinamento dei byte pixel simile al seguente:

RR GG BB AA

Ma il numero intero corrispondente a tali byte è:

AABBGGRR

Il byte meno significativo dell'intero viene archiviato per primo in conformità con l'architettura little-endian. Questo MakePixel metodo non funzionerà correttamente per le bitmap con il Bgra8888 tipo di colore.

Il MakePixel metodo viene contrassegnato con l'opzione MethodImplOptions.AggressiveInlining per incoraggiare il compilatore a evitare di impostare questo metodo separato, ma di compilare il codice in cui viene chiamato il metodo . Ciò dovrebbe migliorare le prestazioni.

È interessante notare che la SKColor struttura definisce una conversione esplicita da SKColor a un intero senza segno, il che significa che è possibile creare un SKColor valore e una conversione da uint usare invece di 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;
}

L'unica domanda è questa: è il formato integer del SKColor valore nell'ordine del SKColorType.Rgba8888 tipo di colore o del SKColorType.Bgra8888 tipo di colore oppure è completamente diverso? La risposta a tale domanda sarà rivelata a breve.

Metodo SetPixels

SKBitmap definisce anche un metodo denominato SetPixels, che viene chiamato come segue:

bitmap.SetPixels(intPtr);

Tenere presente che GetPixels ottiene un IntPtr riferimento al blocco di memoria usato dalla bitmap per archiviarne i pixel. La SetPixels chiamata sostituisce il blocco di memoria con il blocco di memoria a cui fa riferimento l'oggetto IntPtrSetPixels specificato come argomento. La bitmap libera quindi il blocco di memoria usato in precedenza. La volta GetPixels successiva viene chiamata, ottiene il blocco di memoria impostato con SetPixels.

In un primo momento, sembra come se SetPixels non vi dà più potenza e prestazioni di GetPixels mentre essendo meno conveniente. Con GetPixels è possibile ottenere il blocco di memoria bitmap e accedervi. Con SetPixels l'allocazione e l'accesso ad alcune risorse di memoria, quindi impostare come blocco di memoria bitmap.

Tuttavia, l'uso SetPixels di offre un vantaggio sintattico distinto: consente di accedere ai bit di pixel bitmap usando una matrice. Ecco il metodo in GradientBitmapPage che illustra questa tecnica. Il metodo definisce innanzitutto una matrice di byte multidimensionale corrispondente ai byte dei pixel della bitmap. La prima dimensione è la riga, la seconda è la colonna e la terza dimensione corresond ai quattro componenti di ogni pixel:

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;
}

Quindi, dopo che la matrice è stata riempita con pixel, viene usato un unsafe blocco e un'istruzione fixed per ottenere un puntatore a byte che punta a questa matrice. Tale puntatore di byte può quindi essere eseguito il cast a un IntPtr oggetto da passare a SetPixels.

La matrice creata non deve essere una matrice di byte. Può essere una matrice integer con solo due dimensioni per la riga e la colonna:

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;
}

Il MakePixel metodo viene usato di nuovo per combinare i componenti di colore in un pixel a 32 bit.

Solo per completezza, ecco lo stesso codice, ma con un SKColor valore cast a un intero senza segno:

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;
}

Confronto tra le tecniche

Il costruttore della pagina Colore sfumatura chiama tutti e otto i metodi illustrati in precedenza e salva i risultati:

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;
    }
    ···
}

Il costruttore termina creando un oggetto SKCanvasView per visualizzare le bitmap risultanti. Il PaintSurface gestore divide la superficie in otto rettangoli e le chiamate Display per visualizzarne ognuna:

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);
        }
    }
}

Per consentire al compilatore di ottimizzare il codice, questa pagina è stata eseguita in modalità release . Ecco questa pagina in esecuzione su un simulatore i Telefono 8 in un macBook Pro, un telefono Nexus 5 Android e Surface Pro 3 che esegue Windows 10. A causa delle differenze hardware, evitare di confrontare i tempi di prestazioni tra i dispositivi, ma esaminare invece i tempi relativi in ogni dispositivo:

Bitmap sfumatura

Ecco una tabella che consolida i tempi di esecuzione in millisecondi:

API Tipo di dati iOS Android UWP
Setpixel 3,17 10.77 3.49
Pixel 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

Come previsto, la chiamata SetPixel di 65.536 volte è il modo meno efficace per impostare i pixel di una bitmap. Riempire una SKColor matrice e impostare la Pixels proprietà è molto meglio, e anche confronta favorevolmente con alcune delle GetPixels tecniche e SetPixels . L'uso dei uint valori pixel è in genere più veloce rispetto all'impostazione di componenti separati byte e la conversione del SKColor valore in un intero senza segno comporta un sovraccarico per il processo.

È anche interessante confrontare le varie sfumature: le prime righe di ogni piattaforma sono le stesse e mostrano la sfumatura desiderata. Ciò significa che il SetPixel metodo e la Pixels proprietà creano correttamente i pixel dai colori indipendentemente dal formato pixel sottostante.

Anche le due righe successive degli screenshot di iOS e Android sono le stesse, che confermano che il metodo little MakePixel è definito correttamente per il formato pixel predefinito Rgba8888 per queste piattaforme.

La riga inferiore degli screenshot di iOS e Android è all'indietro, che indica che l'intero senza segno ottenuto eseguendo il cast di un SKColor valore è nel formato:

AARRGGBB

I byte sono nell'ordine:

BB GG RR AA

Questo è l'ordinamento Bgra8888 anziché l'ordinamento Rgba8888 . Il Brga8888 formato è l'impostazione predefinita per la piattaforma UWP (Universal Windows Platform), motivo per cui le sfumature nell'ultima riga dello screenshot sono le stesse della prima riga. Tuttavia, le due righe centrali non sono corrette perché il codice che crea tali bitmap presuppone un Rgba8888 ordinamento.

Se vuoi usare lo stesso codice per accedere ai bit pixel in ogni piattaforma, puoi creare in modo esplicito un SKBitmap oggetto usando il Rgba8888 formato o Bgra8888 . Se si desidera eseguire il cast SKColor dei valori in pixel bitmap, usare Bgra8888.

Accesso casuale di pixel

I FillBitmapBytePtr metodi e FillBitmapUintPtr nella pagina Bitmap sfumatura hanno tratto vantaggio dai for cicli progettati per riempire la bitmap in sequenza, dalla riga superiore alla riga inferiore e in ogni riga da sinistra a destra. Il pixel può essere impostato con la stessa istruzione che ha incrementato il puntatore.

A volte è necessario accedere ai pixel in modo casuale anziché in sequenza. Se si usa l'approccio GetPixels , è necessario calcolare i puntatori in base alla riga e alla colonna. Questo è dimostrato nella pagina Del seno arcobaleno, che crea una bitmap che mostra un arcobaleno sotto forma di un ciclo di una curva seno.

I colori dell'arcobaleno sono più semplici da creare usando il modello di colore HSL (tonalità, saturazione, luminosità). Il SKColor.FromHsl metodo crea un SKColor valore usando valori di tonalità compresi tra 0 e 360 (come gli angoli di un cerchio, ma passando dal rosso, al verde e blu e al rosso) e i valori di saturazione e luminosità compresi tra 0 e 100. Per i colori di un arcobaleno, la saturazione deve essere impostata su un massimo di 100 e la luminosità su un punto medio di 50.

Rainbow Sine crea questa immagine eseguendo un ciclo tra le righe della bitmap e quindi eseguendo un ciclo tra 360 valori di tonalità. Da ogni valore di tonalità, calcola una colonna bitmap basata anche su un valore seno:

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);
    }
}

Si noti che il costruttore crea la bitmap in base al SKColorType.Bgra8888 formato :

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

In questo modo il programma può usare la conversione dei SKColor valori in uint pixel senza preoccuparsi. Anche se non gioca un ruolo in questo particolare programma, ogni volta che si usa la SKColor conversione per impostare i pixel, è necessario specificare SKAlphaType.Unpremul anche perché SKColor non esegue premoltiply i relativi componenti di colore in base al valore alfa.

Il costruttore usa quindi il GetPixels metodo per ottenere un puntatore al primo pixel della bitmap:

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

Per qualsiasi riga e colonna specifica, è necessario aggiungere un valore di offset a basePtr. Questo offset è la riga che raggiunge la larghezza della bitmap, più la colonna:

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

Il SKColor valore viene archiviato in memoria usando questo puntatore:

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

PaintSurface Nel gestore di SKCanvasView, la bitmap viene estesa per riempire l'area di visualizzazione:

Seno arcobaleno

Da una bitmap a un'altra

Molte attività di elaborazione delle immagini comportano la modifica dei pixel quando vengono trasferiti da una bitmap a un'altra. Questa tecnica viene illustrata nella pagina Regolazione colori. La pagina carica una delle risorse bitmap e quindi consente di modificare l'immagine usando tre Slider visualizzazioni:

Regolazione del colore

Per ogni colore di pixel, il primo Slider aggiunge un valore compreso tra 0 e 360 alla tonalità, ma quindi usa l'operatore modulo per mantenere il risultato compreso tra 0 e 360, spostando in modo efficace i colori lungo lo spettro (come illustrato nello screenshot UWP). Il secondo Slider consente di selezionare un fattore moltiplicativo compreso tra 0,5 e 2 da applicare alla saturazione e il terzo Slider esegue la stessa operazione per la luminosità, come illustrato nello screenshot di Android.

Il programma gestisce due bitmap, la bitmap di origine originale denominata srcBitmap e la bitmap di destinazione modificata denominata dstBitmap. Ogni volta che un Slider oggetto viene spostato, il programma calcola tutti i nuovi pixel in dstBitmap. Naturalmente, gli utenti proveranno spostando le Slider visualizzazioni molto rapidamente, in modo da ottenere le migliori prestazioni che è possibile gestire. Questo implica il GetPixels metodo per le bitmap di origine e di destinazione.

La pagina Regolazione colori non controlla il formato di colore delle bitmap di origine e di destinazione. Contiene invece una logica leggermente diversa per SKColorType.Rgba8888 i formati e SKColorType.Bgra8888 . L'origine e la destinazione possono essere formati diversi e il programma funzionerà comunque.

Ecco il programma ad eccezione del metodo cruciale TransferPixels che trasferisce i pixel formano l'origine alla destinazione. Il costruttore imposta dstBitmap su srcBitmap. Il PaintSurface gestore visualizza 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);
    }
}

Il ValueChanged gestore per le Slider visualizzazioni calcola i valori di regolazione e chiama TransferPixels.

L'intero TransferPixels metodo viene contrassegnato come unsafe. Inizia ottenendo puntatori di byte ai bit pixel di entrambe le bitmap e quindi scorre tutte le righe e le colonne. Dalla bitmap di origine, il metodo ottiene quattro byte per ogni pixel. Questi potrebbero trovarsi nell'ordine Rgba8888 o Bgra8888 . Il controllo del tipo di colore consente di creare un SKColor valore. I componenti HSL vengono quindi estratti, regolati e usati per ricreare il SKColor valore. A seconda che la bitmap di destinazione sia Rgba8888 o Bgra8888, i byte vengono archiviati nel bitmp di destinazione:

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;
                }
            }
        }
    }
    ···
}

È probabile che le prestazioni di questo metodo possano essere migliorate ancora di più creando metodi separati per le varie combinazioni di tipi di colore delle bitmap di origine e di destinazione ed evitare di controllare il tipo per ogni pixel. Un'altra opzione consiste nell'avere più for cicli per la col variabile in base al tipo di colore.

Posterizzazione

Un altro processo comune che prevede l'accesso ai bit pixel è la posterizzazione. Numero se i colori codificati in pixel di una bitmap vengono ridotti in modo che il risultato sia simile a un poster disegnato a mano usando una tavolozza di colori limitata.

La pagina Posterize esegue questo processo su una delle immagini di scimmia:

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;
    }
}

Il codice nel costruttore accede a ogni pixel, esegue un'operazione AND bit per bit con il valore 0xE0E0E0FF e quindi archivia nuovamente il risultato nella bitmap. I valori 0xE0E0E0FF mantiene alti 3 bit di ogni componente colore e imposta i 5 bit inferiori su 0. Anziché 2 24 o 16.777.216 colori, la bitmap viene ridotta a 29 o 512 colori:

Screenshot che mostra un'immagine posterizzata di una scimmia day su due dispositivi mobili e una finestra desktop.