【画像処理解説】ガウシアンフィルタ（Javaソース付き）

#はじめに
画像処理を簡単に解説していきます。
下部にはJavaでのソースコードも載せているのでぜひご参照ください。
不定期にはなりますが、随時更新していく予定です。

#ガウシアンフィルタとは？
ノイズ除去などでよく用いられるフィルタです。
ガウシアンフィルタで使われる関数は以下の式です。

```math
k(x, y) = \frac{1}{2 \pi \sigma^2}
\exp{( -\frac{x^2 + y^2}{2\sigma^2})} 
```

$sigma=1.3$、カーネルサイズ3×3のガウシアンフィルタは以下のようになります。
なお、このフィルタでは全体の和が1になるように正規化がなされています。

```math
k = \frac{1}{16}
\begin{bmatrix}
 1 & 2 & 1 \\
 2 & 4 & 2 \\
 1 & 2 & 1
\end{bmatrix}
```

画像にフィルタをかける際には「畳み込み演算」という処理を行います。
これについては次節で簡単に説明します。

#畳み込み演算とは？
簡単に言うと、画像上にフィルタを重ね、平行に移動させながら各画素で計算をします。
フィルタのカーネルサイズが3×3である場合、自身の画素だけでなく周囲8つの画素も計算に使用することになります。

カーネルサイズ$n \times n$の場合、周囲$n^2$の画素情報が用いられます。
つまり、カーネルサイズが大きければ大きいほど広い範囲の情報を畳み込みます。
このサイズは使用する画像や状況に応じて設定する必要があります。

#アルゴリズム
1. ガウシアン関数をもとに、カーネル内の各値を計算し、フィルタを生成する。
2. 1で生成したフィルタの合計値をもとに、合計値が1になるようにフィルタを正規化する。
3. 2で生成したガウシアンフィルタをもとに畳み込み演算を行う。

#ガウシアンフィルタの例
元画像（カラー画像）
![imori_256x256_noise.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655420/395f726b-d231-6eff-dc19-34fdacfc900e.png)


結果画像（ガウシアン画像）
![out008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655420/fc3a6567-1018-d8e9-1d88-0d6f30ff06dd.png)


#ソースコード
畳み込み演算は別メソッドとして実装しています。
今後使用頻度が高いため、切り離しています。

```Smoothing.java
public static SImage gaussian(SImage oriImg, int ksize, double sigma) {
    // カーネルサイズが偶数なら奇数にする
    if (ksize % 2 == 0)
        ksize++;

    // 係数の計算
    double de = 2 * Math.PI * sigma;
    double df = -2 * sigma * sigma;

    // フィルタ正規化用の変数を用意
    double sum = 0;

    // ガウシアンフィルタの生成
    SMatrix filter = new SMatrix(3, 3);
    for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
        for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
            double v = Math.exp(dx * dx / df + dy * dy / df) / de;
        filter.set(dx + ksize / 2, dy + ksize / 2, v);
            sum += v;
        }

    // フィルタの正規化
    if (sum != 0.0) {
        for (int dy = 0; dy < ksize; dy++)
            for (int dx = 0; dx < ksize; dx++) {
                filter.set(dx, dy, filter.get(dx, dy) / sum);
            }
    }

    // フィルタの畳み込み処理
    return convolution(oriImg, filter);
}

private static SImage convolution(SImage oriImg, SMatrix filter) {
    // カーネルサイズの取得
    int ksize = filter.row();

    // グレースケール画像の場合
    if (oriImg.channel == 1) {
        // モザイク画像用を生成
        var convImg = new SImage(oriImg.width(), oriImg.height(), 1);

        // 画像をラスタスキャン
        for (int y = ksize / 2; y < oriImg.height() - ksize / 2; y++)
            for (int x = ksize / 2; x < oriImg.width() - ksize / 2; x++) {
                // 合計用変数の初期化
                double sum = 0;

                // カーネル範囲をスキャン
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++)
                        // 画素値を合計に加算
                        sum += oriImg.getGray(x + dx, y + dy) * filter.get(dx + ksize / 2, dy + ksize / 2);

                // 合計値を出力画像にセット
                convImg.setGray(x, y, (int) Math.rint(sum));
            }
        return convImg;

        // RGBカラー画像の場合
    } else if (oriImg.channel == 3) {
        // モザイク画像用を生成
        var convImg = new SImage(oriImg.width(), oriImg.height(), 3);

        // 画像をラスタスキャン
        for (int y = ksize / 2; y < oriImg.height() - ksize / 2; y++)
            for (int x = ksize / 2; x < oriImg.width() - ksize / 2; x++) {
                // 合計用変数の初期化
                double[] sum = new double[3];

                // カーネル範囲をスキャン
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        // 画素値を合計に加算
                        int[] rgb = oriImg.getRGB(x + dx, y + dy);
                        for (int i = 0; i < 3; i++)
                            sum[i] += rgb[i] * filter.get(dx + ksize / 2, dy + ksize / 2);
                    }

                // 合計値のint変換
                int[] sum2 = new int[3];
                for (int i = 0; i < 3; i++)
                    sum2[i] = (int) Math.rint(sum[i]);

                // 合計値を出力画像にセット
                convImg.setRGB(x, y, sum2);
            }
        return convImg;

        // アルファチャネルを持つ場合
    } else {
        // 畳み込み画像用を生成
        var convImg = new SImage(oriImg.width(), oriImg.height(), 4);

        // 画像をラスタスキャン
        for (int y = ksize / 2; y < oriImg.height() - ksize / 2; y++)
            for (int x = ksize / 2; x < oriImg.width() - ksize / 2; x++) {
                // 合計用変数の初期化
                double[] sum = new double[4];

                // カーネル範囲をスキャン
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        // 画素値を合計に加算
                        int[] rgb = oriImg.getARGB(x + dx, y + dy);
                        for (int i = 0; i < 4; i++)
                            sum[i] += rgb[i] * filter.get(dx + ksize / 2, dy + ksize / 2);
                    }

                // 合計値のint変換
                int[] sum2 = new int[4];
                for (int i = 0; i < 4; i++)
                    sum2[i] = (int) Math.rint(sum[i]);

                // 合計値を出力画像にセット
                convImg.setARGB(x, y, sum2);
            }
        return convImg;
    }
}
```