SkiaSharp ビットマップ ピクセル ビットへのアクセス

サンプルのダウンロードサンプルのダウンロード

「SkiaSharp ビットマップをファイルに保存する」の記事で説明したように、ビットマップは通常、JPEG や PNG などの圧縮形式でファイルに格納されます。 constrast では、メモリに格納されている SkiaSharp ビットマップは圧縮されません。 これは、連続する一連のピクセルとして格納されます。 この圧縮されていない形式により、ビットマップを表示サーフェイスに転送できます。

SkiaSharp ビットマップによって占有されるメモリ ブロックは、非常に簡単な方法で整理されています。これは、ピクセルの最初の行 (左から右) で始まり、2 行目から続きます。 フル カラー ビットマップの場合、各ピクセルは 4 バイトで構成されます。つまり、ビットマップに必要な合計メモリ領域は、幅と高さの 4 倍になります。

この記事では、ビットマップのピクセル メモリ ブロックに直接アクセスするか、間接的に、アプリケーションがこれらのピクセルにアクセスする方法について説明します。 場合によっては、プログラムで画像のピクセルを分析し、何らかのヒストグラムを作成することが必要な場合があります。 より一般的には、アプリケーションでは、ビットマップを構成するピクセルをアルゴリズムで作成することで、一意のイメージを構築できます。

ピクセル ビットサンプル

手法

SkiaSharp には、ビットマップのピクセル ビットにアクセスするためのいくつかの手法が用意されています。 どれを選択するかを選択するのは、通常、コーディングの利便性 (メンテナンスとデバッグの容易さに関連する) とパフォーマンスの間の妥協点です。 ほとんどの場合、ビットマップのピクセルにアクセスするために、 のメソッドとプロパティ SKBitmap のいずれかを使用します。

  • GetPixelメソッドと SetPixel メソッドを使用すると、1 つのピクセルの色を取得または設定できます。
  • プロパティは Pixels 、ビットマップ全体のピクセル色の配列を取得するか、色の配列を設定します。
  • GetPixels は、ビットマップで使用されるピクセル メモリのアドレスを返します。
  • SetPixels は、ビットマップで使用されるピクセル メモリのアドレスを置き換えます。

最初の 2 つの手法は "高レベル" と考え、2 つ目の手法は "低レベル" と考えることができます。他にもいくつかの方法とプロパティを使用できますが、これらは最も重要です。

これらの手法のパフォーマンスの違いを確認できるように、 SkiaSharpFormsDemos アプリケーションには Gradient Bitmap という名前のページが含まれており、赤と青の色合いを組み合わせてグラデーションを作成するピクセルを含むビットマップを作成します。 プログラムは、ビットマップ ピクセルを設定するための異なる手法を使用して、このビットマップの 8 つの異なるコピーを作成します。 これら 8 つのビットマップのそれぞれは、手法の簡単なテキストの説明を設定し、すべてのピクセルを設定するために必要な時間を計算する別のメソッドで作成されます。 各メソッドは、ピクセル設定ロジックを 100 回ループ処理して、パフォーマンスをより適切に見積もります。

SetPixel メソッド

複数の個別のピクセルのみを設定または取得する必要がある場合は、 SetPixel メソッドと GetPixel メソッドが理想的です。 これら 2 つのメソッドごとに、整数の列と行を指定します。 ピクセル形式に関係なく、次の 2 つの方法を使用すると、ピクセルを値として SKColor 取得または設定できます。

bitmap.SetPixel(col, row, color);

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

引数の範囲は col 、ビットマップのプロパティより Width 0 ~ 1 小さく、 row プロパティより Height 0 から 1 未満の範囲である必要があります。

メソッドを使用してビットマップの内容を設定する グラデーション ビットマップ のメソッドを次に 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;

ピクセルは、最初の行から始まり、左から右、2 番目の行など、配列内に配置されます。 配列内の色の合計数は、ビットマップの幅と高さの積と同じです。

このプロパティは効率的に見えますが、ピクセルはビットマップから配列にコピーされ、配列からビットマップにコピーされ、ピクセルは と の値に 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 実行して、ビットマップ内の他の場所に移動できます。

1 つの欠点は、このptr変数は、キーワード (keyword)で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;
}

変数が ptr メソッドから最初に ToPointer 取得されると、ビットマップの最初の行の左端のピクセルの最初のバイトを指します。 と のループはfor、各ピクセルの各バイトがptr設定された後に ++ 演算子でインクリメントできるように設定されます。rowcol 他の 99 はピクセルをループ処理するため、 を ptr ビットマップの先頭に戻す必要があります。

各ピクセルは 4 バイトのメモリであるため、各バイトは個別に設定する必要があります。 ここでのコードでは、バイトが赤、緑、青、アルファの順序であり、色の SKColorType.Rgba8888 種類と一致していることを前提としています。 これは iOS と Android の既定の色の種類ですが、ユニバーサル Windows プラットフォームでは使用されないことを思い出してください。 既定では、UWP は色の種類を持つビットマップを SKColorType.Bgra8888 作成します。 このため、そのプラットフォームでいくつかの異なる結果が表示されることを期待してください。

からToPointer返された値をポインターではなくbyteポインターにuintキャストできます。 これにより、1 つのステートメントでピクセル全体にアクセスできます。 そのポインターに演算子を ++ 適用すると、次のピクセルを指す 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

整数の最下位バイトは、リトル エンディアン アーキテクチャに従って最初に格納されます。 この 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.Bgra8888色の種類のSKColorType.Rgba8888順序であるか、それとも完全に他のものかです。 その質問に対する答えは、まもなく明らかにされます。

SetPixels メソッド

SKBitmap では、 という名前 SetPixelsのメソッドも定義されます。このメソッドは次のように呼び出します。

bitmap.SetPixels(intPtr);

は、ビットマップがピクセルをIntPtr格納するために使用するメモリ ブロックを参照する を取得することをGetPixels思い出してください。 呼び出しはSetPixels、そのメモリ ブロックを、 引数として指定された によってIntPtr参照されるメモリ ブロックにSetPixels置き換えます。 その後、ビットマップは、以前に使用していたメモリ ブロックを解放します。 次回 GetPixels が呼び出されると、 で設定 SetPixelsされたメモリ ブロックが取得されます。

最初は、あまり便利ではない一方でGetPixels、より多くのパワーとパフォーマンスを与えるかのようにSetPixels見えます。 を GetPixels 使用して、ビットマップ メモリ ブロックを取得し、それにアクセスします。 では SetPixels 、メモリを割り当ててアクセスし、それをビットマップ メモリ ブロックとして設定します。

ただし、 を使用 SetPixels すると、構文上の利点が異なります。これにより、配列を使用してビットマップ ピクセル ビットにアクセスできます。 この手法を示す の GradientBitmapPage メソッドを次に示します。 メソッドは最初に、ビットマップのピクセルのバイトに対応する多次元バイト配列を定義します。 最初のディメンションは行、2 番目のディメンションは列、3 番目のディメンションは各ピクセルの 4 つのコンポーネントに対応しています。

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 使用して、この配列を指すバイト ポインターを取得します。 そのバイト ポインターを にキャストし、 に IntPtrSetPixelsすことができます。

作成する配列はバイト配列である必要はありません。 行と列の次元が 2 つだけの整数配列を指定できます。

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、Nexus 5 Android フォン、Surface Pro 3 Windows 10で実行されている iPhone 8 シミュレーターで実行されているページを次に示します。 ハードウェアの違いにより、デバイス間のパフォーマンス時間を比較しないようにしますが、代わりに各デバイスの相対的な時間を確認してください。

グラデーション ビットマップ

実行時間をミリ秒単位で統合するテーブルを次に示します。

API データ型 iOS Android UWP
SetPixel 3.17 10.77 3.49
ピクセル 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

予想どおり、65,536 回の呼び出し SetPixel は、ビットマップのピクセルを設定する最も効率的でない方法です。 配列をSKColor埋め、プロパティをPixels設定する方がはるかに優れています。また、 や SetPixels の手法のいくつかGetPixelsと比較しても優れています。 ピクセル値の uint 操作は、通常、個別 byte のコンポーネントを設定するよりも高速であり、値を SKColor 符号なし整数に変換すると、プロセスにオーバーヘッドが発生します。

また、さまざまなグラデーションを比較することも興味深いです。各プラットフォームの上位行は同じで、意図したとおりにグラデーションが表示されます。 つまり、 SetPixel メソッドと プロパティは、基になるピクセル形式に Pixels 関係なく、色からピクセルを正しく作成します。

iOS と Android の次の 2 行のスクリーンショットも同じであり、これらのプラットフォームの既定Rgba8888のピクセル形式に対して小さなMakePixelメソッドが正しく定義されていることを確認します。

iOS と Android のスクリーンショットの一番下の行は後方にあり、値をキャスト SKColor して取得した符号なし整数が次の形式であることを示します。

AARRGGBB

バイトの順序は次のとおりです。

BB GG RR AA

これは Bgra8888 順序ではなく Rgba8888 順序です。 Brga8888ユニバーサル Windows プラットフォームの既定の形式は、そのスクリーンショットの最後の行のグラデーションが最初の行と同じである理由です。 しかし、これらのビットマップを作成するコードは順序付けを想定 Rgba8888 しているため、中央の 2 行は正しくありません。

各プラットフォームでピクセル ビットにアクセスするために同じコードを使用する場合は、 または Bgra8888 形式をRgba8888使用して をSKBitmap明示的に作成できます。 値をビットマップ ピクセルにキャスト SKColor する場合は、 を使用します Bgra8888

ピクセルのランダム アクセス

[グラデーション ビットマップ] ページの メソッドと FillBitmapUintPtr メソッドはFillBitmapBytePtr、ビットマップを順番に、上の行から下の行、各行を左から右に塗りつぶすように設計されたループの恩恵を受forけます。 ポインターをインクリメントしたのと同じステートメントでピクセルを設定できます。

場合によっては、ピクセルに順番ではなくランダムにアクセスする必要があります。 この方法を GetPixels 使用している場合は、行と列に基づいてポインターを計算する必要があります。 これは、 虹サイン 曲線の 1 サイクルの形で虹を示すビットマップを作成する[レインボー サイン]ページで示されています。

虹の色は、HSL (色相、彩度、明度) カラー モデルを使用して作成するのが最も簡単です。 メソッドは SKColor.FromHsl 、0 ~ 360 の範囲の色相値 (円の角度のように、赤、緑、青、赤に戻る) と、0 から 100 の範囲の彩度と明るさの値を使用して値を作成 SKColor します。 虹の色の場合、彩度は最大 100、明度は 50 の中間点に設定する必要があります。

レインボー サイン は、ビットマップの行をループし、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してピクセルを設定するたびに、アルファ値によって色成分が事前に乗算されないためSKColor、 も指定SKAlphaType.Unpremulする必要があります。

次に、コンストラクターは メソッドを GetPixels 使用して、ビットマップの最初のピクセルへのポインターを取得します。

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

特定の行と列の場合は、オフセット値を に追加する basePtr必要があります。 このオフセットは、ビットマップの幅と列の行の倍になります。

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

値は SKColor 、次のポインターを使用してメモリに格納されます。

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

の ハンドラーSKCanvasViewでは、PaintSurface表示領域を塗りつぶすためにビットマップが引き伸ばされます。

レインボー サイン

あるビットマップから別のビットマップへ

非常に多くの画像処理タスクでは、あるビットマップから別のビットマップに転送されるピクセルを変更する必要があります。 この手法は、[ 色の調整 ] ページで示されています。 ページはビットマップ リソースの 1 つを読み込み、次の 3 つの Slider ビューを使用してイメージを変更できます。

色調整 色

ピクセルの色ごとに、最初 Slider に 0 から 360 の値を色相に追加しますが、次に剰余演算子を使用して結果を 0 から 360 の間に維持し、スペクトルに沿って色を効果的にシフトします (UWP のスクリーンショットが示すように)。 2 つ目 Slider では、彩度に適用する 0.5 から 2 の多乗係数を選択できます。3 つ目 Slider は、Android のスクリーンショットに示すように、輝度に対して同じ処理を行います。

プログラムでは、 という名前の元のソース ビットマップと という名前srcBitmapdstBitmapの調整されたコピー先ビットマップの 2 つのビットマップが保持されます。 が Slider 移動されるたびに、プログラムは 内 dstBitmapのすべての新しいピクセルを計算します。 もちろん、ユーザーはビューをすばやく Slider 移動して実験するので、管理できる最高のパフォーマンスが必要です。 これには、 GetPixels ソース ビットマップとコピー先ビットマップの両方の メソッドが含まれます。

[ 色の調整] ページでは、ソースビットマップとコピー先ビットマップの色形式は制御されません。 代わりに、 と 形式に対して若干異なるロジックがSKColorType.Rgba8888SKColorType.Bgra8888含まれています。 ソースと変換先は異なる形式にすることができ、プログラムは引き続き機能します。

ソースを形成するピクセルを変換先に転送する重要な 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);
    }
}

ビューのハンドラーは ValueChangedSlider 調整値を計算し、 を呼び出します TransferPixels

メソッド全体 TransferPixels が として unsafeマークされます。 最初に、両方のビットマップのピクセル ビットへのバイト ポインターを取得し、すべての行と列をループ処理します。 ソース ビットマップから、 メソッドはピクセルごとに 4 バイトを取得します。 これらは、 Rgba8888 または Bgra8888 の順序で指定できます。 色の種類を確認すると、値を SKColor 作成できます。 その後、HSL コンポーネントが抽出、調整、および値の再作成に SKColor 使用されます。 コピー先ビットマップが Rgba8888 であるか Bgra8888であるかに応じて、バイトは宛先 bitmp に格納されます。

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

ソースビットマップとコピー先ビットマップの色の種類のさまざまな組み合わせに対して個別のメソッドを作成することで、このメソッドのパフォーマンスをさらに向上させ、すべてのピクセルの型をチェックしないようにする可能性があります。 もう 1 つのオプションは、色の種類に基づいて変数に対してcol複数forのループを持つことです。

ポスタリゼーション

ピクセル ビットへのアクセスを伴うもう 1 つの一般的なジョブは 、事後化です。 ビットマップのピクセルでエンコードされた色が減り、結果が限られたカラー パレットを使用して手描きのポスターに似ている場合の数値。

[ポスター化] ページでは、次のいずれかのサル画像に対してこのプロセスが実行されます。

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 に設定します。 ビットマップは、2 24 色または 16,777,216 色ではなく、29 または 512 色に縮小されます。

スクリーンショットは、2 台のモバイル デバイスとデスクトップ ウィンドウ上のおもちゃのサルのポスター画像を示しています。