CoNR让二次元动起来

本项目已经将CoNR相关的权重从pytorch转成paddle,参考原作者的知乎讲解AI动画:让手绘动漫人设图动起来!项目已开源,B站官方漂亮姐姐简单讲解视频

效果展示:

论文名称: Collaborative Neural Rendering using Anime Character Sheets,简称CoNR

github官方项目地址:https://github.com/megvii-research/CoNR

1. 任务目标简洁介绍:

旨在通过输入几张任意姿势,任意角度下的手绘人物图片,生成该人物生动的动画视频。

2. 简要原理介绍:

  1. 将一个已绑骨的3d模型导入某个动作后,AI会并将手绘图片贴到该姿势下的3D模型上,渲染得到图片。

3. 基本操作流程:

1.姿势输入

首先,需要将姿势和3D模型输入到3D软件内,并得到超密集姿势表示 Ultra-Dense Pose(UDP)。什么是超密集姿势表示?如图所示,对于UDP上的每一个点,其rgb值为,当人物处于A-pose形态时,该点对应的坐标值。

A-pose指的是人物直立,大臂向下30度的姿态,我们把它作为一种标准人物形态。 在这种表示下,可以看到UDP人物身体各个部位的颜色,在人物摆出不同的姿势时,都是一致的,比如右手的颜色一直是蓝色。

也就是说UDP记录了character某一个时刻的动作的信息。

这里对于具体如何得到UDP,我通过作者在github官方项目中回答一个issue让大家进行更好的理解:
在这里插入图片描述
,通过第二种方式逐帧输入一个动漫人物的PNG,然后得到逐帧的UDP,这种效果是远远比不上第一种方式通过3D模型的。

作者说可以非常轻松地通过对mesh的转换得到UDP(这种通过3D模型的方式得到UDP我也没试过),你只需要记录模型在A-pose下的时候,身体上每个点的坐标,转换为RGB值记录下来固定在该位置,并让这个五颜六色的模型摆出各种姿势即可。在这里插入图片描述

作者提供的MMD2UDP(非官方):https://github.com/KurisuMakise004/MMD2UDP

MMD是MikuMikuDance简称,MikuMikuDance,通常缩写为MMD,是一个免费的动画程序,让用户动画和制作3D动画电影,最初是为Vocaloid角色Hatsune Miku制作的。MikuMikuDance程序本身由Garnek(HiguchiM)编程,自创建以来经历了重大升级。

  1. 手绘人物图片输入

CoNR可以接收任意多张人物图片作为输入。

首先,让我们假设只有一张图片输入。模型将预测这张人物图片的UDP。在第一部分的介绍中,我们已经知道,右手部分的UDP一直是蓝色,因此,如果输入图像的UDP中,某个点的颜色和姿势UDP的某点相同,比如都是蓝色,那么就可以将输入图像的该点颜色直接移动到姿势图像中。对每个UDP上的点做该操作,就可以得到生成结果。如果您熟悉光流相关领域,这里的操作和光流的backward warpping是一致的.

在这里插入图片描述

但是,设想图片输入只有一张,且只有正面,当我们要生成背面的人物时,模型就没有了相应的参考。为此,我们加入了更多手绘图片,设计了一种联合推理策略:

CoNR的backbone是一个UNet,我们在此基础上做了一系列修改,以接收多头输入。每一张输入图片都有一个单独的推理路径,在UNet的解码器中,各个路径发生信息的交流(图中的cross-view message passing)。

具体而言,每一个路径都估计与目标姿势的光流和掩码,在每一个解码器层推理结束后,把该路径的输入图片进行warpping操作,得到一个目标姿势下的该路径结果,再将各个路径的结果通过掩码进行加权平均,作为该解码器层的最终输出。

最后,每个路径共享同一个最终解码器头,即图中的D4,在这里对所有路径的图片做最后的融合,并输出最终渲染结果。

4. 代码部分

  1. 卸载BML已经装好的opencv-python

因为BML环境预先安装的CV2会没有一些需要的东西,得先卸载再下载

  1. 下载opencv-python>=4.5.2
!pip uninstall opencv-python -y
 
!pip install opencv-python>=4.5.2
Found existing installation: opencv-python 4.6.0.66
Uninstalling opencv-python-4.6.0.66:
  Successfully uninstalled opencv-python-4.6.0.66
  1. 解压UDP(这是论文提供的2个UDP,分别是双马尾和短发的)
import os
import shutil 
if os.path.isdir("double_ponytail"):
    shutil.rmtree("double_ponytail")
    
if os.path.isdir("short_hair"):
    shutil.rmtree("short_hair")
!unzip -qo double_ponytail.zip
!unzip -qo short_hair.zip
import os
print(len(os.listdir("double_ponytail")))
print(len(os.listdir("short_hair")))
1459
1459
  1. 准备结果输出的文件夹和character_sheet

character_sheet 就是多张手绘人设图

A character sheet is the image collection of a specific character with multiple postures observed from different views, as shown in Figure 1.

Figure 1在这里插入图片描述

import os
import shutil
if os.path.isdir("character_sheet_double_ponytail"):
    shutil.rmtree("character_sheet_double_ponytail")
os.makedirs("character_sheet_double_ponytail")
!unzip -qo double_ponytail_images.zip -d character_sheet_double_ponytail
    
if os.path.isdir("character_sheet_short_hair"):
    shutil.rmtree("character_sheet_short_hair")
os.makedirs("character_sheet_short_hair")
!unzip -qo short_hair_images.zip -d character_sheet_short_hair

if os.path.isdir("result_double_ponytail"):
    shutil.rmtree("result_double_ponytail")
os.makedirs("result_double_ponytail")

if os.path.isdir("result_short_hair"):
    shutil.rmtree("result_short_hair")
os.makedirs("result_short_hair")
  1. 运行,开始生成
from paddle_conr import CoNR
from data_loader import FileDataset,RandomResizedCropWithAutoCenteringAndZeroPadding
from paddle.io import DataLoader
import paddle
import time
from tqdm import tqdm
import numpy as np

class get_args():
    def __init__(self,input_udp = True,test_input_person_images = "character_sheet_double_ponytail/double_ponytail_images", 
    test_input_poses_images = "double_ponytail",out_folder = "result_double_ponytail"):
        self.test_input_person_images = test_input_person_images #人物各个角度图片路径,至少前后左右4张
        self.test_input_poses_images = test_input_poses_images  #Directory to input UDP sequences or pose images
        self.test_checkpoint_dir = "data/data167835" #CoNR权重路径
        self.test_output_dir = out_folder # Directory to output images'
        self.dataloader_imgsize = 256 #Input image size of the model
        self.world_size = 1 #'world size'
        self.distributed = False
        self.test_pose_use_parser_udp = True #Whether to use UDP detector to generate UDP from pngs, \
                              #pose input MUST be pose images instead of UDP sequences \
                              #while True'
        self.dataloaders = 1#Num of dataloaders
        self.local_rank = 0 #'local_rank, DON\'T change it'
        self.test_output_video = True #Whether to output the final result of CoNR, \
                              #images will be output to test_output_dir while True.
        self.test_output_udp = True #Whether to output UDP generated from UDP detector, \
                              #this is meaningful ONLY when test_input_poses_images \
                              #is not UDP sequences but pose images. Meanwhile, \
                              #test_pose_use_parser_udp need to be True'
        if input_udp:
            self.test_pose_use_parser_udp = False
            self.test_output_udp = False

args = get_args()

def save_output(image_name, inputs_v, d_dir=".", crop=None):
    import cv2

    inputs_v = inputs_v.detach().squeeze()
    input_np = paddle.clip(inputs_v*255, 0, 255).astype("float32").numpy().transpose(
        (1, 2, 0))
    # print("input_np",input_np.shape,input_np.dtype)
    # cv2.setNumThreads(1)
    out_render_scale = cv2.cvtColor(input_np, cv2.COLOR_RGBA2BGRA)
    if crop is not None:
        crop = crop.cpu().numpy()[0]
        output_img = np.zeros((crop[0], crop[1], 4), dtype=np.uint8)
        before_resize_scale = cv2.resize(
            out_render_scale, (crop[5]-crop[4]+crop[8]+crop[9], crop[3]-crop[2]+crop[6]+crop[7]), interpolation=cv2.INTER_AREA)  # w,h
        output_img[crop[2]:crop[3], crop[4]:crop[5]] = before_resize_scale[crop[6]:before_resize_scale.shape[0] -
                                                                           crop[7], crop[8]:before_resize_scale.shape[1]-crop[9]]
    else:
        output_img = out_render_scale
    cv2.imwrite(d_dir+"/"+image_name.split(os.sep)[-1]+'.png',
                output_img
                )



def test():
    source_names_list = []
    for name in sorted(os.listdir(args.test_input_person_images)):
        thissource = os.path.join(args.test_input_person_images, name)
        if os.path.isfile(thissource):
            source_names_list.append(thissource)
        if os.path.isdir(thissource):
            print("skipping empty folder :"+thissource)
    print(source_names_list)
    image_names_list = []
    for name in sorted(os.listdir(args.test_input_poses_images)):
        thistarget = os.path.join(args.test_input_poses_images, name)
        if os.path.isfile(thistarget):
            image_names_list.append([thistarget, *source_names_list])
        if os.path.isdir(thistarget):
            print("skipping folder :"+thistarget)
    # print(image_names_list)

    print("---building models")
    conrmodel = CoNR(args)
    conrmodel.load_model(path=args.test_checkpoint_dir)
    # conrmodel.dist()
    infer(args, conrmodel, image_names_list)


def infer(args, humanflowmodel, image_names_list):
    print("---test images: ", len(image_names_list))
    test_salobj_dataset = FileDataset(image_names_list=image_names_list,
                                      fg_img_lbl_transform=
                                          RandomResizedCropWithAutoCenteringAndZeroPadding(
                                              (args.dataloader_imgsize, args.dataloader_imgsize), scale=(1, 1), ratio=(1.0, 1.0), center_jitter=(0.0, 0.0)
                                          ),
                                      shader_pose_use_gt_udp_test=not args.test_pose_use_parser_udp,
                                      shader_target_use_gt_rgb_debug=False
                                      )
    # for i in test_salobj_dataset:
        # print(i)
    # sampler = data_sampler(test_salobj_dataset, shuffle=False,
    #                        distributed=args.distributed)
    train_data = DataLoader(test_salobj_dataset,
                            batch_size=1,
                            shuffle=False,
                            # batch_sampler=sampler, 
                            num_workers=args.dataloaders)

    # start testing

    train_num = train_data.__len__()
    time_stamp = time.time()
    prev_frame_rgb = []
    prev_frame_a = []
    
    pbar = tqdm(range(train_num), ncols=100)
    for i, data in enumerate(train_data):
        data_time_interval = time.time() - time_stamp
        time_stamp = time.time()
        with paddle.no_grad():
            data["character_images"] =paddle.concat(
                [data["character_images"], *prev_frame_rgb], axis=1)
            data["character_masks"] = paddle.concat(
                [data["character_masks"], *prev_frame_a], axis=1)
            data = humanflowmodel.data_norm_image(data)
            pred = humanflowmodel.model_step(data, training=False)
            # remember to call  humanflowmodel.reset_charactersheet() if you change character .

        train_time_interval = time.time() - time_stamp
        time_stamp = time.time()
        if args.local_rank == 0:
            pbar.set_description(f"Epoch {i}/{train_num}")
            pbar.set_postfix({"data_time": data_time_interval, "train_time":train_time_interval})
            pbar.update(1)

        with paddle.no_grad():

            if args.test_output_video:
                pred_img = pred["shader"]["y_weighted_warp_decoded_rgba"]
                save_output(
                    str(int(data["imidx"].cpu().item())), pred_img, args.test_output_dir, crop=data["pose_crop"])
            
            if args.test_output_udp:
                pred_img = pred["shader"]["x_target_sudp_a"]
                save_output(
                    "udp_"+str(int(data["imidx"].cpu().item())), pred_img, args.test_output_dir)
test()
  1. 图片转视频
# coding=utf-8
import os
import cv2
from PIL import Image
def makevideo(path, fps):
    """ 将图片合成视频. path: 视频路径,fps: 帧率 """
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    path1 = r'result_double_ponytail'
    img_list = os.listdir(path1)
    im = Image.open("result_double_ponytail"+'/1.png')
    print(im.size)
    vw = cv2.VideoWriter(path, fourcc, fps, im.size)
    for i in range(len(img_list)):
        frame = cv2.imread(path1 + '/' + str(i)+ ".png" )
        vw.write(frame)

if __name__ == '__main__':
    video_path = './output_result_double_ponytail.mp4'
    makevideo(video_path, 30)  # 图片转视频

5. 总结

该论文我认为是非常不错的,但是其中直接输入图片png通过UDP检测器得到的png精度并不高,已经通过issue询问作者了,作者团队正在做V2,提升这方面。

同时经过测试我发现了,其实CoNR最后背景看上去都是全黑的,但是实际上有些地方只是RGB的值很低,其实并不是纯黑色。

该结论我通过下方代码发现,你们也可以自己试试:

content_imname = "result_double_ponytail/5.png"
content_image_np = cv2.cvtColor(cv2.imread(content_imname, flags=cv2.IMREAD_COLOR),cv2.COLOR_BGR2RGB)
content_image_mask = np.sum(content_image_np.astype("bool"),axis=2)
content_image_mask = (content_image_mask ==3).astype("uint8")


content_image_mask = np.stack([content_image_mask,content_image_mask,content_image_mask],2)
bg_image_mask = np.stack([bg_image_mask,bg_image_mask,bg_image_mask],2)

cv2.imwrite('cont_mask1.jpg',cv2.cvtColor(np.clip(content_image_mask.astype(np.uint8)*255,0,255),cv2.COLOR_RGB2BGR))

mask图片展示:在这里插入图片描述

6. 如果有需求,可以通过下方代码进行替换黑色背景

除人物以外的mask噪点我通过设置阈值和Opencv的腐蚀进行一定程度的解决

import os

def extract_frames(video_name, out_folder):
    if os.path.exists(out_folder):
        os.system('rm -rf ' + out_folder + '/*')
        os.system('rm -rf ' + out_folder)
    os.makedirs(out_folder)
    cmd = 'ffmpeg -v 0 -i %s  -q 0 %s/%s.jpg' % (video_name,
                                                      out_folder, '%08d')
    os.system(cmd)
stem(cmd)
extract_frames("output_mp4/output2.mp4","video_dir")
import os
import cv2
import numpy as np
from paddle.vision.transforms import CenterCrop,Resize
from scipy import ndimage 

path = "video_dir"
mp4_list = os.listdir(path)


if os.path.isdir("result_withbg"):
    shutil.rmtree("result_withbg")
os.makedirs("result_withbg")

bg_imname = "13.jpg"
bg_image_np = cv2.cvtColor(cv2.imread(bg_imname, flags=cv2.IMREAD_COLOR),cv2.COLOR_BGR2RGB)
transform = Resize(cv2.imread(os.path.join(path,mp4_list[0]), flags=cv2.IMREAD_COLOR).shape[:2])
bg_image_np = transform(bg_image_np)
for i,one_img_path in enumerate(mp4_list):
    one_img_path = os.path.join(path,one_img_path)
    if one_img_path.split(".")[-1] != "jpg":
        continue
    content_image_np = cv2.cvtColor(cv2.imread(one_img_path, flags=cv2.IMREAD_COLOR),cv2.COLOR_BGR2RGB)
    content_image_mask = np.max(content_image_np,axis=2)
    content_image_mask = (content_image_mask >10).astype("uint8")
    kernel = np.ones((3, 3), dtype=np.uint8)
    content_image_mask = cv2.erode(content_image_mask, kernel, iterations=1)
    bg_image_mask = 1 - content_image_mask
    # scipy.ndimage.binary_fill_holes(img, structure=None, output=None, origin=0)
    # bg_image_mask = ndimage.binary_fill_holes(bg_image_mask, structure=np.full((1,1),1)).astype(int)

    content_image_mask = np.stack([content_image_mask,content_image_mask,content_image_mask],2)
    bg_image_mask = np.stack([bg_image_mask,bg_image_mask,bg_image_mask],2)

    # content_image_mask.shape
    # cv2.imwrite('cont_mask1.jpg',cv2.cvtColor(np.clip(content_image_mask.astype(np.uint8)*255,0,255),cv2.COLOR_RGB2BGR))
    # cv2.imwrite('bg_image_mask.jpg',cv2.cvtColor(np.clip(bg_image_mask.astype(np.uint8)*255,0,255),cv2.COLOR_RGB2BGR))
    image_mask = (np.array(1).astype("uint8") - content_image_mask)
    # print(image_mask.shape)
    # cv2.imwrite('bg_mask.jpg',cv2.cvtColor(np.clip(image_mask.astype(np.uint8)*255,0,255),cv2.COLOR_RGB2BGR))

    # # print(bg_image_np.shape)
    img_np = bg_image_np*(bg_image_mask)+content_image_np*content_image_mask
    # img_np = np.clip(img_np,0,255)
    cv2.imwrite('result_withbg/'+str(i)+'.jpg',cv2.cvtColor(img_np.astype(np.uint8),cv2.COLOR_RGB2BGR))
 

此文章为搬运
原项目链接

Logo

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

更多推荐