add mv-adapter texturing
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from PIL import Image
|
||||
from spandrel import ModelLoader
|
||||
|
||||
from MV_Adapter.mvadapter.utils import image_to_tensor, make_image_grid, tensor_to_image
|
||||
from MV_Adapter.mvadapter.utils.mesh_utils import (
|
||||
Camera,
|
||||
CameraProjection,
|
||||
NVDiffRastContextWrapper,
|
||||
SmartPainter,
|
||||
TexturedMesh,
|
||||
get_camera,
|
||||
get_orthogonal_camera,
|
||||
load_mesh,
|
||||
render,
|
||||
replace_mesh_texture_and_save,
|
||||
)
|
||||
from MV_Adapter.mvadapter.utils.mesh_utils.mesh_process import process_raw
|
||||
|
||||
# Import dilate function to remove texture seams
|
||||
def torch_image_dilate(img):
|
||||
"""
|
||||
Remove thin seams on generated texture
|
||||
img (torch): [H, W, C]
|
||||
"""
|
||||
import cv2
|
||||
img = np.asarray(img.cpu().numpy(), dtype=np.float32)
|
||||
img = img * 255
|
||||
img = img.clip(0, 255)
|
||||
mask = np.sum(img.astype(np.float32), axis=-1, keepdims=True)
|
||||
mask = (mask <= 3.0).astype(np.float32)
|
||||
kernel = np.ones((3, 3), 'uint8')
|
||||
dilate_img = cv2.dilate(img, kernel, iterations=1)
|
||||
img = img * (1 - mask) + dilate_img * mask
|
||||
img = (img.clip(0, 255) / 255).astype(np.float32)
|
||||
return torch.from_numpy(img)
|
||||
|
||||
|
||||
def clear():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mesh_use_texture(mesh: TexturedMesh, texture: torch.FloatTensor):
|
||||
texture_ = mesh.texture
|
||||
mesh.texture = texture
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
mesh.texture = texture_
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModProcessConfig:
|
||||
view_upscale: bool = False
|
||||
view_upscale_factor: int = 2
|
||||
inpaint_mode: str = "uv" # in ["none", "uv", "view"]
|
||||
view_inpaint_max_view_score_thresh: float = 0.02
|
||||
view_inpaint_min_rounds: int = 4
|
||||
view_inpaint_max_rounds: int = 8
|
||||
view_inpaint_uv_padding_end: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class TexturePipelineOutput:
|
||||
shaded_model_save_path: Optional[str] = None
|
||||
pbr_model_save_path: Optional[str] = None
|
||||
|
||||
|
||||
class TexturePipeline:
|
||||
def __init__(self, upscaler_ckpt_path: str, inpaint_ckpt_path: str, device: str):
|
||||
self.device = device
|
||||
self.ctx = NVDiffRastContextWrapper(device=self.device)
|
||||
self.cam_proj = CameraProjection(
|
||||
pb_backend="torch-cuda", bg_remover=None, device=self.device
|
||||
)
|
||||
if upscaler_ckpt_path is not None:
|
||||
self.upscaler = ModelLoader().load_from_file(upscaler_ckpt_path)
|
||||
self.upscaler.to(self.device).eval().half()
|
||||
if inpaint_ckpt_path is not None:
|
||||
self.inpainter = ModelLoader().load_from_file(inpaint_ckpt_path)
|
||||
self.inpainter.to(self.device).eval()
|
||||
|
||||
self.smart_painter = SmartPainter(self.device)
|
||||
|
||||
def load_packed_images(self, packed_image_path: Optional[str]) -> List[Image.Image]:
|
||||
if packed_image_path is None:
|
||||
return None
|
||||
packed_image = Image.open(packed_image_path)
|
||||
images = np.array_split(np.array(packed_image), 6, axis=1)
|
||||
images = [Image.fromarray(im) for im in images]
|
||||
return images
|
||||
|
||||
def maybe_upscale_image(
|
||||
self,
|
||||
tensor: Optional[torch.FloatTensor],
|
||||
upscale: bool,
|
||||
upscale_factor: int,
|
||||
batched: bool = False,
|
||||
) -> Optional[torch.FloatTensor]:
|
||||
if upscale:
|
||||
with torch.no_grad():
|
||||
tensor = tensor.permute(0, 3, 1, 2)
|
||||
if batched:
|
||||
tensor = self.upscaler(tensor.half()).float()
|
||||
else:
|
||||
tensor = torch.concat(
|
||||
[
|
||||
self.upscaler(im.unsqueeze(0).half()).float()
|
||||
for im in tensor
|
||||
],
|
||||
dim=0,
|
||||
)
|
||||
tensor = tensor.clamp(0, 1).permute(0, 2, 3, 1)
|
||||
clear()
|
||||
return tensor
|
||||
|
||||
def view_inpaint(
|
||||
self,
|
||||
mod_name: str,
|
||||
mesh: TexturedMesh,
|
||||
uv_proj: torch.FloatTensor,
|
||||
uv_valid_mask: torch.BoolTensor,
|
||||
config: ModProcessConfig,
|
||||
debug_dir: Optional[str] = None,
|
||||
) -> Tuple[torch.FloatTensor, torch.BoolTensor]:
|
||||
def inpaint_func(
|
||||
image: torch.FloatTensor, mask: torch.FloatTensor
|
||||
) -> torch.FloatTensor:
|
||||
with torch.no_grad():
|
||||
return self.inpainter(image.to(torch.float32), mask.to(torch.float32))
|
||||
|
||||
return self.smart_painter(
|
||||
mod_name,
|
||||
mesh,
|
||||
inpaint_func,
|
||||
uv_proj,
|
||||
~uv_valid_mask,
|
||||
max_view_score_thresh=config.view_inpaint_max_view_score_thresh,
|
||||
min_rounds=config.view_inpaint_min_rounds,
|
||||
max_rounds=config.view_inpaint_max_rounds,
|
||||
uv_padding_end=config.view_inpaint_uv_padding_end,
|
||||
debug_dir=debug_dir,
|
||||
debug_visualize_details=False,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
mesh_path: str,
|
||||
save_dir: str,
|
||||
save_name: str = "default",
|
||||
# mesh load settings
|
||||
move_to_center: bool = False,
|
||||
front_x: bool = True,
|
||||
# uv unwarp
|
||||
uv_unwarp: bool = False,
|
||||
preprocess_mesh: bool = False,
|
||||
# projection
|
||||
uv_size: int = 4096,
|
||||
# modes
|
||||
rgb_path: Optional[str] = None,
|
||||
rgb_process_config: ModProcessConfig = ModProcessConfig(),
|
||||
base_color_path: Optional[str] = None,
|
||||
base_color_process_config: ModProcessConfig = ModProcessConfig(),
|
||||
orm_path: Optional[str] = None,
|
||||
orm_process_config: ModProcessConfig = ModProcessConfig(),
|
||||
normal_path: Optional[str] = None,
|
||||
normal_strength: float = 1.0,
|
||||
normal_process_config: ModProcessConfig = ModProcessConfig(),
|
||||
# inpaint
|
||||
uv_inpaint_use_network: bool = False,
|
||||
view_inpaint_include_occlusion_boundary: bool = False,
|
||||
poisson_reprojection: bool = False,
|
||||
# camera
|
||||
camera_projection_type: str = "ORTHO",
|
||||
camera_elevation_deg: List[float] = [0, 0, 0, 0, 89.99, -89.99],
|
||||
camera_azimuth_deg: List[float] = [0, 90, 180, 270, 180, 180],
|
||||
camera_distance: float = 1.0,
|
||||
camera_ortho_scale: float = 1.1,
|
||||
camera_fov_deg: float = 40,
|
||||
# debug
|
||||
debug_mode: bool = False,
|
||||
# seam removal
|
||||
apply_dilate: bool = True,
|
||||
):
|
||||
clear()
|
||||
|
||||
if debug_mode:
|
||||
debug_dir = os.path.join(save_dir, "debug")
|
||||
os.makedirs(debug_dir, exist_ok=True)
|
||||
|
||||
if uv_unwarp:
|
||||
file_suffix = os.path.splitext(mesh_path)[-1]
|
||||
mesh_path_new = mesh_path.replace(file_suffix, f"_unwarp{file_suffix}")
|
||||
process_raw(mesh_path, mesh_path_new, preprocess=preprocess_mesh)
|
||||
mesh_path = mesh_path_new
|
||||
|
||||
mesh: TexturedMesh = load_mesh(
|
||||
mesh_path,
|
||||
rescale=True,
|
||||
move_to_center=move_to_center,
|
||||
front_x_to_y=front_x,
|
||||
default_uv_size=uv_size,
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
# projection
|
||||
if camera_projection_type == "PERSP":
|
||||
raise NotImplementedError
|
||||
elif camera_projection_type == "ORTHO":
|
||||
cameras = get_orthogonal_camera(
|
||||
elevation_deg=camera_elevation_deg,
|
||||
distance=[camera_distance] * 6,
|
||||
left=-camera_ortho_scale / 2,
|
||||
right=camera_ortho_scale / 2,
|
||||
bottom=-camera_ortho_scale / 2,
|
||||
top=camera_ortho_scale / 2,
|
||||
azimuth_deg=[x - 90 for x in camera_azimuth_deg], # -y as front
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
mod_kwargs = {
|
||||
"rgb": (rgb_path, rgb_process_config),
|
||||
"base_color": (base_color_path, base_color_process_config),
|
||||
"orm": (orm_path, orm_process_config),
|
||||
"normal": (normal_path, normal_process_config),
|
||||
}
|
||||
mod_uv_image, mod_uv_tensor = {}, {}
|
||||
for mod_name, (mod_path, mod_process_config) in mod_kwargs.items():
|
||||
if mod_path is None:
|
||||
mod_uv_image[mod_name] = None
|
||||
mod_uv_tensor[mod_name] = None
|
||||
continue
|
||||
mod_images = self.load_packed_images(mod_path)
|
||||
mod_tensor = image_to_tensor(mod_images, device=self.device)
|
||||
mod_tensor = self.maybe_upscale_image(
|
||||
mod_tensor,
|
||||
mod_process_config.view_upscale,
|
||||
mod_process_config.view_upscale_factor,
|
||||
)
|
||||
if mod_process_config.view_upscale and debug_mode:
|
||||
make_image_grid(tensor_to_image(mod_tensor, batched=True), rows=1).save(
|
||||
os.path.join(debug_dir, f"{mod_name}_upscaled.jpg")
|
||||
)
|
||||
|
||||
if mod_name == "normal":
|
||||
_, height, width, _ = mod_tensor.shape
|
||||
render_out = render(
|
||||
self.ctx,
|
||||
mesh,
|
||||
cameras,
|
||||
height=height,
|
||||
width=width,
|
||||
render_attr=False,
|
||||
render_depth=False,
|
||||
render_normal=True,
|
||||
render_tangent=True,
|
||||
)
|
||||
|
||||
# compute UV tangent space
|
||||
vN = render_out.normal
|
||||
vT = render_out.tangent
|
||||
vB = torch.cross(vN, vT, dim=-1)
|
||||
tang_space = F.normalize(torch.stack([vT, vB, vN], dim=-2), dim=-1)
|
||||
|
||||
# compute geometry tangent space
|
||||
vGN = vN
|
||||
vGT = torch.as_tensor(
|
||||
[
|
||||
[1, 0, 0],
|
||||
[0, 1, 0],
|
||||
[-1, 0, 0],
|
||||
[0, -1, 0],
|
||||
[-1, 0, 0],
|
||||
[-1, 0, 0],
|
||||
],
|
||||
dtype=torch.float32,
|
||||
device=self.device,
|
||||
)[:, None, None, :]
|
||||
vGB = torch.cross(vGN, vGT, dim=-1)
|
||||
vGT = torch.cross(vGB, vGN, dim=-1)
|
||||
geo_tang_space = F.normalize(
|
||||
torch.stack([vGT, vGB, vGN], dim=-2), dim=-1
|
||||
)
|
||||
|
||||
# restore world-space normal from geometry tangent space
|
||||
mod_tensor = mod_tensor * 2 - 1
|
||||
mod_tensor = F.normalize(
|
||||
(
|
||||
mod_tensor[:, :, :, None, :]
|
||||
* geo_tang_space.permute(0, 1, 2, 4, 3)
|
||||
).sum(-1),
|
||||
dim=-1,
|
||||
)
|
||||
|
||||
# bake world-space normal to UV tangent space
|
||||
mod_tensor = F.normalize(
|
||||
(mod_tensor[:, :, :, None, :] * tang_space).sum(-1), dim=-1
|
||||
)
|
||||
mod_tensor = (mod_tensor * 0.5 + 0.5).clamp(0, 1)
|
||||
|
||||
uv_proj, uv_valid_mask = self.cam_proj(
|
||||
mod_tensor,
|
||||
mesh,
|
||||
cameras,
|
||||
from_scratch=mod_process_config.inpaint_mode != "none",
|
||||
poisson_blending=False,
|
||||
depth_grad_dilation=5,
|
||||
uv_exp_blend_alpha=3,
|
||||
uv_exp_blend_view_weight=torch.as_tensor([1, 1, 1, 1, 1, 1]),
|
||||
aoi_cos_valid_threshold=0.2,
|
||||
uv_size=uv_size,
|
||||
return_uv_projection_mask=True,
|
||||
)
|
||||
uv_proj[~uv_valid_mask] = torch.as_tensor([0.5, 0.5, 1]).to(uv_proj)
|
||||
else:
|
||||
# TODO: tweak depth_grad_dilation
|
||||
cam_proj_out = self.cam_proj(
|
||||
mod_tensor,
|
||||
mesh,
|
||||
cameras,
|
||||
from_scratch=mod_process_config.inpaint_mode != "none",
|
||||
poisson_blending=False,
|
||||
depth_grad_dilation=5,
|
||||
depth_grad_threshold=0.1,
|
||||
uv_exp_blend_alpha=3,
|
||||
uv_exp_blend_view_weight=torch.as_tensor([1, 1, 1, 1, 1, 1]),
|
||||
aoi_cos_valid_threshold=0.2,
|
||||
uv_size=uv_size,
|
||||
uv_padding=not uv_inpaint_use_network,
|
||||
return_dict=True,
|
||||
)
|
||||
uv_proj, uv_valid_mask, uv_depth_grad = (
|
||||
cam_proj_out.uv_proj,
|
||||
cam_proj_out.uv_proj_mask,
|
||||
cam_proj_out.uv_depth_grad,
|
||||
)
|
||||
if uv_inpaint_use_network:
|
||||
uv_inpaint_mask_input = 1 - uv_valid_mask[None, None].float()
|
||||
uv_inpaint_image_input = uv_proj[None].permute(0, 3, 1, 2)
|
||||
with torch.no_grad():
|
||||
uv_inpaint_result = self.inpainter(
|
||||
uv_inpaint_image_input, uv_inpaint_mask_input
|
||||
)[0].permute(1, 2, 0)
|
||||
clear()
|
||||
if debug_mode:
|
||||
make_image_grid(
|
||||
[
|
||||
tensor_to_image(uv_proj),
|
||||
tensor_to_image(uv_valid_mask),
|
||||
tensor_to_image(uv_inpaint_result),
|
||||
]
|
||||
).save(os.path.join(debug_dir, f"{mod_name}_uv_inpaint.jpg"))
|
||||
uv_proj = uv_inpaint_result.contiguous()
|
||||
|
||||
if mod_process_config.inpaint_mode == "view":
|
||||
if view_inpaint_include_occlusion_boundary:
|
||||
uv_max_depth_grad = uv_depth_grad.max(dim=0)[0]
|
||||
uv_valid_mask = uv_valid_mask & (uv_max_depth_grad < 0.1)
|
||||
uv_proj, uv_valid_mask = self.view_inpaint(
|
||||
mod_name,
|
||||
mesh,
|
||||
uv_proj,
|
||||
uv_valid_mask,
|
||||
mod_process_config,
|
||||
debug_dir=debug_dir if debug_mode else None,
|
||||
)
|
||||
|
||||
if poisson_reprojection:
|
||||
# up and down
|
||||
mesh.texture = uv_proj
|
||||
uv_proj = self.cam_proj(
|
||||
mod_tensor[4:5],
|
||||
mesh,
|
||||
cameras[4:5],
|
||||
from_scratch=False,
|
||||
poisson_blending=True,
|
||||
pb_keep_original_border=True,
|
||||
depth_grad_dilation=5,
|
||||
uv_exp_blend_alpha=3,
|
||||
uv_exp_blend_view_weight=torch.as_tensor([1, 1]),
|
||||
aoi_cos_valid_threshold=0.2,
|
||||
uv_size=uv_size,
|
||||
uv_padding=True,
|
||||
return_dict=False,
|
||||
)
|
||||
# front, sides and back
|
||||
mesh.texture = uv_proj
|
||||
uv_proj = self.cam_proj(
|
||||
mod_tensor[0:4],
|
||||
mesh,
|
||||
cameras[0:4],
|
||||
from_scratch=False,
|
||||
poisson_blending=True,
|
||||
pb_keep_original_border=True,
|
||||
depth_grad_dilation=5,
|
||||
uv_exp_blend_alpha=3,
|
||||
uv_exp_blend_view_weight=torch.as_tensor([1, 1, 1, 1]),
|
||||
aoi_cos_valid_threshold=0.2,
|
||||
uv_size=uv_size,
|
||||
uv_padding=True,
|
||||
return_dict=False,
|
||||
)
|
||||
|
||||
if mod_name == "orm":
|
||||
uv_proj[:, :, 0] = 1.0
|
||||
|
||||
# Apply dilate to remove texture seams before final conversion
|
||||
if mod_name in ["rgb", "base_color"] and apply_dilate: # Apply to color textures only
|
||||
uv_proj = torch_image_dilate(uv_proj)
|
||||
|
||||
mod_uv_image[mod_name] = tensor_to_image(uv_proj)
|
||||
mod_uv_tensor[mod_name] = uv_proj
|
||||
clear()
|
||||
|
||||
shaded_model_save_path = None
|
||||
if mod_uv_image["rgb"] is not None:
|
||||
shaded_model_save_path = os.path.join(save_dir, f"{save_name}_shaded.glb")
|
||||
replace_mesh_texture_and_save(
|
||||
mesh_path,
|
||||
shaded_model_save_path,
|
||||
texture=mod_uv_image["rgb"],
|
||||
backend="trimesh",
|
||||
task_id=save_name,
|
||||
)
|
||||
pbr_model_save_path = None
|
||||
if mod_uv_image["base_color"] is not None:
|
||||
pbr_model_save_path = os.path.join(save_dir, f"{save_name}_pbr.glb")
|
||||
replace_mesh_texture_and_save(
|
||||
mesh_path,
|
||||
pbr_model_save_path,
|
||||
texture=mod_uv_image["base_color"],
|
||||
metallic_roughness_texture=mod_uv_image["orm"],
|
||||
normal_texture=mod_uv_image["normal"],
|
||||
normal_strength=normal_strength,
|
||||
backend="trimesh",
|
||||
task_id=save_name,
|
||||
)
|
||||
|
||||
clear()
|
||||
|
||||
return TexturePipelineOutput(
|
||||
shaded_model_save_path=shaded_model_save_path,
|
||||
pbr_model_save_path=pbr_model_save_path,
|
||||
)
|
||||
@@ -409,80 +409,80 @@ def replace_mesh_texture_and_save_trimesh(
|
||||
tmesh.export(output_path, tree_postprocessor=p_func)
|
||||
|
||||
|
||||
def replace_mesh_texture_and_save_gltflib(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
texture: Union[Image.Image, np.ndarray, torch.Tensor],
|
||||
metallic_roughness_texture: Optional[
|
||||
Union[Image.Image, np.ndarray, torch.Tensor]
|
||||
] = None,
|
||||
normal_texture: Optional[Union[Image.Image, np.ndarray, torch.Tensor]] = None,
|
||||
texture_format: str = "JPEG",
|
||||
normal_strength: float = 1.0,
|
||||
task_id: str = "",
|
||||
**kwargs,
|
||||
) -> None:
|
||||
from gltflib import GLTF
|
||||
from gltflib import Image as gltfImage
|
||||
from gltflib import NormalTextureInfo, Texture, TextureInfo
|
||||
# def replace_mesh_texture_and_save_gltflib(
|
||||
# input_path: str,
|
||||
# output_path: str,
|
||||
# texture: Union[Image.Image, np.ndarray, torch.Tensor],
|
||||
# metallic_roughness_texture: Optional[
|
||||
# Union[Image.Image, np.ndarray, torch.Tensor]
|
||||
# ] = None,
|
||||
# normal_texture: Optional[Union[Image.Image, np.ndarray, torch.Tensor]] = None,
|
||||
# texture_format: str = "JPEG",
|
||||
# normal_strength: float = 1.0,
|
||||
# task_id: str = "",
|
||||
# **kwargs,
|
||||
# ) -> None:
|
||||
# from gltflib import GLTF
|
||||
# from gltflib import Image as gltfImage
|
||||
# from gltflib import NormalTextureInfo, Texture, TextureInfo
|
||||
|
||||
def convert_image(tensor):
|
||||
texture = tensor_to_image(tensor)
|
||||
texture.format = texture_format
|
||||
return texture
|
||||
# def convert_image(tensor):
|
||||
# texture = tensor_to_image(tensor)
|
||||
# texture.format = texture_format
|
||||
# return texture
|
||||
|
||||
def add_texture(gltf: GLTF, image: Image.Image) -> int:
|
||||
jpeg_buffer = io.BytesIO()
|
||||
image.save(jpeg_buffer, format=texture_format)
|
||||
new_texture_data = bytearray(jpeg_buffer.getvalue())
|
||||
_, offset, bytelen = gltf._create_or_extend_glb_resource(new_texture_data)
|
||||
bufferView = gltf._create_embedded_image_buffer_view(offset, bytelen)
|
||||
image = gltfImage(mimeType="image/jpeg", bufferView=bufferView)
|
||||
if gltf.model.images:
|
||||
gltf.model.images.append(image)
|
||||
else:
|
||||
gltf.model.images = [image]
|
||||
texture = Texture(source=len(gltf.model.images) - 1)
|
||||
if gltf.model.textures:
|
||||
gltf.model.textures.append(texture)
|
||||
else:
|
||||
gltf.model.textures = [texture]
|
||||
return len(gltf.model.textures) - 1
|
||||
# def add_texture(gltf: GLTF, image: Image.Image) -> int:
|
||||
# jpeg_buffer = io.BytesIO()
|
||||
# image.save(jpeg_buffer, format=texture_format)
|
||||
# new_texture_data = bytearray(jpeg_buffer.getvalue())
|
||||
# _, offset, bytelen = gltf._create_or_extend_glb_resource(new_texture_data)
|
||||
# bufferView = gltf._create_embedded_image_buffer_view(offset, bytelen)
|
||||
# image = gltfImage(mimeType="image/jpeg", bufferView=bufferView)
|
||||
# if gltf.model.images:
|
||||
# gltf.model.images.append(image)
|
||||
# else:
|
||||
# gltf.model.images = [image]
|
||||
# texture = Texture(source=len(gltf.model.images) - 1)
|
||||
# if gltf.model.textures:
|
||||
# gltf.model.textures.append(texture)
|
||||
# else:
|
||||
# gltf.model.textures = [texture]
|
||||
# return len(gltf.model.textures) - 1
|
||||
|
||||
gltf = GLTF.load(input_path)
|
||||
# gltf = GLTF.load(input_path)
|
||||
|
||||
texture = convert_image(texture)
|
||||
if metallic_roughness_texture is not None:
|
||||
metallic_roughness_texture = convert_image(metallic_roughness_texture)
|
||||
# texture = convert_image(texture)
|
||||
# if metallic_roughness_texture is not None:
|
||||
# metallic_roughness_texture = convert_image(metallic_roughness_texture)
|
||||
|
||||
if normal_texture is not None:
|
||||
normal_texture = convert_image(normal_texture)
|
||||
# if normal_texture is not None:
|
||||
# normal_texture = convert_image(normal_texture)
|
||||
|
||||
pbr_material = gltf.model.materials[0].pbrMetallicRoughness
|
||||
# pbr_material = gltf.model.materials[0].pbrMetallicRoughness
|
||||
|
||||
pbr_material.baseColorTexture = TextureInfo(index=add_texture(gltf, texture))
|
||||
pbr_material.baseColorFactor = [1, 1, 1, 1]
|
||||
# pbr_material.baseColorTexture = TextureInfo(index=add_texture(gltf, texture))
|
||||
# pbr_material.baseColorFactor = [1, 1, 1, 1]
|
||||
|
||||
if metallic_roughness_texture is not None:
|
||||
pbr_material.metallicRoughnessTexture = TextureInfo(
|
||||
index=add_texture(gltf, metallic_roughness_texture)
|
||||
)
|
||||
pbr_material.metallicFactor = 1.0
|
||||
pbr_material.roughnessFactor = 1.0
|
||||
else:
|
||||
pbr_material.metallicFactor = 0.0
|
||||
pbr_material.roughnessFactor = 0.9
|
||||
# if metallic_roughness_texture is not None:
|
||||
# pbr_material.metallicRoughnessTexture = TextureInfo(
|
||||
# index=add_texture(gltf, metallic_roughness_texture)
|
||||
# )
|
||||
# pbr_material.metallicFactor = 1.0
|
||||
# pbr_material.roughnessFactor = 1.0
|
||||
# else:
|
||||
# pbr_material.metallicFactor = 0.0
|
||||
# pbr_material.roughnessFactor = 0.9
|
||||
|
||||
if normal_texture is not None:
|
||||
gltf.model.materials[0].normalTexture = NormalTextureInfo(
|
||||
index=add_texture(gltf, normal_texture), scale=normal_strength
|
||||
)
|
||||
# if normal_texture is not None:
|
||||
# gltf.model.materials[0].normalTexture = NormalTextureInfo(
|
||||
# index=add_texture(gltf, normal_texture), scale=normal_strength
|
||||
# )
|
||||
|
||||
gltf.model.nodes[0].name = f"mvadapter_node_{task_id}"
|
||||
gltf.model.meshes[0].name = f"mvadapter_mesh_{task_id}"
|
||||
gltf.model.materials[0].name = f"mvadapter_material_{task_id}"
|
||||
gltf.model.asset.generator = "https://github.com/huanngzh/MV-Adapter"
|
||||
gltf.export(output_path)
|
||||
# gltf.model.nodes[0].name = f"mvadapter_node_{task_id}"
|
||||
# gltf.model.meshes[0].name = f"mvadapter_mesh_{task_id}"
|
||||
# gltf.model.materials[0].name = f"mvadapter_material_{task_id}"
|
||||
# gltf.model.asset.generator = "https://github.com/huanngzh/MV-Adapter"
|
||||
# gltf.export(output_path)
|
||||
|
||||
|
||||
def replace_mesh_texture_and_save(
|
||||
@@ -508,15 +508,15 @@ def replace_mesh_texture_and_save(
|
||||
texture_format=texture_format,
|
||||
task_id=task_id,
|
||||
)
|
||||
elif backend == "gltflib":
|
||||
replace_mesh_texture_and_save_gltflib(
|
||||
input_path,
|
||||
output_path,
|
||||
texture,
|
||||
metallic_roughness_texture=metallic_roughness_texture,
|
||||
normal_texture=normal_texture,
|
||||
normal_strength=normal_strength,
|
||||
task_id=task_id,
|
||||
)
|
||||
# elif backend == "gltflib":
|
||||
# replace_mesh_texture_and_save_gltflib(
|
||||
# input_path,
|
||||
# output_path,
|
||||
# texture,
|
||||
# metallic_roughness_texture=metallic_roughness_texture,
|
||||
# normal_texture=normal_texture,
|
||||
# normal_strength=normal_strength,
|
||||
# task_id=task_id,
|
||||
# )
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -281,12 +281,10 @@ def run_pipeline(
|
||||
|
||||
|
||||
def create_image_grid(images: List[Image.Image], rows: int = 1) -> Image.Image:
|
||||
"""Создание сетки изображений"""
|
||||
return make_image_grid(images, rows=rows)
|
||||
|
||||
|
||||
def pil_to_torch_tensor(image: Image.Image) -> torch.Tensor:
|
||||
"""Конвертация PIL изображения в torch tensor"""
|
||||
import numpy as np
|
||||
array = np.array(image)
|
||||
tensor = torch.from_numpy(array).float() / 255.0
|
||||
@@ -299,7 +297,6 @@ def pil_to_torch_tensor(image: Image.Image) -> torch.Tensor:
|
||||
|
||||
|
||||
def pils_to_torch_tensor(images: List[Image.Image]) -> torch.Tensor:
|
||||
"""Конвертация списка PIL изображений в torch tensor"""
|
||||
tensors = [pil_to_torch_tensor(img).squeeze(0) for img in images]
|
||||
return torch.stack(tensors, dim=0)
|
||||
|
||||
@@ -453,4 +450,223 @@ def run_t2mv_pipeline(
|
||||
**pipe_kwargs,
|
||||
).images
|
||||
|
||||
return images
|
||||
return images
|
||||
|
||||
|
||||
def prepare_texture_pipeline(
|
||||
upscaler_ckpt_path: Optional[str] = None,
|
||||
inpaint_ckpt_path: Optional[str] = None,
|
||||
device: str = "cuda",
|
||||
use_mmgp: bool = False,
|
||||
):
|
||||
"""
|
||||
Prepare texture projection pipeline
|
||||
|
||||
Args:
|
||||
upscaler_ckpt_path: Path to upscaler checkpoint (optional)
|
||||
inpaint_ckpt_path: Path to inpaint checkpoint (optional)
|
||||
device: Device for computation
|
||||
use_mmgp: Use mmgp for memory optimization
|
||||
"""
|
||||
from .mvadapter.pipelines.pipeline_texture import TexturePipeline
|
||||
|
||||
if upscaler_ckpt_path and not os.path.exists(upscaler_ckpt_path):
|
||||
print(f"Warning: Upscaler checkpoint not found: {upscaler_ckpt_path}, using None")
|
||||
upscaler_ckpt_path = None
|
||||
|
||||
if inpaint_ckpt_path and not os.path.exists(inpaint_ckpt_path):
|
||||
print(f"Warning: Inpaint checkpoint not found: {inpaint_ckpt_path}, using None")
|
||||
inpaint_ckpt_path = None
|
||||
|
||||
texture_pipe = TexturePipeline(
|
||||
upscaler_ckpt_path=upscaler_ckpt_path,
|
||||
inpaint_ckpt_path=inpaint_ckpt_path,
|
||||
device=device
|
||||
)
|
||||
|
||||
# Apply mmgp optimization if available and requested
|
||||
if use_mmgp and MMGP_AVAILABLE:
|
||||
import gc
|
||||
torch.cuda.empty_cache()
|
||||
gc.collect()
|
||||
|
||||
all_models = {}
|
||||
|
||||
if hasattr(texture_pipe, 'upscaler') and texture_pipe.upscaler is not None:
|
||||
all_models["upscaler"] = texture_pipe.upscaler
|
||||
|
||||
if hasattr(texture_pipe, 'inpainter') and texture_pipe.inpainter is not None:
|
||||
all_models["inpainter"] = texture_pipe.inpainter
|
||||
|
||||
if all_models:
|
||||
offload.profile(all_models, profile_type.HighRAM_LowVRAM)
|
||||
print("mmgp profiling applied to texture pipeline successfully.")
|
||||
else:
|
||||
print("No models loaded for texture pipeline, skipping mmgp profiling.")
|
||||
|
||||
return texture_pipe
|
||||
|
||||
|
||||
def run_texture_pipeline(
|
||||
texture_pipe,
|
||||
multiview_images: List[Image.Image],
|
||||
mesh_path: str,
|
||||
save_dir: str,
|
||||
save_name: str = "textured_model",
|
||||
uv_size: int = 4096,
|
||||
view_upscale: bool = True,
|
||||
inpaint_mode: str = "view",
|
||||
uv_unwarp: bool = True,
|
||||
preprocess_mesh: bool = False,
|
||||
move_to_center: bool = False,
|
||||
front_x: bool = True,
|
||||
camera_azimuth_deg: List[float] = None,
|
||||
camera_elevation_deg: List[float] = None,
|
||||
camera_distance: float = 1.0,
|
||||
camera_ortho_scale: float = 1.1,
|
||||
debug_mode: bool = False,
|
||||
apply_dilate: bool = True,
|
||||
):
|
||||
"""
|
||||
Execute texture projection pipeline
|
||||
|
||||
Args:
|
||||
texture_pipe: Prepared texture pipeline
|
||||
multiview_images: List of PIL images (6 views)
|
||||
mesh_path: Path to 3D mesh file
|
||||
save_dir: Directory to save results
|
||||
save_name: Name for saved files
|
||||
uv_size: UV texture resolution
|
||||
view_upscale: Enable view upscaling
|
||||
inpaint_mode: Inpainting mode ("none", "uv", "view")
|
||||
uv_unwarp: Enable UV unwrapping
|
||||
preprocess_mesh: Enable mesh preprocessing
|
||||
move_to_center: Move mesh to center
|
||||
front_x: Front face along X axis
|
||||
camera_azimuth_deg: Camera azimuth angles
|
||||
camera_elevation_deg: Camera elevation angles
|
||||
camera_distance: Camera distance
|
||||
camera_ortho_scale: Orthographic scale
|
||||
debug_mode: Enable debug mode
|
||||
apply_dilate: Apply dilate to remove texture seams
|
||||
"""
|
||||
from .mvadapter.pipelines.pipeline_texture import ModProcessConfig
|
||||
from .mvadapter.utils import make_image_grid
|
||||
|
||||
if not mesh_path or not os.path.exists(mesh_path):
|
||||
raise ValueError(f"Mesh file does not exist: {mesh_path}")
|
||||
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
if camera_azimuth_deg is None:
|
||||
camera_azimuth_deg = [0, 90, 180, 270, 180, 180]
|
||||
if camera_elevation_deg is None:
|
||||
camera_elevation_deg = [0, 0, 0, 0, 89.99, -89.99]
|
||||
|
||||
# Adjust azimuth angles (subtract 90 as in original code)
|
||||
azimuth_deg_corrected = [x - 90 for x in camera_azimuth_deg]
|
||||
|
||||
grid_image = make_image_grid(multiview_images, rows=1)
|
||||
temp_grid_path = os.path.join(save_dir, f"{save_name}_temp_grid.png")
|
||||
grid_image.save(temp_grid_path)
|
||||
|
||||
try:
|
||||
rgb_process_config = ModProcessConfig(
|
||||
view_upscale=view_upscale,
|
||||
inpaint_mode=inpaint_mode
|
||||
)
|
||||
|
||||
output = texture_pipe(
|
||||
mesh_path=mesh_path,
|
||||
save_dir=save_dir,
|
||||
save_name=save_name,
|
||||
move_to_center=move_to_center,
|
||||
front_x=front_x,
|
||||
uv_unwarp=uv_unwarp,
|
||||
preprocess_mesh=preprocess_mesh,
|
||||
uv_size=uv_size,
|
||||
rgb_path=temp_grid_path,
|
||||
rgb_process_config=rgb_process_config,
|
||||
camera_elevation_deg=camera_elevation_deg,
|
||||
camera_azimuth_deg=azimuth_deg_corrected,
|
||||
camera_distance=camera_distance,
|
||||
camera_ortho_scale=camera_ortho_scale,
|
||||
debug_mode=debug_mode,
|
||||
apply_dilate=apply_dilate,
|
||||
)
|
||||
|
||||
if os.path.exists(temp_grid_path):
|
||||
os.remove(temp_grid_path)
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
if os.path.exists(temp_grid_path):
|
||||
os.remove(temp_grid_path)
|
||||
raise e
|
||||
|
||||
|
||||
def download_texture_checkpoints(texture_ckpt_dir, upscaler_ckpt_path, inpaint_ckpt_path):
|
||||
"""Download texture checkpoint files if they don't exist"""
|
||||
import subprocess
|
||||
|
||||
os.makedirs(texture_ckpt_dir, exist_ok=True)
|
||||
|
||||
checkpoints = [
|
||||
{
|
||||
"url": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth",
|
||||
"path": upscaler_ckpt_path,
|
||||
"name": "RealESRGAN upscaler"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt",
|
||||
"path": inpaint_ckpt_path,
|
||||
"name": "Big-LaMa inpainter"
|
||||
}
|
||||
]
|
||||
|
||||
for ckpt in checkpoints:
|
||||
if not os.path.exists(ckpt["path"]):
|
||||
print(f"Downloading {ckpt['name']} to {ckpt['path']}...")
|
||||
|
||||
success = False
|
||||
|
||||
# Try wget first
|
||||
try:
|
||||
subprocess.run([
|
||||
"wget", ckpt["url"], "-O", ckpt["path"]
|
||||
], check=True, capture_output=True, text=True)
|
||||
print(f"Successfully downloaded {ckpt['name']} using wget")
|
||||
success = True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Try curl if wget failed
|
||||
if not success:
|
||||
try:
|
||||
subprocess.run([
|
||||
"curl", "-L", ckpt["url"], "-o", ckpt["path"]
|
||||
], check=True, capture_output=True, text=True)
|
||||
print(f"Successfully downloaded {ckpt['name']} using curl")
|
||||
success = True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Try Python urllib as fallback
|
||||
if not success:
|
||||
try:
|
||||
import urllib.request
|
||||
urllib.request.urlretrieve(ckpt["url"], ckpt["path"])
|
||||
print(f"Successfully downloaded {ckpt['name']} using urllib")
|
||||
success = True
|
||||
except Exception as e:
|
||||
print(f"Failed to download {ckpt['name']} using urllib: {e}")
|
||||
|
||||
# If all methods failed
|
||||
if not success:
|
||||
print(f"Failed to download {ckpt['name']}. Please download manually:")
|
||||
print(f"wget {ckpt['url']} -O {ckpt['path']}")
|
||||
print(f"or")
|
||||
print(f"curl -L {ckpt['url']} -o {ckpt['path']}")
|
||||
else:
|
||||
print(f"{ckpt['name']} already exists at {ckpt['path']}")
|
||||
Reference in New Issue
Block a user