为了你有更好的体验,本项目建议在搭配V100 32G显存的至尊版环境下运行。(16G高级版可能会出现dlib包安装错误。)

项目背景

本项目由PaddleGAN的Photo2cartoon项目改造升级,项目的灵感来源于当前越来越多的视频博主出于容貌不自信或者其他原因不方便露脸。传统的换脸方式有iPhone自带的应用(拟我表情)和视频剪辑等方式,但以上方式存在样式和设备局限或技术要求高等问题,但通过本项目可以实现一行代码把所有视频的大头人像转换成卡通人像,做到千(美)人(白)千(亮)面(肤),并且还自动提取和合并原视频的音频到新视频当中,无需后期再人工编辑对齐。

环境安装

安装PaddleGAN最新版本

!git clone https://gitee.com/paddlepaddle/PaddleGAN -b develop
!pip install -r PaddleGAN/requirements.txt
%cd PaddleGAN
!python setup.py install
%cd ../

通过源码编译安装dlib

此处避免直接使用pip安装dlib包,可能会导致内存溢出。

!git clone https://hub.fastgit.org/davisking/dlib.git
#需先安装和更新cmake和boost包
!pip install cmake
!pip install boost
%cd dlib/
!python setup.py install 
%cd ../

重启内核

点击一次工具栏中的重启按钮,加载刚刚安装好的依赖环境。

改进思路

原项目(Photo2cartoon)参考了U-GAT-IT论文,利用AdaLIN函数(自适应正则化函数)和新的注意力模块实现图像风格转换,关注这个模块详细可查看PaddleGAN文档。这里主要剖析一下原项目封装好的Photo2CartoonPredictor这个类。PaddleGAN/ppgan/apps/photo2cartoon_predictor.py 源代码如下:

import os
import cv2
from PIL import Image
import numpy as np

import paddle
from paddle.utils.download import get_path_from_url
from ppgan.faceutils.dlibutils import align_crop
from ppgan.faceutils.face_segmentation import FaceSeg
from ppgan.models.generators import ResnetUGATITP2CGenerator
from .base_predictor import BasePredictor


P2C_WEIGHT_URL = "https://paddlegan.bj.bcebos.com/models/photo2cartoon_genA2B_weight.pdparams"


class Photo2CartoonPredictor(BasePredictor):
    def __init__(self, output_path='output', weight_path=None):
        self.output_path = output_path
        if not os.path.exists(self.output_path):
            os.makedirs(self.output_path)

        if weight_path is None:
            cur_path = os.path.abspath(os.path.dirname(__file__))
            weight_path = get_path_from_url(P2C_WEIGHT_URL, cur_path)

        self.genA2B = ResnetUGATITP2CGenerator()
        params = paddle.load(weight_path)
        self.genA2B.set_state_dict(params)
        self.genA2B.eval()

        self.faceseg = FaceSeg()

    def run(self, image_path):
        image = Image.open(image_path)
        face_image = align_crop(image)
        face_mask = self.faceseg(face_image)

        face_image = cv2.resize(face_image, (256, 256), interpolation=cv2.INTER_AREA)
        face_mask = cv2.resize(face_mask, (256, 256))[:, :, np.newaxis] / 255.
        face = (face_image * face_mask + (1 - face_mask) * 255) / 127.5 - 1

        face = np.transpose(face[np.newaxis, :, :, :], (0, 3, 1, 2)).astype(np.float32)
        face = paddle.to_tensor(face)

        # inference
        with paddle.no_grad():
            cartoon = self.genA2B(face)[0][0]

        # post-process
        cartoon = np.transpose(cartoon.numpy(), (1, 2, 0))
        cartoon = (cartoon + 1) * 127.5
        cartoon = (cartoon * face_mask + (1 - face_mask) * 255).astype(np.uint8)

        pnoto_save_path = os.path.join(self.output_path, 'p2c_photo.png')
        cv2.imwrite(pnoto_save_path, cv2.cvtColor(face_image, cv2.COLOR_RGB2BGR))
        cartoon_save_path = os.path.join(self.output_path, 'p2c_cartoon.png')
        cv2.imwrite(cartoon_save_path, cv2.cvtColor(cartoon, cv2.COLOR_RGB2BGR))

        print("Cartoon image has been saved at '{}'.".format(cartoon_save_path))
        return cartoon

通过阅读源代码,可以清晰看到真人头像图片转卡通图片的流程。原作者先是写了一个align_crop的函数,根据人脸关键点信息做了人像位置纠偏,并通过脸部位置识别后往四周拓展坐标裁剪出人头的区域,然后用了PaddleGAN内置的人脸处理库中的FaceSeg语义分割函数,从上一步输入图像中分割出人像。

那既然已经摸清了源代码的关系,那么就可以根据源代码进行修改,使得满足我们视频化的需求。首先,我们需要把视频分成若干帧,每一帧就等于是一张图片。然后我们还是需要先识别脸部,并把脸部范围坐标拓宽到人头区域,但与源代码的直接裁剪出人头图像不同,我们需要保留头像外的图像信息,因此我们需要记录人像的坐标信息,提取人像进行卡通化处理之后,需要把处理后的卡通人像重新paste到原图中。最后把原视频的声音提取出来,合并到新的视频即可。

代码解析

import cv2
from PIL import Image
import numpy as np
import paddle
from ppgan.faceutils.face_segmentation import FaceSeg
from ppgan.models.generators import ResnetUGATITP2CGenerator
from face_align import align_crop
from ppgan.apps.base_predictor import BasePredictor
from paddle.utils.download import get_weights_path_from_url

params = paddle.load(get_weights_path_from_url('https://paddlegan.bj.bcebos.com/models/photo2cartoon_genA2B_weight.pdparams'))
genA2B = ResnetUGATITP2CGenerator()
genA2B.set_state_dict(params)
genA2B.eval()

class Predictor(BasePredictor):
    def __init__(self, output_path='output'):

        self.faceseg = FaceSeg()

    def run(self, image_array):
        image = Image.fromarray(cv2.cvtColor(image_array,cv2.COLOR_BGR2RGB))
        face_image,face_bbox = align_crop(image) #裁剪人头区域
        face_mask = self.faceseg(face_image) #人像分割
        face_image_pil = Image.fromarray(face_image)
        face_image_size = face_image_pil.size

        face_image = cv2.resize(face_image, (256, 256), interpolation=cv2.INTER_AREA)
        face_mask = cv2.resize(face_mask, (256, 256))[:, :, np.newaxis] / 255.
        face = (face_image * face_mask + (1 - face_mask) * 255) / 127.5 - 1

        face = np.transpose(face[np.newaxis, :, :, :], (0, 3, 1, 2)).astype(np.float32)
        face = paddle.to_tensor(face)

        # inference
        with paddle.no_grad():
            cartoon = genA2B(face)[0][0]

        # post-process
        cartoon = np.transpose(cartoon.numpy(), (1, 2, 0))
        cartoon = (cartoon + 1) * 127.5
        cartoon = (cartoon * face_mask + (1 - face_mask) * 255).astype(np.uint8)
        #还原原图大小
        cartoon = cv2.resize(cartoon,(face_image_size[0],face_image_size[1]))
        #去除白色背景
        cartoon = Image.fromarray(cartoon).convert('RGBA')
        for i in range(0,cartoon.size[0]):     # 遍历所有长度的点
            for j in range(0,cartoon.size[1]):       # 遍历所有宽度的点
                data = cartoon.getpixel((i,j))  # 获取一个像素
                if (data.count(255) == 4):  # RGBA都是255,改成透明色
                    cartoon.putpixel((i,j),(255,255,255,0))
        image = image.convert('RGBA')
        r, g, b, a = cartoon.split()
        image.paste(cartoon,(face_bbox),mask=a) #替换原图
        return image
import cv2
import os
from PIL import Image
import numpy as np 
from moviepy.editor import * 
import warnings
import logging
from tempfile import NamedTemporaryFile,gettempdir

warnings.filterwarnings('ignore') #忽略warning,如需要修改和调试代码请注释掉

video2Cartoon = Predictor()
logging.info('正在转换,如果视频文件时长过长,消耗时间可能长达数十分钟,请耐心等候……')
input_Video = 'video/driving_video.mp4' #输入视频路径
output_Video = 'yiyi.mp4' #视频输出路径
temp_Video = NamedTemporaryFile(suffix='.mp4',dir='/tmp',delete=False).name
vidcap = cv2.VideoCapture(input_Video)
audio_ori = VideoFileClip(input_Video).audio #提取音频
success, image = vidcap.read()

frame = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT)) #帧数
fps = int(vidcap.get(5)) #帧率
frame_height = int(vidcap.get(cv2.CAP_PROP_FRAME_HEIGHT)) #帧高度
frame_width = int(vidcap.get(cv2.CAP_PROP_FRAME_WIDTH)) #帧宽度

videoWriter = cv2.VideoWriter(temp_Video, cv2.VideoWriter_fourcc(*"mp4v"), fps,(frame_width,frame_height))

while success:
    try:
        img2Cartoon = cv2.cvtColor(np.asarray(video2Cartoon.run(image)),cv2.COLOR_RGB2BGR)
        videoWriter.write(img2Cartoon)
    except:
        videoWriter.write(image)
    success, image = vidcap.read()
videoWriter.release()

#插入音频
audio_Output = VideoFileClip(temp_Video).set_audio(audio_ori)
audio_Output.write_videofile(output_Video)
logging.info('转换完成,文件已保存到{}'.format(output_Video))

生成示例视频大概需要6分钟(V100 32G环境)

一键运行脚本

python v2c.py --input_video 原视频路径 --output 输出视频路径

!python v2c.py --input_video video/driving_video.mp4 --output ./yiyi.mp4

写在最后

目前本项目支持大部分的视频转换,但并不是完美的,首先由于目前缺乏头部分割模型,目前用的方法是识别脸部然后expand裁剪出头部,因此会出现裁剪到衣领位置或者头发不完整,甚至导致个别竖版视频出现推理视频和原视频头像偏移位置较大的情况,这个暂时可以根据实际视频需要通过修改face_align.py中的crop()函数expand部分代码解决。希望有兴趣的开发者fork并完善开源,大家相互交流学习。

参考项目

PaddleGAN
千星人像卡通化项目登陆PaddleGAN

PPDE福利环节

最后,有请一位知名PPDE为大家献上才艺展示!!!欢迎大家一键三连!!!

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐