Acceso a los bits de píxeles de mapas de bits de SkiaSharp

Download SampleDescargar el ejemplo

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:

Pixel Bits Samples

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 y SetPixel 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 SkiaSharpFormsDemos 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 SetPixelsreemplaza 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:

Gradient Bitmap

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:

Rainbow Sine

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:

Color Adjustment

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:

Screenshot shows a posterize image of a toy monkey on two mobile devices and a desktop window.