SkiaSharp 비트맵 픽셀 비트 액세스
SkiaSharp 비트맵을 파일에 저장하는 문서에서 본 것처럼 비트맵은 일반적으로 JPEG 또는 PNG와 같은 압축된 형식의 파일에 저장됩니다. constrast에서 메모리에 저장된 SkiaSharp 비트맵은 압축되지 않습니다. 순차적 픽셀 시리즈로 저장됩니다. 이 압축되지 않은 형식은 비트맵을 디스플레이 화면으로 쉽게 전송할 수 있도록 합니다.
SkiaSharp 비트맵이 차지하는 메모리 블록은 매우 간단한 방식으로 구성됩니다. 이 블록은 왼쪽에서 오른쪽으로 픽셀의 첫 번째 행으로 시작한 다음 두 번째 행으로 계속됩니다. 전체 색 비트맵의 경우 각 픽셀은 4바이트로 구성됩니다. 즉, 비트맵에 필요한 총 메모리 공간은 너비와 높이의 4배에 해당합니다.
이 문서에서는 애플리케이션이 비트맵의 픽셀 메모리 블록에 직접 액세스하거나 간접적으로 해당 픽셀에 액세스할 수 있는 방법을 설명합니다. 경우에 따라 프로그램에서 이미지의 픽셀을 분석하고 일종의 히스토그램을 생성하려고 할 수 있습니다. 더 일반적으로 애플리케이션은 비트맵을 구성하는 픽셀을 알고리즘으로 만들어 고유한 이미지를 생성할 수 있습니다.
기술
SkiaSharp은 비트맵의 픽셀 비트에 액세스하기 위한 몇 가지 기술을 제공합니다. 일반적으로 선택하는 것은 코딩 편의성(기본테넌트 및 디버깅의 용이성과 관련됨)과 성능 간의 절충안입니다. 대부분의 경우 비트맵의 픽셀에 액세스하기 위해 다음 메서드 및 속성 SKBitmap
중 하나를 사용합니다.
GetPixel
및SetPixel
메서드를 사용하면 단일 픽셀의 색을 가져오거나 설정할 수 있습니다.- 이 속성은
Pixels
전체 비트맵에 대한 픽셀 색 배열을 가져오거나 색 배열을 설정합니다. GetPixels
는 비트맵에서 사용하는 픽셀 메모리의 주소를 반환합니다.SetPixels
는 비트맵에서 사용되는 픽셀 메모리의 주소를 바꿉니다.
처음 두 기술은 "상위 수준"으로, 두 번째 기술은 "낮은 수준"으로 생각할 수 있습니다. 사용할 수 있는 몇 가지 다른 메서드와 속성이 있지만 가장 유용합니다.
이러한 기술 간의 성능 차이를 볼 수 있도록 샘플 애플리케이션에는 그라데이션을 만들기 위해 빨간색과 파란색 음영을 결합하는 픽셀이 있는 비트맵을 만드는 그라데이션 비트맵이라는 페이지가 포함되어 있습니다. 이 프로그램은 비트맵 픽셀을 설정하기 위한 다양한 기술을 사용하여 이 비트맵의 8개 복사본을 만듭니다. 이러한 8개의 비트맵은 각각 기술에 대한 간략한 텍스트 설명을 설정하고 모든 픽셀을 설정하는 데 필요한 시간을 계산하는 별도의 메서드로 만들어집니다. 각 메서드는 픽셀 설정 논리를 100번 반복하여 성능을 더 잘 예측합니다.
SetPixel 메서드
여러 개별 픽셀만 설정하거나 가져와야 하는 경우 해당 및 GetPixel
메서드가 SetPixel
이상적입니다. 이러한 두 메서드 각각에 대해 정수 열과 행을 지정합니다. 픽셀 형식에 관계없이 다음 두 메서드를 사용하면 픽셀을 가져오거나 값으로 SKColor
설정할 수 있습니다.
bitmap.SetPixel(col, row, color);
SKColor color = bitmap.GetPixel(col, row);
인수의 범위는 col
비트맵 row
의 속성보다 0에서 1까지 Width
이고 범위는 속성보다 0에서 1까지 Height
여야 합니다.
다음은 메서드를 사용하여 SetPixel
비트맵의 내용을 설정하는 그라데이션 비트맵의 메서드입니다. 비트맵은 256 x 256 픽셀이며 for
루프는 값 범위로 하드 코딩됩니다.
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;
}
···
}
각 픽셀의 색 집합에는 비트맵 열과 같은 빨간색 구성 요소와 행과 같은 파란색 구성 요소가 있습니다. 결과 비트맵은 왼쪽 위에서 검은색, 오른쪽 위에 빨간색, 왼쪽 아래의 파란색, 오른쪽 아래의 마젠타, 다른 곳에서 그라데이션이 있는 마젠타입니다.
이 SetPixel
메서드는 65,536번 호출되며, 이 메서드의 효율성에 관계없이 대체 방법을 사용할 수 있는 경우 많은 API 호출을 수행하는 것은 일반적으로 좋지 않습니다. 다행히도 몇 가지 대안이 있습니다.
Pixels 속성
SKBitmap
는 Pixels
전체 비트맵에 SKColor
대한 값 배열을 반환하는 속성을 정의합니다. 비트맵에 대한 색 값 배열을 설정하는 데 사용할 Pixels
수도 있습니다.
SKColor[] pixels = bitmap.Pixels;
bitmap.Pixels = pixels;
픽셀은 첫 번째 행부터 시작하여 왼쪽에서 오른쪽으로, 두 번째 행 등 배열에 정렬됩니다. 배열의 총 색 수는 비트맵 너비 및 높이의 곱과 같습니다.
이 속성은 효율적인 것처럼 보이지만 픽셀이 비트맵에서 배열로 복사되고 배열에서 비트맵으로 다시 복사되고 픽셀이 값에서 SKColor
값으로 변환됩니다.
다음은 속성을 사용하여 비트맵을 GradientBitmapPage
설정하는 클래스의 메서드입니다 Pixels
. 메서드는 필요한 크기의 배열을 SKColor
할당하지만 이 속성을 사용하여 해당 배열을 Pixels
만들 수 있습니다.
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;
}
배열의 pixels
인덱스는 및 col
변수에서 row
계산해야 합니다. 행에 각 행의 픽셀 수(이 경우 256)를 곱한 다음 열이 추가됩니다.
SKBitmap
또한 전체 비트맵에 대한 바이트 배열을 반환하는 유사한 Bytes
속성을 정의하지만 전체 색 비트맵의 경우 더 번거롭습니다.
GetPixels 포인터
비트맵 픽셀GetPixels
에 액세스하는 가장 강력한 기술은 메서드 또는 Pixels
속성과 GetPixel
혼동하지 않는 것입니다. C# 프로그래밍에서 매우 일반적이지 않은 항목을 반환한다는 측면에서 차이점 GetPixels
을 즉시 확인할 수 있습니다.
IntPtr pixelsAddr = bitmap.GetPixels();
.NET IntPtr
형식은 포인터를 나타냅니다. 프로그램이 실행되는 컴퓨터의 네이티브 프로세서에서 정수 길이(일반적으로 32비트 또는 길이 64비트)이므로 호출 IntPtr
됩니다. IntPtr
반환되는 GetPixels
주소는 비트맵 개체가 해당 픽셀을 저장하는 데 사용하는 실제 메모리 블록의 주소입니다.
메서드를 IntPtr
사용하여 C# 포인터 형식으로 변환할 ToPointer
수 있습니다. C# 포인터 구문은 C 및 C++와 동일합니다.
byte* ptr = (byte*)pixelsAddr.ToPointer();
ptr
변수는 바이트 포인터 형식입니다. 이 ptr
변수를 사용하면 비트맵의 픽셀을 저장하는 데 사용되는 개별 메모리 바이트에 액세스할 수 있습니다. 다음과 같은 코드를 사용하여 이 메모리에서 바이트를 읽거나 메모리에 바이트를 씁니다.
byte pixelComponent = *ptr;
*ptr = pixelComponent;
이 컨텍스트에서 별표는 C# 간접 참조 연산 자이며 가리키는 메모리의 내용을 참조하는 ptr
데 사용됩니다. 처음에는 ptr
비트맵의 첫 번째 행에 있는 첫 번째 픽셀의 첫 번째 바이트를 가리키지만 변수에 대한 ptr
산술 연산을 수행하여 비트맵 내의 다른 위치로 이동할 수 있습니다.
한 가지 단점은 키워드(keyword) 표시된 코드 블록에서만 이 ptr
변수를 unsafe
사용할 수 있다는 것입니다. 또한 안전하지 않은 블록을 허용하는 것으로 어셈블리에 플래그를 지정해야 합니다. 이 작업은 프로젝트의 속성에서 수행됩니다.
C#에서 포인터를 사용하는 것은 매우 강력하지만 매우 위험합니다. 포인터가 참조해야 하는 것 이상으로 메모리에 액세스하지 않도록 주의해야 합니다. 포인터 사용이 "unsafe"라는 단어와 연결된 이유입니다.
메서드를 사용하는 클래스의 GradientBitmapPage
메서드는 GetPixels
다음과 같습니다. 바이트 포인터를 unsafe
사용하여 모든 코드를 포괄하는 블록을 확인합니다.
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;
}
메서드에서 ToPointer
변수를 ptr
처음 가져오는 경우 비트맵의 첫 번째 행에서 가장 왼쪽 픽셀의 첫 번째 바이트를 가리킵니다. for
각 픽셀의 col
row
각 바이트가 설정된 후 연산자를 사용하여 증가 ++
될 수 있도록 ptr
루프를 설정하고 설정합니다. 픽셀을 통과하는 다른 99개 루프의 ptr
경우 비트맵의 시작 부분으로 다시 설정해야 합니다.
각 픽셀은 4바이트의 메모리이므로 각 바이트를 별도로 설정해야 합니다. 여기서 코드는 바이트가 색 형식과 일치하는 빨간색, 녹색, 파랑 및 알파 순서로 SKColorType.Rgba8888
있다고 가정합니다. iOS 및 Android의 기본 색 형식이지만 유니버설 Windows 플랫폼 대해서는 그렇지 않습니다. 기본적으로 UWP는 색 형식의 비트맵을 SKColorType.Bgra8888
만듭니다. 이러한 이유로 해당 플랫폼에서 몇 가지 다른 결과를 볼 수 있습니다.
포인터가 아닌 포인터로 ToPointer
반환된 값을 캐스팅할 uint
수 있습니다 byte
. 이렇게 하면 한 문에서 전체 픽셀에 액세스할 수 있습니다. 해당 포인터에 ++
연산자를 적용하면 4바이트씩 증가하여 다음 픽셀을 가리킵니다.
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);
···
}
픽셀은 빨강, 녹색, 파랑 및 알파 구성 요소에서 정수 픽셀을 생성하는 메서드를 사용하여 MakePixel
설정됩니다. 형식에는 SKColorType.Rgba8888
다음과 같은 픽셀 바이트 순서가 있습니다.
RR GG BB AA
그러나 해당 바이트에 해당하는 정수는 다음과 같습니다.
AABBGGRR
정수의 가장 중요한 바이트는 little-endian 아키텍처에 따라 먼저 저장됩니다. 이 MakePixel
메서드는 색 형식의 비트맵에 Bgra8888
대해 제대로 작동하지 않습니다.
MakePixel
이 메서드는 컴파일러가 이 메서드를 별도의 메서드로 만들지 않고 메서드가 호출되는 코드를 컴파일하도록 권장하는 옵션으로 플래그가 지정 MethodImplOptions.AggressiveInlining
됩니다. 이렇게 하면 성능이 향상되어야 합니다.
흥미롭게도 이 구조체는 SKColor
부호 없는 정수로 SKColor
의 명시적 변환을 정의합니다. 즉SKColor
, 값을 만들 수 있으며 다음 대신 MakePixel
변환을 uint
사용할 수 있습니다.
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;
}
유일한 질문은 다음과 같습니다. 색 형식의 순서에 따라 값의 SKColor
SKColorType.Rgba8888
정수 형식인가요, 아니면 SKColorType.Bgra8888
색 형식인지 아니면 완전히 다른 것입니까? 그 질문에 대한 답변은 곧 밝혀질 것입니다.
SetPixels 메서드
SKBitmap
또한 다음과 같이 호출하는 명명 SetPixels
된 메서드를 정의합니다.
bitmap.SetPixels(intPtr);
비트맵에서 GetPixels
IntPtr
해당 픽셀을 저장하는 데 사용하는 메모리 블록을 참조하는 것을 확인합니다. 호출은 SetPixels
해당 메모리 블록을 인수로 지정된 메모리 블록 IntPtr
으로 SetPixels
바꿉니다. 그런 다음 비트맵은 이전에 사용했던 메모리 블록을 해제합니다. 다음에 GetPixels
호출되면 .로 설정된 SetPixels
메모리 블록을 가져옵니다.
처음에는 덜 편리하면서도 더 이상 힘과 성능을 GetPixels
제공하지 않는 것처럼 보입니다SetPixels
. GetPixels
비트맵 메모리 블록을 가져와서 액세스합니다. SetPixels
일부 메모리를 할당하고 액세스한 다음 비트맵 메모리 블록으로 설정합니다.
그러나 사용 SetPixels
은 고유한 구문적 이점을 제공합니다. 배열을 사용하여 비트맵 픽셀 비트에 액세스할 수 있습니다. 이 기술을 보여 주는 메서드 GradientBitmapPage
는 다음과 같습니다. 이 메서드는 먼저 비트맵 픽셀의 바이트에 해당하는 다차원 바이트 배열을 정의합니다. 첫 번째 차원은 행이고, 두 번째 차원은 열이고, 세 번째 차원은 각 픽셀의 네 가지 구성 요소에 대한 코레슨입니다.
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;
}
그런 다음 배열을 픽셀 unsafe
로 채운 후 블록과 fixed
문을 사용하여 이 배열을 가리키는 바이트 포인터를 가져옵니다. 그런 다음 바이트 포인터를 전달SetPixels
하려면 IntPtr
캐스팅할 수 있습니다.
만드는 배열은 바이트 배열일 필요가 없습니다. 행과 열에 대해 두 개의 차원만 있는 정수 배열일 수 있습니다.
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;
}
이 MakePixel
메서드는 색 구성 요소를 32비트 픽셀로 결합하는 데 다시 사용됩니다.
완전성을 위해 다음과 같은 코드가 있지만 SKColor
부호 없는 정수로 캐스팅된 값은 다음과 같습니다.
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;
}
기술 비교
그라데이션 색 페이지의 생성자는 위에 표시된 8개의 메서드를 모두 호출하고 결과를 저장합니다.
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;
}
···
}
생성자는 결과 비트맵을 SKCanvasView
표시하는 방법을 만들어 마무리합니다. PaintSurface
처리기는 표면을 8개의 사각형으로 나누고 각 사각형을 표시하는 호출 Display
을 실행합니다.
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);
}
}
}
컴파일러가 코드를 최적화할 수 있도록 이 페이지는 릴리스 모드에서 실행되었습니다. 다음은 MacBook Pro의 i전화 8 시뮬레이터, Nexus 5 Android 휴대폰 및 Windows 10을 실행하는 Surface Pro 3에서 실행되는 페이지입니다. 하드웨어 차이로 인해 디바이스 간의 성능 시간을 비교하지 말고 각 디바이스에서 상대적인 시간을 확인합니다.
다음은 실행 시간을 밀리초 단위로 통합하는 테이블입니다.
API | 데이터 형식 | iOS | Android | UWP |
---|---|---|---|---|
Setpixel | 3.17 | 10.77 | 3.49 | |
픽셀 | 0.32 | 1.23 | 0.07 | |
GetPixels | 바이트 | 0.09 | 0.24 | 0.10 |
uint | 0.06 | 0.26 | 0.05 | |
SKColor | 0.29 | 0.99 | 0.07 | |
SetPixels | 바이트 | 1.33 | 6.78 | 0.11 |
uint | 0.14 | 0.69 | 0.06 | |
SKColor | 0.35 | 1.93 | 0.10 |
예상대로 65,536번 호출 SetPixel
하는 것은 비트맵의 픽셀을 설정하는 가장 비효율적인 방법입니다. 배열을 SKColor
채우고 속성을 설정하는 Pixels
것이 훨씬 좋으며 일부 GetPixels
및 SetPixels
기술과도 유리하게 비교됩니다. 픽셀 값으로 uint
작업하는 SKColor
것은 일반적으로 개별 byte
구성 요소를 설정하는 것보다 빠르며 값을 부호 없는 정수로 변환하면 프로세스에 약간의 오버헤드가 추가됩니다.
또한 다양한 그라데이션을 비교하는 것도 흥미롭습니다. 각 플랫폼의 위쪽 행은 동일하며 의도한 대로 그라데이션을 표시합니다. 즉 SetPixel
, 메서드와 속성이 Pixels
기본 픽셀 형식에 관계없이 색에서 픽셀을 올바르게 만듭니다.
iOS 및 Android 스크린샷의 다음 두 행도 동일하므로 이러한 플랫폼의 기본 Rgba8888
픽셀 형식에 대해 작은 MakePixel
메서드가 올바르게 정의되어 있음을 확인합니다.
iOS 및 Android 스크린샷의 아래쪽 행은 뒤로이며, 이는 값을 캐스팅 SKColor
하여 얻은 부호 없는 정수가 다음과 같은 형식임을 나타냅니다.
AARRGGBB
바이트는 다음과 같은 순서로 정렬됩니다.
BB GG RR AA
이는 Bgra8888
주문이 아닌 주문입니다 Rgba8888
. 이 형식은 Brga8888
유니버설 Windows 플랫폼의 기본값이므로 해당 스크린샷의 마지막 행에 있는 그라데이션이 첫 번째 행과 동일합니다. 그러나 이러한 비트맵을 만드는 코드에서 순서를 가정했기 때문에 중간 두 행이 Rgba8888
잘못되었습니다.
각 플랫폼에서 픽셀 비트에 액세스하는 데 동일한 코드를 사용하려는 경우 명시적으로 사용하거나 Bgra8888
형식을 SKBitmap
사용하여 Rgba8888
만들 수 있습니다. 값을 비트맵 픽셀로 캐스팅 SKColor
하려면 .를 사용합니다 Bgra8888
.
픽셀의 임의 액세스
그라데이션 비트맵 페이지의 메서드와 FillBitmapUintPtr
메서드는 FillBitmapBytePtr
비트맵을 순차적으로 채우도록 디자인된 for
루프, 위쪽 행에서 아래쪽 행, 왼쪽에서 오른쪽으로 각 행의 이점을 누릴 수 있습니다. 포인터를 증가시킨 것과 동일한 문으로 픽셀을 설정할 수 있습니다.
경우에 따라 순차적으로 액세스하지 않고 임의로 픽셀에 액세스해야 합니다. 이 GetPixels
방법을 사용하는 경우 행과 열을 기준으로 포인터를 계산해야 합니다. 이것은 레인보우 사인 페이지에서 보여 지며, 사인 곡선의 한 주기 형태로 무지개를 보여주는 비트맵을 만듭니다.
무지개 색은 HSL(색조, 채도, 광도) 색상 모델을 사용하여 만드는 것이 가장 쉽습니다. 이 메서드는 SKColor.FromHsl
0에서 360 사이의 색조 값(예: 원의 각도, 빨간색, 녹색 및 파랑, 다시 빨간색으로) 및 채도 및 광도 값이 0에서 100 사이의 값을 사용하여 값을 만듭니다 SKColor
. 무지개 색의 경우 채도를 최대 100으로 설정하고 광도를 50의 중간점으로 설정해야 합니다.
Rainbow Sine 은 비트맵의 행을 반복한 다음 360개의 색조 값을 반복하여 이 이미지를 만듭니다. 각 색조 값에서 사인 값을 기반으로 하는 비트맵 열을 계산합니다.
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);
}
}
생성자는 형식에 따라 비트맵을 SKColorType.Bgra8888
만듭니다.
bitmap = new SKBitmap(360 * 3, 1024, SKColorType.Bgra8888, SKAlphaType.Unpremul);
이렇게 하면 프로그램에서 값의 SKColor
변환을 걱정 없이 픽셀로 uint
변환할 수 있습니다. 이 특정 프로그램에서는 역할을 수행하지 않지만 변환을 사용하여 SKColor
픽셀을 설정할 때마다 해당 색 구성 요소를 알파 값으로 미리 곱하지 않으므로 지정 SKAlphaType.Unpremul
SKColor
해야 합니다.
그런 다음 생성자는 메서드를 GetPixels
사용하여 비트맵의 첫 번째 픽셀에 대한 포인터를 가져옵니다.
uint* basePtr = (uint*)bitmap.GetPixels().ToPointer();
특정 행 및 열의 경우 오프셋 값을 에 추가 basePtr
해야 합니다. 이 오프셋은 행의 비트맵 너비와 열의 횟수입니다.
uint* ptr = basePtr + bitmap.Width * row + col;
값은 다음 SKColor
포인터를 사용하여 메모리에 저장됩니다.
*ptr = (uint)SKColor.FromHsl(hue, 100, 50);
PaintSurface
처리기SKCanvasView
에서 비트맵은 표시 영역을 채우기 위해 늘입니다.
한 비트맵에서 다른 비트맵으로
매우 많은 이미지 처리 작업에는 한 비트맵에서 다른 비트맵으로 전송되는 픽셀 수정이 포함됩니다. 이 기술은 색 조정 페이지에 설명되어 있습니다. 이 페이지에서는 비트맵 리소스 중 하나를 로드한 다음 세 Slider
가지 보기를 사용하여 이미지를 수정할 수 있습니다.
각 픽셀 색에 대해 첫 번째 Slider
값은 0에서 360 사이의 값을 색조에 추가한 다음, 모듈로 연산자를 사용하여 결과를 0에서 360 사이로 유지하여 스펙트럼을 따라 색을 효과적으로 이동합니다(UWP 스크린샷에서 볼 수 있듯이). 두 번째 Slider
에서는 채도에 적용할 0.5에서 2 사이의 곱셈 요소를 선택할 수 있으며, 세 번째는 Slider
Android 스크린샷에 표시된 것처럼 광도에 대해 동일하게 적용됩니다.
이 프로그램은 기본 명명된 원래 원본 비트맵과 조정된 대상 비트맵이라는 srcBitmap
dstBitmap
두 개의 비트맵을 가져옵니다. A가 Slider
이동될 때마다 프로그램은 모든 새 픽셀을 계산합니다 dstBitmap
. 물론 사용자는 보기를 매우 빠르게 이동하여 Slider
실험하므로 관리할 수 있는 최상의 성능을 원합니다. 여기에는 원본 및 대상 비트맵 모두에 대한 메서드가 포함 GetPixels
됩니다.
색 조정 페이지는 원본 및 대상 비트맵의 색 형식을 제어하지 않습니다. 대신, 약간 다른 논리 및 SKColorType.Bgra8888
형식을 SKColorType.Rgba8888
포함합니다. 원본과 대상은 서로 다른 형식일 수 있으며 프로그램은 계속 작동합니다.
픽셀을 전송하는 중요한 TransferPixels
메서드를 제외하고 원본을 대상으로 구성하는 프로그램은 다음과 같습니다. 생성자는 같게 srcBitmap
설정합니다dstBitmap
. PaintSurface
처리기는 다음을 표시합니다.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);
}
}
ValueChanged
뷰에 Slider
대한 처리기는 조정 값과 호출TransferPixels
을 계산합니다.
전체 TransferPixels
메서드가 .로 unsafe
표시됩니다. 먼저 비트맵의 픽셀 비트에 대한 바이트 포인터를 가져온 다음 모든 행과 열을 반복합니다. 소스 비트맵에서 메서드는 각 픽셀에 대해 4바이트를 가져옵니다. 이러한 순서는 순서 Bgra8888
대로 Rgba8888
될 수 있습니다. 색 유형을 확인하면 값을 만들 수 SKColor
있습니다. 그런 다음 HSL 구성 요소를 추출, 조정 및 사용하여 값을 다시 만듭니 SKColor
다. 대상 비트맵 Rgba8888
Bgra8888
인지 여부에 따라 바이트는 대상 비트mp에 저장됩니다.
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;
}
}
}
}
···
}
원본 및 대상 비트맵의 다양한 색상 형식 조합에 대해 별도의 메서드를 만들어 이 메서드의 성능을 훨씬 향상시키고 모든 픽셀에 대해 형식을 검사 방지할 수 있습니다. 또 다른 옵션은 색 형식에 따라 변수에 col
대해 여러 for
루프를 갖는 것입니다.
포스터화
픽셀 비트에 액세스하는 것과 관련된 또 다른 일반적인 작업은 포스터화입니다. 비트맵의 픽셀로 인코딩된 색을 줄여 결과가 제한된 색상표를 사용하여 손으로 그린 포스터와 유사하게 만드는 경우의 수입니다.
Posterize 페이지는 원숭이 이미지 중 하나에서 이 프로세스를 수행합니다.
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;
}
}
생성자의 코드는 각 픽셀에 액세스하고, 0xE0E0E0FF 값을 사용하여 비트 AND 연산을 수행한 다음, 결과를 비트맵에 다시 저장합니다. 0xE0E0E0FF 값은 각 색 구성 요소의 상위 3비트를 유지하고 하위 5비트를 0으로 설정합니다. 비트맵은 224 또는 16,777,216 색 대신 29 또는 512 색으로 줄어듭니다.