【画像処理解説】モザイク処理（Javaソース付き）

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

#モザイクとは？
現代社会においてモザイクを見たことのない人はいないと思いますが、一応説明します。
モザイクとはブロックごとに平坦化することで、元の画像を分かりにくくする処理です。
ブロックが大きければ大きいほど画像が荒くなります。

一般的なモザイク処理は非可逆変換となっています。
すなわち、一度モザイクをかけると元には戻せないということですね。
そのため、個人情報保護や検閲などで用いられています。

#処理方法は？
先述した通り、まず画像をブロック分けします。
そしてブロックごとに何らかの式を用いて代表値を計算し、ブロックすべてを塗りつぶします。
これだけで簡単にモザイクをかけることができます。

言葉で説明するのは非常に簡単なのですが、実際にプログラム化するのは意外と複雑だったりします。
特に初心者の方には少し難しいかもしれませんね。

なお、以下のプログラムでは計算式として平均値を用います。
この処理は平均プーリングとも呼ばれます。

#アルゴリズム
1. 入力画像をラスタスキャンし、2から6の処理を行う。ただし、移動幅をブロックサイズとする。
2. 平均値計算用の変数を初期化する。
3. ブロックの中をラスタスキャンし、各画素に4の処理を行う。
4. 3のインデックスが画像からはみ出していない場合、画素値を取得し2で用意した変数に加算する。
5. 2の変数を用いて平均値を計算する。
6. ブロックの中をラスタスキャンし、各画素に5で取得した値をセットする。

#モザイク処理の結果例
元画像（カラー画像）
![imori_256x256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655420/3254b128-8a72-03d3-b380-f01a00e08ace.png)


結果画像（モザイク画像）
![out007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/655420/fef5bf76-18a8-75a6-8f6f-df8476ef5d9c.png)


#ソースコード

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

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

        // 画像をラスタスキャン（カーネルサイズ単位で移動）
        for (int y = ksize / 2; y < oriImg.height() + ksize / 2; y += ksize)
            for (int x = ksize / 2; x < oriImg.width() + ksize / 2; x += ksize) {
                // 平均計算用変数の初期化
                double sum = 0;
                int count = 0;

                // カーネル範囲をスキャン
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        // 画像内である場合のみ集計
                        if (x + dx < oriImg.width() && y + dy < oriImg.height()) {
                            sum += oriImg.getGray(x + dx, y + dy);
                            count++;
                        }
                    }

                // 合計値を計算
                int mean = (int) Math.rint(sum / count);

                // カーネル範囲を再スキャン
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        // 画像内である場合のみ平均値をセット
                        if (x + dx < oriImg.width() && y + dy < oriImg.height())
                            mosaicImg.setGray(x + dx, y + dy, mean);
                    }
            }
        return mosaicImg;

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

        // 画像をラスタスキャン（カーネルサイズ単位で移動）
        for (int y = ksize / 2; y < oriImg.height() + ksize / 2; y += ksize)
            for (int x = ksize / 2; x < oriImg.width() + ksize / 2; x += ksize) {
                // 平均計算用変数の初期化
                double[] sum = new double[3];
                int count = 0;

                // カーネル範囲をスキャン
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        // 画像内である場合のみ集計
                        if (x + dx < oriImg.width() && y + dy < oriImg.height()) {
                            int[] rgb = oriImg.getRGB(x + dx, y + dy);
                            for (int i = 0; i < 3; i++)
                                sum[i] += rgb[i];
                            count++;
                        }
                    }

                // 合計値を計算
                int[] mean = new int[3];
                for (int i = 0; i < 3; i++)
                    mean[i] = (int) Math.rint(sum[i] / count);

                // カーネル範囲を再スキャン
                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        // 画像内である場合のみ平均値をセット
                        if (x + dx < oriImg.width() && y + dy < oriImg.height())
                            mosaicImg.setRGB(x + dx, y + dy, mean);
                    }
            }
        return mosaicImg;

        // アルファチャネルを持つ場合
    } else {
        var mosaicImg = new SImage(oriImg.width(), oriImg.height(), 4);

        for (int y = ksize / 2; y < oriImg.height() + ksize / 2; y += ksize)
            for (int x = ksize / 2; x < oriImg.width() + ksize / 2; x += ksize) {
                double[] sum = new double[4];
                int count = 0;

                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        if (x + dx < oriImg.width() && y + dy < oriImg.height()) {
                            int[] rgb = oriImg.getARGB(x + dx, y + dy);
                            for (int i = 0; i < 4; i++)
                                sum[i] += rgb[i];
                            count++;
                        }
                    }

                int[] mean = new int[4];
                for (int i = 0; i < 4; i++)
                    mean[i] = (int) Math.rint(sum[i] / count);

                for (int dy = -ksize / 2; dy <= ksize / 2; dy++)
                    for (int dx = -ksize / 2; dx <= ksize / 2; dx++) {
                        if (x + dx < oriImg.width() && y + dy < oriImg.height())
                            mosaicImg.setARGB(x + dx, y + dy, mean);
                    }
            }
        return mosaicImg;
    }
}
```