Partager via


Accès aux bits de pixel bitmap SkiaSharp

Comme vous l’avez vu dans l’article Enregistrement des bitmaps SkiaSharp dans des fichiers, les bitmaps sont généralement stockées dans des fichiers dans un format compressé, tel que JPEG ou PNG. Dans la constrast, une bitmap SkiaSharp stockée en mémoire n’est pas compressée. Il est stocké sous la forme d’une série séquentielle de pixels. Ce format non compressé facilite le transfert de bitmaps vers une surface d’affichage.

Le bloc de mémoire occupé par une bitmap SkiaSharp est organisé de manière très simple : il commence par la première ligne de pixels, de gauche à droite, puis se poursuit avec la deuxième ligne. Pour les bitmaps de couleur complète, chaque pixel se compose de quatre octets, ce qui signifie que l’espace mémoire total requis par la bitmap est quatre fois le produit de sa largeur et de sa hauteur.

Cet article décrit comment une application peut accéder à ces pixels, soit directement en accédant au bloc de mémoire de pixels de la bitmap, soit indirectement. Dans certains cas, un programme peut souhaiter analyser les pixels d’une image et construire un histogramme d’une sorte. Plus souvent, les applications peuvent construire des images uniques en créant de manière algorithmique les pixels qui composent la bitmap :

Exemples de bits de pixels

Les techniques

SkiaSharp fournit plusieurs techniques pour accéder aux bits de pixels d’une bitmap. Celui que vous choisissez est généralement un compromis entre la commodité de codage (qui est liée à la maintenance et à la facilité de débogage) et aux performances. Dans la plupart des cas, vous allez utiliser l’une des méthodes et propriétés suivantes pour SKBitmap accéder aux pixels de la bitmap :

  • Les GetPixel méthodes et SetPixel les méthodes vous permettent d’obtenir ou de définir la couleur d’un pixel unique.
  • La Pixels propriété obtient un tableau de couleurs de pixels pour l’intégralité de la bitmap ou définit le tableau de couleurs.
  • GetPixels retourne l’adresse de la mémoire de pixel utilisée par la bitmap.
  • SetPixels remplace l’adresse de la mémoire de pixel utilisée par la bitmap.

Vous pouvez considérer les deux premières techniques comme « de haut niveau » et les deux secondes comme « bas niveau ». Il existe d’autres méthodes et propriétés que vous pouvez utiliser, mais elles sont les plus précieuses.

Pour vous permettre de voir les différences de performances entre ces techniques, l’exemple d’application contient une page nommée Bitmap dégradée qui crée une bitmap avec des pixels qui combinent des nuances rouges et bleues pour créer un dégradé. Le programme crée huit copies différentes de cette bitmap, toutes utilisant différentes techniques pour définir les pixels bitmap. Chacune de ces huit bitmaps est créée dans une méthode distincte qui définit également une brève description textuelle de la technique et calcule le temps nécessaire pour définir tous les pixels. Chaque méthode effectue une boucle dans la logique de définition de pixels 100 fois pour obtenir une meilleure estimation des performances.

Méthode SetPixel

Si vous avez uniquement besoin de définir ou d’obtenir plusieurs pixels individuels, les méthodes et GetPixel les SetPixel méthodes sont idéales. Pour chacune de ces deux méthodes, vous spécifiez la colonne et la ligne entières. Quel que soit le format de pixel, ces deux méthodes vous permettent d’obtenir ou de définir le pixel comme SKColor valeur :

bitmap.SetPixel(col, row, color);

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

L’argument col doit passer de 0 à un inférieur à la Width propriété de la bitmap, et row varie de 0 à un de moins que la Height propriété.

Voici la méthode dans Gradient Bitmap qui définit le contenu d’une bitmap à l’aide de la SetPixel méthode. La bitmap est de 256 à 256 pixels et les for boucles sont codées en dur avec la plage de valeurs :

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

La couleur définie pour chaque pixel a un composant rouge égal à la colonne bitmap et un composant bleu égal à la ligne. La bitmap résultante est noire en haut à gauche, rouge en haut à droite, bleu en bas à gauche et magenta en bas à droite, avec des dégradés ailleurs.

La SetPixel méthode est appelée 65 536 fois, et quelle que soit l’efficacité de cette méthode, il n’est généralement pas judicieux d’effectuer ce nombre d’appels d’API si une alternative est disponible. Heureusement, il existe plusieurs alternatives.

Propriété Pixels

SKBitmap définit une Pixels propriété qui retourne un tableau de valeurs pour l’intégralité de SKColor la bitmap. Vous pouvez également utiliser Pixels pour définir un tableau de valeurs de couleur pour l’image bitmap :

SKColor[] pixels = bitmap.Pixels;

bitmap.Pixels = pixels;

Les pixels sont disposés dans le tableau à partir de la première ligne, de gauche à droite, puis de la deuxième ligne, etc. Le nombre total de couleurs dans le tableau est égal au produit de la largeur et de la hauteur bitmap.

Bien que cette propriété semble être efficace, gardez à l’esprit que les pixels sont copiés à partir de la bitmap dans le tableau, et du tableau de retour dans la bitmap, et les pixels sont convertis de et en SKColor valeurs.

Voici la méthode de la GradientBitmapPage classe qui définit la bitmap à l’aide de la Pixels propriété. La méthode alloue un SKColor tableau de la taille requise, mais elle peut avoir utilisé la Pixels propriété pour créer ce tableau :

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

Notez que l’index du pixels tableau doit être calculé à partir des variables et col des row variables. La ligne est multipliée par le nombre de pixels dans chaque ligne (256 dans ce cas), puis la colonne est ajoutée.

SKBitmap définit également une propriété similaire Bytes , qui retourne un tableau d’octets pour l’intégralité de la bitmap, mais elle est plus fastidieuse pour les bitmaps de couleur complète.

Pointeur GetPixels

Potentiellement la technique la plus puissante pour accéder aux pixels bitmap est GetPixels, pas à confondre avec la GetPixel méthode ou la Pixels propriété. Vous remarquerez immédiatement une différence avec GetPixels celle-ci qui retourne quelque chose de peu courant dans la programmation C# :

IntPtr pixelsAddr = bitmap.GetPixels();

Le type .NET IntPtr représente un pointeur. Il est appelé IntPtr , car il s’agit de la longueur d’un entier sur le processeur natif de l’ordinateur sur lequel le programme est exécuté, généralement 32 bits ou 64 bits de longueur. GetPixels Cette IntPtr propriété renvoie l’adresse du bloc de mémoire réel utilisé par l’objet bitmap pour stocker ses pixels.

Vous pouvez convertir le IntPtr type de pointeur C# à l’aide de la ToPointer méthode. La syntaxe du pointeur C# est la même que C et C++ :

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

La ptr variable est de type pointeur d’octet. Cette ptr variable vous permet d’accéder aux octets individuels de mémoire utilisés pour stocker les pixels de la bitmap. Vous utilisez du code comme celui-ci pour lire un octet à partir de cette mémoire ou écrire un octet dans la mémoire :

byte pixelComponent = *ptr;

*ptr = pixelComponent;

Dans ce contexte, l’astérisque est l’opérateur indirection C# et est utilisé pour référencer le contenu de la mémoire pointée par ptr. Initialement, ptr pointe vers le premier octet du premier pixel de la première ligne de la bitmap, mais vous pouvez effectuer des opérations arithmétiques sur la variable pour la ptr déplacer vers d’autres emplacements dans la bitmap.

L’un des inconvénients est que vous pouvez utiliser cette ptr variable uniquement dans un bloc de code marqué avec le unsafe mot clé. En outre, l’assembly doit être marqué comme autorisant des blocs non sécurisés. Cette opération est effectuée dans les propriétés du projet.

L’utilisation de pointeurs en C# est très puissante, mais aussi très dangereuse. Vous devez faire attention à ne pas accéder à la mémoire au-delà de ce que le pointeur est censé référencer. C’est pourquoi l’utilisation du pointeur est associée au mot « non sécurisé ».

Voici la méthode dans la GradientBitmapPage classe qui utilise la GetPixels méthode. Notez le unsafe bloc qui englobe tout le code à l’aide du pointeur d’octet :

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

Lorsque la variable est obtenue pour la ptr première fois à partir de la ToPointer méthode, elle pointe vers le premier octet du pixel le plus à gauche de la première ligne de la bitmap. Les for boucles pour row et col sont configurées afin qu’elles ptr puissent être incrémentées avec l’opérateur ++ après chaque octet de chaque pixel défini. Pour les 99 autres boucles à travers les pixels, vous ptr devez revenir au début de la bitmap.

Chaque pixel est quatre octets de mémoire. Chaque octet doit donc être défini séparément. Le code suppose ici que les octets sont dans l’ordre rouge, vert, bleu et alpha, qui est cohérent avec le type de SKColorType.Rgba8888 couleur. Vous pouvez vous rappeler qu’il s’agit du type de couleur par défaut pour iOS et Android, mais pas pour les plateforme Windows universelle. Par défaut, UWP crée des bitmaps avec le type de SKColorType.Bgra8888 couleur. Pour cette raison, attendez-vous à voir des résultats différents sur cette plateforme !

Il est possible de convertir la valeur retournée par ToPointer un uint pointeur plutôt qu’un byte pointeur. Cela permet d’accéder à un pixel entier dans une instruction. L’application de l’opérateur ++ à ce pointeur l’incrémente de quatre octets pour pointer vers le pixel suivant :

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

Le pixel est défini à l’aide de la MakePixel méthode, qui construit un pixel entier à partir de composants alpha, rouges, verts, bleus et alpha. N’oubliez pas que le SKColorType.Rgba8888 format comporte un ordre d’octets de pixels comme suit :

RR GG BB AA

Mais l’entier correspondant à ces octets est :

AABBGGRR

L’octet le moins significatif de l’entier est stocké en premier conformément à l’architecture little-endian. Cette MakePixel méthode ne fonctionne pas correctement pour les bitmaps avec le type de Bgra8888 couleur.

La MakePixel méthode est marquée avec l’option MethodImplOptions.AggressiveInlining permettant d’encourager le compilateur à éviter d’en faire une méthode distincte, mais au lieu de compiler le code où la méthode est appelée. Cela devrait améliorer les performances.

Il est intéressant de noter que la SKColor structure définit une conversion explicite d’un SKColor entier non signé, ce qui signifie qu’une SKColor valeur peut être créée et qu’une conversion à utiliser au uint lieu de 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;
}

La seule question est la suivante : Est-ce que le format entier de la SKColor valeur dans l’ordre du type de SKColorType.Rgba8888 couleur, ou le type de SKColorType.Bgra8888 couleur, ou est-ce quelque chose d’autre entièrement ? La réponse à cette question sera révélée sous peu.

SetPixels, méthode

SKBitmap définit également une méthode nommée SetPixels, que vous appelez comme suit :

bitmap.SetPixels(intPtr);

Rappelez-vous qu’il GetPixels obtient un IntPtr référencement du bloc de mémoire utilisé par la bitmap pour stocker ses pixels. L’appel SetPixels remplace ce bloc de mémoire par le bloc de mémoire référencé par l’argument IntPtr SetPixels spécifié. La bitmap libère ensuite le bloc de mémoire utilisé précédemment. La prochaine fois GetPixels qu’elle est appelée, elle obtient le jeu de blocs de mémoire avec SetPixels.

Au début, il semble que vous SetPixels ne donne pas plus de puissance et de performance que GetPixels tout en étant moins pratique. Avec GetPixels vous obtenez le bloc de mémoire bitmap et accédez-y. Avec SetPixels vous allouez et accédez à une certaine mémoire, puis définissez-le comme bloc de mémoire bitmap.

Mais l’utilisation SetPixels offre un avantage syntactique distinct : elle vous permet d’accéder aux bits de pixel bitmap à l’aide d’un tableau. Voici la méthode qui GradientBitmapPage illustre cette technique. La méthode définit d’abord un tableau d’octets multidimensionnel correspondant aux octets des pixels de la bitmap. La première dimension est la ligne, la deuxième dimension est la colonne, et la troisième dimension correspond aux quatre composants de chaque 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;
}

Ensuite, une fois le tableau rempli de pixels, un unsafe bloc et une fixed instruction sont utilisés pour obtenir un pointeur d’octet qui pointe vers ce tableau. Ce pointeur d’octet peut ensuite être converti en un IntPtr à passer à SetPixels.

Le tableau que vous créez n’a pas besoin d’être un tableau d’octets. Il peut s’agir d’un tableau entier avec seulement deux dimensions pour la ligne et la colonne :

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

La MakePixel méthode est à nouveau utilisée pour combiner les composants de couleur en pixels 32 bits.

Juste pour l’exhaustivité, voici le même code, mais avec une SKColor conversion de valeur en entier non signé :

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

Comparaison des techniques

Le constructeur de la page De couleur de dégradé appelle les huit méthodes indiquées ci-dessus et enregistre les résultats :

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

Le constructeur conclut en créant un SKCanvasView pour afficher les bitmaps résultantes. Le PaintSurface gestionnaire divise sa surface en huit rectangles et appelle Display chacun d’eux :

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

Pour permettre au compilateur d’optimiser le code, cette page a été exécutée en mode Mise en production. Voici cette page s’exécutant sur un simulateur i Téléphone 8 sur un MacBook Pro, un téléphone Nexus 5 Android et Surface Pro 3 exécutant Windows 10. En raison des différences matérielles, évitez de comparer les temps de performances entre les appareils, mais examinez plutôt les heures relatives sur chaque appareil :

Bitmap dégradée

Voici une table qui consolide les temps d’exécution en millisecondes :

API Type de données iOS Android UWP
SetPixel 3.17 10,77 3.49
Pixels 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

Comme prévu, l’appel SetPixel de 65 536 fois est le moyen le moins effeicient de définir les pixels d’une bitmap. Remplir un SKColor tableau et définir la Pixels propriété est beaucoup mieux, et même compare favorablement certaines des techniquesGetPixels.SetPixels L’utilisation de uint valeurs de pixels est généralement plus rapide que la définition de composants distincts byte et la conversion de la SKColor valeur en entier non signé ajoute une surcharge au processus.

Il est également intéressant de comparer les différents dégradés : les lignes supérieures de chaque plateforme sont identiques et montrent le dégradé tel qu’il était prévu. Cela signifie que la méthode et la SetPixel Pixels propriété créent correctement des pixels à partir de couleurs, quel que soit le format de pixel sous-jacent.

Les deux lignes suivantes des captures d’écran iOS et Android sont également identiques, ce qui confirme que la petite MakePixel méthode est correctement définie pour le format de pixel par défaut Rgba8888 pour ces plateformes.

La ligne inférieure des captures d’écran iOS et Android est en arrière, ce qui indique que l’entier non signé obtenu en cas de conversion d’une SKColor valeur se trouve sous la forme :

AARRGGBB

Les octets sont dans l’ordre :

BB GG RR AA

Il s’agit de l’ordre Bgra8888 plutôt que de l’ordre Rgba8888 . Le Brga8888 format est la valeur par défaut de la plateforme Windows universelle, c’est pourquoi les dégradés de la dernière ligne de cette capture d’écran sont identiques à la première ligne. Toutefois, les deux lignes intermédiaires sont incorrectes, car le code qui crée ces bitmaps suppose un Rgba8888 classement.

Si vous souhaitez utiliser le même code pour accéder aux bits de pixels sur chaque plateforme, vous pouvez créer explicitement un SKBitmap code à l’aide du ou Bgra8888 du Rgba8888 format. Si vous souhaitez convertir SKColor des valeurs en pixels bitmap, utilisez Bgra8888.

Accès aléatoire des pixels

Les FillBitmapBytePtr méthodes et FillBitmapUintPtr méthodes de la page Bitmap dégradée ont bénéficié de for boucles conçues pour remplir séquentiellement la bitmap, de la ligne supérieure à la ligne inférieure, et dans chaque ligne de gauche à droite. Le pixel peut être défini avec la même instruction que celle qui a incrémenté le pointeur.

Il est parfois nécessaire d’accéder aux pixels de manière aléatoire plutôt que séquentielle. Si vous utilisez l’approche GetPixels , vous devez calculer des pointeurs en fonction de la ligne et de la colonne. Ceci est illustré dans la page de sinus arc-en-ciel, qui crée une bitmap montrant un arc-en-ciel sous la forme d’un cycle d’une courbe de sinus.

Les couleurs de l’arc-en-ciel sont plus simples à créer à l’aide du modèle de couleur HSL (teinte, saturation, luminosité). La SKColor.FromHsl méthode crée une SKColor valeur à l’aide de valeurs de teinte comprises entre 0 et 360 (comme les angles d’un cercle, mais allant du rouge, du vert et du bleu, et de retour en rouge), ainsi que des valeurs de saturation et de luminosité allant de 0 à 100. Pour les couleurs d’un arc-en-ciel, la saturation doit être définie sur un maximum de 100, et la luminosité à un point moyen de 50.

Rainbow Sine crée cette image en boucle sur les lignes de l’image bitmap, puis en boucle sur 360 valeurs de teinte. À partir de chaque valeur de teinte, elle calcule une colonne bitmap qui est également basée sur une valeur sinusoïdale :

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

Notez que le constructeur crée la bitmap en fonction du SKColorType.Bgra8888 format :

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

Cela permet au programme d’utiliser la conversion de SKColor valeurs en uint pixels sans vous soucier. Bien qu’il ne joue pas de rôle dans ce programme particulier, chaque fois que vous utilisez la SKColor conversion pour définir des pixels, vous devez également spécifier SKAlphaType.Unpremul , car SKColor ne prémultiply pas ses composants de couleur par la valeur alpha.

Le constructeur utilise ensuite la GetPixels méthode pour obtenir un pointeur vers le premier pixel de la bitmap :

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

Pour toute ligne et colonne particulière, une valeur de décalage doit être ajoutée à basePtr. Ce décalage est la ligne qui timese la largeur bitmap, ainsi que la colonne :

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

La SKColor valeur est stockée en mémoire à l’aide de ce pointeur :

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

Dans le PaintSurface gestionnaire du SKCanvasView, la bitmap est étirée pour remplir la zone d’affichage :

Sine arc-en-ciel

D’une bitmap à une autre

De nombreuses tâches de traitement d’images impliquent de modifier des pixels au fur et à mesure qu’elles sont transférées d’une image bitmap à une autre. Cette technique est illustrée dans la page Ajustement des couleurs. La page charge l’une des ressources bitmap, puis vous permet de modifier l’image à l’aide de trois Slider vues :

Ajustement des couleurs

Pour chaque couleur de pixel, la première Slider ajoute une valeur comprise entre 0 et 360 à la teinte, mais utilise ensuite l’opérateur modulo pour conserver le résultat compris entre 0 et 360, déplaçant efficacement les couleurs le long du spectre (comme le montre la capture d’écran UWP). La deuxième Slider vous permet de sélectionner un facteur multiplicatif compris entre 0,5 et 2 pour s’appliquer à la saturation, et le troisième Slider fait de même pour la luminosité, comme illustré dans la capture d’écran Android.

Le programme gère deux bitmaps, la bitmap source d’origine nommée srcBitmap et la bitmap de destination ajustée nommée dstBitmap. Chaque fois qu’un Slider est déplacé, le programme calcule tous les nouveaux pixels en dstBitmap. Bien sûr, les utilisateurs vont expérimenter en déplaçant Slider les vues très rapidement, de sorte que vous souhaitez obtenir les meilleures performances que vous pouvez gérer. Cela implique la GetPixels méthode pour les bitmaps source et de destination.

La page Ajustement des couleurs ne contrôle pas le format de couleur des bitmaps source et de destination. Au lieu de cela, il contient une logique légèrement différente pour et SKColorType.Bgra8888 des SKColorType.Rgba8888 formats. La source et la destination peuvent être différents formats, et le programme fonctionnera toujours.

Voici le programme, à l’exception de la méthode cruciale TransferPixels qui transfère les pixels sous forme de source à la destination. Le constructeur définit dstBitmap la srcBitmapvaleur . Le PaintSurface gestionnaire affiche 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);
    }
}

Le ValueChanged gestionnaire des Slider vues calcule les valeurs d’ajustement et les appels TransferPixels.

La méthode entière TransferPixels est marquée comme unsafe. Il commence par obtenir des pointeurs d’octets vers les bits de pixels des deux bitmaps, puis effectue une boucle dans toutes les lignes et colonnes. À partir de la bitmap source, la méthode obtient quatre octets pour chaque pixel. Ils pourraient être dans l’ordre ou Bgra8888 dans l’ordreRgba8888. La vérification du type de couleur permet de créer une SKColor valeur. Les composants HSL sont ensuite extraits, ajustés et utilisés pour recréer la SKColor valeur. Selon que la bitmap de destination est Rgba8888 ou Bgra8888, les octets sont stockés dans le bitmp de destination :

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

Il est probable que les performances de cette méthode puissent être améliorées encore plus en créant des méthodes distinctes pour les différentes combinaisons de types de couleurs des bitmaps source et de destination, et éviter de case activée le type pour chaque pixel. Une autre option consiste à avoir plusieurs for boucles pour la col variable en fonction du type de couleur.

Postérisation

Un autre travail courant qui implique l’accès aux bits de pixels est la posterisation. Nombre si les couleurs encodées dans les pixels d’une bitmap sont réduites afin que le résultat ressemble à une affiche dessinée à la main à l’aide d’une palette de couleurs limitée.

La page Posterize effectue ce processus sur l’une des images de singe :

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

Le code du constructeur accède à chaque pixel, effectue une opération AND au niveau du bit avec la valeur 0xE0E0E0FF, puis stocke le résultat dans la bitmap. Les valeurs 0xE0E0E0FF conservent les 3 bits élevés de chaque composant de couleur et définissent les 5 bits inférieurs sur 0. Au lieu de 24 ou 16 777 216 couleurs, la bitmap est réduite à 29 ou 512 couleurs :

Capture d’écran montrant une image posterize d’un singe toy sur deux appareils mobiles et une fenêtre de bureau.