- 公開日: 2018年03月26日
JavaScriptによる画像処理(周辺ピクセルの処理と画像の保存)
JavaScriptで「画像処理」を実現するImageData。これまでピクセルごとの処理をいくつか試してみましたが、「近くのピクセル」と合わせて処理を行うとさらに可能性が広がります。今回は、「近くのピクセルとの色成分」に注目して、色成分を混ぜ合わせる処理を試してみましょう。
近くのピクセルのRGBを取得する
ImageDataに格納された画像データは、RGBA4つの成分で一つのピクセルを表す数値配列です。(x, y)が格納されている位置(配列のインデックス)は
(x + y × 画像の横幅)× 4
の位置になります。
ImageDataオブジェクトimg_dataに格納されている各ピクセルを順次処理するなら、以下のようなループで記述することができます。
for (var y = 0;y < img_data.height;y++) {
for (var x = 0;x < img_data.width;x++) {
var index = (x + y * width) * 4;
r += img_data.data[index];
g += img_data.data[index + 1];
b += img_data.data[index + 2];
・・ r/g/bを使った処理 ・・
}
}
今回は、各ピクセルの「周辺」と合わせた処理を行うので、各ピクセルの処理に周辺ピクセルを探して処理するループも必要ですね。
周辺をどう定義するかですが、一番単純に考えれば「x/y方向それぞれ±1ピクセル」の範囲、でしょうか。座標としては(x - 1, y - 1)から(x + 1, y + 1)の3×3=9ピクセルになります。
処理対象となる(x, y)の±1ピクセルの範囲を処理するなら、先ほどのx/yループの内側にxx/yyループを作るのが簡単でしょう。
for (var y = 0;y < img_data.height;y++) {
for (var x = 0;x < img_data.width;x++) {
for (var yy = y - 1;yy <= y + 1;yy++) {
for (var xx = x - 1;xx <= x + 1;xx++) {
var index = (xx + yy * width) * 4;
r += img_data.data[index];
g += img_data.data[index + 1];
b += img_data.data[index + 2];
・・ r/g/bを使った処理 ・・
}
}
}
}
これで(x, y)の各ピクセルについて、「(x, y)を中心とする周囲3*3ピクセルの範囲」を処理することができます。内側のループでは、「x+1/y+1を含むピクセル」を処理するため、ループの終了条件がxx <= x + 1など等号付きの不等号になっている点に注意してください。
色を混ぜ合わせる
このループの中で、周辺ピクセルの色を「混ぜ合わせる」処理をやってみましょう。混ぜ合わせる処理もいくつかやり方が考えられますが、単純に「周辺9ピクセルの平均」を計算して、それを中心点(x, y)の色に設定してみることにします。
平均を求めるには、各ピクセルの色成分値を足し合わせてから足し合わせたピクセル数で割る、といった処理が必要ですね。
RGBそれぞれの合計値を保存する変数を用意し、内側のループでRGBそれぞれを足しこんで行きましょう。合計したRGB値は、処理対象となるピクセル数9で割ることで平均値になります。
コードにすると、以下のようになります。
for (var y = 1;y < img_data.height - 1;y++) {
for (var x = 1;x < img_data.width - 1;x++) {
var r_sum = 0;
var g_sum = 0;
var b_sum = 0;
// 周囲3*3ピクセルのRGB成分を合計
for (var yy = y - 1;yy <= y + 1;yy++) {
for (var xx = x - 1;xx <= x + 1;xx++) {
var index = (xx + yy * img_data.width) * 4;
r_sum += img_data.data[index];
g_sum += img_data.data[index + 1];
b_sum += img_data.data[index + 2];
}
}
// RGB平均値を算出
var r = r_sum / 9;
var g = g_sum / 9;
var b = b_sum / 9;
}
}
端の方の処理を省くために、処理範囲を(1, 1)から(width - 1, height - 1)にしてみました(0から始めると、x - 1が-1になり範囲外となるため範囲外の処理を行わないようにしないといけません)。
画像の各ピクセルについて「あるピクセル周辺のRGB平均」を求めることができたので、実際に画像を読み込んで平均を計算し、その平均を中心ピクセルに設定していくhtmlファイルを作ってみることにします。
上で求めたrgb値は、そのままでは実数になってしまいますのでMath.floorで整数にまるめimg_data.dataの画像データ配列に格納しましょう。
また、アルファ値は255固定で、RGBがそのまま表示されるようにします。
今回の処理は、「以前に処理した座標」も処理対象に入るため、画像データに直接求めた平均を書きこんで行くと正常な処理が行われません。新しく同じ大きさのImageDataを作成し、RGBの平均(新しいピクセルの値)は、そちらに記録するようにしました。
これらの処理をまとめると、以下のようになります。
var processed_data = cv.getContext('2d').createImageData(img_data.width, img_data.height);
for (var y = 1;y < img_data.height - 1;y++) {
for (var x = 1;x < img_data.width - 1;x++) {
var r_sum = 0;
var g_sum = 0;
var b_sum = 0;
// 周囲3*3ピクセルのRGB成分を合計
for (var yy = y - 1;yy <= y + 1;yy++) {
for (var xx = x - 1;xx <= x + 1;xx++) {
var index = (xx + yy * img_data.width) * 4;
r_sum += img_data.data[index];
g_sum += img_data.data[index + 1];
b_sum += img_data.data[index + 2];
}
}
var index = (x + y * img_data.width) * 4;
// RGB平均値を算出
var r = Math.floor(r_sum / 9);
var g = Math.floor(g_sum / 9);
var b = Math.floor(b_sum / 9);
processed_data.data[index] = r;
processed_data.data[index + 1] = g;
processed_data.data[index + 2] = b;
processed_data.data[index + 3] = 255;
}
}
画像の読み込みは、inputタグで選択する形にし、ファイルが選択されたら画像をdataURL形式(テキスト化されたバイナリデータ)で読み込みます。
function onFileSelected(input) {
var file = input.files[0];
var reader = new FileReader();
reader.onload = onFileLoaded;
reader.readAsDataURL(file);
}
データが読み込まれたら、Imageオブジェクトを作成しそのsrc属性にdataURI形式の画像データを設定します。これでcanvasに描画できる状態になったので、Webブラウザ上に配置したcanvasにdrawImage()で表示できるようになります。
drawImage()で画像を描くときにcanvas要素の大きさを画像に揃え、ImageDataも作成しておく形にしてみました。実際の画像処理は、canvas要素がクリックされたときに実行されます。
htmlファイル全体は、以下のようになります。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script>
function onFileSelected(input) {
var file = input.files[0];
var reader = new FileReader();
reader.onload = onFileLoaded;
reader.readAsDataURL(file);
}
function onFileLoaded(e) {
var src_data = e.target.result;
var img = new Image();
img.onload = onImageSetted;
img.src = src_data;
}
function onImageSetted(e) {
var img_data = createImageData(e.target);
document.getElementById('test_canvas').width = img_data.width;
document.getElementById('test_canvas').height = img_data.height;
document.getElementById('test_canvas').getContext('2d').putImageData(img_data, 0, 0);
document.getElementById('test_canvas').img_data = img_data;
document.getElementById('test_canvas').addEventListener('click', processImageData);
}
function createImageData(img) {
var cv = document.createElement('canvas');
cv.width = img.naturalWidth;
cv.height = img.naturalHeight;
var ct = cv.getContext('2d');
ct.drawImage(img, 0, 0);
var data = ct.getImageData(0, 0, cv.width, cv.height);
return data;
}
function processImageData(e) {
var cv = document.getElementById('test_canvas');
var img_data = cv.img_data;
if (!img_data) {
alert("aa");
}
var processed_data = cv.getContext('2d').createImageData(img_data.width, img_data.height);
for (var y = 1;y < img_data.height - 1;y++) {
for (var x = 1;x < img_data.width - 1;x++) {
var r_sum = 0;
var g_sum = 0;
var b_sum = 0;
// 周囲3*3ピクセルのRGB成分を合計
for (var yy = y - 1;yy <= y + 1;yy++) {
for (var xx = x - 1;xx <= x + 1;xx++) {
var index = (xx + yy * img_data.width) * 4;
r_sum += img_data.data[index];
g_sum += img_data.data[index + 1];
b_sum += img_data.data[index + 2];
}
}
var index = (x + y * img_data.width) * 4;
// RGB平均値を算出
var r = Math.floor(r_sum / 9);
var g = Math.floor(g_sum / 9);
var b = Math.floor(b_sum / 9);
processed_data.data[index] = r;
processed_data.data[index + 1] = g;
processed_data.data[index + 2] = b;
processed_data.data[index + 3] = 255;
}
}
cv.getContext('2d').putImageData(processed_data, 0, 0);
}
</script>
<p>
<input type="file" onchange="onFileSelected(this)">
</p>
<p>
<canvas id="test_canvas" width=256 height=256 style="border: 1px solid;" onclick="processImageData()"></canvas>
</p>
</body>
</html>
Webブラウザにhtmlファイルを読み込んだら、上にある選択ボタンをクリックして適当な画像を選択してみてください。大きな画像だと処理時間がかかるので、縦横数百ピクセル程度の画像にしましょう。
下のcanvas要素に画像が表示されたら、画像をクリックすると画像処理(周辺RGB平均化)が行われ、結果がcanvas要素に表示されます。
少しボケたような感じになりますね。
\Webサイト担当者としてのスキルが身に付く/
画像の保存
こうして作成したcanvas要素の画像は、toDataURL()で「img要素のsrc属性やリンクとして利用可能な文字列のデータ」にすることができます。つまり、toDataURL()の文字列をリンクに設定するとそのリンクをクリックしたときに画像のみを表示/ダウンロードしたり、文字列を他のimg要素のsrc属性に設定して画像を表示したりといったことができるわけです(セキュリティ上の理由でリンクに設定できないWebブラウザもあります)。
デフォルトではpng形式の文字列で作成されるので、(エンコードによる)画像の劣化もありません。画像処理の結果をそのまま「画像ファイル(の文字列表現)」として取り出せるわけですね。
ただpngだとデータが大きくなりすぎるので、実用上はjpg形式で保存する方が良い場合も多いかもしれません。jpg形式にする場合は、toDataURL()の引数に「image/jpeg」を指定します。
ハイパーリンクを作成するa要素には、download属性がありここにファイル名を設定するとダウンロードリンクにすることができるので、これで画像のダウンロードリンクを作成してみましょう。
processImageData()の最後に、以下のコードを追加し、再度Firefoxで開いてみてください(edgeではリンクが機能しないようです)。
// 画像のダウンロードリンクを作成
var a = document.createElement('a');
a.href = cv.toDataURL("image/jpeg");
a.download = "test.jpg"
a.textContent = 'download';
// canvas要素の親要素を取得
var elm = cv.parentElement;
// canvasの次にリンクを追加
elm.appendChild(a);
画像をクリックすると、画像の横あるいは下に「download」リンクが表示され、クリックするとtest.jpgのファイル名で画像を保存できるようになっていると思います。
これで、画像の読み込み、画像処理、処理結果の保存……と「画像処理Webアプリ」に必要な処理の流れを作ることができました。
- この記事を書いた人
- 宍戸輝光