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

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

#メディアンとは？
日本語では中間値といい、名前の通り数集合の中間に位置する数のことです。

簡単な例ですが、あるテストを受けた人が7人いて、点数分布は以下の通りでした。
$\begin{matrix}
34 & 36 & 39 & 42 & 44 & 48 & 100
\end{matrix}$

この例において平均点を計算すると、49点となります。
直感的には49点が真ん中というのは高いように感じませんか？
2位ですら平均点以下となってしまっています。
これは一人だけ100点という高得点を取っている人がいるため、平均点が高めに出ているのです。

次に、メディアンを計算してみましょう。
7人の中間なので、4位の点数である42点が採用されます。
この点は4位の点数そのままなので、1位の100点には引っ張られていません。
メディアンのほうが「真ん中」という感じがしますよね？

以上、説明した通りメディアンには外れ値（極端に他と異なる値）の影響を受けにくいという長所があります。
これを利用したのが以下のメディアンフィルタです。

#メディアンフィルタとは？
ノイズ除去などの平滑化処理で用いられるフィルタです。
名前の通り、自身を含む周囲 $n\times m$ 画素の中から、メディアンを取得し、新しい画素としてセットします。

例えば、以下のカーネルの場合、メディアンは5となり外れ値の255が除外されます。

```math
\begin{bmatrix}
 10 & 4 & 8 \\
 2 & 255 & 12 \\
 1 & 3 & 5
\end{bmatrix}
```

#アルゴリズム
1. 画像をラスタスキャンし、各画素に2から5の処理を行う。
2. カーネル範囲の値を保存するための配列変数を用意する。
3. カーネル範囲を全探索し、各画素の値を2で用意した配列変数に追加する。
4. 3で更新した配列変数を昇順にソートする
4. 4でソートした配列の中間値を取得し、1で選択している画素にセットする

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


結果画像（メディアン画像）
![out010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655420/93bad829-83b3-41b9-768b-0d91ed5a3d42.png)



#ソースコード
```Smoothing.java
public static SImage median(SImage oriImg, int ksize) {
    // グレースケール画像の場合
    if (oriImg.channel == 1) {
        // メディアン画像用を生成
        var medImg = 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++) {
                // メディアン取得用の変数初期化
                int[] vArray = new int[ksize * ksize];

                // カーネル範囲をスキャン
                int index = 0;
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++)
                        // 値配列に値を追加
                        vArray[index++] = oriImg.getGray(x + dx, y + dy);

                // 値配列をソート
                Arrays.sort(vArray);

                // 中間値を出力画像にセット
                medImg.setGray(x, y, vArray[4]);
            }
        return medImg;

        // RGBカラー画像の場合
    } else if (oriImg.channel == 3) {
        // メディアン画像用を生成
        var medImg = 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++) {
                // メディアン取得用の変数初期化
                int[] rArray = new int[ksize * ksize];
                int[] gArray = new int[ksize * ksize];
                int[] bArray = new int[ksize * ksize];

                // カーネル範囲をスキャン
                int index = 0;
                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);
                        rArray[index] = rgb[0];
                        gArray[index] = rgb[1];
                        bArray[index++] = rgb[2];
                    }

                // 値配列をソート
                Arrays.sort(rArray);
                Arrays.sort(gArray);
                Arrays.sort(gArray);

                // 中間値を出力画像にセット
                int[] newRgb = new int[]{rArray[ksize * ksize / 2], gArray[ksize * ksize / 2], bArray[ksize * ksize / 2]};
                medImg.setRGB(x, y, newRgb);
            }
        return medImg;

        // アルファチャネルを持つ場合
    } else {
        // メディアン画像用を生成
        var medImg = 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++) {
                // メディアン取得用の変数初期化
                int[] aArray = new int[ksize * ksize];
                int[] rArray = new int[ksize * ksize];
                int[] gArray = new int[ksize * ksize];
                int[] bArray = new int[ksize * ksize];

                // カーネル範囲をスキャン
                int index = 0;
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        // 値配列に値を追加
                        int[] argb = oriImg.getARGB(x + dx, y + dy);
                        aArray[index] = argb[0];
                        rArray[index] = argb[1];
                        gArray[index] = argb[2];
                        bArray[index++] = argb[3];
                    }

                // 値配列をソート
                Arrays.sort(aArray);
                Arrays.sort(rArray);
                Arrays.sort(gArray);
                Arrays.sort(gArray);

                // 中間値を出力画像にセット
                int[] newArgb = new int[]{aArray[ksize * ksize / 2], rArray[ksize * ksize / 2], gArray[ksize * ksize / 2], bArray[ksize * ksize / 2]};
                medImg.setARGB(x, y, newArgb);
            }
        return medImg;
    }
}
```