MakeItTalkで音声と１枚の顔画像から話すアニメーションを作成する

# MakeItTalkとは

[MakeItTalk](https://github.com/yzhou359/MakeItTalk)とは、端的に言うと顔画像と音声ファイルを用意するだけで、顔画像が音声の内容を話しているようにアニメーションさせるツールです。
![スクリーンショット 2022-01-13 11.40.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/0594b93e-d2c9-79c1-8523-dff3c06f86f5.png)

MakeItTalkはSIGGRAPH Asia 2020で発表されました。
[GitHub](https://github.com/yzhou359/MakeItTalk)・[arXiv](https://arxiv.org/abs/2004.12992)・[YouTube](https://www.youtube.com/watch?v=OU6Ctzhpc6s)

大まかに分けると以下の2つの機能から成り立っています。
(1) 音声から顔ランドマークおよび話者の頭部動きの予測
(2) 顔画像に(1)の表情・動きを適用したビデオの生成

この記事では、

- MakeItTalkのデモを動かす
- 独自の顔画像と音声を使ってアニメーションを作る

上記2点について説明いたします。

## 1. 前準備

### 1.1 用意するもの
- 256x256 の JPG 顔画像
- wav 形式の音声ファイル
- Googleアカウント


### 1.2 Google Colaboratory を使用する
![スクリーンショット 2022-01-26 9.39.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/f01d08a4-6531-3581-c69b-e98c155762ce.png)

Google Colaboratory（以下、Colab）は、Googleが提供している機械学習の学習環境です。Googleアカウントがあれば無料ですぐに始められます。
Colabでは、ノートブック（Webブラウザ上で記述・実行できるコード）を共有することができます。

Colabを使用する準備ができたら、まずは、MakeItTalkのデモを動かしてみましょう。

##  2. MakeItTalkのデモを動かす

[quick_demo_tdlr.ipynb](https://colab.research.google.com/github/yzhou359/MakeItTalk/blob/main/quick_demo_tdlr.ipynb)

用意されているデモ用ノートブックです。
GPUをオンにするため、「ランタイム > ランタイムのタイプを変更 > ハードウェアアクセラレータ > GPUを選択 > 保存」をしてください。
完了したら「ランタイム > 全てのセルを実行」を押して、プログラムを実行します。
全てのセルが実行されると、一番下に作成されたアニメーションの動画が表示されます。
![ダウンロード-_3_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/19d7388c-aa23-839d-c69c-067a0fd5a480.gif)


- 前準備
- 顔画像を選択する
- アニメーション設定
- 実行する
- アニメーションを再生する

各ステップを上から順番に詳しくみていきましょう。
（英語で書かれていた部分を日本語化しています。）

### 2.1 前準備
```bash
# リポジトリのコピー
!git clone https://github.com/yzhou359/MakeItTalk &> /dev/null
%cd MakeItTalk/

# 依存関係をインストール
!export PYTHONPATH=/content/MakeItTalk:$PYTHONPATH
!pip install -r requirements.txt &> /dev/null
!pip install tensorboardX &> /dev/null

# 事前に学習されたモデルをダウンロード
!mkdir examples/dump
!mkdir examples/ckpt
!pip install gdown &> /dev/null
```

Colabではコマンドを!の後に書くことで実行できます。
gdownはGoogle Driveの大きなファイルをダウンロードするためのコマンドです。
```bash
!gdown -O examples/ckpt/ckpt_autovc.pth https://drive.google.com/uc?id=1ZiwPp_h62LtjU0DwpelLUoodKPR85K7x
!gdown -O examples/ckpt/ckpt_content_branch.pth https://drive.google.com/uc?id=1r3bfEvTVl6pCNw5xwUhEglwDHjWtAqQp
!gdown -O examples/ckpt/ckpt_speaker_branch.pth https://drive.google.com/uc?id=1rV0jkyDqPW-aDJcj7xSO6Zt1zSXqn1mu
!gdown -O examples/ckpt/ckpt_116_i2i_comb.pth https://drive.google.com/uc?id=1i2LJXKp-yWKIEEgJ7C6cE3_2NirfY_0a
!gdown -O examples/dump/emb.pickle https://drive.google.com/uc?id=18-0CYl5E6ungS3H4rRSHjfYvvm-WwjTI
```

いくつかの学習済みモデルをダウンロードしています。
うまくいかない時があるので、`examples/ckpt`と`examples/dump`にファイルがきちんとダウンロードできたか確認してください。

ダウンロードできていない時には、このようなエラーがでます
```bash
FileNotFoundError: [Errno 2] No such file or directory: 'examples/ckpt/ckpt_autovc.pth'
```

### 2.2 顔画像を選択する
アニメーションさせる顔画像を選択します。

```python
import ipywidgets as widgets
import glob
import matplotlib.pyplot as plt

# プルダウンに表示するディレクトリを指定する ('examples/'フォルダにあるものを表示)
img_list = glob.glob1('examples', '*.jpg')
img_list.sort()
img_list = [item.split('.')[0] for item in img_list]
default_head_name = widgets.Dropdown(options=img_list, value='paint_boy')
def on_change(change):
    if change['type'] == 'change' and change['name'] == 'value':
        plt.imshow(plt.imread('examples/{}.jpg'.format(default_head_name.value)))
        plt.axis('off')
        plt.show()

# プルダウンの画像選択を検知するように設定
default_head_name.observe(on_change)
display(default_head_name)

# 選択した画像が描画されるようにする
plt.imshow(plt.imread('examples/{}.jpg'.format(default_head_name.value)))
plt.axis('off')
plt.show()
```


### 2.3 アニメーションの設定
スライドを動かしてアニメーションの動きを調節できます。
最初は唇と頭の動きの大きさを大きくしていた方がわかりやすいです。
唇の厚みや横幅はデフォルト値でOKです。

```python
#@markdown # アニメーションの制御
#@markdown 水平方向の唇の動きの大きさ
AMP_LIP_SHAPE_X = 2.5 #@param {type:"slider", min:0.5, max:5.0, step:0.1}

#@markdown 垂直方向の唇の動きの大きさ
AMP_LIP_SHAPE_Y = 2.6 #@param {type:"slider", min:0.5, max:5.0, step:0.1}

#@markdown 頭の動きの大きさ（0なら頭は固定される）
AMP_HEAD_POSE_MOTION = 0.6 #@param {type:"slider", min:0.0, max:1.0, step:0.05}

#@markdown Trueで「まばたき」をする
ADD_NAIVE_EYE = True  #@param ["False", "True"] {type:"raw"}

#@markdown 入力画像の「口が開いている」場合、Trueにする
CLOSE_INPUT_FACE_MOUTH = True  #@param ["False", "True"] {type:"raw"}          


#@markdown # 顔ランドマークの調整

#@markdown 上唇の厚み
UPPER_LIP_ADJUST = 0 #@param {type:"slider", min:-3.0, max:3.0, step:1.0}

#@markdown 下唇の厚み
LOWER_LIP_ADJUST = 0 #@param {type:"slider", min:-3.0, max:3.0, step:1.0}

#@markdown 唇の横幅の大きさ
LIP_WIDTH_ADJUST = 1 #@param {type:"slider", min:0.8, max:1.2, step:0.01}
```

### 2.4 実行
ライブラリのインポートと引数の初期化をします。

```python
import sys
sys.path.append("thirdparty/AdaptiveWingLoss")
import os, glob
import numpy as np
import cv2
import argparse
from src.approaches.train_image_translation import Image_translation_block
import torch
import pickle
import face_alignment
from src.autovc.AutoVC_mel_Convertor_retrain_version import AutoVC_mel_Convertor
import shutil
import time
import util.utils as util
from scipy.signal import savgol_filter
from src.approaches.train_audio2landmark import Audio2landmark_model

#sys.stdout = open(os.devnull, 'a')

parser = argparse.ArgumentParser()
parser.add_argument('--jpg', type=str, default='{}.jpg'.format(default_head_name.value))
parser.add_argument('--close_input_face_mouth', default=CLOSE_INPUT_FACE_MOUTH, action='store_true')
parser.add_argument('--load_AUTOVC_name', type=str, default='examples/ckpt/ckpt_autovc.pth')
parser.add_argument('--load_a2l_G_name', type=str, default='examples/ckpt/ckpt_speaker_branch.pth')
parser.add_argument('--load_a2l_C_name', type=str, default='examples/ckpt/ckpt_content_branch.pth') #ckpt_audio2landmark_c.pth')
parser.add_argument('--load_G_name', type=str, default='examples/ckpt/ckpt_116_i2i_comb.pth') #ckpt_image2image.pth') #ckpt_i2i_finetune_150.pth') #c
parser.add_argument('--amp_lip_x', type=float, default=AMP_LIP_SHAPE_X)
parser.add_argument('--amp_lip_y', type=float, default=AMP_LIP_SHAPE_Y)
parser.add_argument('--amp_pos', type=float, default=AMP_HEAD_POSE_MOTION)
parser.add_argument('--reuse_train_emb_list', type=str, nargs='+', default=[]) #  ['iWeklsXc0H8']) #['45hn7-LXDX8']) #['E_kmpT-EfOg']) #'iWeklsXc0H8', '29k8RtSUjE0', '45hn7-LXDX8',
parser.add_argument('--add_audio_in', default=False, action='store_true')
parser.add_argument('--comb_fan_awing', default=False, action='store_true')
parser.add_argument('--output_folder', type=str, default='examples')
parser.add_argument('--test_end2end', default=True, action='store_true')
parser.add_argument('--dump_dir', type=str, default='', help='')
parser.add_argument('--pos_dim', default=7, type=int)
parser.add_argument('--use_prior_net', default=True, action='store_true')
parser.add_argument('--transformer_d_model', default=32, type=int)
parser.add_argument('--transformer_N', default=2, type=int)
parser.add_argument('--transformer_heads', default=2, type=int)
parser.add_argument('--spk_emb_enc_size', default=16, type=int)
parser.add_argument('--init_content_encoder', type=str, default='')
parser.add_argument('--lr', type=float, default=1e-3, help='learning rate')
parser.add_argument('--reg_lr', type=float, default=1e-6, help='weight decay')
parser.add_argument('--write', default=False, action='store_true')
parser.add_argument('--segment_batch_size', type=int, default=1, help='batch size')
parser.add_argument('--emb_coef', default=3.0, type=float)
parser.add_argument('--lambda_laplacian_smooth_loss', default=1.0, type=float)
parser.add_argument('--use_11spk_only', default=False, action='store_true')
parser.add_argument('-f')
opt_parser = parser.parse_args()
```

画像をロードし、顔ランドマークを検出します。

```python
# 顔画像のロード
img =cv2.imread('examples/' + opt_parser.jpg)

# 3D顔ランドマークの検出
predictor = face_alignment.FaceAlignment(face_alignment.LandmarksType._3D, device='cpu', flip_input=True)
shapes = predictor.get_landmarks(img)
if (not shapes or len(shapes) != 1):
    print('Cannot detect face landmarks. Exit.')
    exit(-1)
shape_3d = shapes[0]
if(opt_parser.close_input_face_mouth):
    util.close_input_face_mouth(shape_3d)
shape_3d, scale, shift = util.norm_input_face(shape_3d)
```

`MakeItTalk/examples/*.wav`下にアップロードされた音声ファイルから顔の動きを推論し生成します。

```python
au_data = []
au_emb = []

# examplesフォルダにある全ての.wavファイル名を取得し ains へ
ains = glob.glob1('examples', '*.wav')
ains = [item for item in ains if item is not 'tmp.wav']
ains.sort()

for ain in ains:
    os.system('ffmpeg -y -loglevel error -i examples/{} -ar 16000 examples/tmp.wav'.format(ain))
    shutil.copyfile('examples/tmp.wav', 'examples/{}'.format(ain))

    # 顔のパーツの動きを求める
    from thirdparty.resemblyer_util.speaker_emb import get_spk_emb
    me, ae = get_spk_emb('examples/{}'.format(ain))
    au_emb.append(me.reshape(-1))

    print('Processing audio file', ain)

    # AUTOVCを使った声質変換
    c = AutoVC_mel_Convertor('examples')

    au_data_i = c.convert_single_wav_to_autovc_input(audio_filename=os.path.join('examples', ain),
           autovc_model_path=opt_parser.load_AUTOVC_name)
    au_data += au_data_i
if(os.path.isfile('examples/tmp.wav')):
    os.remove('examples/tmp.wav')
```


```python
# ランドマークの仮置き
fl_data = []
rot_tran, rot_quat, anchor_t_shape = [], [], []
for au, info in au_data:
    au_length = au.shape[0]
    fl = np.zeros(shape=(au_length, 68 * 3))
    fl_data.append((fl, info))
    rot_tran.append(np.zeros(shape=(au_length, 3, 4)))
    rot_quat.append(np.zeros(shape=(au_length, 4)))
    anchor_t_shape.append(np.zeros(shape=(au_length, 68 * 3)))

# ファイルが存在していたらクリアする
if(os.path.exists(os.path.join('examples', 'dump', 'random_val_fl.pickle'))):
    os.remove(os.path.join('examples', 'dump', 'random_val_fl.pickle'))
if(os.path.exists(os.path.join('examples', 'dump', 'random_val_fl_interp.pickle'))):
    os.remove(os.path.join('examples', 'dump', 'random_val_fl_interp.pickle'))
if(os.path.exists(os.path.join('examples', 'dump', 'random_val_au.pickle'))):
    os.remove(os.path.join('examples', 'dump', 'random_val_au.pickle'))
if (os.path.exists(os.path.join('examples', 'dump', 'random_val_gaze.pickle'))):
    os.remove(os.path.join('examples', 'dump', 'random_val_gaze.pickle'))

# pickleモジュールを使ったオブジェクトの保存
with open(os.path.join('examples', 'dump', 'random_val_fl.pickle'), 'wb') as fp:
    pickle.dump(fl_data, fp)
with open(os.path.join('examples', 'dump', 'random_val_au.pickle'), 'wb') as fp:
    pickle.dump(au_data, fp)
with open(os.path.join('examples', 'dump', 'random_val_gaze.pickle'), 'wb') as fp:
    gaze = {'rot_trans':rot_tran, 'rot_quat':rot_quat, 'anchor_t_shape':anchor_t_shape}
    pickle.dump(gaze, fp)

# 音声と顔ランドマークから顔ランドマークのアニメーションを作成する
model = Audio2landmark_model(opt_parser, jpg_shape=shape_3d)
if(len(opt_parser.reuse_train_emb_list) == 0):
    model.test(au_emb=au_emb)
else:
    model.test(au_emb=None)

```

```python
fls = glob.glob1('examples', 'pred_fls_*.txt')
fls.sort()

for i in range(0,len(fls)):
    fl = np.loadtxt(os.path.join('examples', fls[i])).reshape((-1, 68,3))
    fl[:, :, 0:2] = -fl[:, :, 0:2]
    fl[:, :, 0:2] = fl[:, :, 0:2] / scale - shift

    if (ADD_NAIVE_EYE):
        fl = util.add_naive_eye(fl)

    # 顔の動きの平滑化
    fl = fl.reshape((-1, 204))
    fl[:, :48 * 3] = savgol_filter(fl[:, :48 * 3], 15, 3, axis=0)
    fl[:, 48*3:] = savgol_filter(fl[:, 48*3:], 5, 3, axis=0)
    fl = fl.reshape((-1, 68, 3))

    ''' 顔ランドマークのアニメーションに合わせて顔画像をアニメーションさせる '''
    model = Image_translation_block(opt_parser, single_test=True)
    with torch.no_grad():
        model.single_test(jpg=img, fls=fl, filename=fls[i], prefix=opt_parser.jpg.split('.')[0])
        print('finish image2image gen')
    os.remove(os.path.join('examples', fls[i]))
```

### 2.5 アニメーションの再生
`examples/`に生成されたアニメーション動画を再生します。

```python
from IPython.display import HTML
from base64 import b64encode

# アニメーションを再生する
for ain in ains:
  OUTPUT_MP4_NAME = '{}_pred_fls_{}_audio_embed.mp4'.format(
    opt_parser.jpg.split('.')[0],
    ain.split('.')[0]
    )
  mp4 = open('examples/{}'.format(OUTPUT_MP4_NAME),'rb').read()
  data_url = "data:video/mp4;base64," + b64encode(mp4).decode()

  print('Display animation: examples/{}'.format(OUTPUT_MP4_NAME))
  display(HTML("""
  <video width=600 controls>
        <source src="%s" type="video/mp4">
  </video>
  """ % data_url))
```

デモを通じて、大まかな動かし方やプログラムの構造が分かったと思います。
次は、独自の音声ファイルと顔画像を使用してアニメーションを作る方法を説明します。

## 3. 独自の音声ファイルと顔画像からアニメーションを作る

- `MakeItTalk/examples`に顔画像と音声ファイルをアップロードする
- `顔画像を選択する`でアップロードした顔画像を選択する
- 実行する

### 3.1 顔画像と音声ファイルをアップロード
`MakeItTalk/examples`に顔画像(.jpg)と音声ファイル(.wav)をアップロードします。

以下のものを準備してください。

* face.jpg（顔画像、サイズは**256x256**であること）
* audio.wav（音声ファイル）

今回はランダムに顔を生成するサイト(https://thispersondoesnotexist.com) で生成した顔写真を使います。
![face4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/06016d40-1ae7-7d7b-f0fd-612fb13786a1.jpeg)


`examples`フォルダにマウスオーバーし、右に表示される三点マークをクリックし、そこに顔画像と音声ファイルをアップロードします。
![スクリーンショット 2022-03-25 14.41.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/01a67211-0d6b-0c1c-8cb9-00b3fd0004d2.png)

### 3.2 顔画像を選択する
`3.1 顔画像を選択する`でアップロードした顔画像を選択します。
![スクリーンショット 2022-03-25 17.03.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/38915388-2153-a31b-7bdb-acbb2bfbe4fc.png)

### 3.3 実行する
`2.3 アニメーションの設定`以降を全て実行します。
完了すると出力結果に完成した動画ができます。
![スクリーンショット 2022-03-25 17.03.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/64c69b3a-b3e7-11e7-43bc-13102e86e077.png)

注：こちらは画像です。


以上です。お疲れ様でした。