Pytorch と OpenCV を使用したビデオの顔の置換

「DeepFaceLab」プロジェクトは以前からリリースされており、研究目的のため、この記事では彼の原理を紹介し、PytorchとOpenCVを使用した簡易版を作成します。

この記事は 3 つのパートに分かれており、最初のパートでは 2 つのビデオから顔を抽出し、標準の顔データセットを構築します。2 番目の部分では、データセットとニューラル ネットワークを併用して、潜在空間で顔を表現する方法を学習し、その表現から顔の画像を再構成します。最後の部分では、ニューラル ネットワークを使用して、ビデオの各フレームに、ソース ビデオと同一の顔を作成しますが、ターゲット ビデオの人物の表情を備えています。次に、元の顔を偽の顔に置き換え、新しいフレームを新しいフェイク ビデオとして保存します。

プロジェクトの基本構造(最初の実行前)は次のようになります

 ├── face_masking.py
 ├── main.py
 ├── face_extraction_tools.py
 ├── quick96.py
 ├── merge_frame_to_fake_video.py
 ├── data
 │ ├── data_dst.mp4
 │ ├── data_src.mp4

main.py はメイン スクリプトであり、data フォルダーにはプログラムに必要な data_dst.mp4 および data_src.mp4 ファイルが含まれています。

抽出と位置合わせ - データセットの構築

最初のパートでは、主に face_extraction_tools.py ファイルのコードを紹介します。

最初のステップはビデオからフレームを抽出することなので、フレームを JPEG 画像として保存する関数を構築する必要があります。この関数は、ビデオへのパスと出力フォルダーへの別のパスを受け入れます。

 def extract_frames_from_video(video_path: Union[str, Path], output_folder: Union[str, Path], frames_to_skip: int=0) -> None:
     """
     Extract frame from video as a JPG images.
     Args:
         video_path (str | Path): the path to the input video from it the frame will be extracted
         output_folder (str | Path): the folder where the frames will be saved
         frames_to_skip (int): how many frames to skip after a frame which is saved. 0 will save all the frames.
             If, for example, this value is 2, the first frame will be saved, then frame 2 and 3 will be skipped,
             the 4th frame will be saved, and so on.
 
     Returns:
 
     """
 
     video_path = Path(video_path)
     output_folder = Path(output_folder)
 
     if not video_path.exists():
         raise ValueError(f'The path to the video file {video_path.absolute()} is not exist')
     if not output_folder.exists():
         output_folder.mkdir(parents=True)
 
     video_capture = cv2.VideoCapture(str(video_path))
 
     extract_frame_counter = 0
     saved_frame_counter = 0
     while True:
         ret, frame = video_capture.read()
         if not ret:
             break
 
         if extract_frame_counter % (frames_to_skip + 1) == 0:
             cv2.imwrite(str(output_folder / f'{saved_frame_counter:05d}.jpg'), frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
             saved_frame_counter += 1
 
         extract_frame_counter += 1
 
     print(f'{saved_frame_counter} of {extract_frame_counter} frames saved')

この機能は、まずビデオファイルが存在するかどうか、出力フォルダーが存在するかどうかを確認し、存在しない場合は自動的に作成します。次に、OpenCV の videoccapture クラスを使用して、ビデオを読み取り、出力フォルダーに JPEG ファイルとしてフレームごとに保存するためのオブジェクトを作成します。フレームは、frames_to_skip パラメータに従ってスキップすることもできます。

次に、顔抽出器を構築する必要があります。このツールは、画像内の顔を検出し、抽出して位置合わせできる必要があります。このようなツールを構築する最良の方法は、検出、抽出、位置合わせのためのメソッドを備えた FaceExtractor クラスを作成することです。

検出部分には、OpenCV を備えた YuNet を使用します。YuNet は、OpenCV の FaceDetectorYN クラスで使用できる、高速かつ正確な CNN ベースの顔検出器です。このような FaceDetectorYN オブジェクトを作成するには、重みを含む ONNX ファイルが必要です。このファイルは OpenCV Zoo にあり、現在のバージョンの名前は「face_detection_yunet_2023mar.onnx」です。

init ()メソッドは次のようになります。

 def __init__(self, image_size):
         """
         Create a YuNet face detector to get face from image of size 'image_size'. The YuNet model
         will be downloaded from opencv zoo, if it's not already exist.
         Args:
             image_size (tuple): a tuple of (width: int, height: int) of the image to be analyzed
         """
         detection_model_path = Path('models/face_detection_yunet_2023mar.onnx')
         if not detection_model_path.exists():
             detection_model_path.parent.mkdir(parents=True, exist_ok=True)
             url = "https://github.com/opencv/opencv_zoo/blob/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx"
             print('Downloading face detection model...')
             filename, headers = urlretrieve(url, filename=str(detection_model_path))
             print('Download finish!')
 
         self.detector = cv2.FaceDetectorYN.create(str(detection_model_path), "", image_size)

この関数はまず重みファイルが存在するかどうかを確認し、存在しない場合は Web からダウンロードします。次に、分析する重みファイルと画像サイズを使用して FaceDetectorYN オブジェクトを作成します。検出方法は YuNet 検出方法を使用して画像内の顔を検出します

     def detect(self, image):
         ret, faces = self.detector.detect(image)
         return ret, faces

YuNet の出力は、次の情報を含むサイズ [num_faces, 15] の 2D 配列です。

  • 0-1: 境界ボックスの左上隅の x、y
  • 2-3: フレームの幅と高さ
  • 4-5: 右目のx、y (サンプル画像の青い点)
  • 6-7: 左目 x、y (サンプル画像の赤い点)
  • 8-9: 鼻先 x、y (例の写真の緑色の点)
  • 10-11: 口の右端の x、y (サンプル画像のピンクの点)
  • 12-13: 左口角の x、y (サンプル写真の黄色の点)
  • 14: 顔の採点

顔の位置データを取得したので、それを使用して顔の位置が調整された画像を取得できます。ここでは主に目の位置の情報が利用されます。位置合わせされた画像内で目が同じレベル (同じ y 座標) にあるようにします。

  @staticmethod
     def align(image, face, desired_face_width=256, left_eye_desired_coordinate=np.array((0.37, 0.37))):
         """
         Align the face so the eyes will be at the same level
         Args:
             image (np.ndarray): image with face
             face (np.ndarray):  face coordinates from the detection step
             desired_face_width (int): the final width of the aligned face image
             left_eye_desired_coordinate (np.ndarray): a length 2 array of values between
              0 and 1 where the left eye should be in the aligned image
 
         Returns:
             (np.ndarray): aligned face image
         """
         desired_face_height = desired_face_width
         right_eye_desired_coordinate = np.array((1 - left_eye_desired_coordinate[0], left_eye_desired_coordinate[1]))
 
         # get coordinate of the center of the eyes in the image
         right_eye = face[4:6]
         left_eye = face[6:8]
 
         # compute the angle of the right eye relative to the left eye
         dist_eyes_x = right_eye[0] - left_eye[0]
         dist_eyes_y = right_eye[1] - left_eye[1]
         dist_between_eyes = np.sqrt(dist_eyes_x ** 2 + dist_eyes_y ** 2)
         angles_between_eyes = np.rad2deg(np.arctan2(dist_eyes_y, dist_eyes_x) - np.pi)
         eyes_center = (left_eye + right_eye) // 2
 
         desired_dist_between_eyes = desired_face_width * (
                     right_eye_desired_coordinate[0] - left_eye_desired_coordinate[0])
         scale = desired_dist_between_eyes / dist_between_eyes
 
         M = cv2.getRotationMatrix2D(eyes_center, angles_between_eyes, scale)
 
         M[0, 2] += 0.5 * desired_face_width - eyes_center[0]
         M[1, 2] += left_eye_desired_coordinate[1] * desired_face_height - eyes_center[1]
 
         face_aligned = cv2.warpAffine(image, M, (desired_face_width, desired_face_height), flags=cv2.INTER_CUBIC)
         return face_aligned

このメソッドは、単一の顔の画像と情報を取得し、画像の幅と左目の希望の相対位置を出力します。出力画像が正方形であり、右目の目的の位置が 1 - left_eye_x の同じ y 位置と x 位置を持つと仮定します。両目の間の距離と角度、両目の中心点を計算します。

最後のメソッドは extract メソッドです。これは align メソッドに似ていますが、変換せずに、画像内の顔の境界ボックスも返します。

 def extract_and_align_face_from_image(input_dir: Union[str, Path], desired_face_width: int=256) -> None:
     """
     Extract the face from an image, align it and save to a directory inside in the input directory
     Args:
         input_dir (str|Path): path to the directory contains the images extracted from a video
         desired_face_width (int): the width of the aligned imaged in pixels
 
     Returns:
 
     """
 
     input_dir = Path(input_dir)
     output_dir = input_dir / 'aligned'
     if output_dir.exists():
         rmtree(output_dir)
     output_dir.mkdir()
 
 
     image = cv2.imread(str(input_dir / '00000.jpg'))
     image_height = image.shape[0]
     image_width = image.shape[1]
 
     detector = FaceExtractor((image_width, image_height))
 
     for image_path in tqdm(list(input_dir.glob('*.jpg'))):
         image = cv2.imread(str(image_path))
 
         ret, faces = detector.detect(image)
         if faces is None:
             continue
 
         face_aligned = detector.align(image, faces[0, :], desired_face_width)
         cv2.imwrite(str(output_dir / f'{image_path.name}'), face_aligned, [cv2.IMWRITE_JPEG_QUALITY, 90])

電車

Web の場合は AutoEncoder を使用します。AutoEncoder には、エンコーダーとデコーダーという 2 つの主要なコンポーネントがあります。エンコーダは元の画像を取得してその潜在表現を見つけ、デコーダはその潜在表現を使用して元の画像を再構築します。

私たちのタスクでは、エンコーダーは潜在的な顔表現を見つけるようにトレーニングされ、2 つのデコーダーはソースの顔を再構築し、もう 1 つはターゲットの顔を再構築します。

これら 3 つのコンポーネントがトレーニングされた後、元の目標に戻ります。つまり、ターゲットの表情を備えたソース顔の画像を作成することです。つまり、デコーダ A と顔 B の画像を使用します。

顔の潜在空間には、位置、方向、表情などの顔の主な特徴が保存されます。デコーダは、このエンコードされた情報を取得し、顔全体の画像を構築する方法を学習します。デコーダ A はタイプ A の顔を構築する方法しか知らないため、エンコーダから画像 B の特徴を取得し、そこからタイプ A の画像を構築します。

このペーパーでは、元の DeepFaceLab プロジェクトの Quick96 アーキテクチャのわずかに変更されたバージョンを使用します。

モデルの完全な詳細は、quick96.py ファイルにあります。

モデルをトレーニングする前に、データを処理する必要もあります。モデルを堅牢にして過学習を回避するには、元の顔画像に 2 種類の拡張を適用する必要もあります。1 つ目は、回転、スケーリング、x 方向と y 方向の移動、水平反転などの一般的な変換です。変換ごとに、パラメーターまたは確率の範囲 (たとえば、回転に使用できる角度の範囲) を定義し、その範囲からランダムな値を選択して画像に適用します。

 random_transform_args = {
     'rotation_range': 10,
     'zoom_range': 0.05,
     'shift_range': 0.05,
     'random_flip': 0.5,
   }
 
 def random_transform(image, rotation_range, zoom_range, shift_range, random_flip):
     """
     Make a random transformation for an image, including rotation, zoom, shift and flip.
     Args:
         image (np.array): an image to be transformed
         rotation_range (float): the range of possible angles to rotate - [-rotation_range, rotation_range]
         zoom_range (float): range of possible scales - [1 - zoom_range, 1 + zoom_range]
         shift_range (float): the percent of translation for x  and y
         random_flip (float): the probability of horizontal flip
 
     Returns:
         (np.array): transformed image
     """
     h, w = image.shape[0:2]
     rotation = np.random.uniform(-rotation_range, rotation_range)
     scale = np.random.uniform(1 - zoom_range, 1 + zoom_range)
     tx = np.random.uniform(-shift_range, shift_range) * w
     ty = np.random.uniform(-shift_range, shift_range) * h
     mat = cv2.getRotationMatrix2D((w // 2, h // 2), rotation, scale)
     mat[:, 2] += (tx, ty)
     result = cv2.warpAffine(image, mat, (w, h), borderMode=cv2.BORDER_REPLICATE)
     if np.random.random() < random_flip:
         result = result[:, ::-1]
     return result

2 つ目は、ノイズの多い補間マップを使用して作成された歪みです。この歪みにより、モデルは顔の主要な特徴を理解し、より一般化できるようになります。

 def random_warp(image):
     """
     Create a distorted face image and a target undistorted image
     Args:
         image  (np.array): image to warp
 
     Returns:
         (np.array): warped version of the image
         (np.array): target image to construct from the warped version
     """
     h, w = image.shape[:2]
 
     # build coordinate map to wrap the image according to
     range_ = np.linspace(h / 2 - h * 0.4, h / 2 + h * 0.4, 5)
     mapx = np.broadcast_to(range_, (5, 5))
     mapy = mapx.T
 
     # add noise to get a distortion of the face while warp the image
     mapx = mapx + np.random.normal(size=(5, 5), scale=5*h/256)
     mapy = mapy + np.random.normal(size=(5, 5), scale=5*h/256)
 
     # get interpolation map for the center of the face with size of (96, 96)
     interp_mapx = cv2.resize(mapx, (int(w / 2 * (1 + 0.25)) , int(h / 2 * (1 + 0.25))))[int(w/2 * 0.25/2):int(w / 2 * (1 + 0.25) - w/2 * 0.25/2), int(w/2 * 0.25/2):int(w / 2 * (1 + 0.25) - w/2 * 0.25/2)].astype('float32')
     interp_mapy = cv2.resize(mapy, (int(w / 2 * (1 + 0.25)) , int(h / 2 * (1 + 0.25))))[int(w/2 * 0.25/2):int(w / 2 * (1 + 0.25) - w/2 * 0.25/2), int(w/2 * 0.25/2):int(w / 2 * (1 + 0.25) - w/2 * 0.25/2)].astype('float32')
 
     # remap the face image according to the interpolation map to get warp version
     warped_image = cv2.remap(image, interp_mapx, interp_mapy, cv2.INTER_LINEAR)
 
     # create the target (undistorted) image
     # find a transformation to go from the source coordinates to the destination coordinate
     src_points = np.stack([mapx.ravel(), mapy.ravel()], axis=-1)
     dst_points = np.mgrid[0:w//2+1:w//8, 0:h//2+1:h//8].T.reshape(-1, 2)
 
     # We want to find a similarity matrix (scale rotation and translation) between the
     # source and destination points. The matrix should have the structure
     # [[a, -b, c],
     #  [b,  a, d]]
     # so we can construct unknown vector [a, b, c, d] and solve for it using least
     # squares with the source and destination x and y points.
     A = np.zeros((2 * src_points.shape[0], 2))
     A[0::2, :] = src_points  # [x, y]
     A[0::2, 1] = -A[0::2, 1] # [x, -y]
     A[1::2, :] = src_points[:, ::-1]  # [y, x]
     A = np.hstack((A, np.tile(np.eye(2), (src_points.shape[0], 1))))  # [x, -y, 1, 0] for x coordinate and [y, x, 0 ,1] for y coordinate
     b = dst_points.flatten()  # arrange as [x0, y0, x1, y1, ..., xN, yN]
 
     similarity_mat = np.linalg.lstsq(A, b, rcond=None)[0] # get the similarity matrix elements as vector [a, b, c, d]
     # construct the similarity matrix from the result vector of the least squares
     similarity_mat = np.array([[similarity_mat[0], -similarity_mat[1], similarity_mat[2]],
                                [similarity_mat[1], similarity_mat[0], similarity_mat[3]]])
     # use the similarity matrix to construct the target image using affine transformation
     target_image = cv2.warpAffine(image, similarity_mat, (w // 2, h // 2))
 
     return warped_image, target_image

この関数には 2 つの部分があり、最初に顔の周囲の領域に画像の座標マップを作成します。x 座標のマップと y 座標のマップがあります。mapx 変数と mapy 変数の値はピクセル単位の座標です。次に、画像にノイズを追加して、座標をランダムな方向に移動させます。ノイズを追加すると、歪んだ座標が得られます (ピクセルがランダムな方向に少しシフトします)。次に、補間されたマップが、顔の中心を含むように 96x96 ピクセルのサイズでトリミングされました。これで、ワープされたマップを使用して画像を再マッピングし、新しいワープされた画像を作成できるようになりました。

2 番目の部分では、ワープされていないイメージが作成されます。これは、モデルがワープされたイメージから作成するターゲット イメージです。ノイズをソース座標として使用し、ターゲット イメージのターゲット座標のセットを定義します。次に、最小二乗法を使用して相似変換行列 (スケールの回転と平行移動) を見つけ、それをソース座標からターゲット座標にマッピングし、それを画像に適用してターゲット画像を取得します。

次に、データを処理するための Dataset クラスを作成できます。FaceData クラスは非常に単純です。これは、前のセクションで作成したデータを含む src フォルダーと dst フォルダーを含むフォルダーへのパスを取得し、1 に正規化されたサイズ (2*96,2*96) のランダムなソース イメージと宛先イメージを返します。私たちのネットワークが取得するのは、変換されワープされた画像と、ソースとターゲットの顔のターゲット画像です。したがって、collat​​e_fnを実装する必要があります

 def collate_fn(self, batch):
         """
         Collate function to arrange the data returns from a batch. The batch returns a list
         of tuples contains pairs of source and destination images, which is the input of this
         function, and the function returns a tuple with 4 4D tensors of the warp and target
         images for the source and destination
         Args:
             batch (list): a list of tuples contains pairs of source and destination images
                 as numpy array
 
         Returns:
             (torch.Tensor): a 4D tensor of the wrap version of the source images
             (torch.Tensor): a 4D tensor of the target source images
             (torch.Tensor): a 4D tensor of the wrap version of the destination images
             (torch.Tensor): a 4D tensor of the target destination images
         """
         images_src, images_dst = list(zip(*batch))  # convert list of tuples with pairs of images into tuples of source and destination images
         warp_image_src, target_image_src = get_training_data(images_src, len(images_src))
         warp_image_src = torch.tensor(warp_image_src, dtype=torch.float32).permute(0, 3, 1, 2).to(device)
         target_image_src = torch.tensor(target_image_src, dtype=torch.float32).permute(0, 3, 1, 2).to(device)
         warp_image_dst, target_image_dst = get_training_data(images_dst, len(images_dst))
         warp_image_dst = torch.tensor(warp_image_dst, dtype=torch.float32).permute(0, 3, 1, 2).to(device)
         target_image_dst = torch.tensor(target_image_dst, dtype=torch.float32).permute(0, 3, 1, 2).to(device)
 
         return warp_image_src, target_image_src, warp_image_dst, target_image_dst

Dataloader オブジェクトからデータを取得すると、FaceData オブジェクトからのソース画像とターゲット画像のペアを含むタプルが返されます。Collat​​e_fn は、この結果を受け取り、イメージを変換および歪めてターゲット イメージを取得し、ワープされたソース イメージ、ターゲット ソース イメージ、ワープされたターゲット イメージ、およびターゲット ターゲット イメージの 4 つの 4D テンソルを返します。

トレーニングに使用される損失関数は、MSE (L2) 損失と DSSIM の組み合わせです。

トレーニングの指標と結果は上の図に示されています

ビデオを生成する

最後のステップはビデオを作成することです。このタスクを処理する関数は、merge_frame_to_fake_video.py と呼ばれます。MediaPipeを使用してフェイスマスククラスを作成しました。

フェイスマスク オブジェクトを初期化するときに、MediaPipe 顔検出器を初期化します。

 class FaceMasking:
     def __init__(self):
         landmarks_model_path = Path('models/face_landmarker.task')
         if not landmarks_model_path.exists():
             landmarks_model_path.parent.mkdir(parents=True, exist_ok=True)
             url = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task"
             print('Downloading face landmarks model...')
             filename, headers = urlretrieve(url, filename=str(landmarks_model_path))
             print('Download finish!')
 
         base_options = python_mp.BaseOptions(model_asset_path=str(landmarks_model_path))
         options = vision.FaceLandmarkerOptions(base_options=base_options,
                                                output_face_blendshapes=False,
                                                output_facial_transformation_matrixes=False,
                                                num_faces=1)
         self.detector = vision.FaceLandmarker.create_from_options(options)

このクラスには、顔画像からマスクを取得するメソッドもあります。

 def get_mask(self, image):
         """
         return uint8 mask of the face in image
         Args:
             image (np.ndarray): RGB image with single face
 
         Returns:
             (np.ndarray): single channel uint8 mask of the face
         """
         im_mp = mp.Image(image_format=mp.ImageFormat.SRGB, data=image.astype(np.uint8).copy())
         detection_result = self.detector.detect(im_mp)
 
         x = np.array([landmark.x * image.shape[1] for landmark in detection_result.face_landmarks[0]], dtype=np.float32)
         y = np.array([landmark.y * image.shape[0] for landmark in detection_result.face_landmarks[0]], dtype=np.float32)
 
         hull = np.round(np.squeeze(cv2.convexHull(np.column_stack((x, y))))).astype(np.int32)
         mask = np.zeros(image.shape[:2], dtype=np.uint8)
         mask = cv2.fillConvexPoly(mask, hull, 255)
         kernel = np.ones((7, 7), np.uint8)
         mask = cv2.erode(mask, kernel)
 
         return mask

この関数は、まず入力画像を MediaPipe 画像構造に変換し、次に顔検出器を使用して顔を検索します。次に、OpenCV を使用してポイントの凸包を見つけ、OpenCV の fillConvexPoly 関数を使用して凸包の領域を塗りつぶし、バイナリ マスクが生成されます。最後に、侵食操作を適用してオクルージョンを縮小します。

  def get_mask(self, image):
         """
         return uint8 mask of the face in image
         Args:
             image (np.ndarray): RGB image with single face
 
         Returns:
             (np.ndarray): single channel uint8 mask of the face
         """
         im_mp = mp.Image(image_format=mp.ImageFormat.SRGB, data=image.astype(np.uint8).copy())
         detection_result = self.detector.detect(im_mp)
 
         x = np.array([landmark.x * image.shape[1] for landmark in detection_result.face_landmarks[0]], dtype=np.float32)
         y = np.array([landmark.y * image.shape[0] for landmark in detection_result.face_landmarks[0]], dtype=np.float32)
 
         hull = np.round(np.squeeze(cv2.convexHull(np.column_stack((x, y))))).astype(np.int32)
         mask = np.zeros(image.shape[:2], dtype=np.uint8)
         mask = cv2.fillConvexPoly(mask, hull, 255)
         kernel = np.ones((7, 7), np.uint8)
         mask = cv2.erode(mask, kernel)
 
         return mask

merge_frame_to_fake_video 関数は、上記のすべての手順を統合し、新しいビデオ オブジェクト、FaceExtracot オブジェクト、フェイスマスク オブジェクトを作成し、ニューラル ネットワーク コンポーネントを作成し、それらの重みをロードします。

 def merge_frames_to_fake_video(dst_frames_path, model_name='Quick96', saved_models_dir='saved_model'):
     model_path = Path(saved_models_dir) / f'{model_name}.pth'
     dst_frames_path = Path(dst_frames_path)
     image = Image.open(next(dst_frames_path.glob('*.jpg')))
     image_size = image.size
     result_video = cv2.VideoWriter(str(dst_frames_path.parent / 'fake.mp4'), cv2.VideoWriter_fourcc(*'MJPG'), 30, image.size)
 
     face_extractor = FaceExtractor(image_size)
     face_masker = FaceMasking()
 
     encoder = Encoder().to(device)
     inter = Inter().to(device)
     decoder = Decoder().to(device)
 
     saved_model = torch.load(model_path)
     encoder.load_state_dict(saved_model['encoder'])
     inter.load_state_dict(saved_model['inter'])
     decoder.load_state_dict(saved_model['decoder_src'])
 
     model = torch.nn.Sequential(encoder, inter, decoder)

次に、ターゲット ビデオ内のすべてのフレームについて、顔が検出されます。顔がない場合は、ビデオに写真を書き込みます。顔が存在する場合、それが抽出され、ネットワークへの適切な入力に変換され、新しい顔が生成されます。

元の顔と新しい顔をマスクし、マスクされた画像上のモーメントを使用して元の顔の中心を見つけます。シームレスなクローン作成を使用して、現実的な方法で元の顔を新しい顔に置き換えます (たとえば、元の顔のスキンに合わせて偽の顔の肌の色調を変更します)。最後に、結果を新しいフレームとして元のフレームに戻し、ビデオ ファイルに書き込みます。

     frames_list = sorted(dst_frames_path.glob('*.jpg'))
     for ii, frame_path in enumerate(frames_list, 1):
         print(f'Working om {ii}/{len(frames_list)}')
         frame = cv2.imread(str(frame_path))
         retval, face = face_extractor.detect(frame)
         if face is None:
             result_video.write(frame)
             continue
         face_image, face = face_extractor.extract(frame, face[0])
         face_image = face_image[..., ::-1].copy()
         face_image_cropped = cv2.resize(face_image, (96, 96)) #face_image_resized[96//2:96+96//2, 96//2:96+96//2]
         face_image_cropped_torch = torch.tensor(face_image_cropped / 255., dtype=torch.float32).permute(2, 0, 1).unsqueeze(0).to(device)
         generated_face_torch = model(face_image_cropped_torch)
         generated_face = (generated_face_torch.squeeze().permute(1,2,0).detach().cpu().numpy() * 255).astype(np.uint8)
 
 
         mask_origin = face_masker.get_mask(face_image_cropped)
         mask_fake = face_masker.get_mask(generated_face)
 
         origin_moments = cv2.moments(mask_origin)
         cx = np.round(origin_moments['m10'] / origin_moments['m00']).astype(int)
         cy = np.round(origin_moments['m01'] / origin_moments['m00']).astype(int)
         try:
             output_face = cv2.seamlessClone(generated_face, face_image_cropped, mask_fake, (cx, cy), cv2.NORMAL_CLONE)
         except:
             print('Skip')
             continue
 
         fake_face_image = cv2.resize(output_face, (face_image.shape[1], face_image.shape[0]))
         fake_face_image = fake_face_image[..., ::-1] # change to BGR
         frame[face[1]:face[1]+face[3], face[0]:face[0]+face[2]] = fake_face_image
         result_video.write(frame)
 
     result_video.release()

1フレームの結果はこれです

モデルは完璧ではなく、顔の特定の角度、特に横から見た画像はあまり良くありませんが、全体的な効果は良好です。

統合するために

プロセス全体を実行するには、メイン スクリプトも作成する必要があります。

 from pathlib import Path
 import face_extraction_tools as fet
 import quick96 as q96
 from merge_frame_to_fake_video import merge_frames_to_fake_video
 
 ##### user parameters #####
 # True for executing the step
 extract_and_align_src = True
 extract_and_align_dst = True
 train = True
 eval = False
 
 model_name = 'Quick96'  # use this name to save and load the model
 new_model = False  # True for creating a new model even if a model with the same name already exists
 
 ##### end of user parameters #####
 
 # the path for the videos to process
 data_root = Path('./data')
 src_video_path = data_root / 'data_src.mp4'
 dst_video_path = data_root / 'data_dst.mp4'
 
 # path to folders where the intermediate product will be saved
 src_processing_folder = data_root / 'src'
 dst_processing_folder = data_root / 'dst'
 
 # step 1: extract the frames from the videos
 if extract_and_align_src:
     fet.extract_frames_from_video(video_path=src_video_path, output_folder=src_processing_folder, frames_to_skip=0)
 if extract_and_align_dst:
     fet.extract_frames_from_video(video_path=dst_video_path, output_folder=dst_processing_folder, frames_to_skip=0)
 
 # step 2: extract and align face from frames
 if extract_and_align_src:
     fet.extract_and_align_face_from_image(input_dir=src_processing_folder, desired_face_width=256)
 if extract_and_align_dst:
     fet.extract_and_align_face_from_image(input_dir=dst_processing_folder, desired_face_width=256)
 
 # step 3: train the model
 if train:
     q96.train(str(data_root), model_name, new_model, saved_models_dir='saved_model')
 
 # step 4: create the fake video
 if eval:
     merge_frames_to_fake_video(dst_processing_folder, model_name, saved_models_dir='saved_model')

要約する

この記事では、DeepFaceLabの動作パイプラインを紹介し、独自の手法で処理を実装します。まず動画からフレームを抽出し、次にフレームから顔を抽出して位置合わせしてデータベースを作成します。ニューラル ネットワークを使用して、潜在空間で顔を表現する方法とそれらを再構成する方法を学びます。ターゲットビデオのフレームをトラバースし、顔を見つけて置き換える、これがこのプロジェクトの完全なプロセスです。

この記事は調査と研究のみを目的としています。実際のプロジェクトを参照してください。

https://avoid.overfit.cn/post/ec72d69b57464a08803c86db8720e3e9

著者: DZ

おすすめ

転載: blog.csdn.net/m0_46510245/article/details/132421618