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:
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 eSetPixel
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:
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 SKCanvasView
exibição:
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:
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: