概要
googleから公開されているMediaPipe/Face MeshのReactでの実装例をまとめました。
https://nemutas.github.io/app-mediapipe-facemesh-demo/
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のドキュメントにあるので、それを元にコーディングしました。
全体のコード
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;
`
}
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では、顔の検知の設定を行います。
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箇所)のランドマークが顔のどの位置にあたるかは、ドキュメント上に公開されていません。(おそらく)
また、小一時間調べても確度の高い情報が見つからなかったので、自分で特定する手段を実装しました。
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を見てランドマークにあたりをつけます。
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は、それぞれ左目の下瞼、上瞼にあたります。
リポジトリ