#問題15
問題13~15は問題9がベースとなっています。まず問題9を実装してください。
画像読み込み後、mosaic押下時に8x8のモザイク処理を行え。
モザイク処理とは平均の色で塗りつぶすことである。
以下のHTMLを使用すること。(今回の問題はcanvasのサイズが640x320になっています。)
<!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>
#解答
<!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>
モザイク処理ができました。かっこいいですね。
#解説
基本的な流れは問題13,14と似ています。
やりたいことは8x8の平均の色で塗りつぶすことです。
グレースケールや2値化では1つのfor文で回していましたが
今回のモザイク処理ではx方向とy方向の2つのfor文でループさせたほうがわかりやすいと思います。
下図のようにYの値を8ずつ変えながらX方向に8ずつ走査します。
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文でループさせて、まずは画素の各成分を全部足し合わせます。
それをモザイクの画素数で割れば平均が出ます。
その平均の色で塗りつぶします。
// モザイクの色を計算する
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で割り切れましたが
割り切れない場合はどのように実装すればよいでしょうか?
よい練習になると思うのでぜひやってみてください。(こちらが参考になるかも)