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 :
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 etSetPixel
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 :
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 :
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 :
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 srcBitmap
valeur . 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 :