基礎
ビットマップとピクセル ビット
Charles Petzold

目次
Windows® Presentation Foundation (WPF) の保持モード グラフィックス システムによって、Windows グラフィックスのプログラミングに変革がもたらされました。システムで要求されたときに、プログラムは画面にビジュアルな外観を再作成する必要がなくなりました。コンポジション システムは、すべてのグラフィック図形を保持し、それらを総合的なビジュアル プレゼンテーションに配置します。
保持モード グラフィックスによって便利になったのは確かですが、Windows プログラマにとって、便利さは優先事項ではありませんでした。これは実際には、保持モード グラフィックス システムと、WPF の柔軟性と能力を解放する依存関係プロパティなどの通知メカニズムの組み合わせです。パスやブラシなどのグラフィカル オブジェクトは、コンポジション システム内でまだ "動作" し、プロパティの変更とグラフィックスの変換に応答し続けているように見えるため、これらのオブジェクトをデータ バインドやアニメーションのターゲットにすることができます。
筆者は最近、WPF ビットマップが同様の動的な品質を持つことを発見しました。レンダリングされたビットマップは、依然として変更に (だれもが知っていたグラフィックス変換だけでなく、ビットマップ内の実際のピクセル ビットの変更にも) 応答します。
この動的な応答性を明らかにする 2 つのビットマップ クラスは、WPF のすべてのビットマップ サポートの基礎となる抽象クラス BitmapSource から派生する 9 つのクラスのうちの、RenderTargetBitmap と WriteableBitmap です。プログラムがこれらのビットマップ オブジェクトのどれかをどのように使用するか、つまり Image 要素で表示するか、ImageBrush クラスで並べて表示されたブラシにするか、ImageDrawing クラスでより大きい描画 (場合によってはベクタ グラフィックスとの混合) の一部にするかに関係なく、ビットマップがただレンダリングされて忘れ去られることはまずありません。ビットマップはビジュアル コンポジション システム内で引き続き動作し、アプリケーションの変更に応答し続けます。
RenderTargetBitmap を使用する
RenderTargetBitmap は、Visual 型のオブジェクトをそのサーフェイスに転送することで効果的に描画できるビットマップです。RenderTargetBitmap 型の新しいオブジェクトを作成する唯一の方法は、ビットマップのピクセル寸法、インチあたりのドット数 (dpi) 単位での水平および垂直解像度、および PixelFormat 型のオブジェクトを必要とするコンストラクタを使用することです。
PixelFormat 構造と、関連する静的 PixelFormats クラスについて、簡単に追加説明します。RenderTargetBitmap 型のオブジェクトを作成するには、PixelFormats.Default または PixelFormats.Pbgra32 を RenderTargetBitmap コンストラクタの最後の引数として使用する必要があります。どちらの場合も、ピクセルあたり 32 ビットで透明度のあるビットマップを作成します。
最初は、RenderTargetBitmap オブジェクトは完全に透明です。Visual 型のオブジェクト (FrameworkElement や Control など、Visual から派生するクラスを含む) で Render メソッドを呼び出して、ビットマップ上で描画できます。Clear を呼び出すと、すべて透明なイメージを復元できます。ビットマップが現在表示されている場合、これらの呼び出しは、表示されるビットマップに反映されます。面倒なことは何もありません。
図 1 に、RenderTargetBitmap を示す小さい完全なプログラムを示します。このプログラムは、幅が 1,200 ピクセルで高さが 900 ピクセルのビットマップを作成します。これらは、ビットマップ オブジェクトの PixelWidth および PixelHeight プロパティになります。各ピクセルの幅は 4 バイトなので、ビットマップは 4 MB を超えるメモリを占有します。

図 1 RenderTargetBitmapDemo
class RenderTargetBitmapDemo : Window
{
RenderTargetBitmap bitmap;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new RenderTargetBitmapDemo());
}
public RenderTargetBitmapDemo()
{
Title = "RenderTargetBitmap Demo";
SizeToContent = SizeToContent.WidthAndHeight;
ResizeMode = ResizeMode.CanMinimize;
// Create RenderTargetBitmap
bitmap = new RenderTargetBitmap(1200, 900, 300, 300,
PixelFormats.Default);
// Create Image for bitmap
Image img = new Image();
img.Stretch = Stretch.None;
img.Source = bitmap;
Content = img;
}
protected override void OnMouseDown(MouseButtonEventArgs args)
{
Point ptMouse = args.GetPosition(this);
Random rand = new Random();
Brush brush = new SolidColorBrush(
Color.FromRgb((byte)rand.Next(256), (byte)rand.Next(256),
(byte)rand.Next(256)));
DrawingVisual vis = new DrawingVisual();
DrawingContext dc = vis.RenderOpen();
dc.DrawEllipse(brush, null, ptMouse, 12, 12);
dc.Close();
bitmap.Render(vis);
}
protected override void OnClosed(EventArgs args)
{
PngBitmapEncoder enc = new PngBitmapEncoder();
enc.Frames.Add(BitmapFrame.Create(bitmap));
FileStream stream = new FileStream("RenderTargetBitmapDemo.png",
FileMode.Create, FileAccess.Write);
enc.Save(stream);
stream.Close();
}
}
RenderTargetBitmap コンストラクタの呼び出しも、300 dpi の解像度を指定します。ピクセル寸法と解像度の組み合わせによって、4 インチの幅と 3 インチの高さのビットマップが作成されます。デバイスに依存しない WPF 座標系は 1 インチが 96 単位なので、ビットマップのデバイスに依存しない幅は 384 単位になり、高さは 288 単位になります。これらは、BitmapSource で定義されている Width プロパティと Height プロパティを調べた場合に見られる数値です。
RenderTargetBitmapDemo プログラムは、Image 要素を使用して、ビットマップを拡大しないで表示します。MouseDown イベントもトラップします。マウスをクリックするたびに、プログラムでは、直径 1/4 インチの小さい塗りつぶされた円の DrawingVisual オブジェクトが作成され、Render メソッドを呼び出してこれをビットマップ イメージに追加します。表示されるビットマップに円が表示されます。このプログラムは、単一のビットマップにレンダリングと格納の機能を結合する基本的なペイント アプリケーションと考えることができます。
Image 要素では、OnRender メソッド中に渡される DrawingContext オブジェクトの DrawImage メソッドを呼び出すことで、ビットマップが表示されることがわかります (または、推測できます)。RenderTargetBitmapDemo プログラムがビットマップを変更したときに、Image クラスは OnRender メソッドの呼び出しを繰り返しません。ビットマップに対するこれらの変更は、ビジュアル コンポジション システムのより深いところで発生します。
RenderTargetBitmapDemo プログラムは、プログラムの終了時に結果のビットマップを PNG ファイル形式で保存します。RenderTargetBitmapDemo から保存されたビットマップを調べると、サイズが 1,200 × 900 ピクセルであり、各円の直径が 75 ピクセル (300 dpi の単位で 1/4 インチ) であることがわかります。
WriteableBitmap を使用する
WriteableBitmap クラスには、2 つのコンストラクタがあります。その 1 つは、RenderTargetBitmap コンストラクタに非常によく似ています。最初の 4 つの引数は、ビットマップのピクセル寸法と dpi 単位での解像度です。5 番目の引数は PixelFormat オブジェクトですが、RenderTargetBitmap よりもはるかに柔軟性があります。WriteableBitmap コンストラクタには、カラー パレットを必要とするビットマップ形式に対して、カラー パレット用の追加パラメータがあります。
WriteableBitmap のピクセルは、すべてゼロに初期化されます。この意味は、ピクセル形式によって異なります。多くの場合、ビットマップは完全に黒です。ビットマップが透明度をサポートしている場合、ビットマップは透明になります。ビットマップにカラー パレットがある場合は、ビットマップ全体がパレットの最初の色になります。
WriteableBitmap の変更は、RenderTargetBitmap の変更と大きく異なります。WriteableBitmap の場合は、WritePixels という名前のメソッドを呼び出す必要があります。このメソッドは、実際のピクセル ビットをローカル配列からビットマップにコピーします。明らかに、配列内のデータの形式とサイズがビットマップの寸法およびピクセル形式と一致することが重要です。
比較的簡単な例から始めましょう。図 2 に示す AnimatedBitmapBrush.cs プログラムは WriteableBitmap を作成し、作成した WriteableBitmap を、ウィンドウの Background プロパティに設定する並べて表示された ImageBrush の基礎として使用します。プログラムは、100 ミリ秒のタイマを設定し、WritePixels を繰り返し呼び出してビットマップを変更します。

図 2 AnimatedBitmapBrush
class AnimatedBitmapBrush : Window
{
const int COLS = 48;
const int ROWS = 48;
WriteableBitmap bitmap;
byte[] pixels = new byte[COLS * ROWS];
byte pixelLevel = 0x00;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new AnimatedBitmapBrush());
}
public AnimatedBitmapBrush()
{
Title = "Animated Bitmap Brush";
Width = Height = 300;
bitmap = new WriteableBitmap(COLS, ROWS, 96, 96,
PixelFormats.Gray8, null);
ImageBrush brush = new ImageBrush(bitmap);
brush.TileMode = TileMode.Tile;
brush.Viewport = new Rect(0, 0, COLS, ROWS);
brush.ViewportUnits = BrushMappingMode.Absolute;
Background = brush;
DispatcherTimer tmr = new DispatcherTimer();
tmr.Interval = TimeSpan.FromMilliseconds(100);
tmr.Tick += TimerOnTick;
tmr.Start();
}
void TimerOnTick(object sender, EventArgs args)
{
for (int row = 0; row < ROWS; row++)
for (int col = 0; col < COLS; col++)
{
int index = row * COLS + col;
double distanceFromCenter =
2 * Math.Max(Math.Abs(row - ROWS / 2.0) / ROWS,
Math.Abs(col - COLS / 2.0) / COLS);
pixels[index] =
(byte)(0x80 * (1 + distanceFromCenter * pixelLevel));
}
bitmap.WritePixels(new Int32Rect(0, 0, COLS,ROWS),pixels,COLS,0);
pixelLevel++;
}
}
定数 COLS および ROWS の値は、このビットマップのピクセル寸法を定義します。これらの同じ値は、並べて表示されたブラシ Viewport の四角形およびタイマの Tick イベント ハンドラでも使用されます。ピクセル形式は PixelFormats.Gray8 に設定されます。この形式は、ビットマップの各ピクセルが、グレー シェードを示す 8 ビット値 (ピクセル値 0x00 は黒で、0xFF は白) で表現されることを意味します。Gray8 形式にパレットは不要なので、WriteableBitmap コンストラクタの最後の引数は null に設定されます。
各ピクセルは 1 バイトなので、ピクセルと言い表したバイト配列の寸法は、単純に COLS × ROWS として計算されます。このピクセル配列はフィールドとして定義されるので、Tick イベント ハンドラを呼び出すたびに再作成する必要はありません。配列内のデータは、一番上の行の左から開始して右に進み、次に 2 行目の左から右に進みます。以下も同様です。Tick イベント ハンドラには、ビットマップの行と列のループが 2 つありますが、この 2 つの値は 1 次元配列インデックスに結合されます。
int index = row * COLS + col;
WritePixels は多次元配列を受け付けません。WritePixels の最初の引数は、更新するビットマップの四角形のサブセットを示す、ピクセル座標の単位の Int32Rect 構造です。Int32Rect オブジェクトの X および Y プロパティは、ビットマップの左上隅との相対で四角形の左上隅の座標を示します。Width および Height プロパティは、この四角形のピクセル寸法を示します。ビットマップ全体を更新する場合は、X と Y を 0 に設定し、Width と Height をビットマップの PixelWidth および PixelHeight プロパティに設定します。WritePixels の最後の 2 つの引数について簡単に説明します。
正直に言うと、このビットマップには少々異なるアニメーション パターンをコーディングするつもりでしたが、どちらかと言えばここで偶然に発見したパターンの方が (少なくとも少しは) おもしろそうです。イメージの 1 つを図 3 に示します。
図 3 AnimatedBitmapBrush の表示
ピクセルの配列
WritePixels を呼び出すしくみは、ビットマップに 1 バイト/ピクセル形式がない場合、およびビットマップの四角形のサブセットのみが更新される場合には複雑になる可能性があります。同じ配列形式のピクセル ビットは、新しいビットマップを作成するために静的 BitmapSource.Create メソッドで、また、ビットマップから配列にピクセル ビットをコピーするために BitmapSource の CopyPixels メソッドで必要になるので、学習する必要がある重要な手法です。3 つのすべてのメソッドで、代わりに IntPtr を使用してローカル バッファを指すことができますが、ここでは配列アプローチに注目します。
ビットマップの合計ピクセル数は、BitmapSource クラスで定義されている PixelWidth プロパティと PixelHeight プロパティの積です。BitmapSource は、PixelFormat 型の取得専用 Format プロパティも定義します。これは、1 ~ 128 の範囲の BitsPerPixel という名前の取得専用プロパティを定義します。極端な例として、単一バイトが 8 つの連続ピクセルのデータを格納する場合や、各ピクセルが 16 バイトのデータを必要とする場合があります。多くの場合、ピクセル ビットは byte、ushort、uint、または float の配列に制限します。
WritePixels メソッドに提供する Int32Rect オブジェクトは、ビットマップ内の四角形のサブセットを定義します。ピクセル配列内のバイト数には、Int32Rect オブジェクトによって示される行と列の数に対して十分なデータが含まれている必要があります。いくつかのピクセル形式では 1 バイトに複数のピクセルを格納するので、これは少々複雑になります。そのようなピクセル形式では、データの各行をバイト境界で開始する必要があります。
たとえば、4 ビット/ピクセルの形式のビットマップを処理していると想定します。アクセスまたは更新するビットマップの四角形領域は、幅が 5 ピクセル、高さが 12 ピクセルです。これらは、指定する Int32Rect オブジェクトの Width プロパティと Height プロパティです。配列内の最初のバイトには、最初の 2 ピクセルのデータが含まれ、2 番目のバイトには次の 2 ピクセルのデータが含まれますが、3 番目のバイトには、最初の行の 5 番目のピクセルのデータのみが含まれます。次のバイトは、2 行目の最初の 2 ピクセルに対応します。
データの各行には 3 バイトが必要であり、四角形領域全体に 36 バイトが必要です。この計算に役立てるために、WritePixels メソッドは stride という引数を必要とします。これは、ピクセル データの各行のバイト数です。stride の一般的な計算は次のとおりです。
int stride = (width * bitsPerPixel + 7) / 8;
幅は Int32Rect 構造の Width プロパティと同じです。ushort、uint、または float 値の配列を使用する場合でも、stride 値は常にバイト単位です。さらに、このような配列の合計バイト数を計算できます。
int dimension = height * stride;
ushort、uint、または float の配列を使用している場合は、それぞれ 2、4、または 8 で除算します。
Windows API では、ビットマップ データの各行を 32 ビット メモリ境界で開始する必要があり、そのために stride を 4 の倍数にする必要があることを思い出してください。これは WPF では不要です。ただし、必要に応じて、式で計算された値よりも大きい stride を設定できます。たとえば、ピクセルあたり 1 バイトのビットマップを処理している場合に、配列が byte ではなく uint 型であるとします。この場合、配列内の各要素には 4 ピクセルが格納されます。現在の多くのハードウェア プラットフォームでは通常、整列されたコピーの方が整列されていないコピーよりも高速なので、厳密には必要がない場合でも配列の各行を単位境界で開始します。
ビットマップ ピクセル形式
ビットマップ内の各ピクセルは、そのビットマップの色を定義する 1 つ以上のビットによって表現されます。WPF では、特定のピクセル形式が構造型 PixelFormat のオブジェクトによって表現されます。静的 PixelFormats クラスは、ビットマップの作成時に使用できる PixelFormat 型の 26 個の静的プロパティを定義します。これらのプロパティを書き込み可能な形式と書き込み不可の形式という 2 つのグループに分け、図 4 に示します。3 つの例外 (Bgr555、Bgr565、および Bgr101010) がありますが、プロパティ名の数字はピクセルあたりのビット数と同じです。

図 4 PixelFormat クラスの静的プロパティ
| 書き込み可能な形式 |
| Indexed1 |
| Indexed2 |
| Indexed4 |
| Indexed8 |
| |
| BlackWhite |
| Gray2 |
| Gray4 |
| Gray8 |
| |
| Bgr555 |
| Bgr565 |
| |
| Bgr32 |
| Bgra32 |
| Pbgra32 |
| 書き込み不可の形式 |
| Default |
| |
| Bgr24 |
| Rgb24 |
| Bgr101010 |
| Cmyk32 |
| |
| Gray16 |
| Rgb48 |
| Rgba64 |
| Prgba64 |
| |
| Gray32Float |
| Rgb128Float |
| Rgba128Float |
| Prgba128Float |
BitmapSource.Create でビットマップを作成する場合は、PixelFormats.Default を除き、PixelFormats クラスの任意の静的プロパティを使用できます。WriteableBitmap 型のビットマップを作成する場合は、書き込み可能な形式のみを使用できます。
Indexed という単語で始まる形式は、静的 BitmapSource.Create メソッドまたは WriteableBitmap コンストラクタの ColorPalette オブジェクトを必要とする形式です。各ピクセルは ColorPalette オブジェクトのインデックスであり、この 4 つの形式は、それぞれ 2、4、16、および 256 色に関連付けられています。実際のピクセル ビットが可能な最大の大きさになることがない場合は、ColorPalette を色の最大数と同じ大きさにする必要はありません。
ピクセルあたり 8 ビットよりも少ない形式では、バイト内の最上位桁のビットが一番左のピクセルに対応します。たとえば、Indexed2 形式では、0xC9 というバイトは 2 進 11001001 と等価であり、11、00、10、および 01 の 4 つの 2 ビット値に対応します。これらは、ColorPalette コレクションの 4、1、3、および 2 番目の色に対応します。
BlackWhite、Gray2、Gray4、および Gray8 形式は、ピクセルあたり 1、2、4、または 8 ビットのグレーシェード ビットマップです。すべてゼロのピクセルは黒で、すべて 1 のピクセルは白です。
残りの 5 つの書き込み可能な形式は、色の形式です。文字 B、G、および R は、青、緑、および赤の原色を意味します。文字 A はアルファ チャネルを意味し、ビットマップが透明度をサポートすることを示します。文字 P は、後で簡単に説明するプリマルチプライ済みアルファを意味します。
Bgr555 形式と Bgr565 形式は、どちらもピクセルあたり 16 ビット (2 バイト) を必要とします。Bgr555 形式は、原色ごとに 1 ビットを残して 5 ビットを使用します (したがって、32 とおりのグラデーションが可能です)。青の原色のビットが B0 (最下位ビット) から B4 (最上位ビット) で表現され、緑と赤も同様に表現される場合は、図 5 に示すように、データの連続する 2 バイトに 3 つの原色が格納されます。
図 5 2 バイト ピクセル形式 (クリックすると拡大画像が表示されます)
緑のビットが 2 バイト全体に分散していることに注意してください。この配置は、ピクセルが実際に 16 ビットの符号なし整数で、最下位バイトが最初に格納されていることがわかっている場合に大きな意味を持ちます。図 6 に、単一の short 型整数で原色がどのようにエンコードされるかを示します。
図 6 short 型整数ピクセル形式 (クリックすると拡大画像が表示されます)
これにより、この形式に該当することが保証されるため、図 7 に示す Gradient555Demo プログラムは、この形式のビットマップを作成し、左側の青から右側の緑へのグラデーションを表示するようにピクセルを書き込みます。ushort 配列の寸法は、行と列の合計の積であることに注目してください。

図 7 Gradient555Demo
class Indexed2Demo : Window
{
const int COLS = 50;
const int ROWS = 20;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new Indexed2Demo());
}
public Indexed2Demo()
{
Title = "Bgr555 Bitmap Demo";
WriteableBitmap bitmap = new WriteableBitmap(COLS, ROWS, 96, 96,
PixelFormats.Bgr555, null);
ushort[] pixels = new ushort[ROWS * COLS];
for (int row = 0; row < ROWS; row++)
for (int col = 0; col < COLS; col++)
{
int index = row * COLS + col;
int blue = (COLS - col) * 0x1F / COLS;
int green = col * 0x1F / COLS;
ushort pixel = (ushort)(green << 5 | blue);
pixels[index] = pixel;
}
int stride = (COLS * bitmap.Format.BitsPerPixel + 7) / 8;
bitmap.WritePixels(new Int32Rect(0, 0, COLS, ROWS), pixels,
stride, 0);
Image img = new Image();
img.Source = bitmap;
Content = img;
}
}
このコードは、ピクセルあたりのバイト数に対応する、バイト以外の型の配列を使用する利点を示しています。任意の行および列ピクセル アドレスについて、配列インデックスは、行数に列あたりのピクセル数を乗算した積を単純に列に加えた値です。
Bgr565 形式は、Bgr555 と非常によく似ていますが、緑に 6 ビットを使用する点が異なります (目は緑に最も敏感です)。残りの 3 つの書き込み可能な形式は、操作がはるかに簡単です。すべての形式が青から開始してピクセルあたり 4 バイトを使用します。Bgr32 形式では、最後の 4 バイトがゼロです。透明度はありません。他の 2 つの形式では、第 4 バイトがアルファ チャネルです。アルファ値の範囲は、透明の 0x00 から不透明の 0xFF までです。ピクセルが符号なし 32 ビット整数として扱われる場合、最下位の 8 ビットは青をエンコードし、最上位の 8 ビットはゼロまたはアルファ チャネルです。
一般に、ビットマップ内のバイトの順序は、プロパティ名の文字 B、G、R、および A の順序に対応します。書き込み不可の形式の一覧は、ピクセルあたり 16 または 24 ビットのみを使用するいくつかの色形式から始まります。Bgr101010 形式は、ピクセルあたり 32 ビットを使用しますが、各原色には 10 ビットを使用します。ピクセルが 32 ビットの符号なし整数として表現される場合、最下位の 10 ビットは青のビットです。Cmyk32 形式は、印刷に使用されるシアン、マゼンタ、黄色、および黒のレベルをエンコードします。
Gray16、Rgb48、Rgba64、および Prgba64 形式は、16 ビットのグレー シェードと 16 ビットの原色をエンコードします。医療用の画像など、この高い色精度を必要とするハードウェアとアプリケーションを処理する場合は、このような高解像度ビットマップ データを格納および表示できるようになったので便利です。ただし、それ以外で理由でこれらの形式を使用することは実際にはありません。追加の色精度は、原色あたり 8 ビットで表示する場合、または原色あたり 8 ビットのファイル形式で保存する場合には無視されます。
ピクセル形式の一覧は、色レベルと透明度を表現するために単精度浮動小数点値を使用する 4 つの形式で終わっています。これらの形式は、従来の sRGB 色空間とガンマ値 2.2 ではなく、scRGB の色空間とガンマ値 1 に基づきます (説明については、筆者の『Applications = Code + Markup』(Microsoft Press、2006 年) という著書の 24 ~ 25 ページを参照してください)。0.0 の浮動小数点色値は 0x00 (黒) のバイト値に対応し、1.0 の浮動小数点色値は 0xFF のバイト値に対応しますが、ビデオ ディスプレイよりも広い色範囲を持つ表示デバイスでは、浮動小数点色値が 1 を超える場合があります。
PixelFormats の誤記
PixelFormats のドキュメントでは、いくつかの形式に混同が見られます。Gray16、Rgb48、Rgba64、および Prgba64 形式は、すべてガンマ値 1 に基づくと記載されていますが、Gray16 を除き、逆説的に sRGB 形式であるとも記載されています。単純にそうとは言えません。Float ピクセル形式だけが、scRGB 色形式とガンマ値 1 を使用します。
ピクセルを色成分に分解するため、または色成分からピクセルを構成するために、一般化された手法が必要です。PixelFormat 構造には、Mask という名前のプロパティが含まれます。このプロパティは、PixelFormatChannelMask 型のオブジェクトのコレクションです。青、緑、赤、およびアルファの順の色チャネルごとに、1 つの PixelFormatChannelMask オブジェクトがあります。
PixelFormatChannelMask 構造は、Mask プロパティを定義します。このプロパティはバイトのコレクションであり、その数はピクセルあたりのバイト数と等しく、ピクセルのバイト順序に対応します。たとえば、Bgr555 形式の場合、それぞれ 2 バイトを持つ 3 つの PixelFormatChannelMask オブジェクトがあります。青の場合、2 バイトは 0x1F と 0x00 です。緑の場合は 0xE0 と 0x03 で、赤の場合は 0x00 と 0x7C です。このデータを使用する場合は、自身のビットシフト係数を派生させる必要があります。
BitmapSource.Create メソッドには、PixelFormats.Default を除く任意の PixelFormats メンバを使用できると述べましたが、WriteableBitmap コンストラクタには、図 4 の書き込み可能な形式のみを使用できます。WriteableBitmap のドキュメントを調べると、BitmapSource オブジェクトから WriteableBitmap オブジェクトを作成する代替コンストラクタがあることがわかります。
実際には、まず図 4 の書き込み不可の形式から BitmapSource オブジェクトを作成し、BitmapSource に基づいて WriteableBitmap を作成できます。ただし、次の制限を回避できるとは想定しないでください。書き込み不可の形式を持つビットマップは、アルファ チャネルが存在するかどうかによって Bgr32 形式または Pbgra32 形式に変換されます。
作成するビットマップは、サポートされているファイル形式 (具体的には BMP、GIF、PNG、JPEG、TIFF、および Microsoft Windows Media Photo のいずれかの形式) でファイルに保存できます。ただし、ビットマップ データはプロセスにおいて異なる形式に変換される場合があります。たとえば、GIF ファイルとして保存する場合、ビットマップは必ず最初に Indexed8 形式に変換されます。JPEG ファイルとして保存する場合、ビットマップは必ず Gray8 または Bgr32 に変換されます。現時点では、原色あたり 8 ビットを超えるデータを含むファイルを保存する PixelFormat と BitmapEncoder の組み合わせはありません。
プリマルチプライ済みアルファ
PixelFormats クラスの静的プロパティのうちの 3 つは、プリマルチプライ済みアルファ (Premultiplied Alpha) を意味する文字 P から開始します。プリマルチプライ済みアルファは、部分的に透明なピクセルのビットマップ レンダリングの効率の改善に使用される手法です。アルファ チャネルを持つビットマップにのみ適用されます。
次のように計算される色で SolidColorBrush を作成したと想定します。
Color.FromArgb(128, 0, 0, 255)
これは、50% の透明度の青いブラシです。このブラシをレンダリングする場合、色を表示画面の既存の色と結合する必要があります。黒の背景に描画すると、描画結果の RGB 色は (0, 0, 128) になります。白い背景に対する描画結果の色は (127, 127, 255) です。これは単純な加重平均計算です。
次の式のサブスクリプトは、既存のサーフェイスに部分的に透明なピクセルをレンダリングした結果を示します。
Rresult = [(255 – Apixel) * Rsurface + Apixel * Rpixel] / 255;
Gresult = [(255 – Apixel) * Gsurface + Apixel * Gpixel] / 255;
Bresult = [(255 – Apixel) * Bsurface + Apixel * Bpixel] / 255;
この計算は、ピクセルの R、G、および B の値が既に A 値で乗算され、255 で除算されている場合に高速化できます。その場合、各式の 2 番目の乗算は除外できます。たとえば、Bgra32 ビットマップ内のピクセルが ARGB 値 (192, 40, 60, 255) であるとします。Pbgra32 ビットマップでは、同じピクセルが (192, 30, 45, 192) になります。RGB 値は、既に 192 のアルファ値で乗算され、255 で除算されています。
Pbgra32 ビットマップ内の任意のピクセルについて、R、G、または B 値はいずれも A 値より大きくできません。大きくすると何も表示されません。値は最大 255 にバインドされますが、目的の透明度レベルは得られません。
WriteableBitmap アプリケーション
いくつかの単純な動的グラフィックス (たとえば棒グラフ) を表示する必要があるアプリケーションで WriteableBitmap を使用すると便利です。対応するベクタ グラフィックスが WPF で描画されるよりも高速にビットマップを更新できます。
おそらく、WriteableBitmap で最も一般的なアプリケーションはリアルタイムのイメージ処理と非線形変換の実行です。TwistedBitmap プロジェクトでは、図 8 に示すように、任意の 8 ビット/ピクセルまたは 32 ビット/ピクセルのビットマップを読み込み、Slider コントロールを使用して、その中心でイメージをねじります。最適な結果を得るためには、より小さいイメージを使用します。
図 8 ねじりを加えたビットマップ (クリックすると拡大画像が表示されます)
プログラムでは、BitmapFrame.Create を使用してファイルからビットマップを読み込み、CopyPixels を呼び出して、pixelsSrc という名前の配列内のすべてのピクセル ビットをコピーします。図 9 に示す SliderOnValueChanged イベント ハンドラは、pixelsSrc から、WritePixels の呼び出しに使用する pixelsNew という名前の配列への変換を行います。

図 9 TwistedBitmap での変換
void SliderOnValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> args)
{
if (pixelsSrc == null)
return;
Slider slider = sender as Slider;
int width = bitmap.PixelWidth;
int height = bitmap.PixelHeight;
int xCenter = width / 2;
int yCenter = height / 2;
int bytesPerPixel = bitmap.Format.BitsPerPixel / 8;
for (int row = 0; row < bitmap.PixelHeight; row += 1)
{
for (int col = 0; col < bitmap.PixelWidth; col += 1)
{
// Calculate length of point to center and angle
int xDelta = col - xCenter;
int yDelta = row - yCenter;
double distanceToCenter = Math.Sqrt(xDelta * xDelta +
yDelta * yDelta);
double angleClockwise = Math.Atan2(yDelta, xDelta);
// Calculate angle of rotation for twisting effect
double xEllipse = xCenter * Math.Cos(angleClockwise);
double yEllipse = yCenter * Math.Sin(angleClockwise);
double radius = Math.Sqrt(xEllipse * xEllipse +
yEllipse * yEllipse);
double fraction = Math.Max(0, 1 - distanceToCenter / radius);
double twist = fraction * Math.PI * slider.Value / 180;
// Calculate the source pixel for each destination pixel
int colSrc = (int) (xCenter + (col - xCenter) *
Math.Cos(twist)
(row - yCenter) * Math.Sin(twist));
int rowSrc = (int) (yCenter + (col - xCenter) *
Math.Sin(twist)
+ (row - yCenter) * Math.Cos(twist));
colSrc = Math.Max(0, Math.Min(width - 1, colSrc));
rowSrc = Math.Max(0, Math.Min(height - 1, rowSrc));
// Calculate the indices
int index = stride * row + bytesPerPixel * col;
int indexSrc = stride * rowSrc + bytesPerPixel * colSrc;
// Transfer the pixels
for (int i = 0; i < bytesPerPixel; i++)
pixelsNew[index + i] = pixelsSrc[indexSrc + i];
}
}
// Write out the array
bitmap.WritePixels(rect, pixelsNew, stride, 0);
}
ビットマップ変換を処理する場合は、変換を逆に実行することが不可欠です。元のビットマップのすべてのピクセルを調べ、新しいビットマップでの位置を決定する場合には、元のビットマップにある異なる複数のピクセルが、新しいビットマップの同一のピクセルにマッピングされる可能性が高くなります。つまり、新しいビットマップの一部のピクセルには値が設定されず、イメージに "穴" が開きます。
代わりに、新しいビットマップのすべてのピクセルについて、元のビットマップのどのピクセルがその行と列にマッピングされるかを調べるようにしてください。この方法を使用すると、新しいビットマップのすべてのピクセルが計算済みの値を持つことが保証されます。