WPF ウィンドウ全体にマスク処理

あるWindowの上にAeroGlassのWindowが来ると、下のWindowがボケた感じになります。そんなボケた感じを2つのWindowを使わずに作ろうとするとどうしたらいいかなぁと思ったのがキッカケの実用性無視したエントリ。

Window自体をAeroGlassにはできますが今回目的としているその上のElement群には影響を与えられないので、方針は今あるElementの上にぼかしフィルタを付けること。今ある画面をキャプチャしてぼかしてやろうということです。

 

1.ぼかす対象を作る

ぼかす対象としてはGridを想定してみます。

 MainWindow.xaml

<Window x:Class="WindowMask.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:me="clr-namespace:WindowMask"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="420"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Content="Mask/Unmask" Grid.Column="1" Grid.Row="1"/>
        <Grid x:Name="MaskTargetGrid" Grid.ColumnSpan="2" Background="Gray">
            <Calendar HorizontalAlignment="Left" Margin="38,95,0,0" VerticalAlignment="Top"/>
            <CheckBox Content="CheckBox" HorizontalAlignment="Left" Margin="226,38,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.135,-0.667"/>
            <DataGrid HorizontalAlignment="Left" Height="62" Margin="267,70,0,0" VerticalAlignment="Top" Width="125"/>
            <Ellipse Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="127" Margin="421,95,0,0" Stroke="Black" VerticalAlignment="Top" Width="67"/>
            <Label Content="Label" HorizontalAlignment="Left" Margin="392,29,0,0" VerticalAlignment="Top"/>
            <Button Content="Button" HorizontalAlignment="Left" Margin="36,33,0,0" VerticalAlignment="Top" Width="75"/>
            <Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="274,159,0,0" Stroke="Black" VerticalAlignment="Top" Width="118"/>
        </Grid>
    </Grid>
</Window>

mainwindow

Mask/Unmaskボタンを押すと、灰色背景部分(Grid)がまるっとマスクが掛かるという想定。ボタンに処理を載せているのでセオリー通りICommandを実装するコマンドとしてマスク処理を作ります。

 

2.ぼかす処理を作る

MaskCommand.cs

namespace WindowMask
{
    using System;
    using System.Windows;
    using System.Windows.Media;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media.Imaging;
    using System.IO;
    using System.Drawing;
    using System.Drawing.Imaging;
    public class MaskCommand : ICommand
    {
        private bool masking;
        private Grid maskingGrid = new Grid();

        public bool CanExecute(object parameter){
            return parameter is Window;
        }

        public event EventHandler CanExecuteChanged{
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter){
            var window = parameter as Window;
            if (masking){
                UnMask(window);
            }
            else{
                Mask(window);
            }
            masking = !masking;
        }

        private void Mask(Window window)
        {
            // Render対象のGridの見た目を保存
            var grid = GetRenderTarget(window);
            var bitmap = new RenderTargetBitmap(
                (int)grid.ActualWidth, (int)grid.ActualHeight, 96, 96, PixelFormats.Pbgra32);
            bitmap.Render(window);
            var encoder = new BmpBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(bitmap));
            using(var originalGridVisualStream = new MemoryStream())
            using (var maskedGridVisualStream = new MemoryStream())
            {
                encoder.Save(originalGridVisualStream);
                // マスク処理
                using (Bitmap image = new Bitmap(originalGridVisualStream))
                {
                    var bluredImage = MaskEffect.Blur(image);
                    bluredImage.Save(maskedGridVisualStream, ImageFormat.Bmp);
                }
                originalGridVisualStream.Close();
                // マスク処理した画像をGridに張り、最前面へ配置
                WriteableBitmap maskedBitmap = new WriteableBitmap(BitmapFrame.Create(maskedGridVisualStream));
                maskingGrid.Background = new ImageBrush(maskedBitmap);
                grid.Children.Add(maskingGrid);
            }
        }

        private void UnMask(Window window)
        {
            // 設定したマスクを除去するだけ
            var grid = GetRenderTarget(window);
            grid.Children.Remove(maskingGrid);
        }

        private Grid GetRenderTarget(Window window)
        {
            // ここでは、Windowの2階層下にあるGridを前提とする
            var grid = window.Content as Grid;
            foreach (var element in grid.Children)
            {
                if (element is Grid){
                    return element as Grid;
                }
            }
            throw new InvalidProgramException("Render対象のGridがない");
        }
    }
}

Mask処理がポイントです。今の見た目をBitmapとして保存→画像処理してぼかす→Gridに張り付けて最前面配置という作用をしています。

具体的なマスク処理は適当に書くと↓な感じ。ポインタ操作をしているのでプロジェクトのプロパティのビルドタブでアンセーフコードの実行を許可する必要があります。

MaskEffect.cs

namespace WindowMask
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Linq;
    using System.Text;

    public class MaskEffect
    {
        public unsafe static Bitmap Blur(Bitmap srcImage)
        {
            Bitmap dstImage = new Bitmap(srcImage);
            var srcData = srcImage.LockBits(new Rectangle(Point.Empty, srcImage.Size),
                ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb);
            var dstData = dstImage.LockBits(new Rectangle(Point.Empty, dstImage.Size),
                ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb);
            byte* pbySrc = (byte*)srcData.Scan0;
            byte* pbyDst = (byte*)dstData.Scan0;

            // ぼかし処理(例)
            int operatorSize = 10;
            for (int y = operatorSize; y < srcImage.Height - operatorSize; y++){
                for (int x = operatorSize; x < srcImage.Width - operatorSize; x++){
                    for (int i = 0; i < 4; i++){
                        int sum = 0;
                        for (int offsety = -operatorSize; offsety <= operatorSize; offsety++){
                            for(int offsetx = -operatorSize; offsetx <= operatorSize; offsetx++){
                                sum += pbySrc[(y+offsety) * srcData.Stride + 4 * (x+offsetx) + i];
                            }
                        }
                        pbyDst[y*dstData.Stride+4*x+i] = (byte)( sum / (2*operatorSize+1) / (2*operatorSize+1) );
                    }
                }
            }

            srcImage.UnlockBits(srcData);
            dstImage.UnlockBits(dstData);
            return dstImage;
        }
    }
}

コマンドとして作成したのでこれを元のWindowのMask/Unmaskボタンに割り当てます。

 MainWindow.xaml(改)

<Window x:Class="WindowMask.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:me="clr-namespace:WindowMask"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <me:MaskCommand x:Key="maskCommand"/>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="420"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Content="Mask/Unmask" Grid.Column="1" Grid.Row="1"
                Command="{StaticResource maskCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"/>
        <Grid x:Name="MaskTargetGrid" Grid.ColumnSpan="2" Background="Gray">
            <Calendar HorizontalAlignment="Left" Margin="38,95,0,0" VerticalAlignment="Top"/>
            <CheckBox Content="CheckBox" HorizontalAlignment="Left" Margin="226,38,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.135,-0.667"/>
            <DataGrid HorizontalAlignment="Left" Height="62" Margin="267,70,0,0" VerticalAlignment="Top" Width="125"/>
            <Ellipse Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="127" Margin="421,95,0,0" Stroke="Black" VerticalAlignment="Top" Width="67"/>
            <Label Content="Label" HorizontalAlignment="Left" Margin="392,29,0,0" VerticalAlignment="Top"/>
            <Button Content="Button" HorizontalAlignment="Left" Margin="36,33,0,0" VerticalAlignment="Top" Width="75"/>
            <Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="274,159,0,0" Stroke="Black" VerticalAlignment="Top" Width="118"/>
        </Grid>
    </Grid>
</Window>

これで実行してMask/Unmaskを押すと、以下のような感じ。

maskedwindow

WPFは表現力が優れているのはわかるけどElementの境界を越えて見た目を変更する・・・というのはこんな方法しかないんじゃないかな。だから何?という突っ込みは禁止です。

11/11追記
ぼかすだけならGrid.EffectにBlurEffectをかければもっとスマートにできますね。今回のテクニックは、Element領域を超えた描画をしたいとか、要素全体に対してイベントハンドラを無効化したいとかいった場合に使い道がある。。。かも、なさそう・・・。

C#での画像処理 – 実践編

C#での画像処理、3回目の今回でおしまいです;オィ

今回のプログラム、構想はあるのですが作りこみが必要になるので概要だけさらって終わりにしようかと思います、万が一興味を持った方は作りこんでみてください;オィ

お題はASCIIアート。画像を適切なサイズにリサイズして2値化した上で画素値を元に文字に置き換えるという簡単な処理で実現できます。つまり、イメージはこういうこと。

taiou

ただ上のような3×3の画素値こんな簡単な対応付けでも2^9 = 512パターンを用意しなければなりません。これが厄介、文字にならない場合どうするんだってのもありますし。そこでここでは画素値に基づいて文字の濃さを割り当ててみます。

白は画素値でいうと255ですが文字の’ ’に割り当て、黒は画素値でいうと0ですが、文字の’轟’ というインクをいっぱい使いそうな文字に割り当て、その間の灰色は密度が合いそうな文字を選択する、という方式です。VGAの画像を1/16に圧縮した後で1画素ごとに文字に割り当ててみます。

ImageToChar.cs

namespace NormalizeCorrelation
{
    using System;
    using System.Text;

    internal class ImageToChar
    {
        MonochromeImage image;

        internal ImageToChar(MonochromeImage srcImage){
            // 適宜必要な変換
            image = srcImage.ScalingHalf().ScalingHalf().ScalingHalf().ScalingHalf();
        }

        public override string ToString()
        {
            StringBuilder builder = new StringBuilder();
            for (int y = 0; y < image.Height; y++)
            {
                for (int x = 0; x < image.Width; x++)
                {
                    builder.Append(ByteToChar(image[x, y]));
                }
                builder.Append(Environment.NewLine);
            }
            return builder.ToString();
        }

        private Char ByteToChar(Byte dat)
        {
            if (dat > 240)
            {
                return ' ';
            }
            if (dat > 200)
            {
                return '、';
            }
            if (dat > 150)
            {
                return '。';
            }
            if (dat > 125)
            {
                return 'ー';
            }
            if (dat > 100)
            {
                return 'い';
            }
            if (dat > 80)
            {
                return 'に';
            }
            if (dat > 65)
            {
                return 'た';
            }
            if (dat > 50)
            {
                return 'な';
            }
            if (dat > 25)
            {
                return '@';
            }
            if (dat > 10)
            {
                return 'ぜ';
            }
            if (dat > 0)
            {
                return '車';
            }
            return '轟';
        }
    }
}

image

動かすとこんな感じ。何が写ってるのかわかりにくいですが、手前にキーボードと私の手、奥にPCディスプレイがあります。TextBoxはMultilineをTrueにしたうえで、フォントを等幅フォントにしておきます。

ASCIIアートソフトを作りこんでみたい方はフォントを大きくしたり、プロポーショナルにしたりすると難易度はどんどん上がっていきます。

C#での画像処理 – 基本編

C#での画像処理、2回目は基本編と銘打ってよくある画像処理を用意します。

1回目のデータ構造を使った画像処理の例を出していませんでした。画像処理の基本としてよくある線形フィルタ等を用意してみました。

namespace NormalizeCorrelation
{
    using System;
    using System.Collections.Generic;

    internal static class MonochromeExtension
    {

        internal static MonochromeImage Invert(this MonochromeImage srcImage)
        {
            int dstWidth = srcImage.Width;
            int dstHeight = srcImage.Height;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    dstImage[x, y] = (byte)(~srcImage[x,y]);
                }
            }
            return dstImage;
        }

        internal static MonochromeImage LowPassFilter(this MonochromeImage srcImage)
        {
            int dstWidth = srcImage.Width;
            int dstHeight = srcImage.Height;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    int sum = srcImage[x - 1, y - 1] + srcImage[x, y - 1] + srcImage[x + 1, y - 1] +
                              srcImage[x - 1, y] + srcImage[x, y] + srcImage[x + 1, y] +
                              srcImage[x - 1, y + 1] + srcImage[x, y + 1] + srcImage[x + 1, y + 1];
                    dstImage[x, y] = (byte)( sum / 9);
                }
            }
            return dstImage;
        }

        internal static MonochromeImage ScalingHalf(this MonochromeImage srcImage)
        {
            int dstWidth = srcImage.Width / 2;
            int dstHeight = srcImage.Height/2;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    int sum = srcImage[2 * x, 2 * y] + srcImage[2 * x + 1, 2 * y] +
                              srcImage[2 * x, 2 * y + 1] + srcImage[2 * x + 1, 2 * y + 1];
                    dstImage[x, y] = (byte)(sum / 4);
                }
            }
            return dstImage;
        }

        internal static MonochromeImage LaplacianFilter(this MonochromeImage srcImage )
        {
            int dstWidth = srcImage.Width;
            int dstHeight = srcImage.Height;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    int rowVal = srcImage[x - 1, y - 1] + srcImage[x, y - 1] + srcImage[x + 1, y - 1] +
                                 srcImage[x - 1, y] - 8 * srcImage[x, y] + srcImage[x + 1, y] +
                                 srcImage[x - 1, y + 1] + srcImage[x, y + 1] + srcImage[x + 1, y + 1];
                    int absVal = Math.Abs(rowVal);
                    dstImage[x, y] = (byte)((byte.MaxValue < absVal) ? byte.MaxValue : absVal);
                }
            }
            return dstImage;
        }

        internal static MonochromeImage MedianFilter(this MonochromeImage srcImage)
        {
            int dstWidth = srcImage.Width;
            int dstHeight = srcImage.Height;
            MonochromeImage dstImage = new MonochromeImage(dstWidth, dstHeight);
            for (int y = 0; y < dstHeight; y++)
            {
                for (int x = 0; x < dstWidth; x++)
                {
                    var items = new List<byte>(){
                        srcImage[x - 1, y - 1],
                        srcImage[x, y - 1],
                        srcImage[x + 1, y - 1],
                        srcImage[x - 1, y],
                        srcImage[x, y],
                        srcImage[x + 1, y],
                        srcImage[x - 1, y + 1],
                        srcImage[x, y + 1],
                        srcImage[x + 1, y + 1]
                    };
                    items.Sort();
                    dstImage[x, y] = items[4];
                }
            }
            return dstImage;
        }
    }
}

上の通り、データ型に対する拡張メソッドとして定義すると次々と画像処理をさせたいときに以下のようなコードがかけます。

        private void UpdateDisplayImage(Bitmap img)
        {
            Bitmap capureImage = (Bitmap)img.Clone();
            MonochromeImage image = new MonochromeImage(capureImage);
            // ローパスフィルタかけて、1/2にダウンサンプリング、
            // その後ラプラシアンフィルタでエッジ抽出。
            var resultImage = image.LowPassFilter().ScalingHalf().LaplacianFilter();

            capturedPictureBox.Image = resultImage.GetAsBitmap();
        }

拡張メソッドばんざい。これは以前にUSBカメラをC#で使うのコードの一部です。組み合わせて動かすとこんな感じ。

imageprocessbycsharp

リアルタイムに更新される画像で画像処理を確認できるのはわかりやすくてGOOD。次回はもう少し自分でもやってみたくなる(?)内容に進む予定です;

C#での画像処理 – 土台編

今回から数回に分けてC#での画像処理で遊んでみようかと思います。第1回目となる今回は土台づくり。

画像といえばBitmap、非圧縮でRGBの色抽出もでき、Pixel単位で処理をするならこれ以上ないデータ形式です。ただこのBitmap、2, 3厄介なところがあります。それは

  • 画素ごとのSetPixelが遅いためLockBits, UnlockBitsが必要
  • その結果必要なデータがBitmapとBitmapDataに跨っている
  • さらにBitmapはIDisposableを実装するのでusingのたびにネストが1段深まる

あたりです。この辺りを忘れて処理ができる構造のほうが画像処理に向いている、と思うんです。私は。なので今回は画像処理の土台となるデータ構造を作りましょう。1つ大きな制限を課しておきます、それはモノクロ画像しか扱わないということです。(ちょっとお遊びするにはカラーは情報量が多すぎるのが一番の理由です。

 MonochromeImage.cs

namespace NormalizeCorrelation
{
    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    internal class MonochromeImage
    {
        private byte[,] data;
        private int width;
        private int height;

        internal MonochromeImage(int width, int height)
        {
            data = new byte[width, height];
            this.width = width;
            this.height = height;
        }
        internal unsafe MonochromeImage(Bitmap srcImage)
            : this(srcImage, new Rectangle(Point.Empty, srcImage.Size))
        {
        }

        internal unsafe MonochromeImage(Bitmap srcImage, Rectangle targetRect)
        {
            if ((targetRect.X < 0) ||
                (srcImage.Width < (targetRect.X + targetRect.Width)) ||
                (targetRect.Y < 0) ||
                (srcImage.Height < (targetRect.Y + targetRect.Height)))
            {
                throw new ArgumentOutOfRangeException();
            }
            BitmapData bitmapData = srcImage.LockBits(new Rectangle(Point.Empty, srcImage.Size),
                ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
            data = new byte[srcImage.Width, srcImage.Height];
            byte* topAddress = (byte*)bitmapData.Scan0;
            width = targetRect.Width;
            height = targetRect.Height;
            int stride = bitmapData.Stride;

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    data[x, y] = (byte)(
                        (0.114478 * topAddress[y * stride + 3 * x]) +       // B
                        (0.586611 * topAddress[y * stride + 3 * x + 1]) +   // G
                        (0.298912 * topAddress[y * stride + 3 * x + 2])     // R
                        );
                }
            }
            srcImage.UnlockBits(bitmapData);
        }
        internal int Width
        {
            get { return width; }
            set { width = value; }
        }
        internal int Height
        {
            get { return height; }
            set { height = value; }
        }
        internal byte this[int x, int y]
        {
            // 3x3フィルタ処理などで画像領域外にアクセスしたときに
            // 自動的に折り返して境界を意識不要にしてやる。
            get {
                int roundedX = x < 0 ? 0 : ((width <= x) ? width - 1 : x);
                int roundedY = y < 0 ? 0 : ((height <= y) ? height - 1 : y);
                return data[roundedX, roundedY];
            }
            set {
                if (0 <= x && x < width && 0 <= y && y < height)
                {
                    data[x, y] = value;
                }
            }
        }
        internal unsafe Bitmap GetAsBitmap()
        {
            Bitmap saveImage = new Bitmap(Width, Height, PixelFormat.Format8bppIndexed);
            BitmapData bitmapData = saveImage.LockBits(new Rectangle(Point.Empty, new Size(Width, Height)),
                ImageLockMode.WriteOnly,
                PixelFormat.Format8bppIndexed);
            byte* topAddress = (byte*)bitmapData.Scan0;
            int stride = bitmapData.Stride;
            for(int y = 0; y < Height; y++){
                for (int x = 0; x < Width; x++)
                {
                    topAddress[y * stride + x] = data[x, y];
                }
            }
            saveImage.UnlockBits(bitmapData);

            // ColorPaletteはコンストラクタ公開なしなので参照コピーしてくる。
            ColorPalette palette = saveImage.Palette;
            for(int i = 0; i <= byte.MaxValue; i++){
                palette.Entries[i] = Color.FromArgb(i, i, i);
            }
            saveImage.Palette = palette;
            return saveImage;
        }

    }
}

こんな感じでいかがでしょうか。対象としたのは24bpp(RGB888)のBitmapです。α含めた32bppも対応しても良かったのですがやめときました。NTSCのグレー係数という重みをつけてモノクロ画素を算出・保持しています。データの並びは、B, G, Rなので注意。

画像アクセスにはインデクサを公開します。読み書き両方可能にしています。よくある画像処理フィルタに、オペレータとして3×3や5×5を取るものがあります。つまり対象とする画素を基準としてX, Y方向に±1画素の自分含めた周囲9画素や、±2画素の周囲25画素の画素値に対して演算した結果を自画素値とする処理です。この処理をする際、画像のフチでは領域外アクセスしないよう気をつけねばなりませんが、インデクサとしているので適宜内部で丸めたり例外スローしたりできて便利です。

出力結果確認のためBitmap出力をつけました。8bppのモノクロ画像にしていますが、24bppのほうが都合がよいかも。。?これは今後作りながら手を加えていくことになるかと思います。