ブラウザで顔、表情、パーツ位置の検出、誰かを当ててみる(face-api.js)

さてさて、このところビデオチャットやQRコードなどウェブカメラ関連の記事を続けて公開したので、この流れで何か他にも「リアルとウェブとの融合」がテーマの記事が書けないかなと考えていました。

そして、私は日頃から「これは試してみたい!」という内容はGoogle Keepに保存しているので、何気なく昔書いたメモをチェックしてみると・・・・・

発見しました😊✨
そのうち試してみたいと思っていた機能を。

ズバリそれは、

JavaScriptを使った顔認識システム

です。

この間公開した記事「Python(OpenCV) 顔が上を向くように写真から切り出す方法」では、Pythonを使いましたが、同じことが(ブラウザの)JavaScriptでも実行できるという噂を聞いていたんです。

ということで、今回はJavaScriptを使って以下の機能を実装してみたいと思います。

  • 顔の抽出
  • 顔パーツ(目とか鼻とか)の位置を抽出
  • 表情(笑っている、怒っているなど)を判別する
  • 特定の顔が誰なのかを判別する

ぜひ皆さんのお役に立てると嬉しいです😊✨

開発環境: Google Chrome 75、JavaScript コードはES6を使用、Vue 2.6

前提として

Google Chromeからウェブカメラにアクセスする場合、HTTPS接続が必須となっています。もしローカルをHTTPS化していないようでしたら、以下の記事を参考にしてみてください。

コピペでOK!ローカル環境にHTTPSを導入する(nginx編)

インストール

まずは JavaScriptで顔認証ができるパッケージ face-api.js を準備します。

本来は npm でインストールしてビルドするべきですが、今回は説明がわかりやすいので git cloneしてface-api.jsを利用することにします。

では、ブラウザから/js/face-api.jsにアクセスできるように適当フォルダに移動して以下のコマンドを実行してください。(Laravelの場合はpublic/jsフォルダになります)

git clone https://github.com/justadudewhohacks/face-api.js.git

※ ちなみに、ファイルサイズは200 MB以上あります。

顔認識、表情の判別、顔パーツの位置を取得

では、実際のコードになります。

<html>
<head>
    <title>ウェブカメラから顔を認識する</title>
    <style>
        #app {
            position:relative;
        }
        #app * {
            position: absolute;
        }
    </style>
</head>
<body>
<div id="app">
    <video ref="video" width="720" height="560" @play="onPlay"></video>
    <canvas ref="canvas" width="720" height="560"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
<script src="/js/face-api.js/dist/face-api.min.js"></script>
<script>

    new Vue({
        el: '#app',
        data: {
            MODEL_URI: '/js/face-api.js/weights',   // 顔認識モデルの場所
        },
        computed: {
            video() {

                return this.$refs['video'];

            },
            canvas() {

                return this.$refs['canvas'];

            }
        },
        methods: {
            onPlay() {

                const video = this.video;
                const canvas = this.canvas;

                setInterval(async () => {

                    // ウェブカメラの映像から顔データを取得
                    const detections = await faceapi.detectAllFaces(
                        this.video,
                        new faceapi.TinyFaceDetectorOptions()
                    )
                    .withFaceLandmarks()
                    .withFaceExpressions();

                    // 顔データをリサイズ
                    const resizedDetections = faceapi.resizeResults(detections, {
                        width: video.width,
                        height: video.height
                    });

                    // キャンバスに描画
                    canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
                    faceapi.draw.drawDetections(canvas, resizedDetections);
                    faceapi.draw.drawFaceLandmarks(canvas, resizedDetections);
                    faceapi.draw.drawFaceExpressions(canvas, resizedDetections)

                }, 500);

            }
        },
        mounted() {

            // 顔モデルをロード
            Promise.all([
                faceapi.nets.tinyFaceDetector.loadFromUri(this.MODEL_URI),
                faceapi.nets.faceLandmark68Net.loadFromUri(this.MODEL_URI),
                faceapi.nets.faceExpressionNet.loadFromUri(this.MODEL_URI)
            ])
            .then(() => {

                // ウェブカメラへアクセス
                navigator.mediaDevices.getUserMedia({ video: true })
                    .then((stream) => {

                        this.video.srcObject = stream;
                        this.video.play();

                    });

            });

        }
    });

</script>
</body>
</html>

まず、はじめに重要なのが<video>タグと<canvas>タグの関係です。

ウェブカメラの映像はもちろん<video>タグで表示するのですが、その映像自体を編集することはできないので、<canvas>を「上に」重ねてそこに顔パーツなどを描画する必要があります。そのため、CSSでpositionを指定しています。

↓↓↓ こんなイメージです。

そして、ページが表示後すぐ実行されるmounted()の中で以下3つのモデルデータを読み込みます。

  • tinyFaceDetector ・・・ 顔認識
  • faceLandmark68Net ・・・ 顔パーツの位置
  • faceExpressionNet ・・・ 顔の表情判別

MODEL_URIはそのモデルがある場所(フォルダ)で、face-api.jsに最初から含まれています。

なお、Promiseを使っているのは読み込みに時間がかかるからで、それらが全て完了したらウェブカメラにアクセスするようなっています。

その後、ウェブカメラの再生が開始されたら<video>タグで指定したイベントonPlayが実行され、それぞれ以下の内容を実行することになります。

  1. ウェブカメラの映像の中から全ての顔データを取得
  2. それらの顔データを動画の大きさにリサイズ
  3. 顔の位置(四角の枠)を描画
  4. 顔パーツ(目や鼻など68ヶ所)の位置を描画
  5. 現在の表情(怒っている、笑っている、驚いている)を描画

なお、pixabayのこちらの画像をスマホで表示して顔認識させてみた結果はこうなりました。

2つの顔を認識し、さらに目や鼻の位置、そして笑っているのでhappyと表示されています。成功ですね!

特定の顔が誰なのかを判別する

続いては、ある画像の中に写っている人がいったい誰なのか??を判別するプログラムになります。

今回は私が大ファンということもあって「明石家さんま」さんの画像を使って

  • 「これはさんまさんだ」
  • 「いや、別の人だ」

を判別するためのコードを試してみました。

仕組み

以下2つの画像からそれぞれ顔データを抽出し、それらを比較して「似ている度数」を計算します。そして、ある基準値の範囲内なら「本人」、超えているなら「別人」という判別になります。

  • すでに誰かがわかっている画像 ・・・ 検証用画像
  • 「これは誰??」が知りたい画像 ・・・ チェック画像

実際のコード

では実際のコードです。

<html>
<head>
    <title>指定した顔データが誰なのかをチェック</title>
    <style>

        #container {
            position:relative;
        }
        #container * {
            position: absolute;
        }

    </style>
</head>
<body>
<div id="app">
    <div id="container">
        <img ref="image" src="/images/sanma_try.jpg"></video>
        <canvas ref="canvas"></canvas>
        <div v-if="processing" style="background:#000;color:#fff;padding:5px;">解析中..</div>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js"></script>
<script src="/js/face-api.js/dist/face-api.min.js"></script>
<script>

    new Vue({
        el: '#app',
        data: {
            MODEL_URI: '/js/face-api.js/weights',
            processing: true
        },
        computed: {
            image() {

                return this.$refs['image'];

            },
            canvas() {

                return this.$refs['canvas'];

            }
        },
        methods: {
            getCheckingDescriptors() {

                return new Promise(async (resolve) => {

                    const urls = [
                        '/images/sanma_check_1.jpg',
                        '/images/sanma_check_2.jpg'
                    ];
                    let descriptors = [];

                    for(let i = 0 ; i < urls.length ; i++) {

                        let url = urls[i];
                        const image = await faceapi.fetchImage(url);
                        const detection = await faceapi.detectSingleFace(image)
                            .withFaceLandmarks()
                            .withFaceDescriptor();
                        descriptors.push(detection.descriptor);

                    }

                    resolve(
                        new faceapi.LabeledFaceDescriptors('明石家さんま さん', descriptors)
                    );

                });

            }
        },
        mounted() {

            // 顔モデルをロード
            Promise.all([
                faceapi.nets.faceLandmark68Net.loadFromUri(this.MODEL_URI),
                faceapi.nets.faceRecognitionNet.loadFromUri(this.MODEL_URI),
                faceapi.nets.ssdMobilenetv1.loadFromUri(this.MODEL_URI)
            ])
            .then(async () => {

                const size = {
                    width: this.image.width,
                    height: this.image.height
                };

                // チェックする顔データを取得
                const detection = await faceapi.detectSingleFace(this.image)
                    .withFaceLandmarks()
                    .withFaceDescriptor();

                // 検証用の顔データを取得してマッチするか実行
                this.getCheckingDescriptors()
                    .then((checkingDescriptors) => {

                        const faceMatcher = new faceapi.FaceMatcher(checkingDescriptors, 0.6);
                        const result = faceMatcher.findBestMatch(detection.descriptor);

                        // キャンバスに描画
                        const resizedDetection = faceapi.resizeResults(detection, size);
                        const box = resizedDetection.detection.box;
                        const drawBox = new faceapi.draw.DrawBox(box, {
                            label: result.toString()
                        });
                        faceapi.matchDimensions(this.canvas, size);
                        drawBox.draw(this.canvas);

                        this.processing = false;

                    });

            });

        }
    });

</script>
</body>
</html>

まず、コードの中に登場する画像についてですが、以下の2つが検証用の画像になっています。

  • /images/sanma_check_1.jpg
  • /images/sanma_check_2.jpg

そして、チェック画像は/images/sanma_try.jpgになります。

※ 画像はご自身で用意しておいてください。

そして、先ほどの顔認証と同じく顔モデルをmounted()内で読み込んだら、本人チェックしたい画像と検証用の画像からそれぞれ顔データを取得し、FaceMatcherを使ってそれらのデータを比較することになります。

ちなみに、FaceMatcherに設定されている0.6というのが本人かどうかを判別する基準値になっていて、少ないほうがより本人であるとされます。(0なら本人、1なら別人)

const faceMatcher = new faceapi.FaceMatcher(checkingDescriptors, 0.6);

そして、その判別によってキャンバスには以下2パターンで描画されることになります。

  1. 本人なら、「明石家さんま さん」
  2. 別人なら、unknown

※ ちなみに先ほどの顔認識よりもこちらのコードは時間がかかるので、実行している間は「解析中..」と表示しています。

では、実際にテストしてみましょう!(ただし、インターネットからとってきた画像なのでモザイクをかけています。わかりにくかったらすみません😅)

まずはご本人(明石家さんまさん)の画像でテストです。

はい!
顔に枠がついて、「明石家さんま さん」とラベルがついて「似ている度数」は0.32という結果になりました。0.6が基準値なので相当似ている、つまり本人であるとの判別されたわけですね。

では、逆に違う人であると判別できるのかもテストしてみましょう。次の画像はこちらも大ファンのX Japan・Yoshikiさんです。

こちらは「unknown」つまり、別の人であると判別されました。「似ている度数は」0.84です。0.6の基準値は軽く超えてしまいました。こちらもうまくいきましたね。

では、最後に似ている人はどうか??ということで明石家さんまさんのモノマネ芸で人気の「ほいけんた」さんでもチェックしてみることにしました。

なんと、「明石家さんま さん」であると認識されてしまいました!
ほいさんは嬉しいかもしれませんが、やはりモノマネされている人で実行すると誤認識してしまう可能性もありそうです。

似ている度数も0.4とご本人に近い数値でした😂

おわりに

ということで今回はJavaScriptを使って顔認識する機能をつくってみました。もちろんスピードの部分ではNodeJSPythonなどのローカル環境には及びませんが、全てブラウザだけで実行できるのは、すごいですよね。

また、精度としても以前Pythonでやってみたときとそれほど違いがあるわけではなかったというのが正直な感想になります。

さすがにこのコードで生体認証などを作るには精度が足りないかもしれませんが、簡単な顔認識できる防犯カメラなど応用することもできるんじゃないでしょうか。

ぜひ皆さんもこのテクニックを応用して何かを作ってみてはいかがでしょうか。

ではでは〜!

この記事が役立ちましたらシェアお願いします😊✨