1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > CoNR让二次元动起来

CoNR让二次元动起来

时间:2019-12-21 02:02:13

相关推荐

CoNR让二次元动起来

CoNR让二次元动起来

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

效果展示:

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

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

1. 任务目标简洁介绍:

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

2. 简要原理介绍:

将一个已绑骨的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(非官方):/KurisuMakise004/MMD2UDP

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

手绘人物图片输入

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

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

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

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

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

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

4. 代码部分

卸载BML已经装好的opencv-python

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

下载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.66Uninstalling opencv-python-4.6.0.66:Successfully uninstalled opencv-python-4.6.0.66

解压UDP(这是论文提供的2个UDP,分别是双马尾和短发的)

import osimport 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 osprint(len(os.listdir("double_ponytail")))print(len(os.listdir("short_hair")))

14591459

准备结果输出的文件夹和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 osimport shutilif 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_ponytailif 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_hairif 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")

运行,开始生成

from paddle_conr import CoNRfrom data_loader import FileDataset,RandomResizedCropWithAutoCenteringAndZeroPaddingfrom paddle.io import DataLoaderimport paddleimport timefrom tqdm import tqdmimport numpy as npclass 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 imagesself.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 modelself.world_size = 1 #'world size'self.distributed = Falseself.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 dataloadersself.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 = Falseself.test_output_udp = Falseargs = get_args()def save_output(image_name, inputs_v, d_dir=".", crop=None):import cv2inputs_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,houtput_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_scalecv2.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 testingtrain_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_stamptime_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_stamptime_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()

图片转视频

# coding=utf-8import osimport cv2from PIL import Imagedef 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 osdef 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 osimport cv2import numpy as npfrom paddle.vision.transforms import CenterCrop,Resizefrom 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":continuecontent_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))

此文章为搬运

原项目链接

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。