Webカメラの映像を解析(?)することがあったので、その工程をざっくりとまとめてみる。
今回は、前回(Webカメラの映像をcanvasに表示させる)の続き。
(説明する際、constで宣言したものを便宜上、変数といわせていただきます。)
やること
- canvasからデータを取得する。
- データを判別し、加工する。
canvasからデータを取得する
取得する方法は簡単。
描画済みのコンテキストさんからデータを「ちょうだい(´ω`)」すればよいのである。
const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
コンテキストさんのgetImageData()メソッドで、指定した範囲のピクセルデータであるImageDataオブジェクトを取得している。
変数dataに代入しているのはImageDataオブジェクト内のRGBAのデータ配列。
実際にコンソールに出してみるとこんな感じ。
上が変数imageDataに代入したImageDataオブジェクト、下が変数dataに代入したImageDataオブジェクト内のdata(Uint8ClampedArray)。
Uint8ClampedArrayってなんぞ? ………ぶっちゃけよくわからない。
1ビットを8つ並べた8ビットは1バイト… 2進数で8桁… 8ビットで表せるのは2の8乗で0〜255の256通り…
とりあえず、0〜255の符号なし整数が入る配列と認識している。
0〜255の範囲外の数値を指定した場合は、0もしくは255が設定されるらしい。
整数ではない値を指定すると、最も近い整数に設定されるらしい。
…で、これから使うのは変数dataに代入したImageDataオブジェクト内のRGBAのデータ配列。(Uint8ClampedArray)
配列内の情報はいたって単純。
1ピクセルのRGBAが順番に格納されているのである。(アルファ値も0〜255になる)
1ピクセル目はrgba(77, 61, 66, 1)、2ピクセル目はrgba(73, 61, 63, 1)、3ピクセル目はrgba(96, 73, 52, 1)………といった具合である。
次はここで取得したデータを元に特定の色を変えていこうと思う。
データを判別し、加工する。
canvasを1ピクセルずつ色を確認し、該当すれば色を変更する。
// 変更したい色の範囲を決めておく
const minColor = { r: 108, g: 0, b: 0 };
const maxColor = { r: 255, g: 60, b: 60 };
// コンテキストからデータ取得
const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // rgba、1バイト×4のデータ
// ここに現在のピクセル情報を入れていく
const currentColor = {};
// 1ピクセルずつ確認していく
for(let i = 0, len = data.length; i < len; i += 4) {
currentColor.r = data[i];
currentColor.g = data[i + 1];
currentColor.b = data[i + 2];
// 指定したrgb内であれば黄色に変換する
if(_checkTargetColor(currentColor, minColor, maxColor)) {
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 0;
// data[i + 3] = 0; => アルファ値なので、0にすれば透明になる
}
}
// ImageDataオブジェクトに、変更済みのRGBAデータ(変数data)を代入する
imageData.data = data;
// canvasに変更済みのImageDataオブジェクトを描画する
canvasCtx.putImageData(imageData, 0, 0);
// 色の判定用の関数(引数:現在のピクセルのrgb、指定色の最小値、指定色の最大値)
// 指定したrgb内であれば true を返す
function _checkTargetColor(current, min, max) {
if(min.r > current.r || current.r > max.r) return;
if(min.g > current.g || current.g > max.g) return;
if(min.b > current.b || current.b > max.b) return;
return true;
};
説明するまでもないが、for文で使っている変数iが4ずつ増えていくのは、配列がRGBA4つのデータで1ピクセルのセットだからである。
data[0]〜data[3] => 1ピクセル目、data[4]〜data[7] => 2ピクセル目、data[8]〜data[11] => 3ピクセル目……
内容についてはソースのコメントにほぼ書いてしまったが、
for文で1ピクセルずつチェックしていき、該当する色であればRGBAの配列データをお好きなように変更する。
変更したデータをコンテキストさんのImageDataオブジェクトに代入し、コンテキストさんに描画を「おねがい(´ω`)」すればよい。
全体のソース
ここまではあくまで説明しやすくしているため、このまま使ってもきちんと描画されないと思う。
なぜならrequestAnimationFrame()メソッドのループに組み込んでいないからである。
なので、ここで全体のソースを書いておく。
<div id="videoPreview">
<p>video preview</p>
</div>
<div id="canvasPreview">
<p>canvas preview</p>
</div>
const cameraSize = { w: 360, h: 240 };
const canvasSize = { w: 360, h: 240 };
const resolution = { w: 1080, h: 720 };
const minColor = { r: 108, g: 0, b: 0 };
const maxColor = { r: 255, g: 60, b: 60 };
let video;
let media;
let canvas;
let canvasCtx;
// video要素をつくる
video = document.createElement('video');
video.id = 'video';
video.width = cameraSize.w;
video.height = cameraSize.h;
video.autoplay = true;
document.getElementById('videoPreview').appendChild(video);
// video要素にWebカメラの映像を表示させる
media = navigator.mediaDevices.getUserMedia({
audio: false,
video: {
width: { ideal: resolution.w },
height: { ideal: resolution.h }
}
}).then(function(stream) {
video.srcObject = stream;
});
// canvas要素をつくる
canvas = document.createElement('canvas');
canvas.id = 'canvas';
canvas.width = canvasSize.w;
canvas.height = canvasSize.h;
document.getElementById('canvasPreview').appendChild(canvas);
// コンテキストを取得する
canvasCtx = canvas.getContext('2d');
// video要素の映像をcanvasに描画する
_canvasUpdate();
function _canvasUpdate() {
canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
_changePixelColor(); // ループにこれが追加される
requestAnimationFrame(_canvasUpdate);
};
// canvasを1ピクセルずつ色を確認し、該当すれば色を変更する
function _changePixelColor() {
// コンテキストからデータ取得
const imageData = canvasCtx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // rgba、1バイト×4のデータ
// ここに現在のピクセル情報を入れていく
const currentColor = {};
// 1ピクセルずつ確認していく
for(let i = 0, len = data.length; i < len; i += 4) {
currentColor.r = data[i];
currentColor.g = data[i + 1];
currentColor.b = data[i + 2];
// 指定したrgb内であれば黄色に変換する
if(_checkTargetColor(currentColor, minColor, maxColor)) {
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 0;
// data[i + 3] = 0; => アルファ値なので、0にすれば透明になる
}
}
// ImageDataオブジェクトに、変更済みのRGBAデータ(変数data)を代入する
imageData.data = data;
// canvasに変更済みのImageDataオブジェクトを描画する
canvasCtx.putImageData(imageData, 0, 0);
};
// 色の判定用の関数(引数:現在のピクセルのrgb、指定色の最小値、指定色の最大値)
// 指定したrgb内であれば true を返す
function _checkTargetColor(current, min, max) {
if(min.r > current.r || current.r > max.r) return;
if(min.g > current.g || current.g > max.g) return;
if(min.b > current.b || current.b > max.b) return;
return true;
};
UIをつくって指定する色を変更できるようにするのがベスト。
あと、canvas要素でクリックした箇所1ピクセルの色データが出るようにしてあげるととても便利。
let targetPixel;
function _getPointColor(e) {
targetPixel = canvasCtx.getImageData(e.offsetX, e.offsetY, 1, 1);
console.log(`rgba(${targetPixel.data[0]}, ${targetPixel.data[1]}, ${targetPixel.data[2]}, ${targetPixel.data[3]})`);
};
canvas.addEventListener('mousedown', _getPointColor);
今回は色の最小値〜最大値でチェックしていったが、色差を使ってチェックする方法もある。(ユークリッド距離)
// 色の最小値〜最大値ではなく、色を一色決める
const targetColor = { r: 200, g: 0, b: 0 };
// 色の距離
let colorDistance = 100;
// 中略
// _changePixelColor()内の色判定が、「色の距離がcolorDistance以下であれば〜」に変わる
if(_getColorDistance(currentColor, targetColor) < colorDistance) {
data[i] = 255;
data[i + 1] = 255;
data[i + 2] = 0;
// data[i + 3] = 0; => アルファ値なので、0にすれば透明になる
}
// 中略
// 色の判定で使っていた _checkTargetColor()の代わり
function _getColorDistance(rgb1, rgb2) {
const result = Math.sqrt(
Math.pow((rgb1.r - rgb2.r), 2) +
Math.pow((rgb1.g - rgb2.g), 2) +
Math.pow((rgb1.b - rgb2.b), 2)
);
return result;
};
おそらくこんな感じかと。
色の距離を大きくすればするほど許容する色の範囲が大きくなるので、黄色に変わる色が多くなる。
まとめ
はやく語彙力のあるゴリゴリのエンジニアになりたい。(2回目)


