午後わてんのブログ

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

ガウス関数からカーネル作成、標準偏差とカーネルサイズ、グレースケール画像のぼかし処理

画像のぼかし処理で使うカーネルガウス関数から作成してガウスぼかし
 
標準偏差の指定とぼかし具合
カーネルサイズは3x3標準偏差を0.1、0.5、1.0
イメージ 1
標準偏差=0.1だと全くぼやけない、0.5でほんの少しぼやけたかなくらいで、1.0だとぼやけたのがはっきりわかる
 
 
標準偏差を1.5と2.0、5.0で比較
イメージ 2
どれも大差ない感じ
 
1次のガウス関数標準偏差と距離によるカーネルの数値(重み)の変化
イメージ 3
縦が距離、横が標準偏差を0.5から0.5刻みのとき
中心(距離0)を100%としたときの重み
 
 
カーネルサイズ3x3で使う範囲
イメージ 4
数値は中心(0)ピクセルに対する影響度みたいなものだから
0.5では14%なので中心(0)の輝度はほとんど変化しないので、ぼやけ方が小さい
1.0では61%と急に高くなり、1.5で80%、そこからは変化が少ない
だからさっきの画像の比較でも1.5と5.0も同じように見えたのかも
これだと2以上を指定する意味は薄いねえ、逆に0.5から1.0は細かく分けたい


カーネルサイズ5x5で使う範囲だと
イメージ 5
外側の-2と2は標準偏差2と3の間で、変化量は19あるから意味あるかも



0.1から1.5を0.1刻み

f:id:gogowaten:20191214122009p:plain

サイズ3x3でも0.3以下は全くぼやけないみたい
ぼやけ具合の調整を標準偏差でする場合、カーネルサイズ3x3なら、0.5から1.4までを0.1刻みで10段階の指定が良さそう
 
 
 
カーネルサイズとぼやけ具合
イメージ 7
5x5以上は違いが感じられない
 
カーネル	3x3	5x5	7x7	9x9
距離	0,	1,	2,	3,	4
影響	100,	61,	14,	1,	0
 
5x5と9x9だと14%の差があるけど、この程度だと違いがわからない
 
 
 
イメージ 8
3x3と5x5はかなり差があるけど
それ以上は差は出ているけど小さいかな
んーでも5x5と9x9を比較すると結構差がある感じもする
 
カーネル	3x3	5x5	7x7	9x9
距離	0,	1,	2,	3,	4
影響	100,	88,	61,	32,	14
 
 
 
処理時間
10年目のパソコンで、横x縦ピクセル数=2048x1536の画像を処理するとき
カーネルサイズ	時間
3x3			0.1秒
5x5			1秒
7x7			2秒
9x9			3.5秒
体感だとこんなだった、3x3ならリアルタイムでも行けるかなあってくらい
 
 
 
よりぼかしたいときはカーネルサイズを大きくするより
小さなサイズで処理を繰り返したほうが速い
イメージ 9
カーネルサイズ3x3で5回処理したほうが
速いし、よりぼやける
 
イメージ 10
輝度補正あり
見比べれば違うのがわかるくらいの差
元画像は
イメージ 11
これ
色(輝度)の補正あり
カーネルサイズ	時間
3x3			3秒
5x5			8秒
7x7			14秒
9x9			23秒
かなり時間かかる
この場合もよりぼかすときはカーネルサイズを大きくするより、小さなサイズで処理を繰り返したほうが速そう
 
 
 
 
デザイン画面

f:id:gogowaten:20191214122403p:plain

 
MainWindow.xaml.cs
<feff>using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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;
//ガウス関数からカーネル作成、標準偏差とカーネルサイズ、グレースケール画像のぼかし処理(ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
//https://blogs.yahoo.co.jp/gogowaten/15945699.html

namespace _20190427_ガウス関数
{
    /// <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;
            //MyTest();

        }



        //private void MyTest()
        //{
        //    //string filePath = "";
        //    ImageFileFullPath = @"E:\オレ\雑誌スキャン\2003年pc雑誌\20030115_dosvmag_003.jpg";
        //    //ImageFileFullPath = @"D:\ブログ用\テスト用画像\ノイズ除去\20030115_dosvmag_114.jpg";
        //    //ImageFileFullPath = @" D:\ブログ用\テスト用画像\border_row.bmp";
        //    //ImageFileFullPath = @"D:\ブログ用\テスト用画像\ノイズ除去\20030115_dosvmag_003_.png";
        //    //ImageFileFullPath = @"D:\ブログ用\テスト用画像\ノイズ除去\20030115_dosvmag_003_重.png";
        //    //ImageFileFullPath = @"D:\ブログ用\テスト用画像\ノイズ除去\20030115_dosvmag_003_重_上半分.png";
        //    //ImageFileFullPath = @"D:\ブログ用\Lenna_(test_image).png";
        //    //ImageFileFullPath = @"D:\ブログ用\テスト用画像\ノイズ除去\蓮の花.png";
        //    //ImageFileFullPath = @"D:\ブログ用\テスト用画像\SIDBA\Girl.bmp";
        //    //ImageFileFullPath = @"D:\ブログ用\テスト用画像\ノイズ除去\とり.png";
        //    //ImageFileFullPath = @"D:\ブログ用\テスト用画像\ノイズ除去\風車.png";


        //    (MyPixels, MyBitmapOrigin) = MakeBitmapSourceAndByteArray(ImageFileFullPath, PixelFormats.Gray8, 96, 96);

        //    MyImageOrigin.Source = MyBitmapOrigin;
        //    MyPixelsOrigin = MyPixels;
        //}


        /// <summary>
        /// ガウスぼかし用のカーネル作成
        /// </summary>
        /// <param name="stdev">標準偏差(standard deviation)、0より大きい数値を指定
        /// 0.1~3くらい、大きくするとよりぼやける、</param>
        /// <param name="size">Kernelサイズ、3以上の奇数を指定、3か5が適当</param>
        /// <returns></returns>
        private (double[,] kernel, double div) Makeガウシアンカーネル(double stdev, int size)
        {
            //1次のガウス関数の配列作成
            double variance = stdev * stdev;//分散
            double f = 1 / Math.Sqrt(2 * Math.PI * variance);//expの前
            int length = size / 2;//0からの距離
            double[] temp = new double[size];
            //確率密度関数(ガウス関数)
            for (int i = 0; i < size; i++)
            {
                var f2 = -(Math.Pow(i - length, 2) / (2 * variance));//expの指数
                var f3 = f * Math.Pow(Math.E, f2);//ガウス関数
                temp[i] = f3;
            }

            //1次*1次から2次のガウス関数作成
            double[,] kernel = new double[size, size];
            double div = 0;//割るときに使う総和
            for (int i = 0; i < size; i++)
            {
                for (int j = 0; j < size; j++)
                {
                    kernel[i, j] = temp[i] * temp[j];
                    div += kernel[i, j];
                }
            }

            return (kernel, div);
        }



        /// <summary>
        /// Kernelサイズ3x3専用、PixelFormats.Gray8専用
        /// </summary>
        /// <param name="stdev">標準偏差</param>
        /// <param name="size">カーネルのサイズ</param>
        /// <param name="pixels">ピクセルの輝度の配列</param>
        /// <param name="width">画像の横ピクセル数</param>
        /// <param name="height">画像の縦ピクセル数</param>
        /// <returns></returns>
        private (byte[] pixels, BitmapSource bitmap) Filterガウシアンフィルタ3x3(
            double stdev, int size, byte[] pixels, int width, int height)
        {
            //カーネル作成とその合計値を取得
            (double[,] kernel, double div) = Makeガウシアンカーネル(stdev, size);
            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++)
                {
                    //Kernelサイズ範囲のピクセルに重みをかけた合計の平均値を新しい値にする
                    double total = 0;
                    for (int i = 0; i < 3; i++)
                    {
                        int p = x + ((y + i - 1) * stride);//注目ピクセルの位置
                        for (int j = 0; j < 3; j++)
                        {
                            total += pixels[p + j - 1] * kernel[i, j];
                        }
                    }

                    int average = (int)(total / div);
                    filtered[x + y * stride] = (byte)average;
                }
            }

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


        private (byte[] pixels, BitmapSource bitmap) Filterガウシアンフィルタ2サイズ指定(
             double stdev, int size, byte[] pixels, int width, int height)
        {
            //カーネル作成とその合計値を取得
            (double[,] kernel, double div) = Makeガウシアンカーネル(stdev, size);

            byte[] filtered = new byte[pixels.Length];//処理結果用
            int stride = width;//一行のbyte数            
            //Kernelの範囲外になるピクセルは処理しない(めんどくさい)
            int range = kernel.GetLength(0) / 2;//範囲外ピクセルのライン数
            for (int y = range; y < height - range; y++)
            {
                for (int x = range; x < width - range; x++)
                {
                    //Kernelサイズ範囲のピクセルに重みをかけた合計の平均値を新しい値にする
                    double total = 0;
                    for (int i = 0; i < size; i++)
                    {
                        int p = x + ((y + i - range) * stride);//注目ピクセルの位置
                        for (int j = 0; j < size; j++)
                        {
                            total += pixels[p + j - range] * kernel[i, j];
                        }
                    }
                    int average = (int)(total / div);
                    average = average < 0 ? 0 : average > 255 ? 255 : average;
                    filtered[x + y * stride] = (byte)average;
                }
            }
            return (filtered, BitmapSource.Create(
                width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
        }


        //補正あり
        private (byte[] pixels, BitmapSource bitmap) Filterガウシアンフィルタサイズ指定色補正あり(
             double stdev, int size, byte[] pixels, int width, int height)
        {
            //カーネル作成とその合計値を取得
            (double[,] kernel, double div) = Makeガウシアンカーネル(stdev, size);

            byte[] filtered = new byte[pixels.Length];//処理結果用
            int stride = width;//一行のbyte数            
            //Kernelの範囲外になるピクセルは処理しない(めんどくさい)
            int range = kernel.GetLength(0) / 2;//範囲外ピクセルのライン数
            for (int y = range; y < height - range; y++)
            {
                for (int x = range; x < width - range; x++)
                {
                    //Kernelサイズ範囲のピクセルに重みをかけた合計の平均値を新しい値にする
                    double total = 0;
                    for (int i = 0; i < size; i++)
                    {
                        int p = x + ((y + i - range) * stride);//注目ピクセルの位置
                        for (int j = 0; j < size; j++)
                        {
                            total += Math.Pow(pixels[p + j - range], 2.0) * kernel[i, j];
                        }
                    }
                    int average = (int)Math.Sqrt(total / div);
                    filtered[x + y * stride] = (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ガウシアンフィルタ3x3(
                Slider標準偏差.Value,
                3,
                MyPixels,
                MyBitmapOrigin.PixelWidth,
                MyBitmapOrigin.PixelHeight);
            MyImage.Source = bitmap;
            MyPixels = pixels;
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            if (MyPixels == null) { return; }
            (byte[] pixels, BitmapSource bitmap) = Filterガウシアンフィルタ2サイズ指定(
                Slider標準偏差.Value,
                (int)SliderKernelサイズ.Value,
                MyPixels,
                MyBitmapOrigin.PixelWidth,
                MyBitmapOrigin.PixelHeight);
            MyImage.Source = bitmap;
            MyPixels = pixels;
        }



        private void Button_Click_7(object sender, RoutedEventArgs e)
        {
            if (MyPixels == null) { return; }
            (byte[] pixels, BitmapSource bitmap) = Filterガウシアンフィルタサイズ指定色補正あり(
                Slider標準偏差.Value,
                (int)SliderKernelサイズ.Value,
                MyPixels,
                MyBitmapOrigin.PixelWidth,
                MyBitmapOrigin.PixelHeight);
            MyImage.Source = bitmap;
            MyPixels = pixels;

        }
    }
}
  215~343行目はぼかし処理と関係ない
 
 
ガウス関数を使ってカーネル作成
イメージ 14
ガウスぼかしには平均の値は0でいいのでこれで
 
標準偏差カーネルサイズを指定してカーネル作成
イメージ 13
標準偏差σの2乗σ^2は分散varianceの意味、73行目
74行目の変数fにはガウス関数のexpの前の分数の部分
変数f2にはexpの指数(右側のカッコの中)部分、80行目
自然対数の底はMathクラスにあった!81行目のMath.Eがそれ
イメージ 15
候補にはExpもある
81行目、変数f3にはガウス関数での結果、これは1次元になる
ガウス関数の2次は1次*1次なのでf3同士を掛け算が92行目
93行目のdivはカーネルの中の数値の合計
 
今思ったけど最初から2次のガウス関数を使ったほうがスッキリ書けたかも?
イメージ 16
 
イメージ 17
1以下の小さな数値ばかりが入っているけど、このまま使う
 
エクセルのNORMDIST関数で計算したとき
イメージ 18
これと同じ値になっている
これを端の最小値が1になるようにして、さらに四捨五入すると
イメージ 19
こうなるけど、ここまでする必要がなかったのでそのまま使う
 
 
カーネルの値を使ってぼかし処理
イメージ 20
カーネルサイズ3x3専用、サイズ指定があるけど3x3専用
このあたりは普通のカーネルを使って処理した前回と全く同じかな
整数に整えたカーネルでも、今回のように小数点のままでも処理自体は同じ
 
 
カーネルサイズ指定できる版
イメージ 21
これが本命になるかと思ったけど、カーネルサイズは3x3と5x5があれば十分な感じだからねえ
 
 
輝度補正あり版
イメージ 22
平方根の計算が重いと思うんだけど、なんかいい方法ないかしら
 
 
 
 
いまいちなぼかし
イメージ 23
標準偏差=5.0と大きくするとブレたようなぼかしになる
 
 
 
イメージ 24
REN FONT / タイポグラフィクス蓮=オリジナルフォントのダウンロードサイト
https://renfont.com/index.html
今回のフォントは、ところどころに隙間があるのがおもしろい「和音 Joyo R」でした
 
 

 
 
ギットハブ

github.com

 
 
ウィンドウに画像ファイルドロップで画像表示、画像クリックで元の画像との切り替え
 
 

 
 
関連記事
前回2019/4/27は3日前

gogowaten.hatenablog.com

エクセルで2次のガウス関数(確率密度関数)、正規分布関数のNORMDIST ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15943071.html
 
 
前々回2019/4/27も3日前

gogowaten.hatenablog.com

エクセルで1次のガウス関数(確率密度関数)、正規分布関数のNORMDIST ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15942730.html
 
 
2019/4/25は5日前

gogowaten.hatenablog.com

画像のぼかし処理、8近傍平均とガウスぼかし、グレースケールで処理 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15941552.html
 
 
次、2019/05/10は10日後

gogowaten.hatenablog.com

画像のエッジ抽出、ラプラシアンフィルタ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15953028.html
 
 
画像にノイズ付加するアプリ、一様分布乱数から正規分布乱数生成、エクセルのNORMINV関数で正規乱数 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15961286.html
 
 1年後