Acceso a los bits de píxeles de mapas de bits de SkiaSharp
Como vio en el artículo Almacenamiento de mapas de bits de SkiaSharp en archivos, los mapas de bits se suelen almacenar en archivos en un formato comprimido, como JPEG o PNG. En cambio, los mapas de bits de SkiaSharp almacenados en memoria no se comprimen. Se almacenan como series secuenciales de píxeles. Este formato sin comprimir facilita la transferencia de los mapas de bits a una superficie de visualización.
El bloque de memoria ocupado por un mapa de bits SkiaSharp se organiza de forma muy sencilla: comienza con la primera fila de píxeles, de izquierda a derecha, y continúa con la segunda fila. En el caso de los mapas de bits a todo color, cada píxel consta de cuatro bytes, lo que significa que el espacio total de memoria necesario para el mapa de bits es cuatro veces el producto de su ancho y su alto.
En este artículo se describe cómo una aplicación puede obtener acceso a esos píxeles, ya sea directamente, accediendo al bloque de memoria de píxeles del mapa de bits, o indirectamente. En algunos casos, es posible que un programa tenga que analizar los píxeles de una imagen y crear un histograma de algún tipo. Generalmente, las aplicaciones pueden generar imágenes únicas mediante la creación algorítmica de los píxeles que componen el mapa de bits:
Técnicas
SkiaSharp proporciona varias técnicas para acceder a los bits de píxeles de un mapa de bits. La elección de una u otra suele ser una solución de compromiso entre la facilidad de codificación (que está relacionada con el mantenimiento y la facilidad de depuración) y el rendimiento. En la mayoría de los casos, se usa uno de los métodos y propiedades siguientes de SKBitmap
para acceder a los píxeles del mapa de bits:
- Los métodos
GetPixel
ySetPixel
permiten obtener o establecer el color de un solo píxel. - La propiedad
Pixels
obtiene una matriz de colores de píxeles de todo el mapa de bits o establece la matriz de colores. GetPixels
devuelve la dirección de la memoria de píxeles usada por el mapa de bits.SetPixels
reemplaza la dirección de la memoria de píxeles usada por el mapa de bits.
Las dos primeras técnicas pueden considerarse de "alto nivel" y las dos segundas, de "bajo nivel". Se pueden usar más propiedades y métodos, pero estos son los más valiosos.
Para poder ver las diferencias de rendimiento entre estas técnicas, la aplicación de ejemplo contiene la página denominada Mapa de bits de degradado que crea un mapa de bits con píxeles que combinan tonos rojos y azules para crear un degradado. El programa crea ocho copias diferentes de este mapa de bits, en las que se usan técnicas distintas para establecer los píxeles del mapa de bits. Cada uno de estos ocho mapas de bits se crea en un método independiente que también establece una breve descripción de texto de la técnica y calcula el tiempo necesario para establecer todos los píxeles. Cada método recorre en bucle 100 veces la lógica de configuración de píxeles para obtener una mejor estimación del rendimiento.
Método SetPixel
Si solo necesita establecer u obtener varios píxeles individuales, los métodos SetPixel
y GetPixel
son los más recomendables. Para cada uno de estos dos métodos, debe especificar la fila y la columna de enteros. Independientemente del formato de píxel, estos dos métodos le permiten obtener o establecer el píxel como un valor SKColor
:
bitmap.SetPixel(col, row, color);
SKColor color = bitmap.GetPixel(col, row);
El argumento col
debe oscilar entre 0 y un valor inferior al de la propiedad Width
del mapa de bits, mientras que row
debe oscilar entre 0 y un valor inferior al de la propiedad Height
.
Este es el método de Mapa de bits de degradado que establece el contenido de un mapa de bits mediante el método SetPixel
. El mapa de bits es de 256 por 256 píxeles y los bucles for
están codificados de forma rígida con el intervalo de valores:
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;
}
···
}
El conjunto de colores de cada píxel tiene un componente rojo igual a la columna de mapa de bits y un componente azul igual a la fila. El mapa de bits resultante es negro en la parte superior izquierda, rojo en la esquina superior derecha, azul en la parte inferior izquierda y magenta en la parte inferior derecha, con degradados en el resto.
Se llama al método SetPixel
65 536 veces, pero por muy eficaz que sea este método, no suele ser conveniente realizar tantas llamadas a la API si se dispone de una alternativa. Afortunadamente, existen varias alternativas.
Propiedad Pixels
SKBitmap
define una propiedad Pixels
que devuelve una matriz de valores SKColor
para todo el mapa de bits. También puede usar Pixels
para establecer una matriz de valores de color para el mapa de bits:
SKColor[] pixels = bitmap.Pixels;
bitmap.Pixels = pixels;
Los píxeles se organizan en la matriz comenzando por la primera fila, de izquierda a derecha, después la segunda fila, etc. El número total de colores de la matriz es igual al producto del ancho y alto del mapa de bits.
Aunque esta propiedad parece eficaz, hay que tener en cuenta que los píxeles se copian del mapa de bits en la matriz y, de nuevo, de la matriz en el mapa de bits, por lo que los píxeles se convierten desde (y a) valores SKColor
.
Este es el método de la clase GradientBitmapPage
que establece el mapa de bits mediante la propiedad Pixels
. El método asigna una matriz SKColor
del tamaño necesario, pero podría haber usado la propiedad Pixels
para crear esa matriz:
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;
}
Observe que el índice de la matriz pixels
debe calcularse a partir de las variables row
y col
. La fila se multiplica por el número de píxeles de cada fila (256 en este caso) y, luego, se agrega la columna.
SKBitmap
también define una propiedad Bytes
similar, que devuelve una matriz de bytes para todo el mapa de bits, pero es más compleja para los mapas de bits a todo color.
Puntero GetPixels
Posiblemente, la técnica más eficaz para acceder a los píxeles de mapa de bits es GetPixels
, que no debe confundirse con el método GetPixel
ni con la propiedad Pixels
. Con GetPixels
, observará inmediatamente una diferencia, ya que devuelve algo que no es muy común en la programación en C#:
IntPtr pixelsAddr = bitmap.GetPixels();
El tipo IntPtr
de .NET representa un puntero. Se llama IntPtr
porque es la longitud de un entero en el procesador nativo de la máquina en la que se ejecuta el programa, generalmente de 32 o 64 bits de longitud. El IntPtr
que GetPixels
devuelve es la dirección del bloque real de memoria que usa el objeto de mapa de bits para almacenar sus píxeles.
Puede convertir el IntPtr
un tipo de puntero de C# mediante el método ToPointer
. La sintaxis del puntero de C# es la misma que la de C y C++:
byte* ptr = (byte*)pixelsAddr.ToPointer();
La variable ptr
es de tipo puntero de byte. Esta variable ptr
permite acceder a los bytes individuales de memoria que se usan para almacenar los píxeles del mapa de bits. Para leer un byte de esta memoria o escribir un byte en ella, se usa un código como este:
byte pixelComponent = *ptr;
*ptr = pixelComponent;
En este contexto, el asterisco es el operador de direccionamiento indirecto de C# y se usa para hacer referencia al contenido de la memoria a la que apunta ptr
. Inicialmente, ptr
apunta al primer byte del primer píxel de la primera fila del mapa de bits, pero se pueden realizar operaciones aritméticas en la variable ptr
para moverlo a otras ubicaciones del mapa de bits.
Un inconveniente es que solo se puede usar esta variable ptr
en un bloque de código marcado con la palabra clave unsafe
. Además, se debe marcar que el ensamblado permite bloques inseguros. Esto se hace en las propiedades del proyecto.
El uso de punteros en C# es muy eficaz, pero también muy peligroso. Hay que tener cuidado de no acceder a más memoria que a la que el puntero debe hacer referencia. Este es el motivo por el que el uso del puntero está asociado a la palabra "unsafe".
Este es el método de la clase GradientBitmapPage
que usa el métodoGetPixels
. Observe el bloque de unsafe
que abarca todo el código con el puntero de bytes:
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;
}
Cuando la variable ptr
se obtiene por primera vez del método ToPointer
, apunta al primer byte del píxel situado más a la izquierda de la primera fila del mapa de bits. Los bucles for
de row
y col
se configuran de manera que se pueda incrementar ptr
con el operador ++
después de establecer cada byte de cada píxel. Para los otros 99 bucles a través de los píxeles, se debe volver a establecer el ptr
al principio del mapa de bits.
Cada píxel es de cuatro bytes de memoria, por lo que cada byte se debe establecer separado. El código aquí supone que los bytes siguen el orden rojo, verde, azul y alfa, que es coherente con el tipo de color SKColorType.Rgba8888
. Recuerde que este es el tipo de color predeterminado para iOS y Android, pero no para la Plataforma Universal de Windows (UWP). De forma predeterminada, la UWP crea mapas de bits con el tipo de color SKColorType.Bgra8888
. Por ello, es de esperar que los resultados sean diferentes en esa plataforma.
Se puede convertir el valor devuelto de ToPointer
a un puntero uint
en lugar de a un puntero byte
. Esto permite tener acceso a un píxel completo en una instrucción. Si se aplica el operador ++
a ese puntero, este se incrementa en cuatro bytes para apuntar al siguiente píxel:
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);
···
}
El píxel se establece mediante el método MakePixel
, que crea un píxel entero a partir de los componentes rojo, verde, azul y alfa. Tenga en cuenta que el formato SKColorType.Rgba8888
tiene un orden de bytes de píxeles similar al siguiente:
RR GG BB AA
Pero el entero correspondiente a esos bytes es el siguiente:
AABBGGRR
El byte menos significativo del entero se almacena primero de acuerdo con la arquitectura little-endian. Este método MakePixel
no funcionará correctamente para los mapas de bits con el tipo de color Bgra8888
.
El método MakePixel
se marca con la opción MethodImplOptions.AggressiveInlining
para que el compilador no lo convierta en un método independiente, sino que compile el código donde se llama al método. Esto debería mejorar el rendimiento.
Un detalle interesante es que la estructura SKColor
define una conversión explícita de SKColor
a un entero sin signo, lo que significa que se puede crear un valor SKColor
y se puede usar una conversión a uint
en lugar 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 única pregunta es esta: ¿Es el formato entero del valor SKColor
del orden del tipo de color SKColorType.Rgba8888
o el tipo de color SKColorType.Bgra8888
, o es algo completamente distinto? La respuesta a esta pregunta se revelará en breve.
Método SetPixels
SKBitmap
también define un método denominado SetPixels
, al que se llama de la siguiente manera:
bitmap.SetPixels(intPtr);
Recuerde que GetPixels
obtiene una IntPtr
que hace referencia al bloque de memoria usado por el mapa de bits para almacenar los píxeles. La llamada a SetPixels
reemplaza ese bloque de memoria por el bloque de memoria al que hace referencia el IntPtr
especificado como argumento de SetPixels
. A continuación, el mapa de bits libera el bloque de memoria que estaba usando anteriormente. La próxima vez que se llame a GetPixels
, se obtendrá el bloque de memoria establecido con SetPixels
.
Al principio, parece que SetPixels
no proporciona más eficacia y rendimiento que GetPixels
y que, además, resulta menos práctico. Con GetPixels
, se obtiene el bloque de memoria del mapa de bits y se accede a él. Con SetPixels
se asigna parte de la memoria y se accede a ella, para después establecerla como bloque de memoria del mapa de bits.
Pero el uso de SetPixels
ofrece una clara ventaja sintáctica: permite acceder a los bits de píxeles de mapa de bits mediante una matriz. Este es el método de GradientBitmapPage
que muestra esta técnica. El método define primero una matriz de bytes multidimensional correspondiente a los bytes de los píxeles del mapa de bits. La primera dimensión es la fila, la segunda dimensión es la columna y la tercera dimensión corresponde a los cuatro componentes de cada píxel:
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;
}
A continuación, después de rellenar la matriz con píxeles, se usa un bloque unsafe
y una instrucción fixed
para obtener un puntero de bytes que apunte a esta matriz. Ese puntero de bytes se puede convertir a un IntPtr
para pasarlo a SetPixels
.
No es necesario que la matriz que se cree sea una matriz de bytes. Puede ser una matriz de enteros con solo dos dimensiones para la fila y la columna:
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;
}
El método MakePixel
se usa de nuevo para combinar los componentes de color en un píxel de 32 bits.
Para ser más exhaustivos, este es el mismo código pero con un valor SKColor
convertido en un entero sin signo:
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;
}
Comparación de las técnicas
El constructor de la página Color de degradado llama a los ocho métodos mostrados anteriormente y guarda los resultados:
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;
}
···
}
El constructor concluye creando una SKCanvasView
para mostrar los mapas de bits resultantes. El controlador PaintSurface
divide su superficie en ocho rectángulos y llama a Display
para mostrar cada uno de ellos:
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);
}
}
}
Para permitir que el compilador optimice el código, esta página se ejecutó en modo de versión. Estas son las páginas que se ejecutan en un simulador del iPhone 8 en un MacBook Pro, un teléfono Android Nexus 5 y una Surface Pro 3 con Windows 10. Debido a las diferencias de hardware, no hay que comparar los tiempos de rendimiento entre los dispositivos, sino observar los tiempos relativos en cada uno de ellos:
En esta tabla se consolidan los tiempos de ejecución en milisegundos:
API | Tipo de datos | iOS | Android | UWP |
---|---|---|---|---|
SetPixel | 3,17 | 10.77 | 3.49 | |
Píxeles | 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 |
Como se esperaba, llamar a SetPixel
65 536 veces es la manera menos eficaz de establecer los píxeles de un mapa de bits. Es mucho mejor rellenar una matriz SKColor
y establecer la propiedad Pixels
, lo que, en comparación, es también preferible a algunas de las técnicas de GetPixels
y SetPixels
. Trabajar con valores de píxeles uint
suele ser más rápido que establecer componentes de byte
independientes, mientras que convertir el valor SKColor
en un entero sin signo agrega cierta sobrecarga al proceso.
También es interesante comparar los distintos degradados: las filas superiores de cada plataforma son las mismas y muestran el degradado que se esperaba. Esto significa que el método SetPixel
y la propiedad Pixels
crean correctamente los píxeles a partir de los colores independientemente del formato de píxel subyacente.
Las dos siguientes filas de las capturas de pantalla de iOS y Android también son las mismas, lo que confirma que el método sencillo MakePixel
está definido correctamente para el formato de píxel Rgba8888
predeterminado para estas plataformas.
La fila inferior de las capturas de pantalla de iOS y Android está al revés, lo que indica que el entero sin signo obtenido al convertir un valor SKColor
tiene el formato siguiente:
AARRGGBB
Los bytes están en este orden:
BB GG RR AA
Esta es la ordenación Bgra8888
, no la ordenación Rgba8888
. El formato Brga8888
es el predeterminado para la Plataforma universal de Windows, por lo que los degradados de la última fila de esa captura de pantalla son los mismos que los de la primera fila. Pero las dos filas centrales son incorrectas porque el código que crea esos mapas de bits supone una ordenación Rgba8888
.
Si desea usar el mismo código para acceder a bits de píxeles en cada plataforma, puede crear explícitamente un SKBitmap
con formato Rgba8888
o Bgra8888
. Si desea convertir valores SKColor
en píxeles de mapa de bits, use Bgra8888
.
Acceso aleatorio de píxeles
Los métodos FillBitmapBytePtr
y FillBitmapUintPtr
de la página Mapa de bits de degradado aprovechaban los bucles for
diseñados para rellenar el mapa de bits secuencialmente, desde la fila superior a la inferior y, en cada fila, de izquierda a derecha. El píxel podía establecerse con la misma instrucción que incrementaba el puntero.
A veces es necesario tener acceso a los píxeles de forma aleatoria en lugar de secuencial. Si usa el enfoque de GetPixels
, deberá calcular punteros en función de la fila y la columna. Esto se muestra en la página Rainbow Sine, que crea un mapa de bits que muestra un arcoíris en forma de ciclo de una curva sinusoidal.
Los colores del arcoíris son más fáciles de crear mediante el modelo de color HSL (matiz, saturación, luminosidad). El método SKColor.FromHsl
crea un valor SKColor
mediante valores de matiz que oscilan entre 0 y 360 (como los ángulos de un círculo, pero pasando de rojo a verde y azul y de vuelta a rojo) y valores de saturación y luminosidad que oscilan entre 0 y 100. Para los colores de un arcoíris, se debe establecer la saturación en un máximo de 100 y la luminosidad en un punto medio de 50.
Rainbow Sine crea esta imagen recorriendo en bucle las filas del mapa de bits y, después, 360 valores de matiz. A partir de cada valor de matiz, calcula una columna de mapa de bits que también se basa en un valor de 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);
}
}
Observe que el constructor crea el mapa de bits en función del formato SKColorType.Bgra8888
:
bitmap = new SKBitmap(360 * 3, 1024, SKColorType.Bgra8888, SKAlphaType.Unpremul);
Esto permite al programa convertir valores SKColor
en píxeles uint
sin problemas. Aunque no desempeña un papel en este programa en particular, siempre que se use la conversión de SKColor
para establecer píxeles, también se debe especificar SKAlphaType.Unpremul
, ya que SKColor
no premultiplica sus componentes de color por el valor alfa.
Después, el constructor usa el método GetPixels
para obtener un puntero al primer píxel del mapa de bits:
uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();
Para cualquier fila y columna en particular, se debe agregar un valor de desplazamiento a basePtr
. Este desplazamiento es la fila multiplicada por el ancho del mapa de bits, más la columna:
uint* ptr = basePtr + bitmap.Width * row + col;
El valor SKColor
se almacena en memoria mediante este puntero:
*ptr = (uint)SKColor.FromHsl(hue, 100, 50);
En el controlador PaintSurface
de SKCanvasView
, el mapa de bits se ajusta para rellenar el área de visualización:
De un mapa de bits a otro
Muchas tareas de procesamiento de imágenes implican la modificación de los píxeles a medida que se transfieren de un mapa de bits a otro. Esta técnica se muestra en la página Ajuste de color. La página carga uno de los recursos de mapa de bits y permite modificar la imagen mediante tres vistas de Slider
:
Para cada color de píxel, el primer Slider
agrega un valor de entre 0 y 360 al tono, pero luego usa el operador de módulo para mantener el resultado entre 0 y 360, cambiando eficazmente los colores a lo largo del espectro (como se muestra en la captura de pantalla de la Plataforma universal de Windows). El segundo Slider
permite seleccionar un factor multiplicativo entre 0,5 y 2 para aplicarlo a la saturación y el tercer Slider
hace lo mismo para la luminosidad, como se muestra en la captura de pantalla de Android.
El programa mantiene dos mapas de bits, el mapa de bits de origen inicial, denominado srcBitmap
, y el mapa de bits de destino ajustado, denominado dstBitmap
. Cada vez que se mueve un Slider
, el programa calcula todos los píxeles nuevos en dstBitmap
. Por supuesto, los usuarios experimentarán moviendo las vistas de Slider
muy rápidamente, por lo que es conveniente contar con el mejor rendimiento que se pueda administrar. Esto implica el método GetPixels
tanto para el mapa de bits de origen como para el de destino.
La página Ajuste de color no controla el formato de color de los mapas de bits de origen y destino. En lugar de ello, contiene una lógica ligeramente diferente para los formatos SKColorType.Rgba8888
y SKColorType.Bgra8888
. El origen y el destino pueden tener diferentes formatos y el programa seguirá funcionando.
Este es el programa, excepto el método fundamental TransferPixels
que transfiere los píxeles del origen al destino. El constructor establece dstBitmap
igual a srcBitmap
. El controlador PaintSurface
muestra 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);
}
}
El controlador ValueChanged
de las vistas de Slider
calcula los valores de ajuste y llama a TransferPixels
.
Todo el método TransferPixels
se marca como unsafe
. Comienza obteniendo punteros de bytes a los bits de píxeles de ambos mapas de bits y, después, recorre en bucle todas las filas y columnas. Desde el mapa de bits de origen, el método obtiene cuatro bytes para cada píxel. Estos pueden estar tanto en el orden Rgba8888
como en el orden Bgra8888
. La comprobación del tipo de color permite crear un valor SKColor
. Los componentes de HSL se extraen, se ajustan y se usan para volver a crear el valor SKColor
. En función de si el mapa de bits de destino es Rgba8888
o Bgra8888
, los bytes se almacenan en el mapa de bits de destino:
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;
}
}
}
}
···
}
Probablemente se pueda mejorar aún más el rendimiento de este método mediante la creación de métodos independientes para las distintas combinaciones de tipos de color de los mapas de bits de origen y destino, evitando que se compruebe el tipo de cada píxel. Otra opción es tener varios bucles for
para la variable col
en función del tipo de color.
Posterización
Otro trabajo habitual que implica el acceso a bits de píxeles es la posterización. Se reduce el número de colores codificados en los píxeles de un mapa de bits para que el resultado se parezca a un póster dibujado a mano con una paleta de colores limitada.
La página Posterizar realiza este proceso en una de las imágenes del mono:
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;
}
}
El código del constructor accede a cada píxel, realiza una operación AND bit a bit con el valor 0xE0E0E0FF y, después, almacena el resultado en el mapa de bits. El valor 0xE0E0E0FF mantiene los 3 bits altos de cada componente de color y establece los 5 bits bajos en 0. En lugar de 2 24 o 16 777 216 colores, el mapa de bits se reduce a 29 o 512 colores: