LoginSignup
15
14

More than 1 year has passed since last update.

【React】MediaPipe/Face Mesh で顔を検知する

Last updated at Posted at 2021-11-17

概要

googleから公開されているMediaPipe/Face MeshReactでの実装例をまとめました。

https://nemutas.github.io/app-mediapipe-facemesh-demo/
output(video-cutter-js.com) (6).gif

Webカメラの接続が必要です。

以下の記事とほぼ同じ手順で実装できます。

ドキュメント

パッケージ

  • react - 17.0.2
  • typescript - 4.4.4
  • react-webcam - 6.0.0
  • leva - 0.9.14
  • @emotion/css - 11.5.0
  • @mediapipe/face_mesh - 0.4.1633559619
  • @mediapipe/camera_utils - 0.3.1632432234
  • @mediapipe/drawing_utils - 0.3.1620248257

実装

実装例がgoogleのドキュメントにあるので、それを元にコーディングしました。

全体のコード

App.tsx
import { button, useControls } from 'leva';
import React, { FC, useCallback, useEffect, useRef } from 'react';
import Webcam from 'react-webcam';
import { css } from '@emotion/css';
import { Camera } from '@mediapipe/camera_utils';
import {
    FaceMesh, FACEMESH_LEFT_EYE, FACEMESH_LIPS, FACEMESH_RIGHT_EYE, Results
} from '@mediapipe/face_mesh';
import { draw } from '../utils/drawCanvas';

export const App: FC = () => {
    const webcamRef = useRef<Webcam>(null)
    const canvasRef = useRef<HTMLCanvasElement>(null)
    const resultsRef = useRef<Results>()

    // コントローラーの追加
    const datas = useControls({
        bgImage: false,
        landmark: {
            min: 0,
            max: 477,
            step: 1,
            value: 0
        },
        result: button(() => {
            OutputData()
        })
    })

    /** 検出結果をconsoleに出力する */
    const OutputData = () => {
        const results = resultsRef.current!
        console.log(results.multiFaceLandmarks[0])
        console.log('FACEMESH_LEFT_EYE', FACEMESH_LEFT_EYE)
        console.log('FACEMESH_RIGHT_EYE', FACEMESH_RIGHT_EYE)
        console.log('FACEMESH_LIPS', FACEMESH_LIPS)
    }

    /** 検出結果(フレーム毎に呼び出される) */
    const onResults = useCallback(
        (results: Results) => {
            // 検出結果の格納
            resultsRef.current = results
            // 描画処理
            const ctx = canvasRef.current!.getContext('2d')!
            draw(ctx, results, datas.bgImage, datas.landmark)
        },
        [datas]
    )

    useEffect(() => {
        const faceMesh = new FaceMesh({
            locateFile: file => {
                return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`
            }
        })

        faceMesh.setOptions({
            maxNumFaces: 1,
            refineLandmarks: true, // landmarks 468 -> 478
            minDetectionConfidence: 0.5,
            minTrackingConfidence: 0.5
        })

        faceMesh.onResults(onResults)

        if (webcamRef.current) {
            const camera = new Camera(webcamRef.current.video!, {
                onFrame: async () => {
                    await faceMesh.send({ image: webcamRef.current!.video! })
                },
                width: 1280,
                height: 720
            })
            camera.start()
        }

        return () => {
            faceMesh.close()
        }
    }, [onResults])

    return (
        <div className={styles.container}>
            {/* capture */}
            <Webcam
                ref={webcamRef}
                style={{ visibility: 'hidden' }}
                audio={false}
                width={1280}
                height={720}
                mirrored
                screenshotFormat="image/jpeg"
                videoConstraints={{ width: 1280, height: 720, facingMode: 'user' }}
            />
            {/* draw */}
            <canvas ref={canvasRef} className={styles.canvas} width={1280} height={720} />
        </div>
    )
}

// ==============================================
// styles

const styles = {
    container: css`
        position: relative;
        width: 100vw;
        height: 100vh;
        overflow: hidden;
        display: flex;
        justify-content: center;
        align-items: center;
    `,
    canvas: css`
        position: absolute;
        width: 1280px;
        height: 720px;
        background-color: #1e1e1e;
        border: 1px solid #fff;
    `
}
drawCanvas.ts
import { drawConnectors } from '@mediapipe/drawing_utils';
import {
    FACEMESH_FACE_OVAL, FACEMESH_LEFT_EYE, FACEMESH_LEFT_EYEBROW, FACEMESH_LEFT_IRIS, FACEMESH_LIPS,
    FACEMESH_RIGHT_EYE, FACEMESH_RIGHT_EYEBROW, FACEMESH_RIGHT_IRIS, FACEMESH_TESSELATION,
    NormalizedLandmark, Results
} from '@mediapipe/face_mesh';

/**
 * canvasに描画する
 * @param ctx コンテキスト
 * @param results 検出結果
 * @param bgImage capture imageを描画するか
 * @param emphasis 強調するlandmarkのindex
 */
export const draw = (
    ctx: CanvasRenderingContext2D,
    results: Results,
    bgImage: boolean,
    emphasis: number
) => {
    const width = ctx.canvas.width
    const height = ctx.canvas.height

    ctx.save()
    ctx.clearRect(0, 0, width, height)

    if (bgImage) ctx.drawImage(results.image, 0, 0, width, height)

    if (results.multiFaceLandmarks) {
        const lineWidth = 1
        const tesselation = { color: '#C0C0C070', lineWidth }
        const right_eye = { color: '#FF3030', lineWidth }
        const left_eye = { color: '#30FF30', lineWidth }
        const face_oval = { color: '#E0E0E0', lineWidth }

        for (const landmarks of results.multiFaceLandmarks) {
            // 顔の表面(埋め尽くし)
            drawConnectors(ctx, landmarks, FACEMESH_TESSELATION, tesselation)
            // 右の目・眉・瞳
            drawConnectors(ctx, landmarks, FACEMESH_RIGHT_EYE, right_eye)
            drawConnectors(ctx, landmarks, FACEMESH_RIGHT_EYEBROW, right_eye)
            drawConnectors(ctx, landmarks, FACEMESH_RIGHT_IRIS, right_eye)
            // 左の目・眉・瞳
            drawConnectors(ctx, landmarks, FACEMESH_LEFT_EYE, left_eye)
            drawConnectors(ctx, landmarks, FACEMESH_LEFT_EYEBROW, left_eye)
            drawConnectors(ctx, landmarks, FACEMESH_LEFT_IRIS, left_eye)
            // 顔の輪郭
            drawConnectors(ctx, landmarks, FACEMESH_FACE_OVAL, face_oval)
            // 唇
            drawConnectors(ctx, landmarks, FACEMESH_LIPS, face_oval)

            // landmarkの強調描画
            drawPoint(ctx, landmarks[emphasis])
        }
    }
    ctx.restore()
}

/**
 * 特定のlandmarkを強調する
 * @param ctx
 * @param point
 */
const drawPoint = (ctx: CanvasRenderingContext2D, point: NormalizedLandmark) => {
    const x = ctx.canvas.width * point.x
    const y = ctx.canvas.height * point.y
    const r = 5

    ctx.fillStyle = '#22a7f2'
    ctx.beginPath()
    ctx.arc(x, y, r, 0, Math.PI * 2, true)
    ctx.fill()
}

MediaPipeの設定

faceMesh.setOptionsでは、顔の検知の設定を行います。

App.tsx
faceMesh.setOptions({
    maxNumFaces: 1,
    refineLandmarks: true, // landmarks 468 -> 478
    minDetectionConfidence: 0.5,
    minTrackingConfidence: 0.5
})

特に、

  • maxNumFaces:認識する顔の数
  • refineLandmarks:顔を詳細に認識するか。trueにすることで、目や唇まわりのメッシュを細かくし、瞳のランドマークも出力します。これによりランドマークの出力数が、468から478箇所に増えます。

検出結果のデータ構造

検出結果のresults.multiFaceLandmarksは、検知された顔と顔ごとのランドマークの2次元配列になっています。
例えば、以下のように値を取り出します。

// 1つ目の顔の100個目のランドマーク
const face1_landmark100 = results.multiFaceLandmarks[0][99]

そして、ランドマークはそれぞれ、3次元座標を持っています。

face1_landmark100 = {x: 0.1, y: 0.1, z: -0.5}
  • x, y:描画範囲に対して正規化された座標(0~1)
  • z:頭の中心位置からの座標(ほぼx座標と同じスケール)

ランドマークの特定方法


追記(2022.01.15)
ランドマークは、以下のようになっています。
※ 数字のフォントサイズが小さいので、画像をクリックして拡大して見てください。


https://github.com/google/mediapipe/blob/master/mediapipe/modules/face_geometry/data/canonical_face_model_uv_visualization.png


468箇所(478箇所)のランドマークが顔のどの位置にあたるかは、ドキュメント上に公開されていません。(おそらく)
また、小一時間調べても確度の高い情報が見つからなかったので、自分で特定する手段を実装しました。

特定するために、まずコントローラーを追加します。
スクリーンショット 2021-11-18 022053.png

App.tsx
const datas = useControls({
    bgImage: false,
    landmark: {
        min: 0,
        max: 477,
        step: 1,
        value: 0
    },
    result: button(() => {
        OutputData()
    })
})
  • bgImage:canvasにキャプチャした顔(自分の顔)を描画するかどうか。
  • landmark:この値を調整することで、canvas上の青い点を指定したランドマーク上に描画します。記事TOPのgifで、上唇にある青い点は、ランドマークのインデックスが0の位置です。
  • result:このボタンを押すことで、その瞬間の検出結果をコンソールに出力します。

ただし、468箇所のランドマークをこの方法で特定するのは大変です。
そこで、描画処理で引数として渡されている、LandmarkConnectionArrayを見てランドマークにあたりをつけます。

App.tsx
const OutputData = () => {
    const results = resultsRef.current!
    console.log(results.multiFaceLandmarks[0])
    console.log('FACEMESH_LEFT_EYE', FACEMESH_LEFT_EYE)
    console.log('FACEMESH_RIGHT_EYE', FACEMESH_RIGHT_EYE)
    console.log('FACEMESH_LIPS', FACEMESH_LIPS)
}

たとえばFACEMESH_LEFT_EYEは、左目のランドマークをつなぐための配列が格納されています。
374と386は、それぞれ左目の下瞼、上瞼にあたります。
Inkedスクリーンショット 2021-11-17 152308_LI.jpg

リポジトリ

15
14
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
14