【入門者向け】Canvas入門講座#15 モザイクしてみよう【JavaScript】

#問題15

問題13～15は問題9がベースとなっています。まず問題9を実装してください。

画像読み込み後、mosaic押下時に8x8のモザイク処理を行え。
モザイク処理とは平均の色で塗りつぶすことである。

以下のHTMLを使用すること。(今回の問題はcanvasのサイズが640x320になっています。)

```html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題15</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {

    $('#my-file').on('change', e => {
        const img = new Image();
        $(img).on('load', e => {            
        });
        img.src = URL.createObjectURL(e.target.files[0]);
    });

    $('#mosaic-button').click(e => {        
    });
    
});
</script>
</head>
<body>
<canvas id="my-canvas" width="640" height="320"></canvas>
<br>
<input id="my-file" type="file" />
<br>
<input id="mosaic-button" type="button" value="mosaic" />
</body>
</html>
```

#解答

```html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題15</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {

    const MOSAIC_WIDTH = 8,
        MOSAIC_HEIGHT = 8; 

    $('#my-file').on('change', e => {
        const img = new Image();
        $(img).on('load', e => {
            // コンテキストを取得
            const ctx = $('#my-canvas')[0].getContext('2d');

            // canvasを黒色で塗りつぶす
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);            

            // canvasと画像のアスペクト比を求める
            const canvasWidth = $('#my-canvas').prop('width'),
                canvasHeight = $('#my-canvas').prop('height');
                canvasAspect = canvasWidth / canvasHeight;  // canvasのアスペクト比
                imgAspect = img.width / img.height; // imgのアスペクト比
            
            // canvasと画像のアスペクト比を比較し、貼り付ける領域を決定する
            let dstWidth, dstHeight;
            if(canvasAspect > imgAspect) {// canvasの方が横長
                dstHeight = canvasHeight;
                dstWidth = dstHeight * imgAspect;               
            } else {// canvasの方が縦長
                dstWidth = canvasWidth;
                dstHeight = dstWidth / imgAspect;
            }

            // 画像を貼り付ける
            ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, dstWidth, dstHeight);
        });
        img.src = URL.createObjectURL(e.target.files[0]);
    });

    $('#mosaic-button').click(e => {
        const canvas = $('#my-canvas')[0],
            ctx = canvas.getContext('2d');

        ctx.save();

        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height),
            data = imgData.data;

        for(let y = 0; y < canvas.height; y += MOSAIC_HEIGHT) {
            for(let x = 0; x < canvas.width; x += MOSAIC_WIDTH) {
                
                // モザイクの色を計算する
                let r, g, b;
                r = g = b = 0;
                for(let j = 0; j < MOSAIC_HEIGHT; j += 1) {
                    for(let i = 0; i < MOSAIC_WIDTH; i += 1) {
                        // 画素のインデックスを求める
                        const p = ((y + j) * canvas.width + (x + i)) * 4;
                        r += data[p];
                        g += data[p + 1];
                        b += data[p + 2];
                    }
                }

                // 各色の平均を計算する
                const pixelCount = MOSAIC_WIDTH * MOSAIC_HEIGHT; // モザイクの画素数            
                r /= pixelCount;
                g /= pixelCount;
                b /= pixelCount;

                // モザイクをかける            
                for(let j = 0; j < MOSAIC_HEIGHT; j += 1) {
                    for(let i = 0; i < MOSAIC_WIDTH; i += 1) {
                        // 画素のインデックスを求める
                        const p = ((y + j) * canvas.width + (x + i)) * 4;
                        data[p] = r;
                        data[p + 1] = g;
                        data[p + 2] = b;
                    }
                }
            }
        }

        // imageDataをcanvasに貼り付ける
        ctx.putImageData(imgData, 0, 0);

        ctx.restore();
    });
    
});
</script>
</head>
<body>
<canvas id="my-canvas" width="640" height="320"></canvas>
<br>
<input id="my-file" type="file" />
<br>
<input id="mosaic-button" type="button" value="mosaic" />
</body>
</html>
```

モザイク処理ができました。かっこいいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/6d8c754c-37eb-cf69-a2f9-eed46db73453.png)


#解説
基本的な流れは問題13,14と似ています。
やりたいことは8x8の平均の色で塗りつぶすことです。

グレースケールや2値化では1つのfor文で回していましたが
今回のモザイク処理ではx方向とy方向の2つのfor文でループさせたほうがわかりやすいと思います。
下図のようにYの値を8ずつ変えながらX方向に8ずつ走査します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163605/7fe90929-1c78-6fe0-5de8-465efa61b6c4.png)


```js
for(let y = 0; y < canvas.height; y += MOSAIC_HEIGHT) {
    for(let x = 0; x < canvas.width; x += MOSAIC_WIDTH) {
        // ここでモザイク処理
    }
}
```

今回のモザイクサイズは8x8ですので、
現在の画素からさらにx方向とy方向の2つのfor文でループさせて、まずは画素の各成分を全部足し合わせます。
それをモザイクの画素数で割れば平均が出ます。
その平均の色で塗りつぶします。

```js
// モザイクの色を計算する
let r, g, b;
r = g = b = 0;
for(let j = 0; j < MOSAIC_HEIGHT; j += 1) {
    for(let i = 0; i < MOSAIC_WIDTH; i += 1) {
        // 画素のインデックスを求める
        const p = ((y + j) * canvas.width + (x + i)) * 4;
        r += data[p];
        g += data[p + 1];
        b += data[p + 2];
    }
}

// 各色の平均を計算する
const pixelCount = MOSAIC_WIDTH * MOSAIC_HEIGHT; // モザイクの画素数            
r /= pixelCount;
g /= pixelCount;
b /= pixelCount;

// モザイクをかける            
for(let j = 0; j < MOSAIC_HEIGHT; j += 1) {
    for(let i = 0; i < MOSAIC_WIDTH; i += 1) {
        // 画素のインデックスを求める
        const p = ((y + j) * canvas.width + (x + i)) * 4;
        data[p] = r;
        data[p + 1] = g;
        data[p + 2] = b;
    }
}
```


#発展
今回canvasのサイズが640x320で幅、長さ共に8で割り切れましたが
割り切れない場合はどのように実装すればよいでしょうか？
よい練習になると思うのでぜひやってみてください。(<a href="https://qiita.com/PG0721/items/1d1f088e39147b48e24a">こちら</a>が参考になるかも)