Python3, OpenCV, Dlibを使って世界中の人をMr.ビーンにしてみたかった話

こんにちは！OthloTechの[ぽこひで](https://twitter.com/hyde141421356)です。
[OthloTech](http://www.othlo.tech)とは名古屋を中心に活動するIT系学生のコミュニティーです。

今回はPython3 & 機械学習 & 顔検出とかもろもろ初心者が「世界中の人をMr.ビーンにしてみたかった話」をします。

## `TL;DR;`
- 世界中の人をMr.ビーンにしたかった。
- 与えられた画像に対して、特徴点を抽出して近いビーンを選択して顔をスワッピングする

今回用いたコードは以下のリンクに上げてあります。

https://github.com/hyde2able/wanna_be_bean

## はじめに

ことの発端はこのツイートでした。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">どうしよう、ハマったwww <a href="https://t.co/GGKJcMhaU0">pic.twitter.com/GGKJcMhaU0</a></p>&mdash; ウィル (@Wordsworth923) <a href="https://twitter.com/Wordsworth923/status/912828390662561792?ref_src=twsrc%5Etfw">September 26, 2017</a></blockquote>


このせいで「色んな人をMr.ビーンにしたい」欲求に襲われたので、実装することにしました。Python3初心者、OpenCVで顔検出もしたことすらなかったので、初心者向けの記事かもしれません。

## 成果

今回作成したスクリプトを元に、自動で合成した結果例です。

※ Mr.ビーンを合成しているのですが、たまにジム・キャリーになります。

![007_spectre.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/0c2a41db-6f1b-6099-e893-ac050d05152b.jpeg)


![x-men2.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/6164693a-8d7f-3055-e690-38cf92ed9f15.jpeg)

![koi.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/76f48c47-590b-b38c-7488-5da636031d8b.jpeg)

![kikuti.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/2188e03a-cfc6-5cb1-f362-d4729962094c.jpeg)

![kanna.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/0a48d523-5928-0766-45f5-47a76969f735.jpeg)

最後の画像に関しては多方面から怒られそうですね。

## Mr.ビーン化プロジェクト

顔の交換を色々調べった結果、「`face swapping`」、「`face morphing`」がワードが出てきましたが、今回はその中で`face swap`のみの実装にしました。

1. 入力画像からの顔(特徴点)検出
2. 似ているMr.ビーンを探す
3. Mr.ビーンを合成するために角度やサイズを調整する
4. Mr.ビーンを合成先画像のカラーに調整する
5. Mr.ビーンを合成する

### 1.) 入力画像からの顔(特徴点)検出

はじめは「顔検出といえばOpenCV」というイメージだったので、とりあえず触ってみたのですが、真正面を向いている顔でないと検出精度がかなり低かったので、機械学習のC++、Pythonライブラリ「[dlib](https://github.com/davisking/dlib)」を使うことにしました。

しかし、「OpenCV」の方が画像が扱いやすく「dlib」とも互換性があるようだったので、併用しています。ここで使用する、「OpenCV2」, 「dlib」のインストール方法は以下を参考にしてください。

https://www.learnopencv.com/install-dlib-on-macos/

また、dlibの顔検出に使う「`shape_predictor_68_face_landmarks.dat`」というファイルは大きいのでGit管理はしていません。もし実際にスクリプトを動かしたい場合は、以下のリンクよりダウンロードしてレポジトリ配下においてください。

http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2

```python3
import opencv2
import dlib

detector = dlib.get_frontal_face_detector()
image = cv2.imread(image_path, cv2.IMREAD_COLOR)

rects = detector(image, 1)

# rectsの数だけ顔を検出

PREDICTOR_PATH = './shape_predictor_68_face_landmarks.dat'
predictor = dlib.shape_predictor(PREDICTOR_PATH)

for rect in rects:
  landmarks = numpy.matrix(
    [[p.x, p.y] for p in PREDICTOR(image, rect).parts()]
  )
```

こんな感じで画像パスから顔領域を検出して、特徴点を抽出できます。初心者で知らなかったのですがこの特徴点(landmarks)は長さ68の1次元配列で

- `[ 0:17]`: アゴ
- `[17:22]`: 右の眉
- `[22:27]`: 左の眉
- `[27:35]`: 鼻
- `[36:42]`: 右目
- `[42:48]`: 左目
- `[48:61]`: 口

といった感じで表す部位が決められているようです。

![landmarks.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/3bc98ba4-bccc-042d-e67d-1bb431dcf579.jpeg)

### 2.) 似ているMr.ビーンを探す

手順1で抽出した特徴点とMr.ビーンの特徴点の差分のノルムを計算して、一番小さいMr.ビーンを選択しています。スクリプト実行時に用意しておいたMr.ビーンをロードして、特徴点を検出しておきます。

```python3
class Face:
  def __init__(self, image, rect):
    self.image = image
    self.landmarks = numpy.matrix(
      [[p.x, p.y] for p in PREDICTOR(image, rect).parts()]
    )

class BeBean:
  ...

  def load_faces_from_image(self, image_path):
    image = cv2.imread(image_path, cv2.IMREAD_COLOR)
    image = cv2.resize(image, (image.shape[1] * self.SCALE_FACTOR,
                               image.shape[0] * self.SCALE_FACTOR))

    rects = self.detector(image, 1)

    if len(rects) == 0:
      raise NoFaces
    else:
      print("Number of faces detected: {}".format(len(rects)))

    faces = [Face(image, rect) for rect in rects]
    return image, faces
  
  ...
  
  def _load_beans(self):
    self.beans = []
    for image_path in glob.glob(os.path.join('beans', '*.jpg')):
      image, bean_face = self.load_faces_from_image(image_path)
      self.beans.append(bean_face[0])

  ...
  def _get_bean_similar_to(self, face):
    "特徴点の差分距離が小さいMr.ビーンを返す"

    get_distances = numpy.vectorize(lambda bean: numpy.linalg.norm(face.landmarks - bean.landmarks))

    distances = get_distances(self.beans)
    return self.beans[distances.argmin()]
```

こんな感じで特徴点の差分距離が小さいMr.ビーンのFaceクラスを返します。

余談ですが、今回作った[GitHub上のレポジトリ](https://github.com/hyde2able/wanna_be_bean)に予め用意してある合成元画像は大半はMr.ビーンですが、ジム・キャリーも入っているのでたまに、ジム・キャリーになります。

### 3.) Mr.ビーンを合成するために角度やサイズを調整する

これ以降の手順は主に[Switching Eds: Face swapping with Python, dlib, and OpenCV](https://matthewearl.github.io/2015/07/28/switching-eds-with-python)という記事を参考にしています。

ここまでに入力画像の特徴点と似ているMr.ビーンの特徴点は揃っていますが、顔のサイズや角度は揃っていません。そのため、顔の角度やサイズを調整します。

ここでは顔の角度やサイズを調整すると言っていますが、数学的には各特徴点ごとの差分を小さくする問題に置き換えられるので、2つの`[x座標, y座標]`を持つ特徴点配列を最小化します。


```math

\sum_{i=1}^{68}
\begin{vmatrix}
\begin{vmatrix} 
sRp_i^T + T - q_i^T
\end{vmatrix}
\end{vmatrix} ^2
```


この数式を最小化するように合成元(Mr.ビーン)の特徴点を調整すればいい。ここで、Rは直行2x2行列で、sはスカラー、2ベクトル、p_i, q_iは入力画像とMr.ビーンの特徴点行列の行です。

上記の記事で、この数式を最小化しているのですが、ここの計算内容は正直よく分かっていません...。恐らくここが画像処理の醍醐味な気もしますが、初学者には厳しかったので、そのままお借りしました。

```python3
  def transformation_from_points(self, t_points, o_points):
    """
      特徴点から回転やスケールを調整する。
      t_points: (target points) 対象の特徴点(入力画像)
      o_points: (origin points) 合成元の特徴点(つまりビーン)
    """

    t_points = t_points.astype(numpy.float64)
    o_points = o_points.astype(numpy.float64)

    t_mean = numpy.mean(t_points, axis = 0)
    o_mean = numpy.mean(o_points, axis = 0)

    t_points -= t_mean
    o_points -= o_mean

    t_std = numpy.std(t_points)
    o_std = numpy.std(o_points)

    t_points -= t_std
    o_points -= o_std

    # https://qiita.com/kyoro1/items/4df11e933e737703d549
    U, S, Vt = numpy.linalg.svd(t_points.T * o_points)
    R = (U * Vt).T

    return numpy.vstack(
      [numpy.hstack((( o_std / t_std ) * R, o_mean.T - ( o_std / t_std ) * R * t_mean.T )),
      numpy.matrix([ 0., 0., 1. ])]
    )
```

内容的には

1. 入力した行列(特徴点)を浮動小数点に変換。
2. それぞれの行列から重心を計算して、引きます。後のこの重心を利用することで解を求めます。
3. 次に、行列の各要素を標準偏差で割ることで、問題のスケール要素を削除します。
4. 特異値分解(SVD)を使用して回転部分を計算します。SVDがどのように機能するかは　[ここ](https://ja.wikipedia.org/wiki/%E7%89%B9%E7%95%B0%E5%80%A4%E5%88%86%E8%A7%A3)を参照してください。
5. [アフィン変換](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%95%E3%82%A3%E3%83%B3%E5%86%99%E5%83%8F)行列として完全な変換を返します。

![animated_face.gif](https://qiita-image-store.s3.amazonaws.com/0/85665/2fe9a54a-1078-69c1-4b99-4c6ccb60ca64.gif)

### 4.) Mr.ビーンを合成先画像のカラーに調整する

![hoge.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/aae07a40-7b4f-ea2d-e25f-f5a5574d6023.jpeg)

これで合成する画像のサイズや角度を調整できましたが、このまま合成しても、肌の色合いが違うので上手く境界を調整する必要があります。合成する領域の境界面での色の不連続を修正します。

```python3


  def correct_colors(self, t_image, o_image, t_landmarks):
    """
      対象の画像に合わせて、色を補正する
    """
    blur_amount = self.COLOR_CORRECT_BLUR_FRAC * numpy.linalg.norm(
      numpy.mean(t_landmarks[self.LEFT_EYE_POINTS], axis = 0) -
      numpy.mean(t_landmarks[self.RIGHT_EYE_POINTS], axis = 0)
    )
    blur_amount = int(blur_amount)

    if blur_amount % 2 == 0: blur_amount += 1

    t_blur = cv2.GaussianBlur(t_image, (blur_amount, blur_amount), 0)
    o_blur = cv2.GaussianBlur(o_image, (blur_amount, blur_amount), 0)

    # ゼロ除算を避ける　
    o_blur += (128 * (o_blur <= 1.0)).astype(o_blur.dtype)

    return (o_image.astype(numpy.float64) * t_blur.astype(numpy.float64) / o_blur.astype(numpy.float64))
```

この関数では、`t_image`の色と一致するように`o_image`の色合いを変更します。`o_image`を自信のガウスぼかしで割った後に、`t_image`のガウスぼかしで乗算することで行っています。

![hoge.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/40f8df58-0d0e-b670-7026-e033659223f9.jpeg)

### 5.) Mr.ビーンを合成する

最後に、Mr.ビーンのどの部分を合成するかを選択して、マスクとして切り出します。

```python3
  def get_face_mask(self, face):
    image = numpy.zeros(face.image.shape[:2], dtype = numpy.float64)
    for group in self.OVERLAY_POINTS:
      self._draw_convex_hull(image, face.landmarks[group], color = 1)

    image = numpy.array([ image, image, image ]).transpose((1, 2, 0))
    image = (cv2.GaussianBlur(image, (self.FEATHER_AMOUNT, self.FEATHER_AMOUNT), 0) > 0) * 1.0
    image = cv2.GaussianBlur(image, (self.FEATHER_AMOUNT, self.FEATHER_AMOUNT), 0)

    return image

  def _draw_convex_hull(self, image, points, color):
    "指定したイメージの領域を塗りつぶす"

    points = cv2.convexHull(points)
    cv2.fillConvexPoly(image, points, color = color)

```

`get_face_mask()`関数では、画像と特徴点行列のマスクを生成します。白色で2つの凸多角形を描画します。1爪は、目の領域を取り囲んで、もう1つは鼻と口の領域を囲んでいます。その後、マスクのエッジ部分を外側に`FEATHER_AMOUNT = 11`ピクセル分ぼかしています。このフェザリング処理は、合成時の色合いの不連続性を隠すのに役立ちます。しかし、このピクセル数は固定値よりは合成先の顔の大きさに合わせて調整したほうがいいと思うので、改良の余地があります。

2つのマスクを要素ごとの最大値を取ることで、1つに結合します。マスクを結合することで、合成部分を確定させ、入力画像にマスクを適用します。これを見つけた顔の数だけ繰り返すことで、顔をビーンにします。

```python3
base_image = base_image * (1.0 - combined_mask) + warped_corrected_image * combined_mask
```

合成の割合は`COLOR_CORRECT_BLUR_FRAC = 0.7`という値で制御しています。ここはいい感じに調整してください。

![kikuti2.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/ecc760c1-cea9-d975-ab66-04d35b52c274.jpeg)


## 所感

今回は最終的に目標としていた入力画像をMr.ビーンにすることにある程度は成功したと思っています。ただ、課題はまだあって、Mr.ビーンの画像をGoogle画像検索で調べても大半が正面向きであるので、横向きの画像に対してはdlibで顔の特徴点の検出はできて、似ているMr.ビーンを選択できても、正面の画像を無理矢理圧縮したように合成されてしまうことが多々ありました。そのため、合成元の顔を360度カメラで撮っておけばいいのかなーとか思ったりしました。

色々文献を流し見していくなかで「[OpenCVとdlibを用いた向き検出](http://www.learnopencv.com/head-pose-estimation-using-opencv-and-dlib/)」という記事があったり、3次元の向きまで考慮すればより違和感なく合成できるのかなと。また、色合いの補正も改善の余地があるなと感じた。

それと個人的にMr.ビーンより不意に現れるジム・キャリーの方が面白かったです。

![koi.jpg](https://qiita-image-store.s3.amazonaws.com/0/85665/76f48c47-590b-b38c-7488-5da636031d8b.jpeg)

今回用いたコードは以下のリンクに上げてあります。

https://github.com/hyde2able/wanna_be_bean

## 参考資料
- [Switching Eds: Face swapping with Python, dlib, and OpenCV](https://matthewearl.github.io/2015/07/28/switching-eds-with-python/)
- [Face-Swap](https://github.com/mrgloom/Face-Swap)