こんにちは。
子育て奮闘中のエンジニア、遠藤です。
今日は「顔検出を使って、子供に歯磨きを楽しんでもらおう!」と試みたものの、様々な失敗を経験したお話をしたいと思います。
育児と仕事の両立についてで紹介した娘も、早いもので、もう2歳になりました。
娘の歯は8ヶ月頃から生え始め、少しずつ歯磨きの習慣をつけています。しかし、1歳半頃から始まったイヤイヤ期は、2歳前後でさらに激しくなり、泣き叫ぶことも増えました。そんな娘でも、できれば楽しく歯磨きしてほしいと考え、色々と試行錯誤してみました。
市販の歯磨き支援アプリは多くが大きい子供向けで、まだ絵本もあまり読めない1歳児には少し早いように感じました。そこで、自分で何か作ってみることにしました。
娘は鏡を見るのが好きなので、自分の顔を見ながら「アー」と「イー」の口をすることで、うまくできたとフィードバックを受け取れるアプリが良いのではないかと考えました。
そこで、Androidで顔検出ができるライブラリを調べたところ、「顔検出API」と「顔メッシュ検出API」の2つがあることが分かりました。ちょうどML Kitを試してみたかったので、これを使って顔検出に挑戦することにしました。
公式が提供しているサンプルアプリをインストールして、実際に試してみます。
https://github.com/googlesamples/mlkit/tree/master/android/vision-quickstart
顔検出API
https://developers.google.com/ml-kit/vision/face-detection?hl=ja
画像内の顔を検出し、主な顔の特徴を識別して、検出された顔の輪郭を取得できます。
説明にある通り、こちらは、顔のパーツや輪郭の座標を返してくれるものになります。
公開されているサンプルを起動すると、以下のようになります。
ただ、こちらでは、口を開けた時に、唇の動きに追従できませんでした。
これでは、「あ」や「い」の口の形を判別できなさそうです。
顔メッシュ検出API
https://developers.google.com/ml-kit/vision/face-mesh-detection?hl=ja
ML Kit の顔メッシュ検出 API を使用すると、自撮り写真のような画像用に 468 個の 3D ポイントからなる高精度メッシュをリアルタイムで生成できます。
顔メッシュ検出APIはまだベータ版の扱いですが、上記の説明にある通り、顔の 3D ポイントの座標を返しくれるものになります。
こちらは、口の動きに対応できるのでしょうか?
バッチリできていますね。
座標を取得して描画できそうなことは分かりましたので、この座標の中から口の部分に該当する箇所を探します。
コードを眺めていると、それらしいパラメータがあります。
そして、それらしい描画処理も見つかりました。
この辺りをいじって、唇の部分だけ描画してみるようにしてみます。
元のコードをコメントアウトし、以下の4つの座標を描画してみます。
- FaceMesh.UPPER_LIP_BOTTOM
- FaceMesh.UPPER_LIP_TOP
- FaceMesh.LOWER_LIP_BOTTOM
- FaceMesh.LOWER_LIP_TOP
唇の塗りつぶしコード
// Draw face mesh points
// for (FaceMeshPoint point : points) {
// updatePaintColorByZValue(
// positionPaint,
// canvas,
// /* visualizeZ= */ true,
// /* rescaleZForVisualization= */ true,
// point.getPosition().getZ(),
// zMin,
// zMax);
// canvas.drawCircle(
// translateX(point.getPosition().getX()),
// translateY(point.getPosition().getY()),
// FACE_POSITION_RADIUS,
// positionPaint);
// }
// if (useCase == FaceMeshDetectorOptions.FACE_MESH) {
// // Draw face mesh triangles
// for (Triangle<FaceMeshPoint> triangle : triangles) {
// List<FaceMeshPoint> faceMeshPoints = triangle.getAllPoints();
// PointF3D point1 = faceMeshPoints.get(0).getPosition();
// PointF3D point2 = faceMeshPoints.get(1).getPosition();
// PointF3D point3 = faceMeshPoints.get(2).getPosition();
//
// drawLine(canvas, point1, point2);
// drawLine(canvas, point2, point3);
// drawLine(canvas, point3, point1);
// }
// }
List<FaceMeshPoint> upperLipBottoms = faceMesh.getPoints(FaceMesh.UPPER_LIP_BOTTOM);
List<FaceMeshPoint> upperLipTop = faceMesh.getPoints(FaceMesh.UPPER_LIP_TOP);
List<FaceMeshPoint> lowerLipBottom = faceMesh.getPoints(FaceMesh.LOWER_LIP_BOTTOM);
List<FaceMeshPoint> lowerLipTop = faceMesh.getPoints(FaceMesh.LOWER_LIP_TOP);
Paint paint = new Paint();
Path path = new Path();
paint.setStyle(Style.FILL_AND_STROKE);
paint.setStrokeWidth(2f);
paint.setColor(Color.RED);
paint.setAntiAlias(true);
FaceMeshPoint start = upperLipBottoms.get(0);
path.moveTo(
translateX(start.getPosition().getX()),
translateY(start.getPosition().getY()));
for (FaceMeshPoint point : upperLipBottoms) {
path.lineTo(
translateX(point.getPosition().getX()),
translateY(point.getPosition().getY())
);
}
for (int i = upperLipTop.size() - 1; i >= 0; i--) {
FaceMeshPoint point = upperLipTop.get(i);
path.lineTo(
translateX(point.getPosition().getX()),
translateY(point.getPosition().getY())
);
}
start = lowerLipBottom.get(0);
path.moveTo(
translateX(start.getPosition().getX()),
translateY(start.getPosition().getY()));
for (FaceMeshPoint point : lowerLipBottom) {
// スタート地点を移動
path.lineTo(
translateX(point.getPosition().getX()),
translateY(point.getPosition().getY())
);
}
for (int i = lowerLipTop.size() - 1; i >= 0; i--) {
FaceMeshPoint point = lowerLipTop.get(i);
// スタート地点を移動
path.lineTo(
translateX(point.getPosition().getX()),
translateY(point.getPosition().getY())
);
}
path.close();
canvas.drawPath(path, paint);
このコードで、実行してみた結果がこちら。
どうでしょう。(分かりやすく塗りつぶしてみました)
綺麗に唇周りの座標を取得できていそうなことがわかりました。
口の形を判定する
取得できる座標は、それぞれ以下の値を取得できます。
- FACE_OVAL: 顔の輪郭
- LEFT_EYEBROW_TOP: 左眉の上
- LEFT_EYEBROW_BOTTOM: 左眉の下
- RIGHT_EYEBROW_TOP: 右眉の上
- RIGHT_EYEBROW_BOTTOM: 右眉の下
- LEFT_EYE: 左目
- RIGHT_EYE: 右目
- UPPER_LIP_TOP: 上唇の上部
- UPPER_LIP_BOTTOM: 上唇の下部
- LOWER_LIP_TOP: 下唇の上部
- LOWER_LIP_BOTTOM: 下唇の下部
- NOSE_BRIDGE: 鼻筋
今回は、口の形を判定したいので、以下の4つの値を使ってみます。
- UPPER_LIP_BOTTOM: 上唇の下部
- LOWER_LIP_TOP: 下唇の上部
また、顔とカメラの距離にもよって、値の大小が変わってしまうので、顔のサイズも一緒に取得します。
図にすると、以下になります。
ここから「あ」と「い」の判定をしてみます。
実際にデバッグしながら自分の顔を変えつつ、「あ」「い」の時に、どのような値になるかを判断しました。
基本的には、以下の比較を行いました。
顔の横幅:口の横幅
顔の縦幅:口の縦幅(上唇の下部と下唇の上部)
判定条件としては、以下の設定をした時に、それっぽい判断ができました。
if (lipHeight / faceHeight > 0.12 ) {
// あ, お
if (lipWidth / faceWidth > 0.25 ) {
text = "あ";
} else {
text = "お";
}
} else if (lipHeight / faceHeight > 0.09) {
// え
if (lipWidth / faceWidth > 0.25 ) {
text = "え";
} else {
text = "お";
}
} else if (lipHeight / faceHeight > 0.05) {
// い
if (lipWidth / faceWidth > 0.25 ) {
text = "い";
} else {
text = "お";
}
} else if (lipHeight / faceHeight > 0.02) {
if (lipWidth / faceWidth < 0.20 ) {
text = "う";
}
}
日本語で書くと、こんな感じになります。
- 「あ」:口の高さと顔の高さの比が0.12より大きく、かつ口の幅と顔の幅の比が0.25より大きい場合
- 「い」:口の高さと顔の高さの比が0.05より大きく、かつ0.09以下で、口の幅と顔の幅の比が0.25より大きい場合
- 「う」:口の高さと顔の高さの比が0.02より大きく、かつ0.05以下で、口の幅と顔の幅の比が0.20より小さい場合
- 「え」:口の高さと顔の高さの比が0.09より大きく、かつ0.12以下で、口の幅と顔の幅の比が0.25より大きい場合
- 「お」:
- 口の高さと顔の高さの比が0.12より大きく、かつ口の幅と顔の幅の比が0.25以下の場合
- または口の高さと顔の高さの比が0.09より大きく、かつ0.12以下で、口の幅と顔の幅の比が0.25以下の場合
- または口の高さと顔の高さの比が0.05より大きく、かつ0.09以下で、口の幅と顔の幅の比が0.25以下の場合
フィードバック
ここまできたら、あとは「あ」と「い」 の口の形にしてもらい、上手くできていたら、画面がキラキラとかすれば良いかなと思います。(安直)
「あ」と「い」の口は、イラストやから素材をもらってきました。
画面の右上に表示しておき、今、どちらの口をすれば良いか分かるようにしています。
あ
い
パーティクル
キラキラは、パーティクルのようなものを canvas 上に描画しています。
判定した口の形と指示が一致したら、 Paricle View が表示されます。
その他機能
しっかり、カメラボタンも配置し、子供が可愛い顔をした瞬間を逃しません。
今思うと、これはカメラより動画の方が良かったなぁと思いました。
これで完成?
さて。
実装したい機能はできました。
これで完成のはずだったのですが、このアプリにはいくつもの失敗が含まれています。
失敗1
大きな問題の一つが、開発に時間をかけすぎて、子供が大きくなってしまったことです。
もう、鏡を見てキャッキャしてくれる1歳の娘はいませんでした。
アプリを起動しても、娘は見向きもしてくれません。
納期って大事なんだな、ということを改めて感じました。
失敗2
顔のサイズが違う。
それはまぁそうなんですが、顔のサイズが違うので、自分の顔の間隔で「あ」「い」がそれなりに判定できるようになっても、子供の顔では判定がイマイチでした。。
失敗3
口の形が分かりやすいかな?と思って、唇を赤く塗ってみたのですが、「口が赤くなっちゃったー!」と大泣きしてしまいました。
失敗4
そしてこちらが大問題なのですが、歯磨きをする時って、歯ブラシを持つんだよって知っていましたか?
その手が、画面に映りこんでしまい、口の形がうまく判定できません。
終わりに
コンセプト自体は面白いと思ったのですが、時間が足りず、子供の成長により時間切れになってしまいました。
口のサイズだけでなく、他の要素も見ながら判定できれば、もう少し精確に判定もできたかもしれません。
失敗したアプリではありますが、 git に公開しています。
https://github.com/milldea/baby-tooth-brush-play-sample
歯磨きではなくても、何か面白いことに使えそうな気がしています。
弊社には子育て中の社員が何名かいますので、いい感じで仕上げてもらいたいです...(願望)
さて、歯磨きおてつだいアプリは完成できなかったのですが、その間子供の歯磨きを助けてくれた商品がこちら。
(プロモーションではありません。)
https://www.segatoys.co.jp/anpan/dekitaseries/
おやこでシャカシャカ アンパンマンピカピカはみがきミラー
とっても楽しく歯磨きできるようになりましたので、お困りの方はぜひお試しください(あれ?なんの話だっけ...)
他にも、もう少し大きな子になると、こんなアプリも使えそうでした!
https://okuchi-iku.lion.co.jp/
ごはんを食べたら、さあ冒険だ。まほうハブラシ
ゲーム感覚で楽しみながら、
仕上げみがきの卒業をサポート。
これは楽しそう。
その日が来るのを楽しみに待ちつつ、今はもう少し子供の歯磨きに奮闘します。