午後わてんのブログ

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

画像のぼかし処理、8近傍平均とガウスぼかし、グレースケールで処理

イメージ 1
8近傍平均は移動平均フィルター
ガウシアンフィルタはガウスぼかし
上下左右は4近傍
呼び方が安定しない
 
イメージ 2
8近傍はこの前の上下左右から参照ピクセルが増えただけ、斜め方向の4ピクセルを加えた9ピクセルの合計の平均値を新しい値にする
ガウシアンは中心の注目ピクセルからの距離を考慮して重みを付けたもので、近いほど新しい値に対する影響が大きくなる、各値に重みをかけてから合計して平均値を計算する
 
エクセル方眼紙で8近傍ぼかしを確認

f:id:gogowaten:20191214114900p:plain

8近傍はぼやけ具合が強い

f:id:gogowaten:20191214114911p:plain

9ピクセルを合計して、9で割るだけ
 
 
ガウスぼかし

f:id:gogowaten:20191214114921p:plain

ガウスぼかしはきれいなぼかし具合だと思う
 

f:id:gogowaten:20191214114931p:plain

重みが加わっただけで、合計して割るのは8近傍と同じ
重みの合計になる16で割る
デザイン画面

f:id:gogowaten:20191214114943p:plain

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;
//画像のぼかし処理、8近傍平均とガウスぼかし、グレースケールで処理(ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
//https://blogs.yahoo.co.jp/gogowaten/15941552.html


namespace _20190425_3x3の平均ぼかしとガウシアンフィルタ
{
    /// <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>
        /// ぼかしフィルタ、近傍8ピクセルとの平均値に変換、PixelFormat.Gray8専用
        /// </summary>
        /// <param name="pixels"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <returns></returns>
        private (byte[] pixels, BitmapSource bitmap) Filter近傍8平均(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++)
                {
                    //注目ピクセルの値と、その周囲8ピクセルの合計の平均値を新しい値にする
                    int total = 0;
                    int p = x + y * stride;//注目ピクセルの位置
                    total += pixels[p - stride - 1];//左上
                    total += pixels[p - stride];    //上
                    total += pixels[p - stride + 1];//右上
                    total += pixels[p - 1];     //左
                    total += pixels[p];         //注目ピクセル
                    total += pixels[p + 1];     //右
                    total += pixels[p + stride - 1];//左下
                    total += pixels[p + stride];    //下
                    total += pixels[p + stride + 1];//右下
                    int average = total / 9;
                    filtered[p] = (byte)average;
                }
            }

            return (filtered, BitmapSource.Create(
                width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
        }
        //↑と書き方が違うだけで同じ処理、未使用
        private (byte[] pixels, BitmapSource bitmap) Filter近傍8平均2(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++)
                {
                    //注目ピクセルの値と、その周囲8ピクセルの合計の平均値を新しい値にする
                    int total = 0;
                    for (int i = -1; i < 2; i++)
                    {
                        int p = x + ((y + i) * stride);//注目ピクセルの位置
                        for (int j = -1; j < 2; j++)
                        {
                            total += pixels[p + j];
                        }
                    }

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

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

        //色補正あり版
        //誤差程度にきれい(正確)にぼやけるけど、処理時間は10倍以上かかる
        private (byte[] pixels, BitmapSource bitmap) Filter近傍8平均補正あり(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++)
                {
                    //注目ピクセルの値と、その周囲8ピクセルの2乗和の平均の平方根を新しい値にする
                    //((各値の2乗)/9)の平方根を新しい値にする
                    double total = 0;
                    int p = x + y * stride;//注目ピクセルの位置
                    total += Math.Pow(pixels[p - stride - 1], 2);//左上
                    total += Math.Pow(pixels[p - stride], 2);    //上
                    total += Math.Pow(pixels[p - stride + 1], 2);//右上
                    total += Math.Pow(pixels[p - 1], 2);     //左
                    total += Math.Pow(pixels[p], 2);         //注目ピクセル
                    total += Math.Pow(pixels[p + 1], 2);     //右
                    total += Math.Pow(pixels[p + stride - 1], 2);//左下
                    total += Math.Pow(pixels[p + stride], 2);    //下
                    total += Math.Pow(pixels[p + stride + 1], 2);//右下
                    double average = total / 9;
                    average = Math.Sqrt(average);
                    filtered[p] = (byte)average;
                }
            }

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





        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++)
                {
                    //注目ピクセルの値と、その周囲8ピクセルに重みをかけた合計の平均値を新しい値にする
                    int total = 0;
                    int p = x + y * stride;//注目ピクセルの位置
                    total += pixels[p - stride - 1];//左上
                    total += pixels[p - stride] * 2;//上
                    total += pixels[p - stride + 1];//右上
                    total += pixels[p - 1] * 2;     //左
                    total += pixels[p] * 4;         //注目ピクセル
                    total += pixels[p + 1] * 2;     //右
                    total += pixels[p + stride - 1];//左下
                    total += pixels[p + stride] * 2;//下
                    total += pixels[p + stride + 1];//右下
                    int average = total / 16;
                    filtered[p] = (byte)average;
                }
            }

            return (filtered, BitmapSource.Create(
                width, height, 96, 96, PixelFormats.Gray8, null, filtered, width));
        }
        //↑と書き方が違うだけで同じ処理、未使用
        private (byte[] pixels, BitmapSource bitmap) Filterガウシアンフィルタ2(byte[] pixels, int width, int height)
        {
            //重み
            int[][] weight = new int[][] {
                new int[] { 1, 2, 1 },
                new int[] { 2, 4, 2 },
                new int[] { 1, 2, 1 } };

            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++)
                {
                    //注目ピクセルの値と、その周囲8ピクセルに重みをかけた合計の平均値を新しい値にする
                    int total = 0;
                    for (int i = -1; i < 2; i++)
                    {
                        int p = x + ((y + i) * stride);//注目ピクセルの位置
                        for (int j = -1; j < 2; j++)
                        {
                            total += pixels[p + j] * weight[i + 1][j + 1];
                        }
                    }

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

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

        //色補正あり版
        //誤差程度にきれい(正確)にぼやけるけど、処理時間は10倍以上かかる

        private (byte[] pixels, BitmapSource bitmap) Filterガウシアンフィルタ補正あり(byte[] pixels, int width, int height)
        {
            //重み
            //1,2,1
            //2,4,2
            //1,2,1
            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++)
                {
                    //注目ピクセルの値と、その周囲8ピクセルの2乗和の平均の平方根を新しい値にする
                    //((各値の2乗*重み)/16)の平方根を新しい値にする
                    double total = 0;
                    int p = x + y * stride;//注目ピクセルの位置
                    total += Math.Pow(pixels[p - stride - 1], 2);//左上
                    total += Math.Pow(pixels[p - stride], 2) * 2;//上
                    total += Math.Pow(pixels[p - stride + 1], 2);//右上
                    total += Math.Pow(pixels[p - 1], 2) * 2;     //左
                    total += Math.Pow(pixels[p], 2) * 4;         //注目ピクセル
                    total += Math.Pow(pixels[p + 1], 2) * 2;     //右
                    total += Math.Pow(pixels[p + stride - 1], 2);//左下
                    total += Math.Pow(pixels[p + stride], 2) * 2;//下
                    total += Math.Pow(pixels[p + stride + 1], 2);//右下
                    double average = total / 16;
                    average = Math.Sqrt(average);
                    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近傍8平均(
                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近傍8平均補正あり(
                MyPixels, MyBitmapOrigin.PixelWidth, MyBitmapOrigin.PixelHeight);
            MyImage.Source = bitmap;
            MyPixels = pixels;

        }


        private void Button_Click_5(object sender, RoutedEventArgs e)
        {
            if (MyPixels == null) { return; }
            (byte[] pixels, BitmapSource bitmap) = Filterガウシアンフィルタ(
                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ガウシアンフィルタ補正あり(
                MyPixels, MyBitmapOrigin.PixelWidth, MyBitmapOrigin.PixelHeight);
            MyImage.Source = bitmap;
            MyPixels = pixels;

        }
    }
}
 
 
8近傍

f:id:gogowaten:20191214115058p:plain

この前の上下左右と違うところは80~89行目だけ、斜めの4ピクセルが増えて、そのぶん割る数値も5から9になっているだけ、ナニモイウコトハナイ
 
別の書き方

f:id:gogowaten:20191214115108p:plain

足し算のところをforに書き換えただけ
こっちのほうが短くかけるけど処理時間が長くなるはず
見た目も上の方が好きだからこちらは未使用
でも参照ピクセルが増えて5x5ピクセルとかになったら、こちらの書き方になる
 
 
 
ガウシアンフィルタ

f:id:gogowaten:20191214115118p:plain

8近傍に重みが加わっただけだからほとんど一緒、ナニモイウコトハナイ
 

f:id:gogowaten:20191214115133p:plain

上のガウシアンフィルタの別の書き方したもの
これはこっちのほうがわかりやすいかなあ
 
 
色補正あり版のガウシアンフィルタ

f:id:gogowaten:20191214115144p:plain

おとといの色補正をガウシアンフィルタに入れてみたもの
処理時間が10倍以上、もしかしたら100倍位になるけど誤差程度にきれいなぼかし処理になる、はず
2乗してから重みをかけて合計して(250~258行目)、16で割ったあとの平方根を新しい値にする(259~261行目)
 
 
 
イメージ 13
補正ありも無しも全く同じに見える
 
 
白と黒の境界線で比較
イメージ 14
一番違いが出やすいと思われる状態でも、この程度の差
 
 
境界部分を30倍に拡大
イメージ 15
こうしてみると補正ありのほうが自然な感じ
 
 
ぼかし処理3回
イメージ 16
並べてみるとガウスの補正ありがいいかなって気もするけど
どれも変わらない気もする
30倍に拡大
イメージ 17
補正無しだと黒が残る感じ
 
 
白と黒の市松模様
イメージ 18
1ピクセルごと交互に白と黒が並んでいる画像
 
イメージ 19
かなり差が出て補正ありのほうがいいと思う
 
 
元画像は白(255)と黒(0)のピクセル数が全く同じだから、普通に計算すると(255+0)/2=127.5になる
実際に補正無しのガウスぼかしは
イメージ 20
127になっていた、でも元画像の色より黒く見える
 
補正あり
イメージ 21
自然に見える補正ありのガウスぼかしの色は180
(255^2+0^2)=65025
65025/2=32512.5
√32512.5≒180.3
 
 
 
 
処理時間
10年目のパソコンで
2000x1500ピクセルの画像だと
補正無し 0.1秒
補正あり 3秒強
 
 
 
 
参照したところ
ガウシアンフィルタ
https://imagingsolution.net/imaging/gaussian/
 
 
ギットハブ

github.com

 
 
アプリ
イメージ 22
画像ファイルドロップで画像表示、カラー画像でもグレースケールで表示、ボタンで処理実行、画像クリックしている間は元画像を表示
 
 
関連記事
2019/4/23はおととい

gogowaten.hatenablog.com

画像ぼかし処理、普通のぼかし処理では画像によってイマイチな結果になる ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15940003.html
 
 
2019/4/22はさきおととい

gogowaten.hatenablog.com

画像のぼかし処理、注目ピクセルとその上下左右の平均値に変換 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15938990.html
 
 
2019/04/27、続きみたいなもの

gogowaten.hatenablog.com

エクセルで1次のガウス関数(確率密度関数)、正規分布関数のNORMDIST ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15942730.html
ガウスぼかしのカーネル作成に関係
 
 
 
2019/04/30、続き

gogowaten.hatenablog.com

ガウス関数からカーネル作成、標準偏差カーネルサイズ、グレースケール画像のぼかし処理 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15945699.html