午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

画像のぼかし処理、注目ピクセルとその上下左右の平均値に変換

イメージ 1
イメージ 11
ウィンドウに画像ファイルドロップで
 
イメージ 13
画像が表示される
カラー画像もグレースケールで表示される
 
イメージ 12
ぼかし上下左右ボタンを押すとぼかし処理される
 
処理の重ねがけ
イメージ 14
5回ぼかし処理したところ
 
 
元画像とぼかし処理後画像の切り替え
イメージ 16
画像を左クリック押している間は元画像、離すと処理後画像表示
 
 
今回のアプリのダウンロード先
デザイン画面

f:id:gogowaten:20191214113232p:plain

 
MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace _20190422_平均ぼかし処理
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        string ImageFileFullPath;//ファイルパス、画像保存時に使う
        BitmapSource MyBitmapOrigin;//元画像、リセット用
        byte[] MyPixelsOrigin;//元画像、リセット用
        byte[] MyPixels;//処理後画像
        public MainWindow()
        {
            InitializeComponent();

            this.Drop += MainWindow_Drop;
            this.AllowDrop = true;
        }


        /// <summary>
        /// ぼかしフィルタ、上下左右との平均値に変換、PixelFormat.Gray8専用
        /// </summary>
        /// <param name="pixels"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <returns></returns>
        private (byte[] pixels, BitmapSource bitmap) Filter上下左右(byte[] pixels, int width, int height)
        {
            byte[] filtered = new byte[pixels.Length];//処理結果用
            int stride = width;//一行のbyte数
            //上下左右1ラインは処理しない(めんどくさい)
            for (int y = 1; y < height - 1; y++)
            {
                for (int x = 1; x < width - 1; x++)
                {
                    //注目ピクセルの値と、その上下左右の合計の平均値を新しい値にする
                    int total = 0;
                    int p = x + y * stride;//注目ピクセルの位置
                    total += pixels[p - stride];//上
                    total += pixels[p - 1];     //左
                    total += pixels[p];      //注目ピクセル
                    total += pixels[p + 1];     //右
                    total += pixels[p + stride];//下
                    int average = total / 5;
                    filtered[p] = (byte)average;
                }
            }

            return (filtered, BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
        }


       #region その他

        //画像ファイルドロップ時の処理
        private void MainWindow_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) { return; }
            string[] filePath = (string[])e.Data.GetData(DataFormats.FileDrop);
            var (pixels, bitmap) = MakeBitmapSourceAndByteArray(filePath[0], PixelFormats.Gray8, 96, 96);

            if (bitmap == null)
            {
                MessageBox.Show("画像ファイルじゃないみたい");
            }
            else
            {
                MyPixels = pixels;
                MyPixelsOrigin = pixels;
                MyBitmapOrigin = bitmap;
                MyImage.Source = bitmap;
                MyImageOrigin.Source = bitmap;
                ImageFileFullPath = filePath[0];
            }
        }

        //画像の保存
        private void SaveImage(BitmapSource source)
        {
            var saveFileDialog = new Microsoft.Win32.SaveFileDialog();
            saveFileDialog.Filter = "*.png|*.png|*.bmp|*.bmp|*.tiff|*.tiff";
            saveFileDialog.AddExtension = true;
            saveFileDialog.FileName = System.IO.Path.GetFileNameWithoutExtension(ImageFileFullPath) + "_";
            saveFileDialog.InitialDirectory = System.IO.Path.GetDirectoryName(ImageFileFullPath);
            if (saveFileDialog.ShowDialog() == true)
            {
                BitmapEncoder encoder = new BmpBitmapEncoder();
                if (saveFileDialog.FilterIndex == 1)
                {
                    encoder = new PngBitmapEncoder();
                }
                else if (saveFileDialog.FilterIndex == 2)
                {
                    encoder = new BmpBitmapEncoder();
                }
                else if (saveFileDialog.FilterIndex == 3)
                {
                    encoder = new TiffBitmapEncoder();
                }
                encoder.Frames.Add(BitmapFrame.Create(source));

                using (var fs = new System.IO.FileStream(saveFileDialog.FileName, System.IO.FileMode.Create, System.IO.FileAccess.Write))
                {
                    encoder.Save(fs);
                }
            }
        }

        /// <summary>
        /// 画像ファイルからbitmapと、そのbyte配列を取得、ピクセルフォーマットを指定したものに変換
        /// </summary>
        /// <param name="filePath">画像ファイルのフルパス</param>
        /// <param name="pixelFormat">PixelFormatsを指定</param>
        /// <param name="dpiX">96が基本、指定なしなら元画像と同じにする</param>
        /// <param name="dpiY">96が基本、指定なしなら元画像と同じにする</param>
        /// <returns></returns>
        private (byte[] array, BitmapSource source) MakeBitmapSourceAndByteArray(string filePath, PixelFormat pixelFormat, double dpiX = 0, double dpiY = 0)
        {
            byte[] pixels = null;
            BitmapSource source = null;
            try
            {
                using (System.IO.FileStream fs = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read))
                {
                    var bf = BitmapFrame.Create(fs);

                    var convertedBitmap = new FormatConvertedBitmap(bf, pixelFormat, null, 0);
                    int w = convertedBitmap.PixelWidth;
                    int h = convertedBitmap.PixelHeight;
                    int stride = (w * pixelFormat.BitsPerPixel + 7) / 8;
                    pixels = new byte[h * stride];
                    convertedBitmap.CopyPixels(pixels, stride, 0);
                    //dpi指定がなければ元の画像と同じdpiにする
                    if (dpiX == 0) { dpiX = bf.DpiX; }
                    if (dpiY == 0) { dpiY = bf.DpiY; }
                    //dpiを指定してBitmapSource作成
                    source = BitmapSource.Create(
                        w, h, dpiX, dpiY,
                        convertedBitmap.Format,
                        convertedBitmap.Palette, pixels, stride);
                };
            }
            catch (Exception)
            {
            }
            return (pixels, source);
        }
        //画像クリックで元画像と処理後画像の切り替え
        private void Grid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            int aa = Panel.GetZIndex(MyImage);
            Panel.SetZIndex(MyImageOrigin, aa + 1);
        }
        private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            int aa = Panel.GetZIndex(MyImage);
            Panel.SetZIndex(MyImageOrigin, aa - 1);
        }
        //表示画像リセット
        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            MyImage.Source = MyBitmapOrigin;
            MyPixels = MyPixelsOrigin;
        }
        //画像保存
        private void Button_Click_3(object sender, RoutedEventArgs e)
        {
            if (MyImage.Source == null) { return; }
            //BitmapSource source = (BitmapSource)MyImage.Source;
            //SaveImage(new FormatConvertedBitmap(source, PixelFormats.Indexed4, new BitmapPalette(source, 16), 0));
            //SaveImage(new FormatConvertedBitmap(source, PixelFormats.Indexed4, null, 0));
            SaveImage((BitmapSource)MyImage.Source);
        }

       #endregion
        //ぼかしフィルタ処理
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (MyPixels == null) { return; }
            (byte[] pixels, BitmapSource bitmap) = Filter上下左右(MyPixels, MyBitmapOrigin.PixelWidth, MyBitmapOrigin.PixelHeight);
            MyImage.Source = bitmap;
            MyPixels = pixels;
        }

    }
}
61~183行目は、ぼかし処理と関係ない
 
 
 
エクセル方眼紙で

f:id:gogowaten:20191214113337p:plain

元画像とぼかし処理後の輝度の変化
 
 
イメージ 5
注目ピクセルの輝度と、その上下左右のピクセルの輝度の平均値を
注目ピクセルの新しい値にするとぼかし処理になる
 
イメージ 6
(1,1)の輝度200は
 

f:id:gogowaten:20191214113352p:plain

(200+200+200+100+100)/5=160
 
(1,2)の輝度100は

f:id:gogowaten:20191214113406p:plain

(100+200+100+200+100)/5=140
これを全部のピクセルで行うと
 

f:id:gogowaten:20191214113421p:plain

ぼかし処理になる
 
 
外周のピクセルの処理
イメージ 9
たとえば一番左上ピクセル(0,0)の場合だと、上と左のピクセルは存在しないので、別の処理をする必要がある、存在するピクセルだけでの平均とか、元画像の値そのままにするとか
 
 

f:id:gogowaten:20191214113434p:plain

今回は(も)めんどくさいので処理しないで0で埋めることにした
なので画像の外周は黒色になる
イメージ 15
外周1ピクセルは黒色
 
右上付近を30倍に拡大と値表示
イメージ 18
上が元画像
 
 
 

f:id:gogowaten:20191214113453p:plain

BitmapSourceクラスのCopyPixelsメソッドを使って得られるものを
Filter上下左右に渡す
配列の中の注目ピクセルの位置がわかれば、左は-1、右は+1
上下は1行分のbyte数(stride)で、上は-stride、下は+strideになる
PixelFormats.Gray8専用なのでstrideは画像の横幅ピクセルと同じ
4x4ピクセルの画像のときCopyPixelsの配列は4x4=16の要素
これをピクセルの位置に合わせてみると
 
0	1	2	3
4	5	6	7
8	9	10	11
12	13	14	15
 
strideは横幅と同じなので4
このとき注目ピクセルが1,2のとき、配列での位置(インデックス)は9
これは79行目のp = x + y * stride;は1+2*4=9
左は9-1で8、右は9+1で10、上は9-strideは9-4=5、下は9+strideは9+4=13
 
 
 
 
参照したところ
 
 
ギットハブ
 
 
関連記事
2019/04/23は次の日
画像ぼかし処理、普通のぼかし処理では画像によってイマイチな結果になる ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15940003.html
カラー画像でもぼかし処理
 
2019/04/25
画像のぼかし処理、8近傍平均とガウスぼかし、グレースケールで処理 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15941552.html