👴

怒っている顔画像をAIであまり怒っていない表情に変形させる(笑って怒ってハイチーズ!に使われている技術_前編)

2024/04/19に公開

はじめに

こんにちは! Whatever Co. でエンジニアをしている登山です。

お台場にある 日本科学未来館では、2023 年 11 月に常設展示の大規模リニューアルが行われました。その中の一つである「老いパーク」では、「老い」をテーマに、老化による目・耳・運動器・脳の変化を疑似体験できるコンテンツが展示されています。


日本科学未来館 web ページより引用

Whatever Co. は、「老いパーク」の企画・設計・制作を担当しました。(クレジットは こちら からご確認ください)

本記事では、自分がテクニカルディレクションおよび実装を担当した、脳の変化を疑似体験できるコンテンツ「笑って怒ってハイチーズ!」の開発で使用した技術について解説します。(2 本立ての前編です。後編はこちら

どんなコンテンツ?

「笑って怒ってハイチーズ!」(通称わらおこ)は、 「年をとると、相手の怒りや悲しみなどの表情が読み取りにくくなる」 ことの擬似体験ができるコンテンツです。
加齢によって表情の見え方が実際に変わる訳ではなく、表情から感情を認識する能力が低下すると言われています。でも、笑顔の表情は、年をとっても変わらず認識しやすいのだそうです。


あまり科学館っぽくないインパクトのある外観です

体験者はプリントシール機型の筐体に入ると、笑顔と怒った顔の 2 つの表情を撮影します。笑顔では「ニッコリ笑って!」、怒り顔では「歯を食いしばって」「カメラをにらんで!」などのガイドとともに、撮影をしていきます。

撮影が完了すると、画面上で、怒った表情があまり怒っていない表情へと変化していきます。


こんなに怒っていても、こんなふうに伝わるかも? でも、喜びの表情は伝わりやすい!

撮影した写真は、上記のようなデザインのシートに印刷され、持ち帰ることができます。

X(旧 Twitter)上で、未来館さんが簡単な紹介をしてくれていました。

参考論文

このコンテンツの企画・開発にあたり、参考にした論文のうちの一つ[1]から、一部を引用します。

本研究では,識別閾を心理物理学的に測定する方法で,表情認識における加齢効果を検討した。その結果,(中略),悲しみ,驚き,怒り,嫌悪,恐れの 5 種の基本表情において加齢にともなう有意な感受性の低下が認められた。それに対し,喜び表情の認識に関しては有意な加齢の影響は認められなかった。

とのことで、非常に興味深いですよね!

「老いパーク」の監修の先生や、この論文の著者の研究者などとも相談しながら、どの程度の表現であれば問題ないかを探っていきました。

表情変化技術

このコンテンツのキモとなるのが、「怒った表情を、あまり怒っていない表情に変化させる」技術です。全くの無表情ではなく、あまり怒っていない表情というのがポイントです。

これを実現するためには、パッと思いつくものだと 3 つくらい手法が考えられます。

  1. 怒った顔画像からメッシュを取得し、そのメッシュを変形する
  2. 怒った顔画像と無表情の顔画像を撮影し、2 つの画像をモーフィングして中間の画像を生成する
  3. 怒った顔画像を元に、AI であまり怒っていない顔画像を生成する

手法 1 はこれまでよく使われてきた手法ですが、例えば、開いた口を閉じるような大きな変形を行おうとすると、画像が破綻する場合があります。体験者の中には、少なからず口を開けて撮影する方もいると思うので、この手法だけでは難しそうだと思いました。

手法 2 は最もシンプルな手法ですが、事前に無表情の画像を撮影する必要があり、体験的にはネタバレになってしまいます。(筐体内ではフラッシュを焚いて撮影する必要があり、こっそり隠し撮りするのも困難でした)また、2 回とも同じ位置や顔の向きで撮影してくれる確証がないため、あまり成功しないだろうと考えました。

手法 3 は実装難易度が高く、ロバストかつ精度良く生成できるかが懸念ですが、いまなら GAN ベースでできるかも??と思いました。可能性はありそうです。

結論を言うと、手法 1 と 手法 3 の組み合わせで実現したのですが、今回は 手法 3 をベースに調査・開発していくことにしました。

最終ワークフロー

先に、最終的な画像生成ワークフローを示します。こうしてみると、結構やってることが多い。
(写っているのは、自分と弊社 CD の谷口さんです)

ちなみに、カメラで撮影した画像の解像度は 5472x3648 で、そこから顔の周辺部分を 2048x2048 で切り取り、さらに 1024x1024 にリサイズして、システムに入力しています。(入力画像のサイズが大きすぎると処理に時間がかかるため)

上記の工程を順番に説明していきます。

テキストから表情を変化させる

本プロジェクトが本格的に動き出したのが 2023 年 4 月。
この時点で注目したのは、「プロンプトを入力すると、顔画像をプロンプトに沿った表情に変形できる」 技術です。

いくつかのライブラリを試した結果、顔画像において変形の精度が高く感じられた StyleCLIP を用いることにしました。StyleGAN2 による顔画像生成がベースになっており、さらに CLIP を用いて、プロンプトから潜在空間ベクトルを修正することで、変形された顔画像を生成しています。


github より引用

詳細については、下記を参照ください。
http://cedro3.com/ai/styleclip-e/
https://qiita.com/UMAboogie/items/78d94ee8c691c09bf399

論文中でいくつかの手法が提案されていますが、その中でも Global Direction という手法を用いて実装します。サンプルコードを大いに参考にしました。

顔画像とともに neutral, target のプロンプトをそれぞれ入力すると、入力画像を neutral として、target の表情になるように顔画像が生成されます。
また、alpha, beta というパラメータがあり、表情変化の度合いを調整することができます。( alpha は変化量、 beta は変化の閾値を表す)

ColabReplicate でサクッと試すことができるのでおすすめです。


左が入力画像で右が出力画像。怒った顔が笑顔になりました

やってみたところ、ちゃんと変化してて面白いのですが、入力画像を潜在空間に inversion したときに、結構な確率で別人の顔になってしまう問題 が見受けられました。輪郭やパーツ配置は合っているのですが、全体的に誰???となります。

おそらくエンコーダーの精度の問題で、StyleCLIP ではencoder4editingを使っています。ここを例えば、restyle-encoderなどに置き換えられると、精度が良くなりそうな気配がしましたが、仮にここを改善できたとしても、別人感を完全に払拭することはできないと思いました。(StyleCLIP に restyle を組み込んでいるリポジトリは発見しました)


restyle を試してみた画像。左が入力画像で右が inversion した画像。これでも別人感ある

そのため、この状態で表情変化がうまくいったとしても、体験としては微妙になってしまいそうです。

別画像の表情を転写する

上記の別人顔問題を解決するために、「入力画像に、別画像の表情のみを抽出して、転写する」 技術がないか調べました。

社内の別チームが、偶然にも Thin-Plate-Spline-Motion-Model (TPSMM) というライブラリについて調べてくれており、教えていただきました。
入力画像 A (Source) に素材動画 B (Driving) のモーションを転写することができます。つまり表情も転写できそう。


github より引用

詳細については下記を参照ください。
http://cedro3.com/ai/tpsmm/
https://www.12-technology.com/2022/05/thin-plate-spline-motion-model.html

これも、サンプルコード を参考にして動かしてみます。(Replicateもあります)


左: Source 画像、中: Driving 素材、右: 出力画像

「いー」と開いていた口が、自然に閉じられることが確認できました。

いくつか試してみたところ、入力画像 A(Source) と素材動画 B(Driving) とで、顔の向きや各パーツの位置がなるべく一致していた方が、より精度良く転写できることがわかりました。

はじめは、

  • 事前に動画素材を何パターンか作っておいて
  • 入力画像から最適な素材を選び、当てはめる

といった工程を考えていましたが、体験者が自由な顔の向きで撮影できる都合上、その手法では転写時に違和感が残ってしまう可能性が懸念されました。

組み合わせる

ということで、これら 2 つを組み合わせてみます。

TPSMM の Source 画像を入力画像、Driving 素材を「StyleCLIP でプロンプトから生成した画像」として、Source 画像に Driving 素材の表情を転写します。

Driving 動画の 1 フレームごとに転写の工程が発生するため、生成時間短縮&リソース節約のために、Driving 素材を動画ではなく 2 枚の画像から設定できるようにしました。(1 枚目: 入力画像を潜在空間に inversion した画像、2 枚目: プロンプトで表情を変形した画像)


左: Source 画像、中: Driving 素材、右: 出力画像

ぱっと見、いい感じな気がします。

懸念点は、転写後の画像サイズが 256x256 と小さくなってしまうことです。そのため、後ほど高解像度化の手順を行います。

目元を変形する

これで口元はいい感じに変形することができましたが、よく見ると、目元や眉の部分があまりいい感じに変形できていないことがわかります。
プロンプトを調整するなどの試行錯誤を行いましたが、最終的に、この部分は GAN ベースではなく、顔画像のメッシュ変形で対応することにしました。

出力画像から顔のランドマーク座標を抽出し、その座標を基に画像を変形させます。
ランドマーク検出に関しては、SPIGA という高精度でランドマーク数が程よいライブラリを選定しました。メッシュ変形に関しては、PyChubbyというライブラリを参考にしました。

具体的には、下記のような変形を行いました。

  • 眉毛の両端から角度を計算し、平坦になるように回転させる
    • 完全な平坦にはしないで、少し角度を残す
  • 目と眉毛の間が狭い場合は、間隔を少し開ける
  • 口の幅を少し狭くする(目元じゃないけど)


左:メッシュ変形前、右:メッシュ変形後

これで目元に関しても変形することができました。

シワを除去する

全体的な表情がいい感じに変形できましたが、眉間のシワやほうれい線などのシワが残っているため、まだ違和感のある画像になってしまっています。
あまり怒っていない表情は、そこまでシワが発生しないはずなので、シワの除去を試みます。

具体的には、Face Segmentation により顔の皮膚領域を抽出し、そこからエッジ(シワ)を検出して、ぼかす工程を行います。(これだけだと不十分に感じるかもですが、後述する高解像度化の時にいい感じになります。)

心配していたのは、もしメガネ等を身につけていた場合、その部分が余計にエッジ検出されてしまうということでしたが、mediapipe の Multi-class selfie segmentation model が優秀で、顔画像から髪やアクセサリーを除いた肌の領域だけのマスクを得ることができました。
具体的には、下記のカテゴリでマスクが得られるのですが、これの 3 - face-skin を用いました。ありがたい。

0 - background
1 - hair
2 - body-skin
3 - face-skin
4 - clothes
5 - others (accessories)

これで無事にマスク画像が得られましたが、ここで完全にシワを除去してしまうと、今度は若返ったように見えてしまうことがわかりました。
そのような印象は与えたくないため、チームで相談した結果、一部のシワ(目の下など)は残したまま、ほうれい線などの大きなシワを除去するという対策を行いました。

この工程は、弊社エンジニアの大森さんがやってくれました。ありがとうございます。

色味を合わせる

上記の手法で画像合成を行っていたのですが、よく見ると、変形後の画像の色味が撮影画像と少し異なっていることがわかりました。


これは別画像ですが、合成したら顔周辺の領域だけ色が微妙に違った

どうやら、生成した画像の色味が入力画像と少し異なっており、表情転写を行う工程で、出力画像の色味が全体的に少し変わってしまったようです。(特に白い服を着ていると違いが顕著に現れました)

この問題を回避するために、合成前にヒストグラムマッチングを用いて、入力画像の色味に合わせる処理を行いました。
Source をシワ除去後の画像、Reference を撮影画像とします。

これにより、顔画像を合成した時の違和感を軽減できます。(白い服でも、明らかに合成したな〜感が出なくなります)

画像を合成する(四辺を inpaiting する)

やっと合成の段階です。
実は、目元のメッシュ変化をさせたときに、周辺のピクセルも合わせて動くため、顔画像の四辺のピクセルが一部欠損してしまう場合があることがわかりました。(黒くなってしまう)


そのまま合成すると、こうなる

これだと、明らかに合成した感が出てしまうため、欠損したピクセルを埋めることを考えました。
アルファブレンディングなども検討しましたが、仕上がりが微妙だったため、ここも AI ベースでやってみようということで、 inpainting を試してみます。

画像合成後、顔部分の四辺が含まれるような矩形(辺のみ)を作り、マスク画像として指定します。


左: 入力画像、中: マスク画像、右: 出力画像

こんな感じで、いい感じに欠損を埋めてくれました。

ここも、懸念としては、今回使用したモデルだと画像サイズが 512x512 にダウンしてしまう点があります。

高解像度化する

先述しましたが、これまでの処理で画像サイズが小さくなってしまっているので、ここで高解像度化を行います。

これも調べるといくつかライブラリがありますが、特に顔画像の restration に特化している GFPGAN の v1.4 モデルを用いました。

512x512 の画像を 1024x1024 にアップコンバートします。

GFPGAN を使うと、高解像度化以外に、自動で美肌化のような効果も少し付与されてしまうのですが、今回に関しては、それが逆に良い方向になりました。先述していたシワ除去の処理がやや不完全な状態だったものの、綺麗に restoration してくれました。ありがたい。

「盛り」のエフェクトを加える

これで一連の表情変化の処理が完了しましたが、最後に、プリ機ならではの「盛り」のエフェクトを加えます。

先述したメッシュ変形を用いて、下記の変形を行います。

  • 目を全体的に少し大きくする
  • 顔の下半分の輪郭を少し小さくする

また、全体的に明るくしたいので、下記の画像処理を行います。
(ここは実際の現場で調整した結果、かなり控えめな処理になりました)

  • ソフトフォーカス処理
  • 明度・彩度調整

完成!

上記以外にも工夫した点がちょこちょこありましたが、ついに完成です。長かった…

多くの工程がありましたが、これらのワークフローは設計段階の時から考えられていたものではありません。とにかく試行錯誤を繰り返しながら、少しでもクオリティを高めようとした結果、最終的にこのような形になりました。(特に初期段階のリサーチ部分は、弊社 TD の貴田さんに助力いただきました。ありがとうございます)

ちなみに、処理時間と精度の都合上、本番機では一度に 3 人までの体験となっております。ご了承ください。

通信

後編でも少し触れますが、この表情変化処理を担うアプリは、UI 表示アプリとは切り離されていて、別 PC で動いています。LAN で接続されていて、HTTP 通信で画像をやり取りしています。
表情変化側は、 Flask を使ってサーバを立てています。(いつの間にか v:3.0.x になっている…)

ちなみに、今回の表情変化アプリは、WSL2 で開発を行いました。
理由は、開発初期に WSL2 を使っていてそのまま進んでしまったことと、他のコンテンツがほとんど Windows で開発されており、PC の共通設定が簡単にできるということがありました。

WSL2 との通信に関して、同一マシン内であれば、 127.0.0.1 でそのまま通信を行うことができますが、外部の PC と HTTP 通信する時には、少し工夫が必要となります。

具体的には、netsh コマンドを用いて port fowarding を行い、Windows の IP アドレスの特定ポートから、 WSL の IP アドレスの特定ポートに転送を行いました。
これにより、外部 PC(UI 表示 PC)からは Windows PC(表情変化 PC)の IP アドレスとポートを指定するだけで、内部の WSL2 と通信が可能になります。

詳細については下記を参照ください。
https://arubeh.com/archives/1749

工夫したこととしては、WSL2 は起動時に毎回 IP アドレスが変わるため、port fowarding の設定を行うバッチファイルを作成し、PC 起動時にタスクスケジューラから自動で実行するように設定しました。(実行に管理者権限が必要なため、タスクスケジューラを用いています)

終わりに

ここまで読んでいただきありがとうございました。
後編はプリ機のデバイス制御まわりについて解説しています。
ぜひご覧ください!!


ちなみに、弊社 Whatever Co. では「さまざまなテクノロジー領域を越境し、なんでもつくる」エンジニアを募集しています。
もし興味があれば、下記ページをご覧ください & お気軽にご連絡ください!
https://whatever.co/ja/careers/tokyo/engineer/

脚注
  1. 熊田真宙, 吉田弘司, 橋本優花里, 澤田梢, 丸石正治, & 宮谷真人. (2011). 表情認識における加齢の影響について── 表情識別閾の測定による検討──. 心理学研究, 82(1), 56-62. ↩︎

Discussion