clmtrackr.jsで顔認識してへのへのもへじを描画する

投稿日:

ペッパー警部 邪魔をしないで

というわけで、clmtrackr.jsで顔認識してへのへのもへじを描画してみました。
はい。完全にお遊びです。

https://github.com/shimabox/henohenomohe-js

Demo

https://shimabox.github.io/henohenomohe-js/

工夫したところ

こんなお遊びでも、この2点は工夫(努力)をしました。

  • transform: scaleX(-1); を意識する
  • Webカメラの映像をcanvasに描画するところを分ける

以降、この2点について軽く説明していきます。

transform: scaleX(-1); を意識する

  • Webカメラの映像を読み込んで描画する videoタグ
  • videoタグの映像を読み込んで描画する canvasタグ

これらの要素は前面カメラ(フロントカメラ)を使う場合、鏡のように映さないとならないという謎の暗黙ルールがあります。
※ ルールというかそれが自然
※ 普段カメラを使う場合そうなっていますよね?

となると、Webカメラの映像を描画する要素に対しては

1
transform: scaleX(-1);

を設定する必要があります。

で、こうすると何が起こるかというと transform: scaleX(-1); している要素 に対して、transform: scaleX(-1); していない要素 で何かを表現する場合、座標を意識する必要が出てきます。

このプログラムは、

  • 顔を認識して顔の座標を返す … A
    • transform: scaleX(-1); されている
  • Aの座標を使ってへのへのもへじを描画する … B
    • transform: scaleX(-1);されていない

というざっくりいうと2つのレイヤーを持っていますが、前面カメラを使っている場合、Bのところで Aの座標(transform: scaleX(-1); されている)を求めて描画しないとうまくいきません。と書いても何言ってんだこいつみたいな状態なので図で見てみます。

transform: scaleX(-1); を意識しないとうまくいかない例

本来の顔座標

Reference にある通り、本来の顔座標は以下の位置で返ってきます。

transform: scaleX(-1); されていると

それが、transform: scaleX(-1); されている場合、このような位置になります。

この座標をそのまま使って描画してみると、こうなります。

鏡のようになる という言葉がしっくりきますね。

座標を入れ替えて対応する

じゃあどうしたかというと自分は前面カメラを使っている場合、座標を入れ替えるようにしました。
henohenomohe-js/henohenomohe.js at master · shimabox/henohenomohe-js

0番目の座標は、14番目。1番目の座標は、13番目。。
のように扱えるようになったので、本来の顔座標を意識したまま描画することができるようになります。

描画

座標の対応が済み、いざ描画するぞ!!と、単純にその座標をそのまま使っても、まだうまくいきません(なぜかというと、その座標は入れ替わっているので) 。
transform: scaleX(-1); 後のx座標を求めて描画する必要があります。

で、このへん以下のプログラムを書いてサクッと確認してみました。

transform: scaleX(-1) されている要素のx座標を求める

<!DOCTYPE html>
<html>
<head>
<title>transform: scaleX(-1) されている要素のx座標を求める</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body {
background: #b1acacb3;
}
.container {
height: 100vh;
display: flex;
align-items: center;
flex-direction: column;
}
.container h3 {
margin: 0;
}
.container canvas {
width: 320px;
height: 240px;
background: #fff;
}
.canvas-wrapper {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-around;
}
#transform-scaleX-canvas {
transform: scaleX(-1);
}
.desc {
font-weight: bold;
text-align: center;
display: block;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<div class="buttons">
<input type="radio" name="scaleX" value="1" id="style_1" checked>
<label for="style_1">離す</label>
</input>
<input type="radio" name="scaleX" value="2" id="style_2">
<label for="style_2">くっつける</label>
</input>
<input type="radio" name="scaleX" value="3" id="style_3">
<label for="style_3">重ねる</label>
</input>
</div>
<div class="canvas-wrapper">
<div class="desc"><span>通常</span></div>
<canvas id="canvas"></canvas>
<div class="desc"><span>transform: scaleX(-1)</span></div>
<canvas id="transform-scaleX-canvas"></canvas>
<div class="desc"><span>(transform: scaleX(-1) されている要素の)<br>x座標 = canvas幅 - x座標 - 文字の幅</span></div>
<canvas id="not-scaleX-canvas"></canvas>
</div>
</div>
<script>
'use strict';
const txt = 'あ';
const positionX = 200;
// Top.
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
ctx.font = "4em 'sans-serif'";
const ctxFontWidth = ctx.measureText(txt).width;
ctx.fillText(txt, positionX, ctxFontWidth);
// Middle.
const scaleXCanvas = document.querySelector('#transform-scaleX-canvas');
const scaleXCanvasCtx = scaleXCanvas.getContext('2d');
scaleXCanvasCtx.font = "4em 'sans-serif'";
const scaleXCtxFontWidth = scaleXCanvasCtx.measureText(txt).width;
scaleXCanvasCtx.fillText(txt, positionX, scaleXCtxFontWidth);
// Bottom.
const notScaleXCanvas = document.querySelector('#not-scaleX-canvas');
const notScaleXCanvasCtx = notScaleXCanvas.getContext('2d');
notScaleXCanvasCtx.font = "4em 'sans-serif'";
const notScaleXCtxFontWidth = notScaleXCanvasCtx.measureText(txt).width;
const notScaleXCanvasWidth = notScaleXCanvas.width;
// transform: scaleX(-1) されている要素のx座標 = canvasサイズ - x座標 - 文字の幅
const notScaleXPositionX = notScaleXCanvasWidth - positionX - notScaleXCtxFontWidth;
notScaleXCanvasCtx.fillText(txt, notScaleXPositionX, notScaleXCtxFontWidth);
const wrapper = document.querySelector('.canvas-wrapper');
const buttons = document.querySelector('.buttons');
const radioButtons = document.querySelectorAll('.buttons input[type="radio"]');
Array.from(radioButtons, function(radio) {
radio.addEventListener('change', function(e){
switch (this.value) {
case "2":
toggleDesc(false);
wrapper.style.justifyContent = 'initial';
buttons.style.margin = '0 0 4em 0';
scaleXCanvas.style.background = 'rgba(255,255,255, 1)';
scaleXCanvas.style.position = 'relative';
notScaleXCanvas.style.background = 'rgba(255,255,255, 1)';
notScaleXCanvas.style.position = 'relative';
break;
case "3":
toggleDesc(false);
wrapper.style.justifyContent = 'initial';
buttons.style.margin = '0 0 4em 0';
scaleXCanvas.style.background = 'rgba(255,255,255, 0)';
scaleXCanvas.style.position = 'absolute';
notScaleXCanvas.style.background = 'rgba(255,255,255, 0)';
notScaleXCanvas.style.position = 'absolute';
break;
case "1":
default:
toggleDesc(true);
wrapper.style.justifyContent = 'space-around';
buttons.style.margin = 0;
scaleXCanvas.style.background = 'rgba(255,255,255, 1)';
scaleXCanvas.style.position = 'relative';
notScaleXCanvas.style.background = 'rgba(255,255,255, 1)';
notScaleXCanvas.style.position = 'relative';
}
});
});
const desc = document.querySelectorAll('.desc');
function toggleDesc(isShow) {
Array.from(desc, function(elem) {
if (isShow === true) {
elem.style.display = 'block';
} else {
elem.style.display = 'none';
}
});
}
</script>
</body>
</html>

結果、transform: scaleX(-1) されている要素のx座標は以下のザックリとした式で求められることがわかったので

transform: scaleX(-1) されている要素のx座標 = canvas幅 – x座標 – 文字の幅;

この計算式をとりいれて

無事、描画が完了です。

Webカメラの映像をcanvasに描画するところを分けた

もう一つ工夫したところは、Webカメラの映像をcanvasに描画するところを分けたところです。
分けた部分は別ライブラリーとして出してみたので、こちらもよろしくお願いします。

https://github.com/shimabox/v2c/

これはこれで上手く使うと面白いものが書ける気がします。

ポイント

このプログラムのポイントはぶっちゃけ、へのへのもへじではなくて、顔認識している要素に対してレイヤーを1つ噛ましている部分にあると思っています。
カメラの認証さえ済ましておけば、リロードされない限りは裏で顔認識し続けることができます。
つまり、被せるレイヤーが素朴なコンテンツなどであれば、ユーザーに気づかれず個人情報が取り放題というわけです。
昨今タブレットやデジタルサイネージなど溢れていますが、その裏で何が行われているか想像しないといけないかもしれません。
もちろん、出す側としてはこういった注意喚起をユーザーに対して行うべきだと思います。

つまり何が言いたいかというと、良い子のみんなは悪いことに使っちゃダメだぞ!っていうことです。

おわりに

最初お遊びではじめた時はサクッと終わるかなぁと思ったのですが、上にある通り色々とめんどくさいことが重なってかなり時間がかかりました。(座標が絡んでくるとマジわからんから時間かかる。。)

作成者: shimabox

Web系のプログラマをやっています。 なるべく楽しく生きていきたい。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

This site uses Akismet to reduce spam. Learn how your comment data is processed.