【python】Mediapipeを使って「お化粧」をしてみた

# TL;DR

ノートブックは[こちら](https://github.com/YomamaBanana/Mediapipe-python/blob/main/notebooks/makeup.ipynb)！

# はじめに

この記事は[こちら](https://qiita.com/YomamaBanana/items/978689d6ec6c7e7be5d7)の記事の続きだ！
目的としてはWEB会議やビデオ通話などの場面で顔のフィルターを自前で実装したいである。
前回はMediapipeを用いて簡単に口紅を塗ってみたが、今回は顔全体に対して色を塗ってみよう！


# 手順

今回の「顔フィルターは」は下記の手順で行う。

1. 画像を読み込む
1. 「お化粧」する色の定義
1. 顔認識を行う
1. 顔のパーツを抽出し色をつける
1. 入力画像との融合

# 使用するツール

顔のパーツの色と融合する時の重みをまとめるため、yamlファイルを活用する。

* [pyyaml](https://pyyaml.org/)

# インストール

```shell
$ pip install pyyaml
```

# コード

### ①必要なモジュールをimportする
```python
import cv2
import yaml
import numpy as np
import mediapipe as mp
import matplotlib.pyplot as plt
```

### ②画像データを読み込む

前回のイケメン君(笑)
(https://thispersondoesnotexist.com/)

```python
img_path = "../input/image.jpg"

src = cv2.imread(img_path)
src = cv2.cvtColor(src, cv2.COLOR_BGR2RGB)
img = src.copy()
plt.imshow(img)
```
![before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290515/39f2ebe6-e19b-d8af-5361-5556c41b9c11.png)


### ③パラメータを定義し、読み込む

今回はyaml形式でパーツの色と重みをmake_up.yamlに保存する。

config/make_up.yaml

```yaml
# color: [R, G, B]

face:
  color: [165, 57, 0]
  weight: 0.1

eyes:
  color: [52, 124, 44]
  weight: 0.15

lip:
  color: [142, 30, 29]
  weight: 0.2

eyebrow:
  color: [200, 200, 200]
  weight: 0.08
```

notebook/makeup.ipynb

```python
def read_yaml(path):
　　"""yamlファイルを読み込むための関数
    
　　入力：yamlファイルのパス
　　出力：dict形式のデータ
    """
    with open(path, "r") as f:
        cfg = yaml.safe_load(f)
    return cfg

cfg = read_yaml("../config/make_up.yaml")
```

簡単な色の確認

```python
# パーツの数に合わせてプロットを初期化する
# 偶数と奇数に注意！
fig, axs = plt.subplots(len(cfg.keys())//2, 2)
axs = np.ravel(axs)

# パーツをループし、色を描画する
for idx, (part, param) in enumerate(cfg.items()):
    tmp = []
    for _ in range(100):
        tmp.append(param["color"])
    axs[idx].set_title(part)
    axs[idx].imshow(np.reshape(tmp, (10, 10, 3)))
```

![color.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290515/05c0d616-c3cd-85b6-e72f-37431242330e.png)

### ④Mediapipeを用いて顔認識を行う
```python
# 検知器のインスタンス化
drawing = mp.solutions.drawing_utils
drawing_spec = drawing.DrawingSpec(thickness=1, circle_radius=1, color=(0, 255, 0))

drawing_styles = mp.solutions.drawing_styles

face_mesh = mp.solutions.face_mesh
face_detector = face_mesh.FaceMesh(min_detection_confidence=0.5, min_tracking_confidence=0.5)

# 検知処理
results = face_detector.process(img)

# 可視化
if results.multi_face_landmarks:
    for face_landmarks in results.multi_face_landmarks:
        drawing.draw_landmarks(
            image=img,
            landmark_list=face_landmarks,
            connections=face_mesh.FACEMESH_TESSELATION,
            connection_drawing_spec=drawing_styles.get_default_face_mesh_tesselation_style()
            )
plt.imshow(img)
```

![detection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290515/353f82c2-9ded-44f6-a132-1ec6266bea18.png)

### ⑤パーツを抽出し、マスクを作成する
```python
# マスクの初期化
gray = np.zeros((src.shape[0], src.shape[1], 3), dtype=np.uint8)

# 唇のINDEX
lips = list(face_mesh.FACEMESH_LIPS)
lips = np.ravel(lips)

# 目（左）のINDEX
l_eyes = list(face_mesh.FACEMESH_LEFT_EYE)
l_eyes = np.ravel(l_eyes)

# 目（右）のINDEX
r_eyes = list(face_mesh.FACEMESH_RIGHT_EYE)
r_eyes = np.ravel(r_eyes)

# 眉毛（左）のINDEX
l_eyebrow = list(face_mesh.FACEMESH_LEFT_EYEBROW)
l_eyebrow = np.ravel(l_eyebrow)

# 眉毛（右）のINDEX
r_eyebrow = list(face_mesh.FACEMESH_RIGHT_EYEBROW)
r_eyebrow = np.ravel(r_eyebrow)

# 抽出したパーツをdict形式で保存する
face_dict = {}
if results.multi_face_landmarks:
    for face_landmarks in results.multi_face_landmarks:
        mask_lip = []
        mask_face = []
        mask_l_eyes = []
        mask_r_eyes = []
        mask_l_eyebrow = []
        mask_r_eyebrow = []
        for i in range(face_mesh.FACEMESH_NUM_LANDMARKS):
            
　　　　　　 # 唇
            if i in lips:
                pt1 = face_landmarks.landmark[i]
                x = int(pt1.x * img.shape[1])
                y = int(pt1.y * img.shape[0])
                mask_lip.append((x, y))

            # 目（左）
            elif i in l_eyes:
                pt1 = face_landmarks.landmark[i]
                x = int(pt1.x * img.shape[1])
                y = int(pt1.y * img.shape[0])
                mask_l_eyes.append((x, y))
            
            # 目（右）
            elif i in r_eyes:
                pt1 = face_landmarks.landmark[i]
                x = int(pt1.x * img.shape[1])
                y = int(pt1.y * img.shape[0])
                mask_r_eyes.append((x, y))
            
            # 眉毛（左）　
            elif i in l_eyebrow:
                pt1 = face_landmarks.landmark[i]
                x = int(pt1.x * img.shape[1])
                y = int(pt1.y * img.shape[0])
                mask_l_eyebrow.append((x, y))
            
            # 眉毛（右）
            elif i in r_eyebrow:
                pt1 = face_landmarks.landmark[i]
                x = int(pt1.x * img.shape[1])
                y = int(pt1.y * img.shape[0])
                mask_r_eyebrow.append((x, y))
            
            # それ以外の部分
            else:
                pt1 = face_landmarks.landmark[i]
                x = int(pt1.x * img.shape[1])
                y = int(pt1.y * img.shape[0])
                mask_face.append((x, y))

# 描画のためnumpy配列へ変換する
face_dict["mask_lip"] = np.array(mask_lip)
face_dict["mask_face"] = np.array(mask_face)
face_dict["mask_l_eyes"] = np.array(mask_l_eyes)
face_dict["mask_r_eyes"] = np.array(mask_r_eyes)
face_dict["mask_l_eyebrow"] = np.array(mask_l_eyebrow)
face_dict["mask_r_eyebrow"] = np.array(mask_r_eyebrow)
```

### ⑥抽出したパーツに対して色と重みをつける

```python
# パーツの数に合わせてプロットを初期化する
fig, axs = plt.subplots(2, len(face_dict.keys())//2)
axs = np.ravel(axs)

# パーツの融合のベースマスク
full_mask = gray.copy()

# パーツをループし処理を行う
for idx, (part, v) in enumerate(face_dict.items()):
　　# マスクをコピーし初期化する
    mask = gray.copy()
    
　　# パーツの範囲を定義する
　　convexhull = cv2.convexHull(v)

    # 色と重みを定義する
    if "eyes" in part:
        color = cfg["eyes"]["color"]
        weight = cfg["eyes"]["weight"]
    elif "eyebrow" in part:
        color = cfg["eyebrow"]["color"]
        weight = cfg["eyebrow"]["weight"]
    elif "face" in part:
        color = cfg["face"]["color"]
        weight = cfg["face"]["weight"]
    elif "lip" in part:
        color = cfg["lip"]["color"]
        weight = cfg["lip"]["weight"]
    else:
        color = (0, 0, 0)
    
    # 色を塗る
    mask = cv2.fillConvexPoly(mask, convexhull, color)
    mask = cv2.GaussianBlur(mask, (7, 7), 20)
    
　　# パーツのマスクを全体のマスクに追加していく
    full_mask = cv2.addWeighted(full_mask, 1, mask, weight, 1)
    
　　# 描画する
    axs[idx].set_title(part)
    axs[idx].imshow(mask)
```

パーツ毎のマスク
![parts_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290515/db20c22a-71e7-ed52-9086-4a21e37ed8be.png)

全体のマスク
![Mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290515/82b67721-4d06-2c85-5a7e-600bc5bae59f.png)

### ⑦融合する
```python
# 入力画像にマスクをかける
result = cv2.addWeighted(full_mask, 1, src, 1, 1)

# 描画する
fig, (ax0, ax1) = plt.subplots(1,2)
ax0.set_title("Before")
ax0.imshow(src)
ax1.set_title("After")
ax1.imshow(result)
```

![After.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290515/9e734afe-310e-1abd-71f5-239032fd4890.png)

比較する

![comparison.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290515/bec4b4c4-d913-1a83-5146-48b0d6002956.png)

# あとがき

今回は唇だけではなく、顔全体に対して色を付けてみた！
大まかな範囲で色を塗っているが、本当のお化粧とは細かいところで調整することで、まだまだ本当の目標までは遠い！
ただ、これを通していい土台にはなるかなと思う。

一旦「お化粧」をここまでとして、次回はリアルタイムで「お化粧」してみよう！