다음을 통해 공유


SkiaSharp 비트맵 픽셀 비트 액세스

SkiaSharp 비트맵을 파일에 저장하는 문서에서 본 것처럼 비트맵은 일반적으로 JPEG 또는 PNG와 같은 압축된 형식의 파일에 저장됩니다. constrast에서 메모리에 저장된 SkiaSharp 비트맵은 압축되지 않습니다. 순차적 픽셀 시리즈로 저장됩니다. 이 압축되지 않은 형식은 비트맵을 디스플레이 화면으로 쉽게 전송할 수 있도록 합니다.

SkiaSharp 비트맵이 차지하는 메모리 블록은 매우 간단한 방식으로 구성됩니다. 이 블록은 왼쪽에서 오른쪽으로 픽셀의 첫 번째 행으로 시작한 다음 두 번째 행으로 계속됩니다. 전체 색 비트맵의 경우 각 픽셀은 4바이트로 구성됩니다. 즉, 비트맵에 필요한 총 메모리 공간은 너비와 높이의 4배에 해당합니다.

이 문서에서는 애플리케이션이 비트맵의 픽셀 메모리 블록에 직접 액세스하거나 간접적으로 해당 픽셀에 액세스할 수 있는 방법을 설명합니다. 경우에 따라 프로그램에서 이미지의 픽셀을 분석하고 일종의 히스토그램을 생성하려고 할 수 있습니다. 더 일반적으로 애플리케이션은 비트맵을 구성하는 픽셀을 알고리즘으로 만들어 고유한 이미지를 생성할 수 있습니다.

픽셀 비트 샘플

기술

SkiaSharp은 비트맵의 픽셀 비트에 액세스하기 위한 몇 가지 기술을 제공합니다. 일반적으로 선택하는 것은 코딩 편의성(기본테넌트 및 디버깅의 용이성과 관련됨)과 성능 간의 절충안입니다. 대부분의 경우 비트맵의 픽셀에 액세스하기 위해 다음 메서드 및 속성 SKBitmap 중 하나를 사용합니다.

  • GetPixelSetPixel 메서드를 사용하면 단일 픽셀의 색을 가져오거나 설정할 수 있습니다.
  • 이 속성은 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 속성

SKBitmapPixels 전체 비트맵에 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 것이 훨씬 좋으며 일부 GetPixelsSetPixels 기술과도 유리하게 비교됩니다. 픽셀 값으로 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 색으로 줄어듭니다.

스크린샷은 두 개의 모바일 장치와 데스크톱 창에 있는 토이 원숭이의 포스터 이미지를 보여줍니다.