SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

WriteableBitmapExその3

前回に引き続きWriteableBitmapExで色々やってみた。
基本的な画像処理とかをWPFで実装してみました。
ベースとなるXAMLは前回と同じ。
コードビハインドは、ちょこっと訂正。

XAML
<Window x:Class="WriteableBitmapExTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        mc:Ignorable="d" 
        d:DesignHeight="350" d:DesignWidth="525" SizeToContent="WidthAndHeight">
    <Grid>
        <Image Name="imgTarget" Stretch="None" />
    </Grid>
</Window>
コードビハインド

前回はコンストラクタで画像の読み込みや描画をしてたけど、
OnInitializeをオーバーライドして、ここで各種初期化・読み込み・描画をするように修正。
コンストラクタの中で、リソースの画像を読もうとしたら、例外が出ることがあったため。。。
WriteableBitmapのインスタンスとして、以下の二つをメンバにしています。
original・・・元画像用バッファ
processed・・処理結果格納用のインスタンス

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

        public MainWindow()
        {
            InitializeComponent();
        }

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            // 画像読み込み
            var bmp = new BitmapImage(new Uri("pack://application:,,,/Images/Koala.jpg"));
            original = BitmapFactory.ConvertToPbgra32Format(bmp);
            // 処理結果格納用インスタンス準備
            processed = BitmapFactory.New(original.PixelWidth, original.PixelHeight);

            // 画像処理
            Processing();

            // 描画結果をコントロールに反映
            imgTarget.Source = processed;
        }

        /// <summary>
        /// 画像処理ルーチン
        /// </summary>
        private void Processing()
        {
            // ここに画像への処理を書いてきます。
        }
    }
}

以下では、この中のProcessingメソッドに色々と処理を書いてます。

全ピクセルに対して、同じ処理を行うもの

全ピクセルに対して同じ処理を行う場合には、WriteableBitmapExで拡張メソッドとして用意されている、FoaEachメソッドが使えます。

モノクロ化
        private void Processing()
        {
            // ここに画像への処理を書いてきます。
            processed = original;
            using (processed.GetBitmapContext())
            {
                processed.ForEach((x, y, c) => { 
                                    byte v = (byte)((c.R + c.G + c.B) / 3);
                                    return Color.FromArgb(255, v, v, v); }
                                 );
            }
        }

輝度値でモノクロ化するなら、以下の式を使う

byte v = (byte)(0.299*c.R + 0.587*c.G + 0.114*c.B);


GetBrightnessなんてメソッドがあるので、これ使った方が早いかも。

byte v = original.GetBrightness(x, y);
ネガポジ反転
        private void Processing()
        {
            // ここに画像への処理を書いてきます。
            processed = original;
            using (processed.GetBitmapContext())
            {
                processed.ForEach((x, y, c) => Color.FromArgb(255, (byte)(255-c.R), (byte)(255-c.G), (byte)(255-c.B)));
            }
        }

で、このくらいの処理は、ライブラリ内にメソッドとしてすでに用意されてたりする。

var inverted = wb.Invert();
コントラスト調整
        private void Processing()
        {
            // ここに画像への処理を書いてきます。
            processed = original;
            using (processed.GetBitmapContext())
            {
                float val = 1.2f;
                processed.ForEach((x, y, c) => {
                                    float r = 0.5f + val * (c.ScR - 0.5f);
                                    float g = 0.5f + val * (c.ScG - 0.5f);
                                    float b = 0.5f + val * (c.ScB - 0.5f);
                                    Color result = Color.FromScRgb(1, r, g, b);
                                    result.Clamp();
                                    return result; }
                                 );
            }
        }


フィルター処理

ぼかし処理

隣接する8近傍の値の平均を取るだけのぼかしフィルタ

        private void Processing()
        {
            // ここに画像への処理を書いてきます。
            processed = original;
            using (processed.GetBitmapContext())
            {
                processed.ForEach((x, y) => {
                                    int count = 0;
                                    Color Sum = Color.FromScRgb(1, 0, 0, 0);
                                    for (int dy = -1; dy < 2; dy++)
                                        for (int dx = -1; dx < 2; dx++)
                                        {
                                            // 画像の領域をはみ出してしまう場合にはスキップ
                                            if (x + dx < 0 || original.PixelWidth <= x + dx ||
                                                y + dy < 0 || original.PixelHeight <= y + dy)
                                                continue;
                                            Color c = original.GetPixel(x + dx, y + dy);
                                            Sum += c;
                                            count++;
                                        }
                                    Sum = Color.FromScRgb(1, Sum.ScR / count, Sum.ScG / count, Sum.ScB / count);
                                    return Sum;
                                    }
                                 );
            }
        }


Convoluteメソッド

で、やっぱりこういうフィルター処理用にConvoluteというメソッドが用意されてたりする。
二次元配列で、フィルターの内容を定義して、Convoluteメソッドに渡すだけ。
もう至れり尽くせりなライブラリです。こんな楽しちゃっていいのかな、と思うくらい。。

さっきのぼかし処理は、以下のように書けます。

        private void Processing()
        {
            // ここに画像への処理を書いてきます。
            processed = original;
            using (processed.GetBitmapContext())
            {
                int[,] kernel = {
                                   {1, 1, 1},
                                   {1, 1, 1},
                                   {1, 1, 1},
                               };
                processed = original.Convolute(kernel);
            }
        }
ラプラシアン・フィルタ

このConvoluteメソッド、行列の全要素の和で割るという処理が内部にあるため、ラプラシアン・フィルタのような行列を渡すと例外を吐いてしまいます。
で、引数が3つのオーバーロード版があるので、そちらを使います。
・第二引数に行列全体の和(1を渡せば、渡した行列を使った計算結果そのままの値になるっぽい。)
・第三引数は、バイアス値。すべてのピクセルにこの値が加算されてかえって来ます。

        private void Processing()
        {
            // ここに画像への処理を書いてきます。
            processed = original;
            using (processed.GetBitmapContext())
            {
                int[,] kernel = {
                                   {-1, -1, -1},
                                   {-1,  8, -1},
                                   {-1, -1, -1},
                               };
                processed = original.Convolute(kernel, 1, 0);
                // ↑のような変換をすると、αチャンネルの値が0になってしまって表示されないので、強制的に書き換え。
                processed.ForEach((x, y, c) => Color.FromArgb(255, c.R, c.G, c.B));
            }
        }


エンボス処理
        private void Processing()
        {
            // ここに画像への処理を書いてきます。
            processed = original;
            using (processed.GetBitmapContext())
            {
                int[,] kernel = {
                                   {-1,  0, 1},
                                   {-1,  0, 1},
                                   {-1,  0, 1},
                               };
                processed = original.Convolute(kernel, 1, 127);
                // ↑のような変換をすると、αチャンネルの値が0になってしまって表示されないので、強制的に書き換え。
                processed.ForEach((x, y, c) => Color.FromArgb(255, c.R, c.G, c.B));
            }
        }