Partilhar via


Acessando bits de pixel de bitmap SkiaSharp

Como você viu no artigo Salvando bitmaps SkiaSharp em arquivos, os bitmaps geralmente são armazenados em arquivos em um formato compactado, como JPEG ou PNG. No constraste, um bitmap SkiaSharp armazenado na memória não é compactado. Ele é armazenado como uma série sequencial de pixels. Esse formato não compactado facilita a transferência de bitmaps para uma superfície de exibição.

O bloco de memória ocupado por um bitmap SkiaSharp é organizado de maneira muito direta: ele começa com a primeira linha de pixels, da esquerda para a direita, e depois continua com a segunda linha. Para bitmaps coloridos, cada pixel consiste em quatro bytes, o que significa que o espaço total de memória exigido pelo bitmap é quatro vezes o produto de sua largura e altura.

Este artigo descreve como um aplicativo pode obter acesso a esses pixels, diretamente acessando o bloco de memória de pixel do bitmap ou indiretamente. Em alguns casos, um programa pode querer analisar os pixels de uma imagem e construir um histograma de algum tipo. Mais comumente, os aplicativos podem construir imagens exclusivas criando algoritmicamente os pixels que compõem o bitmap:

Exemplos de Pixel Bits

As técnicas

O SkiaSharp fornece várias técnicas para acessar os bits de pixel de um bitmap. Qual deles você escolhe geralmente é um compromisso entre a conveniência de codificação (que está relacionada à manutenção e facilidade de depuração) e o desempenho. Na maioria dos casos, você usará um dos seguintes métodos e propriedades de para acessar os pixels do SKBitmap bitmap:

  • Os GetPixel métodos e SetPixel permitem que você obtenha ou defina a cor de um único pixel.
  • A Pixels propriedade obtém uma matriz de cores de pixel para o bitmap inteiro ou define a matriz de cores.
  • GetPixels Retorna o endereço da memória de pixel usada pelo bitmap.
  • SetPixels Substitui o endereço da memória de pixel usada pelo bitmap.

Você pode pensar nas duas primeiras técnicas como "alto nível" e as duas segundas como "baixo nível". Existem alguns outros métodos e propriedades que você pode usar, mas estes são os mais valiosos.

Para permitir que você veja as diferenças de desempenho entre essas técnicas, o aplicativo de exemplo contém uma página chamada Bitmap de gradiente que cria um bitmap com pixels que combinam tons de vermelho e azul para criar um gradiente. O programa cria oito cópias diferentes desse bitmap, todas usando técnicas diferentes para definir os pixels de bitmap. Cada um desses oito bitmaps é criado em um método separado que também define uma breve descrição de texto da técnica e calcula o tempo necessário para definir todos os pixels. Cada método percorre a lógica de configuração de pixels 100 vezes para obter uma melhor estimativa do desempenho.

O método SetPixel

Se você só precisa definir ou obter vários pixels individuais, os SetPixel métodos e GetPixel são ideais. Para cada um desses dois métodos, você especifica a coluna e a linha inteiras. Independentemente do formato de pixel, esses dois métodos permitem obter ou definir o pixel como um SKColor valor:

bitmap.SetPixel(col, row, color);

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

O col argumento deve variar de 0 a um a menos que a Width propriedade do bitmap e row varia de 0 a um a menos que a Height propriedade.

Este é o método em Gradient Bitmap que define o conteúdo de um bitmap usando o SetPixel método. O bitmap é de 256 por 256 pixels e os for loops são codificados com o 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;
    }
    ···
}

O conjunto de cores para cada pixel tem um componente vermelho igual à coluna de bitmap e um componente azul igual à linha. O bitmap resultante é preto no canto superior esquerdo, vermelho no canto superior direito, azul no canto inferior esquerdo e magenta no canto inferior direito, com gradientes em outros lugares.

O SetPixel método é chamado 65.536 vezes e, independentemente de quão eficiente esse método possa ser, geralmente não é uma boa ideia fazer tantas chamadas de API se uma alternativa estiver disponível. Felizmente, existem várias alternativas.

A propriedade Pixels

SKBitmap Define uma Pixels propriedade que retorna uma matriz de SKColor valores para o bitmap inteiro. Você também pode usar Pixels para definir uma matriz de valores de cor para o bitmap:

SKColor[] pixels = bitmap.Pixels;

bitmap.Pixels = pixels;

Os pixels são organizados na matriz começando com a primeira linha, da esquerda para a direita, depois a segunda linha e assim por diante. O número total de cores na matriz é igual ao produto da largura e altura do bitmap.

Embora essa propriedade pareça ser eficiente, lembre-se de que os pixels estão sendo copiados do bitmap para a matriz e da matriz de volta para o bitmap, e os pixels são convertidos de e para SKColor valores.

Aqui está o GradientBitmapPage método na classe que define o bitmap usando a Pixels propriedade. O método aloca uma SKColor matriz do tamanho necessário, mas poderia ter usado a Pixels propriedade para criar essa 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 o pixels índice da matriz precisa ser calculado a row partir das variáveis e col . A linha é multiplicada pelo número de pixels em cada linha (256 neste caso) e, em seguida, a coluna é adicionada.

SKBitmap também define uma propriedade semelhante Bytes , que retorna uma matriz de bytes para o bitmap inteiro, mas é mais complicado para bitmaps coloridos.

O ponteiro GetPixels

Potencialmente, a técnica mais poderosa para acessar os pixels de bitmap é GetPixels, não deve ser confundida com o GetPixel método ou a Pixels propriedade. Você notará imediatamente uma diferença no GetPixels fato de que ele retorna algo não muito comum na programação em C#:

IntPtr pixelsAddr = bitmap.GetPixels();

O tipo .NET IntPtr representa um ponteiro. É chamado IntPtr porque é o comprimento de um inteiro no processador nativo da máquina em que o programa é executado, geralmente 32 bits ou 64 bits de comprimento. O IntPtr que GetPixels retorna é o endereço do bloco real de memória que o objeto de bitmap está usando para armazenar seus pixels.

Você pode converter o IntPtr em um tipo de ponteiro C# usando o ToPointer método. A sintaxe do ponteiro C# é a mesma que C e C++:

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

A ptr variável é do tipo ponteiro de byte. Essa ptr variável permite que você acesse os bytes individuais de memória que são usados para armazenar os pixels do bitmap. Use um código como este para ler um byte dessa memória ou gravar um byte na memória:

byte pixelComponent = *ptr;

*ptr = pixelComponent;

Nesse contexto, o asterisco é o operador de indireção do C# e é usado para referenciar o conteúdo da memória apontado pelo ptr. Inicialmente, ptr aponta para o primeiro byte do primeiro pixel da primeira linha do bitmap, mas você pode executar aritmética na ptr variável para movê-la para outros locais dentro do bitmap.

Uma desvantagem é que você pode usar essa ptr variável apenas em um bloco de código marcado com a unsafe palavra-chave. Além disso, o conjunto deve ser sinalizado como permitindo blocos inseguros. Isso é feito nas propriedades do projeto.

Usar ponteiros em C# é muito poderoso, mas também muito perigoso. Você precisa ter cuidado para não acessar a memória além do que o ponteiro deve referenciar. É por isso que o uso do ponteiro está associado à palavra "inseguro".

Aqui está o método na GradientBitmapPage classe que usa o GetPixels método. Observe o unsafe bloco que engloba todo o código usando o ponteiro de 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 a ptr variável é obtida pela primeira vez do ToPointer método, ela aponta para o primeiro byte do pixel mais à esquerda da primeira linha do bitmap. Os for loops para e col são configurados para row que possam ser incrementados com o ++ operador depois que ptr cada byte de cada pixel é definido. Para os outros 99 loops através dos pixels, o ptr deve ser definido de volta para o início do bitmap.

Cada pixel tem quatro bytes de memória, portanto, cada byte deve ser definido separadamente. O código aqui assume que os bytes estão na ordem vermelho, verde, azul e alfa, o que é consistente com o SKColorType.Rgba8888 tipo de cor. Você deve se lembrar que esse é o tipo de cor padrão para iOS e Android, mas não para a Plataforma Universal do Windows. Por padrão, a UWP cria bitmaps com o SKColorType.Bgra8888 tipo de cor. Por esse motivo, espere ver alguns resultados diferentes nessa plataforma!

É possível converter o valor retornado de um uint ponteiro em vez de ToPointer um byte ponteiro. Isso permite que um pixel inteiro seja acessado em uma instrução. A aplicação do ++ operador a esse ponteiro o incrementa em quatro bytes para apontar para o próximo pixel:

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

O pixel é definido usando o MakePixel método, que constrói um pixel inteiro a partir de componentes vermelho, verde, azul e alfa. Lembre-se de que o SKColorType.Rgba8888 formato tem uma ordenação de bytes de pixel como esta:

RR GG BB AA

Mas o inteiro correspondente a esses bytes é:

AABBGGRR

O byte menos significativo do inteiro é armazenado primeiro de acordo com a arquitetura little-endian. Esse MakePixel método não funcionará corretamente para bitmaps com o Bgra8888 tipo de cor.

O MakePixel método é sinalizado com a MethodImplOptions.AggressiveInlining opção para incentivar o compilador a evitar tornar isso um método separado, mas em vez disso, para compilar o código onde o método é chamado. Isso deve melhorar o desempenho.

Curiosamente, a estrutura define uma conversão explícita de para um inteiro não assinado, o SKColor que significa que um SKColor valor pode ser criado e uma conversão para uint pode ser usada em vez deMakePixel:SKColor

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

A única questão é esta: o formato inteiro do SKColor valor está na ordem do tipo de cor, ou do SKColorType.Bgra8888 tipo de SKColorType.Rgba8888 cor, ou é algo totalmente diferente? A resposta a essa pergunta será revelada em breve.

O SetPixels Método

SKBitmap também define um método chamado SetPixels, que você chama assim:

bitmap.SetPixels(intPtr);

Lembre-se de que GetPixels obtém uma IntPtr referência ao bloco de memória usado pelo bitmap para armazenar seus pixels. A SetPixels chamada substitui esse bloco de memória pelo bloco de memória referenciado IntPtr pelo especificado como o SetPixels argumento. Em seguida, o bitmap libera o bloco de memória que estava usando anteriormente. Na próxima vez GetPixels que for chamado, ele obtém o bloco de memória definido com SetPixels.

No início, parece SetPixels que não lhe dá mais potência e desempenho do que GetPixels sendo menos conveniente. Com GetPixels você obter o bloco de memória bitmap e acessá-lo. Com SetPixels você alocar e acessar alguma memória e, em seguida, defini-lo como o bloco de memória bitmap.

Mas o uso SetPixels oferece uma vantagem sintática distinta: permite que você acesse os bits de pixel bitmap usando uma matriz. Aqui está o método em GradientBitmapPage que demonstra esta técnica. O método primeiro define uma matriz de bytes multidimensional correspondente aos bytes dos pixels do bitmap. A primeira dimensão é a linha, a segunda dimensão é a coluna e a terceira dimensão corresponde aos quatro componentes de cada 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;
}

Em seguida, depois que a matriz tiver sido preenchida com pixels, um unsafe bloco e uma fixed instrução serão usados para obter um ponteiro de byte que aponte para essa matriz. Esse ponteiro de byte pode então ser convertido em um IntPtr para passar para SetPixels.

A matriz que você cria não precisa ser uma matriz de bytes. Pode ser uma matriz inteira com apenas duas dimensões para a linha e a coluna:

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

O MakePixel método é novamente usado para combinar os componentes de cor em um pixel de 32 bits.

Apenas para completar, aqui está o mesmo código, mas com um SKColor valor convertido em um inteiro não assinado:

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

Comparando as técnicas

O construtor da página Gradient Color chama todos os oito métodos mostrados acima e salva os 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;
    }
    ···
}

O construtor conclui criando um SKCanvasView para exibir os bitmaps resultantes. O PaintSurface manipulador divide sua superfície em oito retângulos e chama Display para exibir cada um:

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 o compilador otimize o código, esta página foi executada no modo Release . Aqui está essa página em execução em um simulador do iPhone 8 em um MacBook Pro, um telefone Android Nexus 5 e Surface Pro 3 executando o Windows 10. Devido às diferenças de hardware, evite comparar os tempos de desempenho entre os dispositivos, mas observe os tempos relativos em cada dispositivo:

Bitmap de gradiente

Aqui está uma tabela que consolida os tempos de execução em milissegundos:

API Tipo de dados iOS Android UWP
SetPixel 3,17 10,77 3.49
Pixels 0,32 1.23 0,07
GetPixels byte 0,09 0.24 0,10
uint 0,06 0.26 0,05
SKColor 0.29 0,99 0,07
SetPixels byte 1.33 6.78 0,11
uint 0,14 0,69 0,06
SKColor 0,35 1,93 0,10

Como esperado, ligar SetPixel 65.536 vezes é a maneira menos eficaz de definir os pixels de um bitmap. Preencher uma SKColor matriz e definir a Pixels propriedade é muito melhor, e até se compara favoravelmente com algumas das GetPixels e SetPixels técnicas. Trabalhar com uint valores de pixel geralmente é mais rápido do que definir componentes separados byte , e converter o SKColor valor em um inteiro não assinado adiciona alguma sobrecarga ao processo.

Também é interessante comparar os vários gradientes: as linhas superiores de cada plataforma são as mesmas e mostram o gradiente como foi planejado. Isso significa que o SetPixel método e a Pixels propriedade criam corretamente pixels a partir de cores, independentemente do formato de pixel subjacente.

As próximas duas linhas das capturas de tela do iOS e Android também são as mesmas, o que confirma que o pequeno MakePixel método está definido corretamente para o formato de pixel padrão Rgba8888 para essas plataformas.

A linha inferior das capturas de tela do iOS e Android é invertida, o que indica que o inteiro não assinado obtido pela conversão de um SKColor valor está no formato:

AARRGGBB

Os bytes estão na ordem:

BB GG RR AA

Esta é a Bgra8888 ordenação e não a Rgba8888 ordem. O Brga8888 formato é o padrão para a plataforma Universal do Windows, e é por isso que os gradientes na última linha dessa captura de tela são os mesmos da primeira linha. Mas as duas linhas do meio estão incorretas porque o código que cria esses bitmaps assumiu uma Rgba8888 ordem.

Se você quiser usar o mesmo código para acessar bits de pixel em cada plataforma, poderá criar explicitamente um SKBitmap usando o Rgba8888 formato ou Bgra8888 . Se você quiser converter SKColor valores em pixels de bitmap, use Bgra8888.

Acesso aleatório de pixels

Os FillBitmapBytePtr métodos e FillBitmapUintPtr na página Bitmap de gradiente se beneficiaram de for loops projetados para preencher o bitmap sequencialmente, da linha superior para a linha inferior e em cada linha da esquerda para a direita. O pixel poderia ser definido com a mesma instrução que incrementou o ponteiro.

Às vezes, é necessário acessar os pixels aleatoriamente, em vez de sequencialmente. Se você estiver usando a GetPixels abordagem, precisará calcular ponteiros com base na linha e na coluna. Isso é demonstrado na página Rainbow Sine , que cria um bitmap mostrando um arco-íris na forma de um ciclo de uma curva senoidal.

As cores do arco-íris são mais fáceis de criar usando o modelo de cores HSL (matiz, saturação, luminosidade). O SKColor.FromHsl método cria um SKColor valor usando valores de matiz que variam de 0 a 360 (como os ângulos de um círculo, mas indo de vermelho, passando por verde e azul, e de volta para vermelho), e valores de saturação e luminosidade que variam de 0 a 100. Para as cores de um arco-íris, a saturação deve ser ajustada para um máximo de 100, e a luminosidade para um ponto médio de 50.

O Rainbow Sine cria essa imagem fazendo um loop pelas linhas do bitmap e, em seguida, fazendo um loop pelos valores de matiz 360. A partir de cada valor de matiz, ele calcula uma coluna de bitmap que também é baseada em um valor senoidal :

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 o construtor cria o bitmap com base no SKColorType.Bgra8888 formato:

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

Isso permite que o programa use a conversão de SKColor valores em uint pixels sem se preocupar. Embora ele não desempenhe um papel neste programa específico, sempre que você usar a SKColor conversão para definir pixels, você também deve especificar SKAlphaType.Unpremul porque SKColor não pré-multiplica seus componentes de cor pelo valor alfa.

Em seguida, o construtor usa o GetPixels método para obter um ponteiro para o primeiro pixel do bitmap:

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

Para qualquer linha e coluna específica, um valor de deslocamento deve ser adicionado ao basePtr. Esse deslocamento é a linha vezes a largura do bitmap, mais a coluna:

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

O SKColor valor é armazenado na memória usando este ponteiro:

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

PaintSurface No manipulador do , o bitmap é esticado para preencher a área de SKCanvasViewexibição:

Arco-íris Sine

De um bitmap para outro

Muitas tarefas de processamento de imagem envolvem a modificação de pixels à medida que eles são transferidos de um bitmap para outro. Essa técnica é demonstrada na página Ajuste de cor . A página carrega um dos recursos de bitmap e, em seguida, permite que você modifique a imagem usando três Slider modos de exibição:

Ajuste de cor

Para cada cor de pixel, o primeiro Slider adiciona um valor de 0 a 360 à tonalidade, mas depois usa o operador de módulo para manter o resultado entre 0 e 360, mudando efetivamente as cores ao longo do espectro (como a captura de tela UWP demonstra). O segundo Slider permite selecionar um fator multiplicativo entre 0,5 e 2 para aplicar à saturação, e o terceiro Slider faz o mesmo para a luminosidade, como demonstrado na captura de tela do Android.

O programa mantém dois bitmaps, o bitmap de origem original nomeado srcBitmap e o bitmap de destino ajustado chamado dstBitmap. Cada vez que um Slider é movido, o programa calcula todos os novos pixels no dstBitmap. É claro que os usuários experimentarão movendo as Slider exibições muito rapidamente, para que você queira o melhor desempenho que possa gerenciar. Isso envolve o GetPixels método para os bitmaps de origem e destino.

A página Ajuste de Cor não controla o formato de cores dos bitmaps de origem e destino. Em vez disso, ele contém lógica ligeiramente diferente para SKColorType.Rgba8888 e SKColorType.Bgra8888 formatos. A origem e o destino podem ser formatos diferentes, e o programa ainda funcionará.

Aqui está o programa, exceto pelo método crucial TransferPixels que transfere os pixels da origem para o destino. O construtor define dstBitmap igual a srcBitmap. O PaintSurface manipulador exibe 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);
    }
}

O ValueChanged manipulador para as Slider exibições calcula os valores de ajuste e chama TransferPixels.

O método inteiro TransferPixels é marcado como unsafe. Ele começa obtendo ponteiros de bytes para os bits de pixel de ambos os bitmaps e, em seguida, faz um loop por todas as linhas e colunas. A partir do bitmap de origem, o método obtém quatro bytes para cada pixel. Estes podem estar na Rgba8888 ordem ou Bgra8888 na ordem. A verificação do tipo de cor permite que um SKColor valor seja criado. Os componentes HSL são então extraídos, ajustados e usados para recriar o SKColor valor. Dependendo se o bitmap de destino é Rgba8888 ou Bgra8888, os bytes são armazenados no bitmp 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;
                }
            }
        }
    }
    ···
}

É provável que o desempenho desse método possa ser melhorado ainda mais criando métodos separados para as várias combinações de tipos de cores dos bitmaps de origem e destino e evite verificar o tipo para cada pixel. Outra opção é ter vários for loops para a col variável com base no tipo de cor.

Posterization

Outro trabalho comum que envolve o acesso a pixel bits é a posterização. O número de cores codificadas nos pixels de um bitmap é reduzido para que o resultado se assemelhe a um pôster desenhado à mão usando uma paleta de cores limitada.

A página Posterize executa esse processo em uma das imagens do macaco:

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

O código no construtor acessa cada pixel, executa uma operação bit a bit AND com o valor 0xE0E0E0FF e, em seguida, armazena o resultado de volta no bitmap. Os valores 0xE0E0E0FF mantém os 3 bits altos de cada componente de cor e define os 5 bits inferiores como 0. Em vez de 224 ou 16.777.216 cores, o bitmap é reduzido para 29 ou 512 cores:

A captura de tela mostra uma imagem de cartaz de um macaco de brinquedo em dois dispositivos móveis e uma janela da área de trabalho.