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:
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 eSetPixel
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 ptr
cui 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 IntPtr
SetPixels
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:
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:
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:
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: