読者です 読者をやめる 読者になる 読者になる

WriteableBitmapの画素をポインタから操作する

C# WPF

WPFには様々なビットマップのクラスがあってややこしいですが、System.Windows.Media.Imaging.WriteableBitmapクラスは低レベルな操作ができるので、他の環境とのやり取りもしやすく私好みです。今回はWriteableBitmapの画素値にポインタで直接アクセスします。

各メソッド

BackBuffer, BackBufferStride

WriteableBitmapの画素データを示すポインタがどこにあるかというと、BackBufferプロパティです。バックバッファというと更にプライマリバッファに転送するために何かしなければいけないような気になりますが、編集が終わればすぐに前面に出てきますので心配は無用のようです。

また、1行ごとのバイト数はBackBufferStrideプロパティから得られます。

Lock 〜 Unlock

画素のポインタにアクセスする前に、System.Drawing.Bitmap の場合は LockBits でバッファを固定しますが、WriteableBitmap でも同様に LockUnlock というメソッドがあります。この2つの呼び出しの間でピクセルをいじることになります。

wb.Lock();
.
.
wb.Unlock();

AddDirtyRect

Bitmapと違うのは、AddDirtyRect メソッドによって、ピクセルデータを変更させる領域を通知する必要があることです。

これはロック中ならどこに書いてもいいようで、ピクセルデータの操作の後に書いても効きます。

wb.Lock();
.
.
wb.AddDirtyRect(new Int32Rect(0, 0, wb.PixelWidth, wb.PixelHeight));
wb.Unlock();

もしポインタ操作で画像全体の画素値を変化させていても、AddDirtyRectで一部分のみを指定していればその部分の変更のみが適用され、他の部分は無視されます。AddDirtyRectで指定した部分のみがバックバッファから転送されるという仕組みのようです。

ピクセルデータの操作

特に変わったことはありません。ここでソースコード全体を載せてしまいます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfTest
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private WriteableBitmap wb;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            wb = new WriteableBitmap(640, 480, 96, 96, PixelFormats.Bgr32, null);
            wb.Lock();
            unsafe
            {
                byte* ptr = (byte*)wb.BackBuffer;
                for (int y = 0; y < wb.PixelHeight; y++)
                {
                    for (int x = 0; x < wb.PixelWidth; x++)
                    {
                        byte* p = ptr + (y * wb.BackBufferStride) + (x * 4);
                        p[0] = 255;
                        p[1] = 64;
                        p[2] = 0;
                        //p[3] = 0;
                    }
                }
            }
            wb.AddDirtyRect(new Int32Rect(0, 0, wb.PixelWidth - 200, wb.PixelHeight));
            wb.Unlock();
            image1.Source = wb;
        }
    }
}

AddDirtyRectの効きを確かめるため、幅を200ピクセル減らした部分にのみ適用してみた結果がこちらです。

余談

CopyPixels, WritePixels

以下余談。CopyPixelsメソッドで画素値をすべて byte[] として取得することができ、WritePixelsメソッドで byte配列 もしくは IntPtr が示す画素値データを書き込むことができます。

この両メソッドは比較的使い勝手が良いので、実のところこれを使えばわざわざ今回のようにポインタを持ってくる必要に迫られることはそれほどありません。

Int32Rectによる領域指定

CopyPixels, WritePixels ともに、System.Windows.Int32Rect構造体によって適用する範囲を指定することができます。

ここで CopyPixels の方では、Int32Rect.Empty を指定することでビットマップ全体という意味を与えることができます。便利です。しかしながら、WritePixelsの方ではそうなっておらず、字面通り「一切適用範囲無し」の意味と取られてしまい何も処理されません。ちなみにAddDirtyRectもWritePixelsと同様です。

これはどちらかに統一してほしいものです。利便性が良いので個人的には前者にあわせてほしいです。