【WebRTC使ってみた】ブラウザカメラとあそぼ!
[markdown]
どうもこんにちは。本当はいつまでも新人でいたかった、フロントエンドエンジニア大月です。春ですね。
さて、昨年9月にリリースされたiOS11でMedia Capture APIがサポートに加わり、iOS版Safariから本体のカメラにアクセスできるようになったようですね。
## Media Capture APIとは
[Media Capture and Streams API (Media Streams) – Web API インターフェイス | MDN](https://developer.mozilla.org/ja/docs/Web/API/Media_Streams_API)
難しいことが色々書いてあります。ざっくりと説明するとWebRTC関連のブラウザ用APIの一つで、これを使うとブラウザからカメラ・マイクにアクセスして動画や音声を扱うことができます。ちなみにここでいうWebRTCとはWebブラウザ同士でリアルタイムコミュニケーションを実現できる技術のことです。[こちらのページ](https://gist.github.com/voluntas/67e5a26915751226fdcf)で大変詳しく解説されています。勉強になりますね。
ニジボックスきってのカメラフェチ(自称)としてはこれを試さずにはいられません。むしろなぜ今まで試していなかったのかが分かりません。
早速やってみましょう!
## カメラにアクセス
iOSでブラウザからカメラを扱うときは、httpsでアクセスしないと動きません(android chromeならlocalhostもOK)。
毎回サーバーにアップするのは骨が折れますね。ローカルでも効率的に作業したいので、今回は[http-server](https://www.npmjs.com/package/http-server)を使いました。
グローバルにインストール
“` dark
$ npm install http-server -g
“`
オレオレ証明書発行
“` dark
$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
“`
サーバー立ち上げ
“` dark
$ http-server -S -C cert.pem
“`
これでコマンドラインからサクッとwebサーバを立ちあげられます。
カメラにアクセスするには、MediaDevices.getUserMedia()メソッドを使います。
■ index.html
“`html dark:sample01
“`
とても簡単ですね。
デモを置いておきます。
今回は下記の環境で動作を確認しています。
・ Safari(iOS版11.2/デスクトップ版11.0.3)
・ Google Chrome(mac版)
フロントカメラが立ち上がって画面に自分の顔が映ると毎回ビクッとしてしまうので、今回はスマホではバックカメラを使うことにしました。PCだと環境に合わせてカメラが立ち上がります。
バックカメラを使うデメリットは自分以外の誰かの顔を探さなければならないということです。
フロントカメラを使いたいときは、
“`js dark
var constraints = {
audio: false,
video: true
};
“`
もしくは、
“`js dark
var constraints = {
audio: false,
video: {
facingMode: ‘user’
}
};
“`
とするとセルフィ出来ます。
また、CSSでvideoタグにobject-fit: fillを指定することで、映像を親要素の大きさいっぱいなるようにしています。
デフォルト以外のアスペクト比でvideoを扱いたい時には、こちらを指定すると良い感じになります。
## 顔認識してみる
こんなに簡単にカメラが使えるとなると、もっと何かしたくてうずうずしてきました。
[clmtrackr](https://github.com/auduno/clmtrackr)という素敵なライブラリを使って顔認識をしてみようと思います。
こちらのライブラリはデモが充実しているので、そちらを大いに参考にさせていただくことにします。機能が豊富でデモをいじっているだけでもかなり楽しめます。
■ index.html
“`html dark:index.html
“`
■ style.css
“`css dark:style.css
html {
height: 100%;
}
body {
height: 100%;
margin: 0 auto;
}
.wrapper {
width: 100%;
height: 100%;
background: #000;
position: relative;
}
.main {
width: 100%;
position: relative;
}
.video {
width: 100%;
max-width: 100%;
height: 100%;
object-fit: fill;
display: block;
margin: 0 auto;
}
.overlay {
width: 100%;
max-width: 100%;
height: 100%;
display: block;
position: absolute;
top: 0;
left: 0;
}
“`
■ app.js
“`js dark:app.js
var video = document.querySelector(‘#video’);
var canvas = document.querySelector(‘#overlay’);
var context = canvas.getContext(‘2d’);
var constraints = {
audio: false,
video: {
// スマホのバックカメラを使用
facingMode: ‘environment’
}
};
var track = new clm.tracker({
useWebGL: true
});
function adjustVideo() {
// 映像が画面幅いっぱいに表示されるように調整
var ratio = window.innerWidth / video.videoWidth;
video.width = window.innerWidth;
video.height = video.videoHeight * ratio;
canvas.width = video.width;
canvas.height = video.height;
}
function startTracking() {
// トラッキング開始
track.start(video);
drawLoop();
}
function drawLoop() {
// 描画をクリア
context.clearRect(0, 0, canvas.width, canvas.height);
// videoをcanvasにトレース
context.drawImage(video, 0, 0, canvas.width, canvas.height);
if (track.getCurrentPosition()) {
// 顔のパーツの現在位置が存在
track.draw(canvas);
}
requestAnimationFrame(drawLoop);
}
track.init(pModel);
// カメラから映像を取得
navigator.mediaDevices.getUserMedia(constraints)
.then((stream) => {
video.srcObject = stream;
// 動画のメタ情報のロードが完了したら実行
video.onloadedmetadata = function() {
adjustVideo();
startTracking();
};
})
.catch((err) => {
window.alert(err.name + ‘: ‘ + err.message);
});
“`
なんとも簡単に顔をトラッキングすることができました!
興奮します。
デモはこちら
## 表情解析してエフェクトを加える
やはり笑顔というものはいいもので、元気が出ますね。
私はいつもアイドルの笑顔に元気をもらっています。
実はこのライブラリ、モデルを読み込むことで顔のパーツの座標を取得して表情解析もできちゃうのです。
取得した映像に手を加えたいので映像をvideoタグからcanvasにトレースしています。
■ app.js
“`js dark:app.js
var video = document.getElementById(‘video’);
var canvas = document.getElementById(‘overlay’);
var context = canvas.getContext(‘2d’);
var isPortrait = true;
var imageData;
var mosaicSize;
var constraints = {
audio: false,
video: {
// スマホのバックカメラを使用
facingMode: ‘environment’
}
};
var track = new clm.tracker({
useWebGL: true
});
var emotionClassifier = new emotionClassifier();
function successFunc (stream) {
video.srcObject = stream;
// 動画のメタ情報のロードが完了したら実行
video.onloadedmetadata = function() {
adjustProportions();
startTracking();
};
};
function startTracking() {
// トラッキング開始
track.start(video);
drawLoop();
}
function adjustProportions() {
var ratio = video.videoWidth / video.videoHeight;
if (ratio < 1) {
// 画面縦長フラグ
isPortrait = false;
}
video.width = Math.round(video.height * ratio);
canvas.width = video.width;
canvas.height = video.height;
}
function displaySnapshot() {
var snapshot = new Image();
snapshot.src = canvas.toDataURL('image/png');
snapshot.onload = function(){
snapshot.width = snapshot.width / 2;
snapshot.height = snapshot.height / 2;
gallary.appendChild(snapshot);
}
}
function drawLoop() {
// 描画をクリア
context.clearRect(0, 0, canvas.width, canvas.height);
// videoをcanvasにトレース
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// canvasの情報を取得
imageData = context.getImageData(0, 0, canvas.width, canvas.height);
if (track.getCurrentPosition()) {
// 顔のパーツの現在位置が存在
determineEmotion();
}
requestAnimationFrame(drawLoop);
}
function determineEmotion() {
// 顔の顔のパーツのパラメータ
var currentParam = track.getCurrentParameters();
var emotionResult = emotionClassifier.meanPredict(currentParam);
if (emotionResult) {
// 感情解析結果が得られた時の処理
}
}
function makeRosesBloom(level) {
for (i = 0; i < level; i++) {
if (isPortrait) {
roseImage.src = portraitImagePath[i];
} else {
roseImage.src = landscapeImagePath[i];
}
context.drawImage(roseImage, 0, 0, canvas.width, canvas.height);
}
}
pModel.shapeModel.nonRegularizedVectors.push(9);
pModel.shapeModel.nonRegularizedVectors.push(11);
track.init(pModel);
emotionClassifier.init(emotionModel);
// カメラから映像を取得
navigator.mediaDevices.getUserMedia(constraints)
.then(successFunc)
.catch((err) => {
window.alert(err.name + ‘: ‘ + err.message);
});
“`
笑顔の時にお花が咲くと平和な気持ちになれそうなので、ハッピーなときはセクシーローズを咲かせることにします。
■ app.js
“`js dark:app.js
var isHappy = false;
var happyLevel;
var roseImage = new Image;
// 画像のパス
var portraitImagePath = [
‘./img/roses_portrait_1.png’,
‘./img/roses_portrait_2.png’,
‘./img/roses_portrait_3.png’
];
var landscapeImagePath = [
‘./img/roses_landscape_1.png’,
‘./img/roses_landscape_2.png’,
‘./img/roses_landscape_3.png’
];
function drawLoop() {
// 描画をクリア
context.clearRect(0, 0, canvas.width, canvas.height);
// videoをcanvasにトレース
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// canvasの情報を取得
imageData = context.getImageData(0, 0, canvas.width, canvas.height);
if (track.getCurrentPosition()) {
// 顔のパーツの現在位置が存在
determineEmotion();
if (isHappy) {
makeRosesBloom(happyLevel);
}
}
requestAnimationFrame(drawLoop);
}
function determineEmotion() {
// 顔の顔のパーツのパラメータ
var currentParam = track.getCurrentParameters();
var emotionResult = emotionClassifier.meanPredict(currentParam);
if (emotionResult) {
for (var param in emotionResult) {
var emotion = emotionResult[param].emotion;
var value = emotionResult[param].value;
if (value) {
score = value.toFixed(1) * 100;
switch(emotion) {
case ‘happy’:
if (80 < score) {
happyLevel = 3;
isHappy = true;
} else if (70 < score) {
happyLevel = 2;
isHappy = true;
} else if (60 < score) {
happyLevel = 1;
isHappy = true;
} else {
isHappy = false;
}
break;
}
}
}
}
}
// バラの画像描画処理
function makeRosesBloom(level) {
for (i = 0; i < level; i++) {
if (isPortrait) {
roseImage.src = portraitImagePath[i];
} else {
roseImage.src = landscapeImagePath[i];
}
context.drawImage(roseImage, 0, 0, canvas.width, canvas.height);
}
}
```
悲しい顔の時には顔にモザイクを入れます。
だって涙は見せられないから!
モザイクの処理は[こちら](https://www.ipentec.com/document/html-canvas-create-mosic-image)の記事を参考にしました。
■ app.js
```js dark:app.js
var isSad = false;
function drawLoop() {
// 描画をクリア
context.clearRect(0, 0, canvas.width, canvas.height);
// videoをcanvasにトレース
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// canvasの情報を取得
imageData = context.getImageData(0, 0, canvas.width, canvas.height);
if (track.getCurrentPosition()) {
// 顔のパーツの現在位置が存在
determineEmotion();
if (isSad) {
createMosaic(mosaicSize);
}
if (isHappy) {
makeRosesBloom(happyLevel);
}
}
requestAnimationFrame(drawLoop);
}
function determineEmotion() {
// 顔の顔のパーツのパラメータ
var currentParam = track.getCurrentParameters();
var emotionResult = emotionClassifier.meanPredict(currentParam);
if (emotionResult) {
for (var param in emotionResult) {
var emotion = emotionResult[param].emotion;
var value = emotionResult[param].value;
if (value) {
score = value.toFixed(1) * 100;
switch(emotion) {
case 'sad':
if (80 < score) {
mosaicSize = 16;
isSad = true;
} else if (60 < score) {
mosaicSize = 8;
isSad = true;
} else {
isSad = false;
}
break;
case 'happy':
if (80 < score) {
happyLevel = 3;
isHappy = true;
} else if (70 < score) {
happyLevel = 2;
isHappy = true;
} else if (60 < score) {
happyLevel = 1;
isHappy = true;
} else {
isHappy = false;
}
break;
}
}
}
}
}
function createMosaic(mosaicSize) {
for (y = 0; y < canvas.height; y = y + mosaicSize) {
for (x = 0; x < canvas.width; x = x + mosaicSize) {
// getImageData で取得したピクセル情報から該当するピクセルのカラー情報を取得
var cR = imageData.data[(y * canvas.width + x) * 4];
var cG = imageData.data[(y * canvas.width + x) * 4 + 1];
var cB = imageData.data[(y * canvas.width + x) * 4 + 2];
context.fillStyle = 'rgb(' +cR+ ',' +cG+ ',' +cB+ ')';
context.fillRect(x, y, x + mosaicSize, y + mosaicSize);
}
}
}
```
今回は使用しませんが、clmtrackrではhappy・sadの他にも以下の表情の解析値が使えます。
・ anger: 怒り
・ disgusted: 嫌悪
・ fear: 恐れ
・ surprised: 驚き
## キャプチャ機能を追加
せっかくお花がきれいに咲いたら、思い出にとっておきたいので、キャプチャ機能をつけましょう。
■ app.js
```js dark:app.js
// 保存ボタン
var button = document.getElementById('button');
// 画像を表示する要素
var gallary = document.getElementById('gallary');
function displaySnapshot() {
var snapshot = new Image();
snapshot.src = canvas.toDataURL('image/png');
snapshot.onload = function(){
snapshot.width = snapshot.width / 2;
snapshot.height = snapshot.height / 2;
gallary.appendChild(snapshot);
}
}
// 保存ボタンを押したら実行
button.addEventListener('click', displaySnapshot);
```
## 完成
あとは細かい処理やスタイルの調整をして...
さあ、できました。
デモはこちらです。
全ソースはこちらでご覧いただけます。
## さあ、お花を咲かせに出発だ!
我らがリーダー、デザイナーの[神田さん](https://creatorsblog.nijibox.jp/web_director_interview-02/)に出会いました。
怒っていらっしゃいます…?
あっ悲しいのかな?
弾けんばかりの笑顔ですね。いつもの神田さんで安心しました。
## あそんでみて
とても簡単にブラウザからカメラの映像を取得してあそぶことができました。
ネイティブアプリではなくWEBブラウザから実現できると、htmlやjsで手軽に、端末依存も少なく実装できるので嬉しいですね!
現段階ではiOSでWebRTCを扱う場合コーデックがH.264で固定されてしまう等、実用レベルではまだまだ制限があるようです。今後のアップデートに期待ですね。
今回はjsのライブラリを使用しましたが、[Google CLOUD VISION API](https://cloud.google.com/vision/?hl=ja)や[Amazon Rekognition](https://aws.amazon.com/jp/rekognition/)、[Microsoft Azure Computer Vision](https://azure.microsoft.com/ja-jp/services/cognitive-services/computer-vision/)といった画像解析APIサービスも日に日に進化しているため、こちらと組み合わせるとさらに色々できそうでとても夢が広がります。
### 喜びと悲しみのマリアージュ
[/markdown]