[VToonify] AIで動画をアニメ風に変換する [スタイル転送]

2022年9月29日木曜日

Artificial Intelligence

本記事では、VToonifyと呼ばれる機械学習手法を用いて任意の動画をアニメ風画像に変換する方法をご紹介します。

アイキャッチ

VToonify

概要

VToonifyは高解像度のビデオスタイル転送技術です。

従来技術によるビデオのToonifyモデルは、顔の位置合わせの要件、顔以外の精細さの欠落などビデオに適用する場合に制限がありました。

VToonifyでは、StyleGANの中解像度レイヤーと高解像度レイヤーを活用し、エンコーダーによって抽出したマルチスケールな特徴量を用いて高品質なポートレートをレンダリングします。
この方式により可変サイズのビデオ内の位置合わせされていない顔を入力可能としています。

Architecture

詳細はこちらの論文をご参照ください。

本記事では上記手法を用いて、任意の動画をアニメ風に変換していきます。

スポンサーリンク

デモ(Colaboratory)

それでは、実際に動かしながら任意の動画のスタイル転送を行っていきます。
ソースコードは本記事にも記載していますが、下記のGitHubでも取得可能です。
GitHub - Colaboratory demo

また、下記から直接Google Colaboratoryで開くこともできます。
Open In Colab

なお、このデモはPythonで実装しています。
Pythonの実装に不安がある方、Pythonを使った機械学習について詳しく勉強したい方は、以下の書籍やオンライン講座などがおすすめです。

環境セットアップ

それではセットアップしていきます。 Colaboratoryを開いたら下記を設定しGPUを使用するようにしてください。

「ランタイムのタイプを変更」→「ハードウェアアクセラレータ」をGPUに変更

初めにGithubからソースコードを取得します。

  1. %cd /content
  2.  
  3. !git clone https://github.com/williamyang1991/VToonify.git
  4.  
  5.  
  6. # Commits on Sep 23, 2022
  7. %cd /content/VToonify
  8. !git checkout 920b56478835873169b31dd3d134d29e7e16f94b

次にライブラリをインストールします。

  1. %cd /content/VToonify
  2.  
  3. !wget https://github.com/ninja-build/ninja/releases/download/v1.8.2/ninja-linux.zip
  4. !sudo unzip ninja-linux.zip -d /usr/local/bin/
  5. !sudo update-alternatives --install /usr/bin/ninja ninja /usr/local/bin/ninja 1 --force
  6. !pip install wget
  7. !pip install --upgrade gdown
  8.  
  9. !pip install moviepy==0.2.3.5 imageio==2.4.1
  10. !pip install yt-dlp

最後にライブラリをインポートします。

  1. %load_ext autoreload
  2. %autoreload 2
  3.  
  4. import sys
  5. sys.path.append(".")
  6. sys.path.append("..")
  7.  
  8. import os
  9. import argparse
  10. import numpy as np
  11. import cv2
  12. import dlib
  13. import bz2
  14. import torch
  15. from torchvision import transforms
  16. import torchvision
  17. import torch.nn.functional as F
  18. from tqdm import tqdm
  19. import matplotlib.pyplot as plt
  20. from model.vtoonify import VToonify
  21. from model.bisenet.model import BiSeNet
  22. from model.encoder.align_all_parallel import align_face
  23. from util import save_image, load_image, visualize, load_psp_standalone, get_video_crop_parameter, tensor2cv2
  24.  
  25. from yt_dlp import YoutubeDL
  26. from moviepy.video.fx.resize import resize
  27. from moviepy.editor import VideoFileClip
  28.  
  29. device = 'cuda' if torch.cuda.is_available() else "cpu"
  30. print("using device is", device)

以上で環境セットアップは完了です。

学習済みモデルのセットアップ

ここでは、論文発表元が公開する学習済みモデルをGoogle Colaboratoryにダウンロードします。

  1. %cd /content/VToonify
  2.  
  3. !mkdir ckpts
  4.  
  5. encoder_path = 'ckpts/encoder.pt'
  6. if not os.path.exists(encoder_path):
  7. !gdown https://drive.google.com/uc?id=1NgI4mPkboYvYw3MWcdUaQhkr0OWgs9ej \
  8. -O {encoder_path}
  9.  
  10. faceparsing_path = 'ckpts/faceparsing.pth'
  11. if not os.path.exists(faceparsing_path):
  12. !gdown https://drive.google.com/uc?id=1jY0mTjVB8njDh6e0LP_2UxuRK3MnjoIR \
  13. -O {faceparsing_path}
  14.  
  15. # cartoon_exstyle
  16. exstyle_path = 'ckpts/exstyle_code.npy'
  17. if not os.path.exists(exstyle_path):
  18. !gdown https://drive.google.com/uc?id=1BuCeLk3ASZcoHlbfT28qNru4r5f-hErr \
  19. -O {exstyle_path}
  20.  
  21. # cartoon026
  22. style_type = 'cartoon026'
  23. generator_path = 'ckpts/generator.pt'
  24. if not os.path.exists(generator_path):
  25. !gdown https://drive.google.com/uc?id=1YJYODh_vEyUrL0q02okjcicpJhdYY8An \
  26. -O {generator_path}
  27.  
  28. landmark_path = 'ckpts/shape_predictor_68_face_landmarks.dat'
  29. if not os.path.exists(landmark_path):
  30. !wget -c http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2 \
  31. -O ckpts/shape_predictor_68_face_landmarks.dat.bz2
  32. zipfile = bz2.BZ2File('ckpts/shape_predictor_68_face_landmarks.dat.bz2')
  33. data = zipfile.read()
  34. open(landmark_path, 'wb').write(data)

モデルのロード

続いて、ダウンロードしたモデルをロードしていきます。

  1. transform = transforms.Compose([
  2. transforms.ToTensor(),
  3. transforms.Normalize(mean=[0.5, 0.5, 0.5],std=[0.5,0.5,0.5]),
  4. ])
  5.  
  6. %cd /content/VToonify
  7.  
  8. # load generator
  9. vtoonify = VToonify(backbone = 'dualstylegan')
  10. vtoonify.load_state_dict(torch.load(generator_path, map_location=lambda storage, loc: storage)['g_ema'])
  11. vtoonify.to(device)
  12.  
  13. # load faceparsing
  14. parsingpredictor = BiSeNet(n_classes=19)
  15. parsingpredictor.load_state_dict(torch.load(faceparsing_path, map_location=lambda storage, loc: storage))
  16. parsingpredictor.to(device).eval()
  17.  
  18. # load randmark predicter
  19. landmarkpredictor = dlib.shape_predictor(landmark_path)
  20.  
  21. # load encoder
  22. pspencoder = load_psp_standalone(encoder_path, device)
  23.  
  24. exstyles = np.load(exstyle_path, allow_pickle='TRUE').item()
  25. stylename = list(exstyles.keys())[int(style_type[-3:])]
  26. exstyle = torch.tensor(exstyles[stylename]).to(device)
  27. with torch.no_grad():
  28. exstyle = vtoonify.zplus2wplus(exstyle)

Image Toonification

まず、動画をToonificationしていきます。

  1. %cd /content/VToonify
  2.  
  3. # image_path = './data/077436.jpg'
  4. !wget -c https://www.pakutaso.com/shared/img/thumb/SAYA160312500I9A3721_TP_V.jpg \
  5. -O ./data/test01.jpg
  6. image_path = './data/test01.jpg'
  7.  
  8. original_image = load_image(image_path)
  9.  
  10. visualize(original_image[0], 30)

こちらの画像をモデルに入力します。

入力画像

画像から顔部分を検出します。

  1. frame = cv2.imread(image_path)
  2. frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
  3.  
  4. scale = 1
  5. kernel_1d = np.array([[0.125],[0.375],[0.375],[0.125]])
  6. # We detect the face in the image, and resize the image so that the eye distance is 64 pixels.
  7. # Centered on the eyes, we crop the image to almost 400x400 (based on args.padding).
  8. paras = get_video_crop_parameter(frame, landmarkpredictor, padding=[200,200,200,200])
  9. if paras is not None:
  10. h,w,top,bottom,left,right,scale = paras
  11. H, W = int(bottom-top), int(right-left)
  12. # for HR image, we apply gaussian blur to it to avoid over-sharp stylization results
  13. if scale <= 0.75:
  14. frame = cv2.sepFilter2D(frame, -1, kernel_1d, kernel_1d)
  15. if scale <= 0.375:
  16. frame = cv2.sepFilter2D(frame, -1, kernel_1d, kernel_1d)
  17. frame = cv2.resize(frame, (w, h))[top:bottom, left:right]
  18. x = transform(frame).unsqueeze(dim=0).to(device)
  19. else:
  20. print('no face detected!')

Toonificationを実行します。

  1. with torch.no_grad():
  2. I = align_face(frame, landmarkpredictor)
  3. I = transform(I).unsqueeze(dim=0).to(device)
  4. s_w = pspencoder(I)
  5. s_w = vtoonify.zplus2wplus(s_w)
  6. s_w[:,:7] = exstyle[:,:7]
  7. # parsing network works best on 512x512 images, so we predict parsing maps on upsmapled frames
  8. # followed by downsampling the parsing maps
  9. x_p = F.interpolate(parsingpredictor(2*(F.interpolate(x, scale_factor=2, mode='bilinear', align_corners=False)))[0],
  10. scale_factor=0.5, recompute_scale_factor=False).detach()
  11. # we give parsing maps lower weight (1/16)
  12. inputs = torch.cat((x, x_p/16.), dim=1)
  13. # d_s has no effect when backbone is toonify
  14. y_tilde = vtoonify(inputs, s_w.repeat(inputs.size(0), 1, 1), d_s = 0.5)
  15. y_tilde = torch.clamp(y_tilde, -1, 1)
  16.  
  17. visualize(y_tilde[0].cpu(), 60)

出力結果は以下の通りです。

Toonify結果1

Video Toonification

続いて、動画のToonificationを動かしていきます。

まず、入力動画を以下の通りセットアップしていきます。

  1. video_url = 'https://www.youtube.com/watch?v=gkr57P0fwbI' #@param {type:"string"}
  2.  
  3. #@markdown 動画の切り抜き範囲(秒)を指定してください。\
  4. #@markdown 30秒以上の場合OOM発生の可能性が高いため注意
  5. start_sec = 30#@param {type:"integer"}
  6. end_sec = 33#@param {type:"integer"}
  7.  
  8. (start_pt, end_pt) = (start_sec, end_sec)
  9.  
  10. %cd /content/VToonify
  11.  
  12. !mkdir -p test_video/frames
  13.  
  14. download_resolution = 720
  15. full_video_path = '/content/VToonify/data/full_video.mp4'
  16. input_clip_path = '/content/VToonify/data/clip_video.mp4'
  17.  
  18. # 動画ダウンロード
  19. ydl_opts = {'format': f'best[height<={download_resolution}]', 'overwrites': True, 'outtmpl': full_video_path}
  20. with YoutubeDL(ydl_opts) as ydl:
  21. ydl.download([video_url])
  22.  
  23. # 指定区間切り抜き
  24. with VideoFileClip(full_video_path) as video:
  25. subclip = video.subclip(start_pt, end_pt)
  26. subclip.write_videofile(input_clip_path)
  27.  
  28. video_path = './data/clip_video.mp4'
  29. video_cap = cv2.VideoCapture(video_path)
  30. num = int(video_cap.get(7))
  31.  
  32. success, frame = video_cap.read()
  33. if success == False:
  34. assert('load video frames error')
  35. frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

動画をフレーム画像に分割し、フレーム画像ごとにToonificationを実行します。

  1. scale = 1
  2. kernel_1d = np.array([[0.125],[0.375],[0.375],[0.125]])
  3. # We proprocess the video by detecting the face in the first frame,
  4. # and resizing the frame so that the eye distance is 64 pixels.
  5. # Centered on the eyes, we crop the first frame to almost 400x400 (based on args.padding).
  6. # All other frames use the same resizing and cropping parameters as the first frame.
  7. paras = get_video_crop_parameter(frame, landmarkpredictor, padding=[200,200,200,200])
  8. if paras is None:
  9. print('no face detected!')
  10. else:
  11. h,w,top,bottom,left,right,scale = paras
  12. H, W = int(bottom-top), int(right-left)
  13. # for HR video, we apply gaussian blur to the frames to avoid flickers caused by bilinear downsampling
  14. # this can also prevent over-sharp stylization results.
  15. if scale <= 0.75:
  16. frame = cv2.sepFilter2D(frame, -1, kernel_1d, kernel_1d)
  17. if scale <= 0.375:
  18. frame = cv2.sepFilter2D(frame, -1, kernel_1d, kernel_1d)
  19. frame = cv2.resize(frame, (w, h))[top:bottom, left:right]
  20.  
  21. fourcc = cv2.VideoWriter_fourcc(*'mp4v')
  22. videoWriter = cv2.VideoWriter('./output/result.mp4', fourcc, video_cap.get(5), (4*W, 4*H))
  23. batch_size = 4
  24.  
  25. with torch.no_grad():
  26. batch_frames = []
  27. for i in tqdm(range(num)):
  28. if i == 0:
  29. I = align_face(frame, landmarkpredictor)
  30. I = transform(I).unsqueeze(dim=0).to(device)
  31. s_w = pspencoder(I)
  32. s_w = vtoonify.zplus2wplus(s_w)
  33. s_w[:,:7] = exstyle[:,:7]
  34. else:
  35. success, frame = video_cap.read()
  36. frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
  37. if scale <= 0.75:
  38. frame = cv2.sepFilter2D(frame, -1, kernel_1d, kernel_1d)
  39. if scale <= 0.375:
  40. frame = cv2.sepFilter2D(frame, -1, kernel_1d, kernel_1d)
  41. frame = cv2.resize(frame, (w, h))[top:bottom, left:right]
  42.  
  43. batch_frames += [transform(frame).unsqueeze(dim=0).to(device)]
  44.  
  45. if len(batch_frames) == batch_size or (i+1) == num:
  46. x = torch.cat(batch_frames, dim=0)
  47. batch_frames = []
  48. # parsing network works best on 512x512 images, so we predict parsing maps on upsmapled frames
  49. # followed by downsampling the parsing maps
  50. x_p = F.interpolate(parsingpredictor(2*(F.interpolate(x, scale_factor=2, mode='bilinear', align_corners=False)))[0],
  51. scale_factor=0.5, recompute_scale_factor=False).detach()
  52. # we give parsing maps lower weight (1/16)
  53. inputs = torch.cat((x, x_p/16.), dim=1)
  54. # d_s has no effect when backbone is toonify
  55. y_tilde = vtoonify(inputs, s_w.repeat(inputs.size(0), 1, 1), d_s = 0.5)
  56. y_tilde = torch.clamp(y_tilde, -1, 1)
  57. for k in range(y_tilde.size(0)):
  58. videoWriter.write(tensor2cv2(y_tilde[k].cpu()))
  59. videoWriter.release()
  60. video_cap.release()

出力結果は以下の通りです。

Video結果

まとめ

本記事では、VToonifyを用いて任意の画像・動画にアニメ風スタイルを転送する方法をご紹介しました。

また本記事では、機械学習を動かすことにフォーカスしてご紹介しました。
もう少し学術的に体系立てて学びたいという方には以下の書籍などがお勧めです。ぜひご一読下さい。


また動かせるだけから理解して応用できるエンジニアの足掛かりに下記のUdemyなどもお勧めです。

スポンサーリンク

参考文献

1.  論文 - VToonify: Controllable High-Resolution Portrait Video Style Transfer

2. GitHub - williamyang1991/VToonify

AIで副業ならココから!

まずは無料会員登録

プロフィール

メーカーで研究開発を行う現役エンジニア
組み込み機器開発や機会学習モデル開発に従事しています

本ブログでは最新AI技術を中心にソースコード付きでご紹介します


Twitter

カテゴリ

このブログを検索

ブログ アーカイブ

TeDokology