HorizonRobotics/EmbodiedGen-Image-to-3D
47
1# Project EmbodiedGen2#3# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.4#5# Licensed under the Apache License, Version 2.0 (the "License");6# you may not use this file except in compliance with the License.7# You may obtain a copy of the License at8#9# http://www.apache.org/licenses/LICENSE-2.010#11# Unless required by applicable law or agreed to in writing, software12# distributed under the License is distributed on an "AS IS" BASIS,13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or14# implied. See the License for the specific language governing15# permissions and limitations under the License.16 17 18import argparse19import logging20import math21import os22 23import cv224import numpy as np25import nvdiffrast.torch as dr26import spaces27import torch28import torch.nn.functional as F29import trimesh30import xatlas31from PIL import Image32from embodied_gen.data.mesh_operator import MeshFixer33from embodied_gen.data.utils import (34 CameraSetting,35 DiffrastRender,36 as_list,37 get_images_from_grid,38 init_kal_camera,39 normalize_vertices_array,40 post_process_texture,41 save_mesh_with_mtl,42)43from embodied_gen.models.delight_model import DelightingModel44from embodied_gen.models.sr_model import ImageRealESRGAN45from embodied_gen.utils.process_media import vcat_pil_images46 47logging.basicConfig(48 format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO49)50logger = logging.getLogger(__name__)51 52 53__all__ = [54 "TextureBacker",55]56 57 58def _transform_vertices(59 mtx: torch.Tensor, pos: torch.Tensor, keepdim: bool = False60) -> torch.Tensor:61 """Transforms 3D vertices using a projection matrix.62 63 Args:64 mtx (torch.Tensor): Projection matrix.65 pos (torch.Tensor): Vertex positions.66 keepdim (bool, optional): If True, keeps the batch dimension.67 68 Returns:69 torch.Tensor: Transformed vertices.70 """71 t_mtx = torch.as_tensor(mtx, device=pos.device, dtype=pos.dtype)72 if pos.size(-1) == 3:73 pos = torch.cat([pos, torch.ones_like(pos[..., :1])], dim=-1)74 75 result = pos @ t_mtx.T76 77 return result if keepdim else result.unsqueeze(0)78 79 80def _bilinear_interpolation_scattering(81 image_h: int, image_w: int, coords: torch.Tensor, values: torch.Tensor82) -> torch.Tensor:83 """Performs bilinear interpolation scattering for grid-based value accumulation.84 85 Args:86 image_h (int): Image height.87 image_w (int): Image width.88 coords (torch.Tensor): Normalized coordinates.89 values (torch.Tensor): Values to scatter.90 91 Returns:92 torch.Tensor: Interpolated grid.93 """94 device = values.device95 dtype = values.dtype96 C = values.shape[-1]97 98 indices = coords * torch.tensor(99 [image_h - 1, image_w - 1], dtype=dtype, device=device100 )101 i, j = indices.unbind(-1)102 103 i0, j0 = (104 indices.floor()105 .long()106 .clamp(0, image_h - 2)107 .clamp(0, image_w - 2)108 .unbind(-1)109 )110 i1, j1 = i0 + 1, j0 + 1111 112 w_i = i - i0.float()113 w_j = j - j0.float()114 weights = torch.stack(115 [(1 - w_i) * (1 - w_j), (1 - w_i) * w_j, w_i * (1 - w_j), w_i * w_j],116 dim=1,117 )118 119 indices_comb = torch.stack(120 [121 torch.stack([i0, j0], dim=1),122 torch.stack([i0, j1], dim=1),123 torch.stack([i1, j0], dim=1),124 torch.stack([i1, j1], dim=1),125 ],126 dim=1,127 )128 129 grid = torch.zeros(image_h, image_w, C, device=device, dtype=dtype)130 cnt = torch.zeros(image_h, image_w, 1, device=device, dtype=dtype)131 132 for k in range(4):133 idx = indices_comb[:, k]134 w = weights[:, k].unsqueeze(-1)135 136 stride = torch.tensor([image_w, 1], device=device, dtype=torch.long)137 flat_idx = (idx * stride).sum(-1)138 139 grid.view(-1, C).scatter_add_(140 0, flat_idx.unsqueeze(-1).expand(-1, C), values * w141 )142 cnt.view(-1, 1).scatter_add_(0, flat_idx.unsqueeze(-1), w)143 144 mask = cnt.squeeze(-1) > 0145 grid[mask] = grid[mask] / cnt[mask].repeat(1, C)146 147 return grid148 149 150def _texture_inpaint_smooth(151 texture: np.ndarray,152 mask: np.ndarray,153 vertices: np.ndarray,154 faces: np.ndarray,155 uv_map: np.ndarray,156) -> tuple[np.ndarray, np.ndarray]:157 """Performs texture inpainting using vertex-based color propagation.158 159 Args:160 texture (np.ndarray): Texture image.161 mask (np.ndarray): Mask image.162 vertices (np.ndarray): Mesh vertices.163 faces (np.ndarray): Mesh faces.164 uv_map (np.ndarray): UV coordinates.165 166 Returns:167 tuple[np.ndarray, np.ndarray]: Inpainted texture and updated mask.168 """169 image_h, image_w, C = texture.shape170 N = vertices.shape[0]171 172 # Initialize vertex data structures173 vtx_mask = np.zeros(N, dtype=np.float32)174 vtx_colors = np.zeros((N, C), dtype=np.float32)175 unprocessed = []176 adjacency = [[] for _ in range(N)]177 178 # Build adjacency graph and initial color assignment179 for face_idx in range(faces.shape[0]):180 for k in range(3):181 uv_idx_k = faces[face_idx, k]182 v_idx = faces[face_idx, k]183 184 # Convert UV to pixel coordinates with boundary clamping185 u = np.clip(186 int(round(uv_map[uv_idx_k, 0] * (image_w - 1))), 0, image_w - 1187 )188 v = np.clip(189 int(round((1.0 - uv_map[uv_idx_k, 1]) * (image_h - 1))),190 0,191 image_h - 1,192 )193 194 if mask[v, u]:195 vtx_mask[v_idx] = 1.0196 vtx_colors[v_idx] = texture[v, u]197 elif v_idx not in unprocessed:198 unprocessed.append(v_idx)199 200 # Build undirected adjacency graph201 neighbor = faces[face_idx, (k + 1) % 3]202 if neighbor not in adjacency[v_idx]:203 adjacency[v_idx].append(neighbor)204 if v_idx not in adjacency[neighbor]:205 adjacency[neighbor].append(v_idx)206 207 # Color propagation with dynamic stopping208 remaining_iters, prev_count = 2, 0209 while remaining_iters > 0:210 current_unprocessed = []211 212 for v_idx in unprocessed:213 valid_neighbors = [n for n in adjacency[v_idx] if vtx_mask[n] > 0]214 if not valid_neighbors:215 current_unprocessed.append(v_idx)216 continue217 218 # Calculate inverse square distance weights219 neighbors_pos = vertices[valid_neighbors]220 dist_sq = np.sum((vertices[v_idx] - neighbors_pos) ** 2, axis=1)221 weights = 1 / np.maximum(dist_sq, 1e-8)222 223 vtx_colors[v_idx] = np.average(224 vtx_colors[valid_neighbors], weights=weights, axis=0225 )226 vtx_mask[v_idx] = 1.0227 228 # Update iteration control229 if len(current_unprocessed) == prev_count:230 remaining_iters -= 1231 else:232 remaining_iters = min(remaining_iters + 1, 2)233 prev_count = len(current_unprocessed)234 unprocessed = current_unprocessed235 236 # Generate output texture237 inpainted_texture, updated_mask = texture.copy(), mask.copy()238 for face_idx in range(faces.shape[0]):239 for k in range(3):240 v_idx = faces[face_idx, k]241 if not vtx_mask[v_idx]:242 continue243 244 # UV coordinate conversion245 uv_idx_k = faces[face_idx, k]246 u = np.clip(247 int(round(uv_map[uv_idx_k, 0] * (image_w - 1))), 0, image_w - 1248 )249 v = np.clip(250 int(round((1.0 - uv_map[uv_idx_k, 1]) * (image_h - 1))),251 0,252 image_h - 1,253 )254 255 inpainted_texture[v, u] = vtx_colors[v_idx]256 updated_mask[v, u] = 255257 258 return inpainted_texture, updated_mask259 260 261class TextureBacker:262 """Texture baking pipeline for multi-view projection and fusion.263 264 This class generates UV-based textures for a 3D mesh using multi-view images,265 depth, and normal information. It includes mesh normalization, UV unwrapping,266 visibility-aware back-projection, confidence-weighted fusion, and inpainting.267 268 Args:269 camera_params (CameraSetting): Camera intrinsics and extrinsics.270 view_weights (list[float]): Weights for each view in texture fusion.271 render_wh (tuple[int, int], optional): Intermediate rendering resolution.272 texture_wh (tuple[int, int], optional): Output texture resolution.273 bake_angle_thresh (int, optional): Max angle for valid projection.274 mask_thresh (float, optional): Threshold for visibility masks.275 smooth_texture (bool, optional): Apply post-processing to texture.276 inpaint_smooth (bool, optional): Apply inpainting smoothing.277 mesh_post_process (bool, optional): False for preventing modification of vertices.278 279 Example:280 ```py281 from embodied_gen.data.backproject_v2 import TextureBacker282 from embodied_gen.data.utils import CameraSetting283 import trimesh284 from PIL import Image285 286 camera_params = CameraSetting(287 num_images=6,288 elevation=[20, -10],289 distance=5,290 resolution_hw=(2048,2048),291 fov=math.radians(30),292 device='cuda',293 )294 view_weights = [1, 0.1, 0.02, 0.1, 1, 0.02]295 mesh = trimesh.load('mesh.obj')296 images = [Image.open(f'view_{i}.png') for i in range(6)]297 texture_backer = TextureBacker(camera_params, view_weights)298 textured_mesh = texture_backer(images, mesh, 'output.obj')299 ```300 """301 302 def __init__(303 self,304 camera_params: CameraSetting,305 view_weights: list[float],306 render_wh: tuple[int, int] = (2048, 2048),307 texture_wh: tuple[int, int] = (2048, 2048),308 bake_angle_thresh: int = 75,309 mask_thresh: float = 0.5,310 smooth_texture: bool = True,311 inpaint_smooth: bool = False,312 mesh_post_process: bool = True,313 ) -> None:314 self.camera_params = camera_params315 self.renderer = None316 self.view_weights = view_weights317 self.device = camera_params.device318 self.render_wh = render_wh319 self.texture_wh = texture_wh320 self.mask_thresh = mask_thresh321 self.smooth_texture = smooth_texture322 self.inpaint_smooth = inpaint_smooth323 self.mesh_post_process = mesh_post_process324 325 self.bake_angle_thresh = bake_angle_thresh326 self.bake_unreliable_kernel_size = int(327 (2 / 512) * max(self.render_wh[0], self.render_wh[1])328 )329 330 def _lazy_init_render(self, camera_params, mask_thresh):331 """Lazily initializes the renderer.332 333 Args:334 camera_params (CameraSetting): Camera settings.335 mask_thresh (float): Mask threshold.336 """337 if self.renderer is None:338 camera = init_kal_camera(camera_params)339 mv = camera.view_matrix() # (n 4 4) world2cam340 p = camera.intrinsics.projection_matrix()341 # NOTE: add a negative sign at P[0, 2] as the y axis is flipped in `nvdiffrast` output. # noqa342 p[:, 1, 1] = -p[:, 1, 1]343 self.renderer = DiffrastRender(344 p_matrix=p,345 mv_matrix=mv,346 resolution_hw=camera_params.resolution_hw,347 context=dr.RasterizeCudaContext(),348 mask_thresh=mask_thresh,349 grad_db=False,350 device=self.device,351 antialias_mask=True,352 )353 354 def load_mesh(self, mesh: trimesh.Trimesh) -> trimesh.Trimesh:355 """Normalizes mesh and unwraps UVs.356 357 Args:358 mesh (trimesh.Trimesh): Input mesh.359 360 Returns:361 trimesh.Trimesh: Mesh with normalized vertices and UVs.362 """363 mesh.vertices, scale, center = normalize_vertices_array(mesh.vertices)364 self.scale, self.center = scale, center365 366 vmapping, indices, uvs = xatlas.parametrize(mesh.vertices, mesh.faces)367 uvs[:, 1] = 1 - uvs[:, 1]368 mesh.vertices = mesh.vertices[vmapping]369 mesh.faces = indices370 mesh.visual.uv = uvs371 372 return mesh373 374 def get_mesh_np_attrs(375 self,376 mesh: trimesh.Trimesh,377 scale: float = None,378 center: np.ndarray = None,379 ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:380 """Gets mesh attributes as numpy arrays.381 382 Args:383 mesh (trimesh.Trimesh): Input mesh.384 scale (float, optional): Scale factor.385 center (np.ndarray, optional): Center offset.386 387 Returns:388 tuple: (vertices, faces, uv_map)389 """390 vertices = mesh.vertices.copy()391 faces = mesh.faces.copy()392 uv_map = mesh.visual.uv.copy()393 uv_map[:, 1] = 1.0 - uv_map[:, 1]394 395 if scale is not None:396 vertices = vertices / scale397 if center is not None:398 vertices = vertices + center399 400 return vertices, faces, uv_map401 402 def _render_depth_edges(self, depth_image: torch.Tensor) -> torch.Tensor:403 """Computes edge image from depth map.404 405 Args:406 depth_image (torch.Tensor): Depth map.407 408 Returns:409 torch.Tensor: Edge image.410 """411 depth_image_np = depth_image.cpu().numpy()412 depth_image_np = (depth_image_np * 255).astype(np.uint8)413 depth_edges = cv2.Canny(depth_image_np, 30, 80)414 sketch_image = (415 torch.from_numpy(depth_edges).to(depth_image.device).float() / 255416 )417 sketch_image = sketch_image.unsqueeze(-1)418 419 return sketch_image420 421 def compute_enhanced_viewnormal(422 self, mv_mtx: torch.Tensor, vertices: torch.Tensor, faces: torch.Tensor423 ) -> torch.Tensor:424 """Computes enhanced view normals for mesh faces.425 426 Args:427 mv_mtx (torch.Tensor): View matrices.428 vertices (torch.Tensor): Mesh vertices.429 faces (torch.Tensor): Mesh faces.430 431 Returns:432 torch.Tensor: View normals.433 """434 rast, _ = self.renderer.compute_dr_raster(vertices, faces)435 rendered_view_normals = []436 for idx in range(len(mv_mtx)):437 pos_cam = _transform_vertices(mv_mtx[idx], vertices, keepdim=True)438 pos_cam = pos_cam[:, :3] / pos_cam[:, 3:]439 v0, v1, v2 = (pos_cam[faces[:, i]] for i in range(3))440 face_norm = F.normalize(441 torch.cross(v1 - v0, v2 - v0, dim=-1), dim=-1442 )443 vertex_norm = (444 torch.from_numpy(445 trimesh.geometry.mean_vertex_normals(446 len(pos_cam), faces.cpu(), face_norm.cpu()447 )448 )449 .to(vertices.device)450 .contiguous()451 )452 im_base_normals, _ = dr.interpolate(453 vertex_norm[None, ...].float(),454 rast[idx : idx + 1],455 faces.to(torch.int32),456 )457 rendered_view_normals.append(im_base_normals)458 459 rendered_view_normals = torch.cat(rendered_view_normals, dim=0)460 461 return rendered_view_normals462 463 def back_project(464 self, image, vis_mask, depth, normal, uv465 ) -> tuple[torch.Tensor, torch.Tensor]:466 """Back-projects image and confidence to UV texture space.467 468 Args:469 image (PIL.Image or np.ndarray): Input image.470 vis_mask (torch.Tensor): Visibility mask.471 depth (torch.Tensor): Depth map.472 normal (torch.Tensor): Normal map.473 uv (torch.Tensor): UV coordinates.474 475 Returns:476 tuple[torch.Tensor, torch.Tensor]: Texture and confidence map.477 """478 image = np.array(image)479 image = torch.as_tensor(image, device=self.device, dtype=torch.float32)480 if image.ndim == 2:481 image = image.unsqueeze(-1)482 image = image / 255483 484 depth_inv = (1.0 - depth) * vis_mask485 sketch_image = self._render_depth_edges(depth_inv)486 487 cos = F.cosine_similarity(488 torch.tensor([[0, 0, 1]], device=self.device),489 normal.view(-1, 3),490 ).view_as(normal[..., :1])491 cos[cos < np.cos(np.radians(self.bake_angle_thresh))] = 0492 493 k = self.bake_unreliable_kernel_size * 2 + 1494 kernel = torch.ones((1, 1, k, k), device=self.device)495 496 vis_mask = vis_mask.permute(2, 0, 1).unsqueeze(0).float()497 vis_mask = F.conv2d(498 1.0 - vis_mask,499 kernel,500 padding=k // 2,501 )502 vis_mask = 1.0 - (vis_mask > 0).float()503 vis_mask = vis_mask.squeeze(0).permute(1, 2, 0)504 505 sketch_image = sketch_image.permute(2, 0, 1).unsqueeze(0)506 sketch_image = F.conv2d(sketch_image, kernel, padding=k // 2)507 sketch_image = (sketch_image > 0).float()508 sketch_image = sketch_image.squeeze(0).permute(1, 2, 0)509 vis_mask = vis_mask * (sketch_image < 0.5)510 511 cos[vis_mask == 0] = 0512 valid_pixels = (vis_mask != 0).view(-1)513 514 return (515 self._scatter_texture(uv, image, valid_pixels),516 self._scatter_texture(uv, cos, valid_pixels),517 )518 519 def _scatter_texture(self, uv, data, mask):520 """Scatters data to texture using UV coordinates and mask.521 522 Args:523 uv (torch.Tensor): UV coordinates.524 data (torch.Tensor): Data to scatter.525 mask (torch.Tensor): Mask for valid pixels.526 527 Returns:528 torch.Tensor: Scattered texture.529 """530 531 def __filter_data(data, mask):532 return data.view(-1, data.shape[-1])[mask]533 534 return _bilinear_interpolation_scattering(535 self.texture_wh[1],536 self.texture_wh[0],537 __filter_data(uv, mask)[..., [1, 0]],538 __filter_data(data, mask),539 )540 541 @torch.no_grad()542 def fast_bake_texture(543 self, textures: list[torch.Tensor], confidence_maps: list[torch.Tensor]544 ) -> tuple[torch.Tensor, torch.Tensor]:545 """Fuses multiple textures and confidence maps.546 547 Args:548 textures (list[torch.Tensor]): List of textures.549 confidence_maps (list[torch.Tensor]): List of confidence maps.550 551 Returns:552 tuple[torch.Tensor, torch.Tensor]: Fused texture and mask.553 """554 channel = textures[0].shape[-1]555 texture_merge = torch.zeros(self.texture_wh + [channel]).to(556 self.device557 )558 trust_map_merge = torch.zeros(self.texture_wh + [1]).to(self.device)559 for texture, cos_map in zip(textures, confidence_maps):560 view_sum = (cos_map > 0).sum()561 painted_sum = ((cos_map > 0) * (trust_map_merge > 0)).sum()562 if painted_sum / view_sum > 0.99:563 continue564 texture_merge += texture * cos_map565 trust_map_merge += cos_map566 texture_merge = texture_merge / torch.clamp(trust_map_merge, min=1e-8)567 568 return texture_merge, trust_map_merge > 1e-8569 570 def uv_inpaint(571 self, mesh: trimesh.Trimesh, texture: np.ndarray, mask: np.ndarray572 ) -> np.ndarray:573 """Inpaints missing regions in the UV texture.574 575 Args:576 mesh (trimesh.Trimesh): Mesh.577 texture (np.ndarray): Texture image.578 mask (np.ndarray): Mask image.579 580 Returns:581 np.ndarray: Inpainted texture.582 """583 if self.inpaint_smooth:584 vertices, faces, uv_map = self.get_mesh_np_attrs(mesh)585 texture, mask = _texture_inpaint_smooth(586 texture, mask, vertices, faces, uv_map587 )588 589 texture = texture.clip(0, 1)590 texture = cv2.inpaint(591 (texture * 255).astype(np.uint8),592 255 - mask,593 3,594 cv2.INPAINT_NS,595 )596 597 return texture598 599 @spaces.GPU600 def compute_texture(601 self,602 colors: list[Image.Image],603 mesh: trimesh.Trimesh,604 ) -> trimesh.Trimesh:605 """Computes the fused texture for the mesh from multi-view images.606 607 Args:608 colors (list[Image.Image]): List of view images.609 mesh (trimesh.Trimesh): Mesh to texture.610 611 Returns:612 tuple[np.ndarray, np.ndarray]: Texture and mask.613 """614 self._lazy_init_render(self.camera_params, self.mask_thresh)615 616 vertices = torch.from_numpy(mesh.vertices).to(self.device).float()617 faces = torch.from_numpy(mesh.faces).to(self.device).to(torch.int)618 uv_map = torch.from_numpy(mesh.visual.uv).to(self.device).float()619 620 rendered_depth, masks = self.renderer.render_depth(vertices, faces)621 norm_deps = self.renderer.normalize_map_by_mask(rendered_depth, masks)622 render_uvs, _ = self.renderer.render_uv(vertices, faces, uv_map)623 view_normals = self.compute_enhanced_viewnormal(624 self.renderer.mv_mtx, vertices, faces625 )626 627 textures, weighted_cos_maps = [], []628 for color, mask, dep, normal, uv, weight in zip(629 colors,630 masks,631 norm_deps,632 view_normals,633 render_uvs,634 self.view_weights,635 ):636 texture, cos_map = self.back_project(color, mask, dep, normal, uv)637 textures.append(texture)638 weighted_cos_maps.append(weight * (cos_map**4))639 640 texture, mask = self.fast_bake_texture(textures, weighted_cos_maps)641 642 texture_np = texture.cpu().numpy()643 mask_np = (mask.squeeze(-1).cpu().numpy() * 255).astype(np.uint8)644 645 return texture_np, mask_np646 647 def __call__(648 self,649 colors: list[Image.Image],650 mesh: trimesh.Trimesh,651 output_path: str,652 ) -> trimesh.Trimesh:653 """Runs the texture baking and exports the textured mesh.654 655 Args:656 colors (list[Image.Image]): List of input view images.657 mesh (trimesh.Trimesh): Input mesh to be textured.658 output_path (str): Path to save the output textured mesh.659 660 Returns:661 trimesh.Trimesh: The textured mesh with UV and texture image.662 """663 mesh = self.load_mesh(mesh)664 texture_np, mask_np = self.compute_texture(colors, mesh)665 666 texture_np = self.uv_inpaint(mesh, texture_np, mask_np)667 if self.smooth_texture:668 texture_np = post_process_texture(texture_np)669 670 vertices, faces, uv_map = self.get_mesh_np_attrs(671 mesh, self.scale, self.center672 )673 textured_mesh = save_mesh_with_mtl(674 vertices,675 faces,676 uv_map,677 texture_np,678 output_path,679 mesh_process=self.mesh_post_process,680 )681 682 return textured_mesh683 684 685def parse_args():686 """Parses command-line arguments for texture backprojection.687 688 Returns:689 argparse.Namespace: Parsed arguments.690 """691 parser = argparse.ArgumentParser(description="Backproject texture")692 parser.add_argument(693 "--color_path",694 nargs="+",695 type=str,696 help="Multiview color image in grid file paths",697 )698 parser.add_argument(699 "--mesh_path",700 type=str,701 help="Mesh path, .obj, .glb or .ply",702 )703 parser.add_argument(704 "--output_path",705 type=str,706 help="Output mesh path with suffix",707 )708 parser.add_argument(709 "--num_images", type=int, default=6, help="Number of images to render."710 )711 parser.add_argument(712 "--elevation",713 nargs="+",714 type=float,715 default=[20.0, -10.0],716 help="Elevation angles for the camera (default: [20.0, -10.0])",717 )718 parser.add_argument(719 "--distance",720 type=float,721 default=5,722 help="Camera distance (default: 5)",723 )724 parser.add_argument(725 "--resolution_hw",726 type=int,727 nargs=2,728 default=(2048, 2048),729 help="Resolution of the output images (default: (2048, 2048))",730 )731 parser.add_argument(732 "--fov",733 type=float,734 default=30,735 help="Field of view in degrees (default: 30)",736 )737 parser.add_argument(738 "--device",739 type=str,740 choices=["cpu", "cuda"],741 default="cuda",742 help="Device to run on (default: `cuda`)",743 )744 parser.add_argument(745 "--skip_fix_mesh", action="store_true", help="Fix mesh geometry."746 )747 parser.add_argument(748 "--texture_wh",749 nargs=2,750 type=int,751 default=[2048, 2048],752 help="Texture resolution width and height",753 )754 parser.add_argument(755 "--mesh_sipmlify_ratio",756 type=float,757 default=0.9,758 help="Mesh simplification ratio (default: 0.9)",759 )760 parser.add_argument(761 "--delight", action="store_true", help="Use delighting model."762 )763 parser.add_argument(764 "--no_smooth_texture",765 action="store_true",766 help="Do not smooth the texture.",767 )768 parser.add_argument(769 "--save_glb_path", type=str, default=None, help="Save glb path."770 )771 parser.add_argument(772 "--no_save_delight_img",773 action="store_true",774 help="Disable saving delight image",775 )776 parser.add_argument("--n_max_faces", type=int, default=30000)777 parser.add_argument("--no_mesh_post_process", action="store_true")778 args, unknown = parser.parse_known_args()779 780 return args781 782 783def entrypoint(784 delight_model: DelightingModel = None,785 imagesr_model: ImageRealESRGAN = None,786 **kwargs,787) -> trimesh.Trimesh:788 """Entrypoint for texture backprojection from multi-view images.789 790 Args:791 delight_model (DelightingModel, optional): Delighting model.792 imagesr_model (ImageRealESRGAN, optional): Super-resolution model.793 **kwargs: Additional arguments to override CLI.794 795 Returns:796 trimesh.Trimesh: Textured mesh.797 """798 args = parse_args()799 for k, v in kwargs.items():800 if hasattr(args, k) and v is not None:801 setattr(args, k, v)802 803 # Setup camera parameters.804 camera_params = CameraSetting(805 num_images=args.num_images,806 elevation=args.elevation,807 distance=args.distance,808 resolution_hw=args.resolution_hw,809 fov=math.radians(args.fov),810 device=args.device,811 )812 813 args.color_path = as_list(args.color_path)814 if args.delight and delight_model is None:815 delight_model = DelightingModel()816 817 color_grid = [Image.open(color_path) for color_path in args.color_path]818 color_grid = vcat_pil_images(color_grid, image_mode="RGBA")819 if args.delight:820 color_grid = delight_model(color_grid)821 if not args.no_save_delight_img:822 save_dir = os.path.dirname(args.output_path)823 os.makedirs(save_dir, exist_ok=True)824 color_grid.save(f"{save_dir}/color_delight.png")825 826 multiviews = get_images_from_grid(color_grid, img_size=512)827 view_weights = [1, 0.1, 0.02, 0.1, 1, 0.02]828 view_weights += [0.01] * (len(multiviews) - len(view_weights))829 830 # Use RealESRGAN_x4plus for x4 (512->2048) image super resolution.831 if imagesr_model is None:832 imagesr_model = ImageRealESRGAN(outscale=4)833 multiviews = [imagesr_model(img) for img in multiviews]834 multiviews = [img.convert("RGB") for img in multiviews]835 mesh = trimesh.load(args.mesh_path)836 if isinstance(mesh, trimesh.Scene):837 mesh = mesh.dump(concatenate=True)838 839 if not args.skip_fix_mesh:840 mesh.vertices, scale, center = normalize_vertices_array(mesh.vertices)841 mesh_fixer = MeshFixer(mesh.vertices, mesh.faces, args.device)842 mesh.vertices, mesh.faces = mesh_fixer(843 filter_ratio=args.mesh_sipmlify_ratio,844 max_hole_size=0.04,845 resolution=1024,846 num_views=1000,847 norm_mesh_ratio=0.5,848 )849 if len(mesh.faces) > args.n_max_faces:850 mesh.vertices, mesh.faces = mesh_fixer(851 filter_ratio=0.8,852 max_hole_size=0.04,853 resolution=1024,854 num_views=1000,855 norm_mesh_ratio=0.5,856 )857 # Restore scale.858 mesh.vertices = mesh.vertices / scale859 mesh.vertices = mesh.vertices + center860 861 # Baking texture to mesh.862 texture_backer = TextureBacker(863 camera_params=camera_params,864 view_weights=view_weights,865 render_wh=args.resolution_hw,866 texture_wh=args.texture_wh,867 smooth_texture=not args.no_smooth_texture,868 mesh_post_process=not args.no_mesh_post_process,869 )870 871 textured_mesh = texture_backer(multiviews, mesh, args.output_path)872 873 if args.save_glb_path is not None:874 os.makedirs(os.path.dirname(args.save_glb_path), exist_ok=True)875 textured_mesh.export(args.save_glb_path)876 877 return textured_mesh878 879 880if __name__ == "__main__":881 entrypoint()882 