前回のcanvasでクロマキー合成っぽいことしてみるの派生。
映像の中から輪郭を検出して色を変えてみる。
やること
- 映像を取り込む
- canvasに描画する
- 描画されたcontextを走査し、隣接する色の差によって色を塗り分ける
- 色の塗り方を変える
※前回と同じ処理の説明は割愛
htmlファイルを用意
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Language" content="ja">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0,minimal-ui">
<title>Outline</title>
</head>
<body>
<div>
<input id="color" type="color" value="#ff0000" />
<input id="distance" type="number" value="10" />
</div>
<div>
<video id="video" autoplay></video>
<canvas id="canvas" width="480" height="360"></canvas>
</div>
<script type="text/javascript" src="./script.js"></script>
</body>
</html>
script.js
に処理を書いていく。
省略
描画されたcontextを走査し、隣接する色の差によって色を塗り分ける
ユークリッド距離を求める関数を使い、今回は隣接するピクセルの色が遠いかどうかを判定する。
隣り合う色のユークリッド距離の差が大きければ、映像上そこは境界であると認識できる。
左上から順に走査していき、垂直方向の境界線は右隣の色、水平方向の境界線は下の色と比較をして検出する。
アウトライン処理
var outlineColor = {r: 255, g: 0, b: 0},
colorDistance = 10;
// アウトライン処理
var outline = function () {
var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
data = imageData.data;
// dataはUint8ClampedArray
// 長さはcanvasの width * height * 4(r,g,b,a)
// 先頭から、一番左上のピクセルのr,g,b,aの値が順に入っており、
// 右隣のピクセルのr,g,b,aの値が続く
// n から n+4 までが1つのピクセルの情報となる
for (var i = 0, l = data.length; i < l; i += 4) {
// この条件の時、currentは右端の色、nextは1px下の段の左端の色になるので透明にしてスキップする
if ((i / 4 + 1) % canvas.width === 0) {
data[i + 3] = 0;
continue;
}
var currentIndex = i,
nextIndex = currentIndex + 4,
underIndex = currentIndex + (canvas.width * 4),
// チェックするピクセルの色
current = {
r: data[currentIndex],
g: data[currentIndex + 1],
b: data[currentIndex + 2]
},
// 右隣の色
next = {
r: data[nextIndex],
g: data[nextIndex + 1],
b: data[nextIndex + 2]
},
// 下の色
under = {
r: data[underIndex],
g: data[underIndex + 1],
b: data[underIndex + 2]
};
// 現在のピクセルと右隣、下の色の三次元空間上の距離を閾値と比較する
// 閾値より大きい(色が遠い)場合、境界線とみなしそのピクセルをoutlineColorに変更
// 閾値より小さい(色が近い)場合、そのピクセルを消す
if (getColorDistance(current, next) > colorDistance || getColorDistance(current, under) > colorDistance) {
data[i] = outlineColor.r;
data[i + 1] = outlineColor.g;
data[i + 2] = outlineColor.b;
} else {
// alpha値を0にすることで見えなくする
data[i + 3] = 0;
}
}
// 書き換えたdataをimageDataにもどし、描画する
imageData.data = data;
context.putImageData(imageData, 0, 0);
};
ここまでで映像内の輪郭を検出して単色で塗り、その他の部分を透明にすることができる。
色の塗り方を変える
単色塗りだとおもしろくないので、指定された色からその色の補色にグラデーションをかけるようにする。
補色の求め方はAdobeぱいせんを参考にした。
参考:Adobe Illustrator/カラーの調整#カラーの反転色または補色への変更
補色を求める関数
// 補色の計算
var getComplementaryColor = function (rgb) {
var max = Math.max(rgb.r, rgb.g, rgb.b),
min = Math.min(rgb.r, rgb.g, rgb.b),
sum = max + min;
return {
r: sum - rgb.r,
g: sum - rgb.g,
b: sum - rgb.b
};
};
指定色とその補色をもとに、contextの上から一段ずつ色を変えるように計算する。
アウトライン処理部分を次のように修正。
グラデーションをで塗るアウトライン処理
var outlineColor = {r: 255, g: 0, b: 0},
complementaryColor = {r: 0, g: 255, b: 255},
colorDistance = 10;
// アウトライン処理
var outline = function () {
var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
data = imageData.data;
// dataはUint8ClampedArray
// 長さはcanvasの width * height * 4(r,g,b,a)
// 先頭から、一番左上のピクセルのr,g,b,aの値が順に入っており、
// 右隣のピクセルのr,g,b,aの値が続く
// n から n+4 までが1つのピクセルの情報となる
var currentOutlineColor = outlineColor;
for (var i = 0, l = data.length; i < l; i += 4) {
// この条件の時、currentは右端の色、nextは1px下の段の左端の色になるので透明にしてスキップする
if ((i / 4 + 1) % canvas.width === 0) {
data[i + 3] = 0;
continue;
}
// 段が変わったら色を変える
// 一段ずつoutlineColorからcomplementaryColorにグラデーションにする
if ((i / 4) % canvas.width === 0) {
var row = (i / 4) / canvas.width,
r = (outlineColor.r - complementaryColor.r) / canvas.height,
g = (outlineColor.g - complementaryColor.g) / canvas.height,
b = (outlineColor.b - complementaryColor.b) / canvas.height;
currentOutlineColor = {
r: outlineColor.r - (r * row),
g: outlineColor.g - (g * row),
b: outlineColor.b - (b * row)
};
}
var currentIndex = i,
nextIndex = currentIndex + 4,
underIndex = currentIndex + (canvas.width * 4),
// チェックするピクセルの色
current = {
r: data[currentIndex],
g: data[currentIndex + 1],
b: data[currentIndex + 2]
},
// 右隣の色
next = {
r: data[nextIndex],
g: data[nextIndex + 1],
b: data[nextIndex + 2]
},
// 下の色
under = {
r: data[underIndex],
g: data[underIndex + 1],
b: data[underIndex + 2]
};
// 現在のピクセルと右隣、下の色の三次元空間上の距離を閾値と比較する
// 閾値より大きい(色が遠い)場合、境界線とみなしそのピクセルをcurrentOutlineColorに変更
// 閾値より小さい(色が近い)場合、そのピクセルを消す
if (getColorDistance(current, next) > colorDistance || getColorDistance(current, under) > colorDistance) {
data[i] = currentOutlineColor.r;
data[i + 1] = currentOutlineColor.g;
data[i + 2] = currentOutlineColor.b;
} else {
// alpha値を0にすることで見えなくする
data[i + 3] = 0;
}
}
// 書き換えたdataをimageDataにもどし、描画する
imageData.data = data;
context.putImageData(imageData, 0, 0);
};
ここまでのコードをまとめると↓のようになる。
コード全体像
script.js
(function () {
var video = document.getElementById('video');
// videoは非表示にしておく
video.style.display = 'none';
var canvas = document.getElementById('canvas');
// そのまま表示すると鏡像にならないので反転させておく
canvas.style.transform = 'rotateY(180deg)';
var context = canvas.getContext('2d');
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
navigator.getUserMedia({video: true, audio: false}, function (stream) {
video.src = URL.createObjectURL(stream);
draw();
}, function () {});
// videoの映像をcanvasに描画する
var draw = function () {
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// ここでアウトライン処理をする
outline();
requestAnimationFrame(draw);
};
// 境界線とする閾値
var outlineColor = {r: 255, g: 0, b: 0},
complementaryColor = {r: 0, g: 255, b: 255},
colorDistance = 10;
// アウトライン処理
var outline = function () {
var imageData = context.getImageData(0, 0, canvas.width, canvas.height),
data = imageData.data;
// dataはUint8ClampedArray
// 長さはcanvasの width * height * 4(r,g,b,a)
// 先頭から、一番左上のピクセルのr,g,b,aの値が順に入っており、
// 右隣のピクセルのr,g,b,aの値が続く
// n から n+4 までが1つのピクセルの情報となる
var currentOutlineColor = outlineColor;
for (var i = 0, l = data.length; i < l; i += 4) {
// この条件の時、currentは右端の色、nextは1px下の段の左端の色になるので透明にしてスキップする
if ((i / 4 + 1) % canvas.width === 0) {
data[i + 3] = 0;
continue;
}
// 段が変わったら色を変える
// 一段ずつoutlineColorからcomplementaryColorにグラデーションにする
if ((i / 4) % canvas.width === 0) {
var row = (i / 4) / canvas.width,
r = (outlineColor.r - complementaryColor.r) / canvas.height,
g = (outlineColor.g - complementaryColor.g) / canvas.height,
b = (outlineColor.b - complementaryColor.b) / canvas.height;
currentOutlineColor = {
r: outlineColor.r - (r * row),
g: outlineColor.g - (g * row),
b: outlineColor.b - (b * row)
};
}
var currentIndex = i,
nextIndex = currentIndex + 4,
underIndex = currentIndex + (canvas.width * 4),
// チェックするピクセルの色
current = {
r: data[currentIndex],
g: data[currentIndex + 1],
b: data[currentIndex + 2]
},
// 右隣の色
next = {
r: data[nextIndex],
g: data[nextIndex + 1],
b: data[nextIndex + 2]
},
// 下の色
under = {
r: data[underIndex],
g: data[underIndex + 1],
b: data[underIndex + 2]
};
// 現在のピクセルと右隣、下の色の三次元空間上の距離を閾値と比較する
// 閾値より大きい(色が遠い)場合、境界線とみなしそのピクセルをcurrentOutlineColorに変更
// 閾値より小さい(色が近い)場合、そのピクセルを消す
if (getColorDistance(current, next) > colorDistance || getColorDistance(current, under) > colorDistance) {
data[i] = currentOutlineColor.r;
data[i + 1] = currentOutlineColor.g;
data[i + 2] = currentOutlineColor.b;
} else {
// alpha値を0にすることで見えなくする
data[i + 3] = 0;
}
}
// 書き換えたdataをimageDataにもどし、描画する
imageData.data = data;
context.putImageData(imageData, 0, 0);
};
// r,g,bというkeyを持ったobjectが第一引数と第二引数に渡される想定
var getColorDistance = function (rgb1, rgb2) {
// 三次元空間の距離が返る
return Math.sqrt(
Math.pow((rgb1.r - rgb2.r), 2) +
Math.pow((rgb1.g - rgb2.g), 2) +
Math.pow((rgb1.b - rgb2.b), 2)
);
};
var color = document.getElementById('color');
color.addEventListener('change', function () {
// フォームの値は16進カラーコードなのでrgb値に変換する
outlineColor = color2rgb(this.value);
complementaryColor = getComplementaryColor(outlineColor);
});
var color2rgb = function (color) {
color = color.replace(/^#/, '');
return {
r: parseInt(color.substr(0, 2), 16),
g: parseInt(color.substr(2, 2), 16),
b: parseInt(color.substr(4, 2), 16)
};
};
// 補色の計算
var getComplementaryColor = function (rgb) {
var max = Math.max(rgb.r, rgb.g, rgb.b),
min = Math.min(rgb.r, rgb.g, rgb.b),
sum = max + min;
return {
r: sum - rgb.r,
g: sum - rgb.g,
b: sum - rgb.b
};
};
var distance = document.getElementById('distance');
distance.style.textAlign = 'right';
distance.addEventListener('change', function () {
colorDistance = this.value;
});
})();
ええでええで。
demo
※chrome推奨
デザイナーからみたらアートでもなんでもないんだろうけどな!!!!!