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
設定された後に ++
演算子でインクリメントできるように設定されます。row
col
他の 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
使用して、この配列を指すバイト ポインターを取得します。 そのバイト ポインターを にキャストし、 に IntPtr
渡 SetPixels
すことができます。
作成する配列はバイト配列である必要はありません。 行と列の次元が 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 のスクリーンショットに示すように、輝度に対して同じ処理を行います。
プログラムでは、 という名前の元のソース ビットマップと という名前srcBitmap
dstBitmap
の調整されたコピー先ビットマップの 2 つのビットマップが保持されます。 が Slider
移動されるたびに、プログラムは 内 dstBitmap
のすべての新しいピクセルを計算します。 もちろん、ユーザーはビューをすばやく Slider
移動して実験するので、管理できる最高のパフォーマンスが必要です。 これには、 GetPixels
ソース ビットマップとコピー先ビットマップの両方の メソッドが含まれます。
[ 色の調整] ページでは、ソースビットマップとコピー先ビットマップの色形式は制御されません。 代わりに、 と 形式に対して若干異なるロジックがSKColorType.Rgba8888
SKColorType.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);
}
}
ビューのハンドラーは ValueChanged
、 Slider
調整値を計算し、 を呼び出します 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 色に縮小されます。