CoNR让二次元动起来
使用论文CoNR让二次元手绘动起来,进行了官方权重转换
CoNR让二次元动起来
本项目已经将CoNR相关的权重从pytorch转成paddle,参考原作者的知乎讲解AI动画:让手绘动漫人设图动起来!项目已开源,B站官方漂亮姐姐简单讲解视频
效果展示:
论文名称: Collaborative Neural Rendering using Anime Character Sheets,简称CoNR
github官方项目地址:https://github.com/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(非官方):https://github.com/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.66
Uninstalling opencv-python-4.6.0.66:
Successfully uninstalled opencv-python-4.6.0.66
- 解压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
- 准备结果输出的文件夹和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")
- 运行,开始生成
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()
- 图片转视频
# 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))
此文章为搬运
原项目链接
更多推荐
所有评论(0)