LoginSignup
9
0

More than 1 year has passed since last update.

【python】MediapipeとOpenCVでリアルタイムの顔フィルター

Last updated at Posted at 2021-12-04

TL;DR

ノートブックはこちら

はじめに

今までの記事をあらすじ:

  1. Mediapipeを用いて口紅を塗る (1/4)
  2. Mediapipeを使って「お化粧」をしてみた (2/4)
  3. MediapipeとOpenCVを用いてリアルタイムで「お化粧」(3/4) ← ここ
  4. WEB会議などで自前の顔フィルタを実装 (4/4)

今回は前回作成したフィルターを用いて、PCカメラやUSEカメラの画像に対して適用させる。これが我々が目指す目標「WEB会議やビデオ通話などの場面で顔のフィルターを自前で実装」において核心となるもの。

手順

今回の場合は画像データをリアルタイムで取得する必要があるため、ノートブックではなく.pyファイルにする。
また、今回のフォルダ構成は下記の通りである。

root/
│
├── config/
|     └── makeup.yaml
├── src/
      ├── README.md
      └── face_filter.py
└── requirements.txt

プログラムの大まかな処理を下記の通り:

  1. Configの読み込む
  2. Mediapipeのインスタンス化
  3. カメラからの画像取得
  4. フィルター適用
  5. 結果を表示する

使用するツール

前回までのモジュールの他に、今回はロギングするため以下のモジュールを利用する。

loguru

もちろんpython既存の"logging"モジュールはあるが、loguruもかなり使い勝手がよいので、ぜひ試してみてください。

インストール

$ pip install loguru

コード

①コード構成

大まかな機能としては「Config処理」「検知器」「画像取得」の3つがあり、それぞれクラス化する。さらに、yamlファイルと使うカメラの番号の指定を引数として与える。

コードの構成

src/face_filter.py

import #省略

class Config():
# Config処理クラス

class Detector():
# 検知器クラス

class Camera():
# カメラクラス

def arg_parser():
# 引数管理

def main():
# メイン関数

yamlファイル

config/makeup.yaml

face:
  color: [140, 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

②モジュールをimportする

# src/face_filter.py
import yaml

import sys
import argparse
from loguru import logger

import cv2
import numpy as np
import mediapipe as mp

③Configクラス

# src/face_filter.py
class Config():
    def __init__(self, yaml_file) -> None:
        logger.info(f"Reading config file: {yaml_file}")
        with open(yaml_file, "r") as f:
            self.yaml_cfg = yaml.safe_load(f)

④検知器クラス

Mediapipeのインスタンス化

# src/face_filter.py
class Detector():
    def __init__(self, thickness=1, circle_radius=1, color=(255,0,255), min_detection_confidence=0.5, min_tracking_confidence=0.5) -> None:
        # Mediapipeのインスタンス化
        logger.info("Mediapipe detector initiated.")
        self.drawing = mp.solutions.drawing_utils
        self.drawing_spec = self.drawing.DrawingSpec(
            thickness=thickness, 
            circle_radius=circle_radius, 
            color=color)

        self.drawing_styles = mp.solutions.drawing_styles
        self.face_mesh = mp.solutions.face_mesh

        self.face_detector = self.face_mesh.FaceMesh(
            static_image_mode=False,
            refine_landmarks=True,
            min_detection_confidence=min_detection_confidence,
            min_tracking_confidence=min_tracking_confidence)

        self._parts_init()

顔のバーツの指定

    def _parts_init(self) -> None:
        self.lips = list(self.face_mesh.FACEMESH_LIPS)
        self.lips = np.ravel(self.lips)

        self.l_eyes = list(self.face_mesh.FACEMESH_LEFT_EYE)
        self.l_eyes = np.ravel(self.l_eyes)

        self.r_eyes = list(self.face_mesh.FACEMESH_RIGHT_EYE)
        self.r_eyes = np.ravel(self.r_eyes)

        self.l_eyebrow = list(self.face_mesh.FACEMESH_LEFT_EYEBROW)
        self.l_eyebrow = np.ravel(self.l_eyebrow)

        self.r_eyebrow = list(self.face_mesh.FACEMESH_RIGHT_EYEBROW)
        self.r_eyebrow = np.ravel(self.r_eyebrow)

顔検知を行う

    def __call__(self, image):
        self.img = image
        self.results = self.face_detector.process(image)

検知した顔に対して色と重みをつけ、後処理を行う。

    def post_processing(self, mask, cfg):
        face_dict = {}
        if self.results.multi_face_landmarks:
            for face_landmarks in self.results.multi_face_landmarks:

                mask_lip = []
                mask_face = []
                mask_l_eyes = []
                mask_r_eyes = []
                mask_l_eyebrow = []
                mask_r_eyebrow = []
                for i in range(self.face_mesh.FACEMESH_NUM_LANDMARKS):

                    if i in self.lips:
                        pt1 = face_landmarks.landmark[i]
                        x = int(pt1.x * self.img.shape[1])
                        y = int(pt1.y * self.img.shape[0])
                        mask_lip.append((x, y))

                    elif i in self.l_eyes:
                        pt1 = face_landmarks.landmark[i]
                        x = int(pt1.x * self.img.shape[1])
                        y = int(pt1.y * self.img.shape[0])

                        mask_l_eyes.append((x, y))

                    elif i in self.r_eyes:
                        pt1 = face_landmarks.landmark[i]
                        x = int(pt1.x * self.img.shape[1])
                        y = int(pt1.y * self.img.shape[0])
                        mask_r_eyes.append((x, y))

                    elif i in self.r_eyebrow:
                        pt1 = face_landmarks.landmark[i]
                        x = int(pt1.x * self.img.shape[1])
                        y = int(pt1.y * self.img.shape[0])
                        mask_r_eyebrow.append((x, y))

                    elif i in self.l_eyebrow:
                        pt1 = face_landmarks.landmark[i]
                        x = int(pt1.x * self.img.shape[1])
                        y = int(pt1.y * self.img.shape[0])
                        mask_l_eyebrow.append((x, y))

                    else:
                        pt1 = face_landmarks.landmark[i]
                        x = int(pt1.x * self.img.shape[1])
                        y = int(pt1.y * self.img.shape[0])
                        mask_face.append((x, y))

            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)

            full_mask = mask.copy()

            for part, v in face_dict.items():
                base = mask.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)

                base = cv2.fillConvexPoly(base, convexhull, (color[2], color[1], color[0]))

                full_mask = cv2.addWeighted(full_mask, 1, base, weight, 1)
            full_mask = cv2.GaussianBlur(full_mask, (7, 7), 20)
            tmp = cv2.addWeighted(self.img, 1, full_mask, 1, 1)

            return tmp, full_mask
        logger.warning("Face not detected.")
        return self.img, mask

 ⑤画像取得クラス

カメルを稼働させ、yamlファイルと検知器を読み込む

# src/face_filter.py
class Camera():
    def __init__(self, index, config, detector) -> None:
        self.start(index)
        self.config = config
        self.detector = detector

指定したカメルを起動する

    def start(self, index) -> None:
        self.cap = cv2.VideoCapture(index)
        success, frame = self.cap.read()
        if not success:
            logger.error(f"Camera not successful: video input: {index}")
            sys.exit()
        self.mask = np.zeros((frame.shape[0], frame.shape[1], 3), dtype=np.uint8)
    def capture(self) -> None:
        logger.info("Catpuring images from video input... (press 'q' to exit.)")
        while True:
            _, frame = self.cap.read()    
            frame.flags.writeable = False
            # frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

            # 顔検知 
            self.detector(frame)
            frame.flags.writeable = True

            # 後処理を行う。
            frame, mask = self.detector.post_processing(self.mask, self.config.yaml_cfg)

            # 分かりやすくするため、 左右反転を行う。
            frame = cv2.flip(frame, 1)

            # 表示する。
            cv2.imshow("demo", frame)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break

        self.cap.release()
        cv2.destroyAllWindows()
        logger.info("Process terminated.")

⑥引数管理(argparse)

def arg_parser() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("yaml_file") #yamlファイルの指定
    parser.add_argument("--video", required=True, type=int) #利用するカメル(ビデオ入力)の番号
    args = parser.parse_args()
    return args

⑦メイン関数

def main(yaml_file, cam_idx):
    # Config処理
    config = Config(yaml_file)

    # 検知器定義
    detector = Detector()

    # カメル
    CAM = Camera(cam_idx, config, detector)

    # 画像を取得し、フィルターを適用する。
    CAM.capture()

⑧実行

# src/face_filter.py
if __name__ == "__main__":
    args = arg_parser()
    main(args.yaml_file, args.video)

デモ

demo.gif

ソースコード

ソースコードはこちら!(github)

あとがき

今回は単純にOpenCVを使って画像データ取得しながら、今までのフィルターを適用した。
今フィルターをかけた画像がリアルタイムで表示されているので、これを用いてWEB会議などで活用する!
いよいよ次回!

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0