3D 建模费时费力,Python 让照片秒变模型

2021-04-21 15:10:10 浏览数 (2)

作者 | 李秋键

出品 | AI科技大本营(ID:rgznai100)

引言:

不论是在游戏还是动画中,三维模型的引用始终都很广泛。但是三维模型的搭建却始终存在着较大的难点,一是模型的搭建是个非常费时费力的技术,其次学习三维建模也需要掌握较多的工具,对于日常没有接触过的人而言难度较大。

所以,今天我们将实现一个可以将照片的人体创建成三维模型FBX格式的项目。通过这个项目,可以大量的节省建模工作者的时间,对游戏开发者和动画制作者而言将会是很大的福利。完整代码见文末!

其中生成的图片效果如下可见:

使用3维模型查看效果如下:

模型建立

1.1 环境要求

本次环境使用的是python3.6.5 windows平台,主要用的库有:

  • Argparse库是python自带的命令行参数解析包,可以用来方便地读取命令行参数。;
  • glob获取本地文件,在这里用来快速获取训练数据集;
  • numpy模块用来矩阵和数据的运算处理,其中也包括和深度学习框架之间的交互等。
  • Cv2模块是python的opencv模型,用来图像读入,存储和边缘计算等图像处理;
  • Torch模块是深度学习框架。torch不仅更加灵活,支持动态图,而且提供了Python接口。它是由Torch7团队开发,是一个以Python优先的深度学习框架,不仅能够实现强大的GPU加速,同时还支持动态神经网络,这是很多主流深度学习框架比如Tensorflow等都不支持的。PyTorch既可以看作加入了GPU支持的numpy,同时也可以看成一个拥有自动求导功能的强大的深度神经网络。
  • Matplotlib模块用来可视化训练效果等数据图的制作。
  • PIL是python中用来处理图形的pillow库。

1.2 图像处理

对于需要处理成三维模型的图片,我们需要提供照片所对应的骨骼点json文件。Json文件的生成,可以使用openpose生成。其中openpose环境的搭建需要利用cmake工具进行编译,包括大量需要版本对应的C 环境等,较为复杂,这里不再阐述。其中主要使用指令如下:分别是对摄像头、本地图片等生成json文件。

代码语言:javascript复制
OpenPoseDemo.exe --body 1 --face --face_detector 1  --hand --hand_detector 1
OpenPoseDemo.exe --camera 0 --write_json 1.json  --model_pose COCO
OpenPoseDemo.exe --image_dir json/  --write_json 1.json  --model_pose COCO
OpenPoseDemo.exe --camera 0 --write_json 1.json  --model_pose  BODY_25
OpenPoseDemo.exe --image_dir json/  --write_json 1.json  --model_pose  BODY_25

1.3 算法搭建

(1)本文所使用的是算法是PiFuHD算法。需要分别定义生成模型,模型颜色和模型贴图的函数,代码如下:

代码语言:javascript复制
def save_obj_mesh(mesh_path, verts, faces=None):
    file = open(mesh_path, 'w')
    for v in verts:
        file.write('v %.4f %.4f %.4fn' % (v[0], v[1], v[2]))
    if faces is not None:
        for f in faces:
            if f[0] == f[1] or f[1] == f[2] or f[0] == f[2]:
                continue
            f_plus = f   1
            file.write('f %d %d %dn' % (f_plus[0], f_plus[2], f_plus[1]))
    file.close()
def save_obj_mesh_with_color(mesh_path, verts, faces, colors):
    file = open(mesh_path, 'w')
    for idx, v in enumerate(verts):
        c = colors[idx]
        file.write('v %.4f %.4f %.4f %.4f %.4f %.4fn' % (v[0], v[1], v[2], c[0], c[1], c[2]))
    for f in faces:
        f_plus = f   1
        file.write('f %d %d %dn' % (f_plus[0], f_plus[2], f_plus[1]))
    file.close()
def save_obj_mesh_with_uv(mesh_path, verts, faces, uvs):
    file = open(mesh_path, 'w')
    for idx, v in enumerate(verts):
        vt = uvs[idx]
        file.write('v %.4f %.4f %.4fn' % (v[0], v[1], v[2]))
        file.write('vt %.4f %.4fn' % (vt[0], vt[1]))
    for f in faces:
        f_plus = f   1
        file.write('f %d/%d %d/%d %d/%dn' % (f_plus[0], f_plus[0],
                                              f_plus[2], f_plus[2],
                                              f_plus[1], f_plus[1]))
    file.close()

(2)PiFuHD算法是使用图片中的上下文信息,通过对原始不清晰图像先进行了超分辨率重建获得,其中高分辨率详细信息是通过在相似的轻量级PIFu网络中使用这些第一个3D输出作为高分辨率输入来添加的。

代码语言:javascript复制
def gen_mesh(res, net, cuda, data, save_path, thresh=0.5, use_octree=True, components=False):
    image_tensor_global = data['img_512']
    image_tensor = data['img']
    calib_tensor = data['calib']
    net.filter_global(image_tensor_global)
    net.filter_local(image_tensor[:,None])
    try:
        if net.netG.netF is not None:
            image_tensor_global = torch.cat([image_tensor_global, net.netG.nmlF], 0)
        if net.netG.netB is not None:
            image_tensor_global = torch.cat([image_tensor_global, net.netG.nmlB], 0)
    except:
        pass
    b_min = data['b_min']
    b_max = data['b_max']
    try:
        save_img_path = save_path[:-4]   '.png'
        save_img_list = []
        for v in range(image_tensor_global.shape[0]):
            save_img = (np.transpose(image_tensor_global[v].detach().cpu().numpy(), (1, 2, 0)) * 0.5   0.5)[:, :, ::-1] * 255.0
            save_img_list.append(save_img)
        save_img = np.concatenate(save_img_list, axis=1)
        cv2.imwrite(save_img_path, save_img)
        verts, faces, _, _ = reconstruction(
            net, cuda, calib_tensor, res, b_min, b_max, thresh, use_octree=use_octree, num_samples=50000)
        verts_tensor = torch.from_numpy(verts.T).unsqueeze(0).to(device=cuda).float()
        # if 'calib_world' in data:
        #     calib_world = data['calib_world'].numpy()[0]
        #     verts = np.matmul(np.concatenate([verts, np.ones_like(verts[:,:1])],1), inv(calib_world).T)[:,:3]
        color = np.zeros(verts.shape)
        interval = 50000
        for i in range(len(color) // interval   1):
            left = i * interval
            if i == len(color) // interval:
                right = -1
            else:
                right = (i   1) * interval
            net.calc_normal(verts_tensor[:, None, :, left:right], calib_tensor[:,None], calib_tensor)
            nml = net.nmls.detach().cpu().numpy()[0] * 0.5   0.5
            color[left:right] = nml.T
        save_obj_mesh_with_color(save_path, verts, faces, color)
    except Exception as e:
        print(e)

(3)人体表面颜色重建。基于上一步的重建深度特征,这是模型只需要关注于模型各点的颜色即可,而不需要关注潜在的3D信息。

代码语言:javascript复制
def gen_mesh_imgColor(res, net, cuda, data, save_path, thresh=0.5, use_octree=True, components=False):
    image_tensor_global = data['img_512'].to(device=cuda)
    image_tensor = data['img'].to(device=cuda)
    calib_tensor = data['calib'].to(device=cuda)
    net.filter_global(image_tensor_global)
    net.filter_local(image_tensor[:,None])
    try:
        if net.netG.netF is not None:
            image_tensor_global = torch.cat([image_tensor_global, net.netG.nmlF], 0)
        if net.netG.netB is not None:
            image_tensor_global = torch.cat([image_tensor_global, net.netG.nmlB], 0)
    except:
        pass
    b_min = data['b_min']
    b_max = data['b_max']
    try:
        save_img_path = save_path[:-4]   '.png'
        save_img_list = []
        for v in range(image_tensor_global.shape[0]):
            save_img = (np.transpose(image_tensor_global[v].detach().cpu().numpy(), (1, 2, 0)) * 0.5   0.5)[:, :, ::-1] * 255.0
            save_img_list.append(save_img)
        save_img = np.concatenate(save_img_list, axis=1)
        cv2.imwrite(save_img_path, save_img)
        verts, faces, _, _ = reconstruction(
            net, cuda, calib_tensor, res, b_min, b_max, thresh, use_octree=use_octree, num_samples=100000)
        verts_tensor = torch.from_numpy(verts.T).unsqueeze(0).to(device=cuda).float()
        # if this returns error, projection must be defined somewhere else
        xyz_tensor = net.projection(verts_tensor, calib_tensor[:1])
        uv = xyz_tensor[:, :2, :]
        color = index(image_tensor[:1], uv).detach().cpu().numpy()[0].T
        color = color * 0.5   0.5
        if 'calib_world' in data:
            calib_world = data['calib_world'].numpy()[0]
            verts = np.matmul(np.concatenate([verts, np.ones_like(verts[:,:1])],1), inv(calib_world).T)[:,:3]
        save_obj_mesh_with_color(save_path, verts, faces, color)
    except Exception as e:
        print(e)

模型文件的生成

2.1 模型参数的定义

其中模型参数的定义主要是使用Argparse库,其中参数包括输入图像、读取的尺寸、模式、学习率等定义。代码如下:

代码语言:javascript复制
class BaseOptions():
    def __init__(self):
        self.initialized = False
        self.parser = None
    def initialize(self, parser):
        # Datasets related
        g_data = parser.add_argument_group('Data')
        g_data.add_argument('--dataset', type=str, default='renderppl', help='dataset name')
        g_data.add_argument('--dataroot', type=str, default='./data',
                            help='path to images (data folder)')
        g_data.add_argument('--loadSize', type=int, default=512, help='load size of input image')
        # Experiment related
        g_exp = parser.add_argument_group('Experiment')
        g_exp.add_argument('--name', type=str, default='',
                           help='name of the experiment. It decides where to store samples and models')
        g_exp.add_argument('--debug', action='store_true', help='debug mode or not')
        g_exp.add_argument('--mode', type=str, default='inout', help='inout || color')
        # Training related
        g_train = parser.add_argument_group('Training')
        g_train.add_argument('--tmp_id', type=int, default=0, help='tmp_id')
        g_train.add_argument('--gpu_id', type=int, default=0, help='gpu id for cuda')
        g_train.add_argument('--batch_size', type=int, default=32, help='input batch size')
        g_train.add_argument('--num_threads', default=1, type=int, help='# sthreads for loading data')
        g_train.add_argument('--serial_batches', action='store_true',
                             help='if true, takes images in order to make batches, otherwise takes them randomly')
        g_train.add_argument('--pin_memory', action='store_true', help='pin_memory')
        g_train.add_argument('--learning_rate', type=float, default=1e-3, help='adam learning rate')
        g_train.add_argument('--num_iter', type=int, default=30000, help='num iterations to train')
        g_train.add_argument('--freq_plot', type=int, default=100, help='freqency of the error plot')
        g_train.add_argument('--freq_mesh', type=int, default=20000, help='freqency of the save_checkpoints')
        g_train.add_argument('--freq_eval', type=int, default=5000, help='freqency of the save_checkpoints')
        g_train.add_argument('--freq_save_ply', type=int, default=5000, help='freqency of the save ply')
        g_train.add_argument('--freq_save_image', type=int, default=100, help='freqency of the save input image')
        g_train.add_argument('--resume_epoch', type=int, default=-1, help='epoch resuming the training')
        g_train.add_argument('--continue_train', action='store_true', help='continue training: load the latest model')
        g_train.add_argument('--finetune', action='store_true', help='fine tuning netG in training C')
        # Testing related
        g_test = parser.add_argument_group('Testing')
        g_test.add_argument('--resolution', type=int, default=512, help='# of grid in mesh reconstruction')
        g_test.add_argument('--no_numel_eval', action='store_true', help='no numerical evaluation')
        g_test.add_argument('--no_mesh_recon', action='store_true', help='no mesh reconstruction')
        # Sampling related
        g_sample = parser.add_argument_group('Sampling')
        g_sample.add_argument('--num_sample_inout', type=int, default=6000, help='# of sampling points')
        g_sample.add_argument('--num_sample_surface', type=int, default=0, help='# of sampling points')
        g_sample.add_argument('--num_sample_normal', type=int, default=0, help='# of sampling points')
        g_sample.add_argument('--num_sample_color', type=int, default=0, help='# of sampling points')
        g_sample.add_argument('--num_pts_dic', type=int, default=1, help='# of pts dic you load')
        g_sample.add_argument('--crop_type', type=str, default='fullbody', help='Sampling file name.')
        g_sample.add_argument('--uniform_ratio', type=float, default=0.1, help='maximum sigma for sampling')
        g_sample.add_argument('--mask_ratio', type=float, default=0.5, help='maximum sigma for sampling')
        g_sample.add_argument('--sampling_parts', action='store_true', help='Sampling on the fly')
        g_sample.add_argument('--sampling_otf', action='store_true', help='Sampling on the fly')
        g_sample.add_argument('--sampling_mode', type=str, default='sigma_uniform', help='Sampling file name.')
        g_sample.add_argument('--linear_anneal_sigma', action='store_true', help='linear annealing of sigma')
        g_sample.add_argument('--sigma_max', type=float, default=0.0, help='maximum sigma for sampling')
        g_sample.add_argument('--sigma_min', type=float, default=0.0, help='minimum sigma for sampling')
        g_sample.add_argument('--sigma', type=float, default=1.0, help='sigma for sampling')
        g_sample.add_argument('--sigma_surface', type=float, default=1.0, help='sigma for sampling')

2.1 模型测试的定义

设立模型测试基本参数和使用方法。

代码语言:javascript复制
from .recon import reconWrapper
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input_path', type=str, default='./sample_images')
parser.add_argument('-o', '--out_path', type=str, default='./results')
parser.add_argument('-c', '--ckpt_path', type=str, default='./checkpoints/pifuhd.pt')
parser.add_argument('-r', '--resolution', type=int, default=512)
parser.add_argument('--use_rect', action='store_true', help='use rectangle for cropping')
args = parser.parse_args()
resolution = str(args.resolution)
start_id = -1
end_id = -1
cmd = ['--dataroot', args.input_path, '--results_path', args.out_path,
       '--loadSize', '1024', '--resolution', resolution, '--load_netMR_checkpoint_path', 
       args.ckpt_path,
       '--start_id', '%d' % start_id, '--end_id', '%d' % end_id]
reconWrapper(cmd, args.use_rect)

使用方法

进入到项目文件夹,执行“python -m apps.simple_test ”即可运行。

源码

完整代码下载链接:

https://pan.baidu.com/s/1dVo57yp89GiNExSfCisPMA

提取码:tetg

作者简介:李秋键,CSDN博客专家,CSDN达人课作者。硕士在读于中国矿业大学,开发有taptap竞赛获奖等。

0 人点赞