|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import logging |
|
import os |
|
|
|
import numpy as np |
|
import torch |
|
from PIL import Image |
|
|
|
from .differentiable_renderer.mesh_render import MeshRender |
|
from .utils.dehighlight_utils import Light_Shadow_Remover |
|
from .utils.multiview_utils import Multiview_Diffusion_Net |
|
from .utils.uv_warp_utils import mesh_uv_wrap |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
class Hunyuan3DTexGenConfig: |
|
|
|
def __init__(self, light_remover_ckpt_path, multiview_ckpt_path): |
|
self.device = 'cuda' |
|
self.light_remover_ckpt_path = light_remover_ckpt_path |
|
self.multiview_ckpt_path = multiview_ckpt_path |
|
|
|
self.candidate_camera_azims = [0, 90, 180, 270, 0, 180] |
|
self.candidate_camera_elevs = [0, 0, 0, 0, 90, -90] |
|
self.candidate_view_weights = [1, 0.1, 0.5, 0.1, 0.05, 0.05] |
|
|
|
self.render_size = 2048 |
|
self.texture_size = 1024 |
|
self.bake_exp = 4 |
|
self.merge_method = 'fast' |
|
|
|
|
|
class Hunyuan3DPaintPipeline: |
|
@classmethod |
|
def from_pretrained(cls, model_path): |
|
original_model_path = model_path |
|
if not os.path.exists(model_path): |
|
|
|
base_dir = os.environ.get('HY3DGEN_MODELS', '~/.cache/hy3dgen') |
|
model_path = os.path.expanduser(os.path.join(base_dir, model_path)) |
|
|
|
delight_model_path = os.path.join(model_path, 'hunyuan3d-delight-v2-0') |
|
multiview_model_path = os.path.join(model_path, 'hunyuan3d-paint-v2-0') |
|
|
|
if not os.path.exists(delight_model_path) or not os.path.exists(multiview_model_path): |
|
try: |
|
import huggingface_hub |
|
|
|
model_path = huggingface_hub.snapshot_download(repo_id=original_model_path) |
|
delight_model_path = os.path.join(model_path, 'hunyuan3d-delight-v2-0') |
|
multiview_model_path = os.path.join(model_path, 'hunyuan3d-paint-v2-0') |
|
return cls(Hunyuan3DTexGenConfig(delight_model_path, multiview_model_path)) |
|
except ImportError: |
|
logger.warning( |
|
"You need to install HuggingFace Hub to load models from the hub." |
|
) |
|
raise RuntimeError(f"Model path {model_path} not found") |
|
else: |
|
return cls(Hunyuan3DTexGenConfig(delight_model_path, multiview_model_path)) |
|
|
|
raise FileNotFoundError(f"Model path {original_model_path} not found and we could not find it at huggingface") |
|
|
|
def __init__(self, config): |
|
self.config = config |
|
self.models = {} |
|
self.render = MeshRender( |
|
default_resolution=self.config.render_size, |
|
texture_size=self.config.texture_size) |
|
|
|
self.load_models() |
|
|
|
def load_models(self): |
|
|
|
torch.cuda.empty_cache() |
|
|
|
self.models['delight_model'] = Light_Shadow_Remover(self.config) |
|
self.models['multiview_model'] = Multiview_Diffusion_Net(self.config) |
|
|
|
def render_normal_multiview(self, camera_elevs, camera_azims, use_abs_coor=True): |
|
normal_maps = [] |
|
for elev, azim in zip(camera_elevs, camera_azims): |
|
normal_map = self.render.render_normal( |
|
elev, azim, use_abs_coor=use_abs_coor, return_type='pl') |
|
normal_maps.append(normal_map) |
|
|
|
return normal_maps |
|
|
|
def render_position_multiview(self, camera_elevs, camera_azims): |
|
position_maps = [] |
|
for elev, azim in zip(camera_elevs, camera_azims): |
|
position_map = self.render.render_position( |
|
elev, azim, return_type='pl') |
|
position_maps.append(position_map) |
|
|
|
return position_maps |
|
|
|
def bake_from_multiview(self, views, camera_elevs, |
|
camera_azims, view_weights, method='graphcut'): |
|
project_textures, project_weighted_cos_maps = [], [] |
|
project_boundary_maps = [] |
|
for view, camera_elev, camera_azim, weight in zip( |
|
views, camera_elevs, camera_azims, view_weights): |
|
project_texture, project_cos_map, project_boundary_map = self.render.back_project( |
|
view, camera_elev, camera_azim) |
|
project_cos_map = weight * (project_cos_map ** self.config.bake_exp) |
|
project_textures.append(project_texture) |
|
project_weighted_cos_maps.append(project_cos_map) |
|
project_boundary_maps.append(project_boundary_map) |
|
|
|
if method == 'fast': |
|
texture, ori_trust_map = self.render.fast_bake_texture( |
|
project_textures, project_weighted_cos_maps) |
|
else: |
|
raise f'no method {method}' |
|
return texture, ori_trust_map > 1E-8 |
|
|
|
def texture_inpaint(self, texture, mask): |
|
|
|
texture_np = self.render.uv_inpaint(texture, mask) |
|
texture = torch.tensor(texture_np / 255).float().to(texture.device) |
|
|
|
return texture |
|
|
|
@torch.no_grad() |
|
def __call__(self, mesh, image): |
|
|
|
if isinstance(image, str): |
|
image_prompt = Image.open(image) |
|
else: |
|
image_prompt = image |
|
|
|
image_prompt = self.models['delight_model'](image_prompt) |
|
|
|
mesh = mesh_uv_wrap(mesh) |
|
|
|
self.render.load_mesh(mesh) |
|
|
|
selected_camera_elevs, selected_camera_azims, selected_view_weights = \ |
|
self.config.candidate_camera_elevs, self.config.candidate_camera_azims, self.config.candidate_view_weights |
|
|
|
normal_maps = self.render_normal_multiview( |
|
selected_camera_elevs, selected_camera_azims, use_abs_coor=True) |
|
position_maps = self.render_position_multiview( |
|
selected_camera_elevs, selected_camera_azims) |
|
|
|
camera_info = [(((azim // 30) + 9) % 12) // {-20: 1, 0: 1, 20: 1, -90: 3, 90: 3}[ |
|
elev] + {-20: 0, 0: 12, 20: 24, -90: 36, 90: 40}[elev] for azim, elev in |
|
zip(selected_camera_azims, selected_camera_elevs)] |
|
multiviews = self.models['multiview_model'](image_prompt, normal_maps + position_maps, camera_info) |
|
|
|
for i in range(len(multiviews)): |
|
multiviews[i] = multiviews[i].resize( |
|
(self.config.render_size, self.config.render_size)) |
|
|
|
texture, mask = self.bake_from_multiview(multiviews, |
|
selected_camera_elevs, selected_camera_azims, selected_view_weights, |
|
method=self.config.merge_method) |
|
|
|
mask_np = (mask.squeeze(-1).cpu().numpy() * 255).astype(np.uint8) |
|
|
|
texture = self.texture_inpaint(texture, mask_np) |
|
|
|
self.render.set_texture(texture) |
|
textured_mesh = self.render.save_mesh() |
|
|
|
return textured_mesh |
|
|