mul / utils /postprocessing.py
MayMeHappy's picture
Upload folder using huggingface_hub
90ca518 verified
Raw History Blame Contribute Delete
42.1 kB
from typing import *
import numpy as np
import torch
import utils3d
import nvdiffrast.torch as dr
from tqdm import tqdm
import trimesh
import trimesh.visual
import pyvista as pv
from pymeshfix import _meshfix
import igraph
PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53]
import itertools
import os as _os, sys as _sys, contextlib as _contextlib
MESHFIX_QUIET = _os.environ.get("MESHFIX_QUIET", "1") == "1" # 屏蔽 PyMeshFix/TMesh C++ 层日志(Loading..%, WARNING-, % done)
@_contextlib.contextmanager
def _suppress_c_output(enabled=True):
"""临时把 fd1/fd2 重定向到 /dev/null: TMesh 的日志是 C 层 printf, Python 的 sys.stdout 拦不住。"""
if not enabled:
yield; return
_sys.stdout.flush(); _sys.stderr.flush()
saved_out, saved_err = _os.dup(1), _os.dup(2)
devnull = _os.open(_os.devnull, _os.O_WRONLY)
try:
_os.dup2(devnull, 1); _os.dup2(devnull, 2)
yield
finally:
_os.dup2(saved_out, 1); _os.dup2(saved_err, 2)
_os.close(saved_out); _os.close(saved_err); _os.close(devnull)
def radical_inverse(base, n):
val = 0
inv_base = 1.0 / base
inv_base_n = inv_base
while n > 0:
digit = n % base
val += digit * inv_base_n
n //= base
inv_base_n *= inv_base
return val
def halton_sequence(dim, n):
return [radical_inverse(PRIMES[dim], n) for dim in range(dim)]
def hammersley_sequence(dim, n, num_samples):
return [n / num_samples] + halton_sequence(dim - 1, n)
def sphere_hammersley_sequence(n, num_samples, offset=(0, 0), remap=False):
u, v = hammersley_sequence(2, n, num_samples)
u += offset[0] / num_samples
v += offset[1]
if remap:
u = 2 * u if u < 0.25 else 2 / 3 * u + 1 / 3
theta = np.arccos(1 - 2 * u) - np.pi / 2
phi = v * 2 * np.pi
return [phi, theta]
@torch.no_grad()
# def _fill_holes(
# verts,
# faces,
# max_hole_size=0.04,
# max_hole_nbe=32,
# resolution=128,
# num_views=500,
# debug=False,
# verbose=False,
# only_fill_holes=False,
# ):
# """
# Rasterize a mesh from multiple views and remove invisible faces.
# Also includes postprocessing to:
# 1. Remove connected components that are have low visibility.
# 2. Mincut to remove faces at the inner side of the mesh connected to the outer side with a small hole.
# Args:
# verts (torch.Tensor): Vertices of the mesh. Shape (V, 3).
# faces (torch.Tensor): Faces of the mesh. Shape (F, 3).
# max_hole_size (float): Maximum area of a hole to fill.
# resolution (int): Resolution of the rasterization.
# num_views (int): Number of views to rasterize the mesh.
# verbose (bool): Whether to print progress.
# """
# if only_fill_holes:
# mesh = _meshfix.PyTMesh()
# mesh.load_array(verts.cpu().numpy(), faces.cpu().numpy())
# mesh.fill_small_boundaries(nbe=max_hole_nbe, refine=True)
# verts, faces = mesh.return_arrays()
# verts = torch.tensor(verts, device='cuda', dtype=torch.float32)
# faces = torch.tensor(faces, device='cuda', dtype=torch.int32)
# if verbose:
# tqdm.write('Skip mincut and run meshfix hole filling only')
# return verts, faces
# # Construct cameras
# yaws = []
# pitchs = []
# for i in range(num_views):
# y, p = sphere_hammersley_sequence(i, num_views)
# yaws.append(y)
# pitchs.append(p)
# yaws = torch.tensor(yaws).cuda()
# pitchs = torch.tensor(pitchs).cuda()
# radius = 2.0
# fov = torch.deg2rad(torch.tensor(40)).cuda()
# projection = utils3d.torch.perspective_from_fov_xy(fov, fov, 1, 3)
# views = []
# for (yaw, pitch) in zip(yaws, pitchs):
# orig = torch.tensor([
# torch.sin(yaw) * torch.cos(pitch),
# torch.cos(yaw) * torch.cos(pitch),
# torch.sin(pitch),
# ]).cuda().float() * radius
# view = utils3d.torch.view_look_at(orig, torch.tensor([0, 0, 0]).float().cuda(), torch.tensor([0, 0, 1]).float().cuda())
# views.append(view)
# views = torch.stack(views, dim=0)
# # Rasterize
# visblity = torch.zeros(faces.shape[0], dtype=torch.int32, device=verts.device)
# rastctx = utils3d.torch.RastContext(backend='cuda')
# for i in tqdm(range(views.shape[0]), total=views.shape[0], disable=not verbose, desc='Rasterizing'):
# view = views[i]
# buffers = utils3d.torch.rasterize_triangle_faces(
# rastctx, verts[None], faces, resolution, resolution, view=view, projection=projection
# )
# face_id = buffers['face_id'][0][buffers['mask'][0] > 0.95] - 1
# face_id = torch.unique(face_id).long()
# visblity[face_id] += 1
# visblity = visblity.float() / num_views
# # Mincut
# ## construct outer faces
# edges, face2edge, edge_degrees = utils3d.torch.compute_edges(faces)
# boundary_edge_indices = torch.nonzero(edge_degrees == 1).reshape(-1)
# connected_components = utils3d.torch.compute_connected_components(faces, edges, face2edge)
# outer_face_indices = torch.zeros(faces.shape[0], dtype=torch.bool, device=faces.device)
# for i in range(len(connected_components)):
# outer_face_indices[connected_components[i]] = visblity[connected_components[i]] > min(max(visblity[connected_components[i]].quantile(0.75).item(), 0.25), 0.5)
# outer_face_indices = outer_face_indices.nonzero().reshape(-1)
# ## construct inner faces
# inner_face_indices = torch.nonzero(visblity == 0).reshape(-1)
# if verbose:
# tqdm.write(f'Found {inner_face_indices.shape[0]} invisible faces')
# if inner_face_indices.shape[0] == 0:
# return verts, faces
# ## Construct dual graph (faces as nodes, edges as edges)
# dual_edges, dual_edge2edge = utils3d.torch.compute_dual_graph(face2edge)
# dual_edge2edge = edges[dual_edge2edge]
# dual_edges_weights = torch.norm(verts[dual_edge2edge[:, 0]] - verts[dual_edge2edge[:, 1]], dim=1)
# if verbose:
# tqdm.write(f'Dual graph: {dual_edges.shape[0]} edges')
# ## solve mincut problem
# ### construct main graph
# g = igraph.Graph()
# g.add_vertices(faces.shape[0])
# g.add_edges(dual_edges.cpu().numpy())
# g.es['weight'] = dual_edges_weights.cpu().numpy()
# ### source and target
# g.add_vertex('s')
# g.add_vertex('t')
# ### connect invisible faces to source
# g.add_edges([(f, 's') for f in inner_face_indices], attributes={'weight': torch.ones(inner_face_indices.shape[0], dtype=torch.float32).cpu().numpy()})
# ### connect outer faces to target
# g.add_edges([(f, 't') for f in outer_face_indices], attributes={'weight': torch.ones(outer_face_indices.shape[0], dtype=torch.float32).cpu().numpy()})
# ### solve mincut
# cut = g.mincut('s', 't', (np.array(g.es['weight']) * 1000).tolist())
# remove_face_indices = torch.tensor([v for v in cut.partition[0] if v < faces.shape[0]], dtype=torch.long, device=faces.device)
# if verbose:
# tqdm.write(f'Mincut solved, start checking the cut')
# ### check if the cut is valid with each connected component
# to_remove_cc = utils3d.torch.compute_connected_components(faces[remove_face_indices])
# if debug:
# tqdm.write(f'Number of connected components of the cut: {len(to_remove_cc)}')
# valid_remove_cc = []
# cutting_edges = []
# for cc in to_remove_cc:
# #### check if the connected component has low visibility
# visblity_median = visblity[remove_face_indices[cc]].median()
# if debug:
# tqdm.write(f'visblity_median: {visblity_median}')
# if visblity_median > 0.25:
# continue
# #### check if the cuting loop is small enough
# cc_edge_indices, cc_edges_degree = torch.unique(face2edge[remove_face_indices[cc]], return_counts=True)
# cc_boundary_edge_indices = cc_edge_indices[cc_edges_degree == 1]
# cc_new_boundary_edge_indices = cc_boundary_edge_indices[~torch.isin(cc_boundary_edge_indices, boundary_edge_indices)]
# if len(cc_new_boundary_edge_indices) > 0:
# cc_new_boundary_edge_cc = utils3d.torch.compute_edge_connected_components(edges[cc_new_boundary_edge_indices])
# cc_new_boundary_edges_cc_center = [verts[edges[cc_new_boundary_edge_indices[edge_cc]]].mean(dim=1).mean(dim=0) for edge_cc in cc_new_boundary_edge_cc]
# cc_new_boundary_edges_cc_area = []
# for i, edge_cc in enumerate(cc_new_boundary_edge_cc):
# _e1 = verts[edges[cc_new_boundary_edge_indices[edge_cc]][:, 0]] - cc_new_boundary_edges_cc_center[i]
# _e2 = verts[edges[cc_new_boundary_edge_indices[edge_cc]][:, 1]] - cc_new_boundary_edges_cc_center[i]
# cc_new_boundary_edges_cc_area.append(torch.norm(torch.cross(_e1, _e2, dim=-1), dim=1).sum() * 0.5)
# if debug:
# cutting_edges.append(cc_new_boundary_edge_indices)
# tqdm.write(f'Area of the cutting loop: {cc_new_boundary_edges_cc_area}')
# if any([l > max_hole_size for l in cc_new_boundary_edges_cc_area]):
# continue
# valid_remove_cc.append(cc)
# if debug:
# face_v = verts[faces].mean(dim=1).cpu().numpy()
# vis_dual_edges = dual_edges.cpu().numpy()
# vis_colors = np.zeros((faces.shape[0], 3), dtype=np.uint8)
# vis_colors[inner_face_indices.cpu().numpy()] = [0, 0, 255]
# vis_colors[outer_face_indices.cpu().numpy()] = [0, 255, 0]
# vis_colors[remove_face_indices.cpu().numpy()] = [255, 0, 255]
# if len(valid_remove_cc) > 0:
# vis_colors[remove_face_indices[torch.cat(valid_remove_cc)].cpu().numpy()] = [255, 0, 0]
# utils3d.io.write_ply('dbg_dual.ply', face_v, edges=vis_dual_edges, vertex_colors=vis_colors)
# vis_verts = verts.cpu().numpy()
# vis_edges = edges[torch.cat(cutting_edges)].cpu().numpy()
# utils3d.io.write_ply('dbg_cut.ply', vis_verts, edges=vis_edges)
# if len(valid_remove_cc) > 0:
# remove_face_indices = remove_face_indices[torch.cat(valid_remove_cc)]
# mask = torch.ones(faces.shape[0], dtype=torch.bool, device=faces.device)
# mask[remove_face_indices] = 0
# faces = faces[mask]
# faces, verts = utils3d.torch.remove_unreferenced_vertices(faces, verts)
# if verbose:
# tqdm.write(f'Removed {(~mask).sum()} faces by mincut')
# else:
# if verbose:
# tqdm.write(f'Removed 0 faces by mincut')
# mesh = _meshfix.PyTMesh()
# mesh.load_array(verts.cpu().numpy(), faces.cpu().numpy())
# mesh.fill_small_boundaries(nbe=max_hole_nbe, refine=True)
# verts, faces = mesh.return_arrays()
# verts, faces = torch.tensor(verts, device='cuda', dtype=torch.float32), torch.tensor(faces, device='cuda', dtype=torch.int32)
# return verts, faces
def _detect_degenerate_faces(
verts: torch.Tensor,
faces: torch.Tensor,
area_eps: float = 1e-12,
) -> torch.Tensor:
"""
检测退化三角面。
判定条件:
1. 一个面中存在重复顶点索引;
2. 三角形二倍面积小于等于 area_eps;
3. 三角形顶点坐标中存在 NaN 或 Inf。
Args:
verts:
[V, 3] 顶点。
faces:
[F, 3] 三角面。
area_eps:
二倍面积阈值。
Returns:
[F] bool mask,True 表示退化面。
"""
if faces.numel() == 0:
return torch.zeros(
faces.shape[0],
dtype=torch.bool,
device=faces.device,
)
faces_long = faces.long()
repeated_vertex = (
(faces_long[:, 0] == faces_long[:, 1])
| (faces_long[:, 1] == faces_long[:, 2])
| (faces_long[:, 2] == faces_long[:, 0])
)
triangles = verts[faces_long]
invalid_position = ~torch.isfinite(triangles).all(
dim=-1
).all(dim=-1)
edge_1 = triangles[:, 1] - triangles[:, 0]
edge_2 = triangles[:, 2] - triangles[:, 0]
double_area = torch.linalg.vector_norm(
torch.cross(edge_1, edge_2, dim=-1),
dim=-1,
)
zero_area = double_area <= area_eps
return repeated_vertex | zero_area | invalid_position
def _find_matching_vertex(
position: np.ndarray,
vertices_list: list,
vertex_buckets: dict,
tolerance: float,
unavailable_indices: set,
):
"""
在已有顶点中查找与 position 接近的顶点。
会搜索当前量化单元及其相邻的 26 个单元,避免浮点量化边界问题。
"""
base_key = np.round(
position / tolerance
).astype(np.int64)
best_index = None
best_distance = np.inf
for offset in itertools.product(
(-1, 0, 1),
repeat=3,
):
key = tuple(
base_key + np.asarray(offset, dtype=np.int64)
)
for candidate in vertex_buckets.get(key, []):
if candidate in unavailable_indices:
continue
distance = np.linalg.norm(
vertices_list[candidate] - position
)
if (
distance <= tolerance * 2.0
and distance < best_distance
):
best_distance = distance
best_index = candidate
return best_index
def _restore_degenerate_faces(
vertices: np.ndarray,
faces: np.ndarray,
original_vertices: np.ndarray,
preserved_faces: np.ndarray,
vertex_match_eps: float = 1e-7,
):
"""
将预先保存的退化面恢复到处理后的 mesh 中。
PyMeshFix 可能重排或删除顶点,因此不能直接使用原 face index。
本函数根据顶点坐标重新建立映射。
同一个原始顶点 ID 会映射到同一个新顶点 ID;
不同但坐标相同的原始顶点会尽量保持为不同顶点。
Args:
vertices:
处理后的 [V, 3] 顶点。
faces:
处理后的 [F, 3] 面。
original_vertices:
处理前的原始顶点。
preserved_faces:
需要恢复的原始退化面索引。
vertex_match_eps:
顶点坐标匹配的相对容差。
Returns:
output_vertices, output_faces
"""
vertices = np.asarray(vertices)
faces = np.asarray(faces)
original_vertices = np.asarray(original_vertices)
preserved_faces = np.asarray(preserved_faces)
if preserved_faces.size == 0:
return vertices, faces
if vertices.ndim != 2:
vertices = vertices.reshape(-1, 3)
if faces.size == 0:
faces = np.empty(
(0, 3),
dtype=preserved_faces.dtype,
)
else:
faces = faces.reshape(-1, 3)
if len(vertices) > 0:
bbox_diagonal = np.linalg.norm(
vertices.max(axis=0)
- vertices.min(axis=0)
)
elif len(original_vertices) > 0:
bbox_diagonal = np.linalg.norm(
original_vertices.max(axis=0)
- original_vertices.min(axis=0)
)
else:
bbox_diagonal = 1.0
tolerance = max(
float(vertex_match_eps)
* max(float(bbox_diagonal), 1.0),
1e-12,
)
vertices_list = [
vertex.copy()
for vertex in vertices
]
vertex_buckets = {}
for vertex_index, vertex in enumerate(vertices_list):
key = tuple(
np.round(
vertex / tolerance
).astype(np.int64)
)
vertex_buckets.setdefault(
key,
[],
).append(vertex_index)
original_vertex_ids = np.unique(
preserved_faces.reshape(-1)
)
old_to_new = {}
# 防止两个不同的原始顶点 ID 因为坐标相同而被错误合并。
claimed_existing_indices = set()
for old_vertex_id in original_vertex_ids:
old_vertex_id = int(old_vertex_id)
position = original_vertices[old_vertex_id]
matched_index = _find_matching_vertex(
position=position,
vertices_list=vertices_list,
vertex_buckets=vertex_buckets,
tolerance=tolerance,
unavailable_indices=claimed_existing_indices,
)
if matched_index is None:
matched_index = len(vertices_list)
vertices_list.append(position.copy())
key = tuple(
np.round(
position / tolerance
).astype(np.int64)
)
vertex_buckets.setdefault(
key,
[],
).append(matched_index)
else:
claimed_existing_indices.add(matched_index)
old_to_new[old_vertex_id] = matched_index
restored_faces = np.empty_like(
preserved_faces,
dtype=np.int64,
)
for column in range(3):
restored_faces[:, column] = np.fromiter(
(
old_to_new[int(vertex_id)]
for vertex_id
in preserved_faces[:, column]
),
dtype=np.int64,
count=preserved_faces.shape[0],
)
output_vertices = np.asarray(
vertices_list,
dtype=vertices.dtype,
)
restored_faces = restored_faces.astype(
faces.dtype,
copy=False,
)
output_faces = np.concatenate(
[faces, restored_faces],
axis=0,
)
return output_vertices, output_faces
def _run_meshfix_hole_filling(
verts: torch.Tensor,
faces: torch.Tensor,
max_hole_nbe: int,
):
"""
调用 PyMeshFix 进行小边界补洞。
注意:PyMeshFix 可能自行清理退化面。
需要保留的退化面应在调用本函数前暂时移除,
并在本函数执行结束后恢复。
"""
if faces.numel() == 0:
return verts, faces
device = verts.device
verts_dtype = verts.dtype
faces_dtype = faces.dtype
with _suppress_c_output(MESHFIX_QUIET):
mesh = _meshfix.PyTMesh()
mesh.load_array(
verts.detach().cpu().numpy(),
faces.detach().cpu().numpy(),
)
mesh.fill_small_boundaries(
nbe=max_hole_nbe,
refine=True,
)
output_verts, output_faces = mesh.return_arrays()
output_verts = torch.as_tensor(
output_verts,
device=device,
dtype=verts_dtype,
)
output_faces = torch.as_tensor(
output_faces,
device=device,
dtype=faces_dtype,
)
return output_verts, output_faces
def _fill_holes(
verts,
faces,
max_hole_size=0.04,
max_hole_nbe=32,
resolution=128,
num_views=500,
debug=False,
verbose=False,
only_fill_holes=False,
keep_degenerate_faces=False,
degenerate_area_eps=1e-12,
vertex_match_eps=1e-7,
):
"""
Rasterize a mesh from multiple views and remove invisible faces.
后处理包括:
1. 删除低可见性的连通区域;
2. 使用 mincut 删除通过小孔与外侧连接的内部面;
3. 使用 PyMeshFix 填补小边界洞。
Args:
verts:
torch.Tensor,[V, 3] 顶点坐标。
faces:
torch.Tensor,[F, 3] 三角面。
max_hole_size:
mincut 新产生切割环允许的最大面积。
max_hole_nbe:
PyMeshFix 填补边界时允许的最大边数量。
resolution:
光栅化分辨率。
num_views:
多视角光栅化视角数量。
debug:
是否输出 debug 文件。
verbose:
是否输出进度信息。
only_fill_holes:
True 时跳过光栅化和 mincut,只运行 MeshFix 补洞。
keep_degenerate_faces:
False:
保持原始行为。退化面可能在 mincut 或 PyMeshFix 中消失。
True:
处理前保存所有退化面,并暂时将它们从处理网格中移除;
补洞结束后再将其恢复。
degenerate_area_eps:
判断零面积面的二倍面积阈值。
vertex_match_eps:
恢复退化面时的顶点坐标匹配容差。
Returns:
verts:
处理后的顶点。
faces:
处理后的三角面。
注意:
keep_degenerate_faces=True 时,退化面会被追加到输出 faces 末尾,
所以输出 face 顺序不保证与输入完全一致。
"""
if verts is None or faces is None:
return verts, faces
if faces.numel() == 0:
return verts, faces
original_device = verts.device
original_verts_dtype = verts.dtype
original_faces_dtype = faces.dtype
original_vertices_numpy = None
preserved_degenerate_faces_numpy = None
# ============================================================
# 保存退化面,并将其从后续处理网格中暂时移除
# ============================================================
if keep_degenerate_faces:
degenerate_mask = _detect_degenerate_faces(
verts=verts,
faces=faces,
area_eps=degenerate_area_eps,
)
num_degenerate = int(
degenerate_mask.sum().item()
)
if verbose:
tqdm.write(
f'Found {num_degenerate} degenerate input faces'
)
if num_degenerate > 0:
original_vertices_numpy = (
verts.detach().cpu().numpy().copy()
)
preserved_degenerate_faces_numpy = (
faces[degenerate_mask]
.detach()
.cpu()
.numpy()
.copy()
)
faces = faces[~degenerate_mask]
def finalize_output(
output_verts: torch.Tensor,
output_faces: torch.Tensor,
):
"""
在返回前恢复之前保存的退化面。
"""
if (
not keep_degenerate_faces
or preserved_degenerate_faces_numpy is None
or preserved_degenerate_faces_numpy.size == 0
):
return output_verts, output_faces
output_vertices_numpy = (
output_verts.detach().cpu().numpy()
)
output_faces_numpy = (
output_faces.detach().cpu().numpy()
)
(
output_vertices_numpy,
output_faces_numpy,
) = _restore_degenerate_faces(
vertices=output_vertices_numpy,
faces=output_faces_numpy,
original_vertices=original_vertices_numpy,
preserved_faces=preserved_degenerate_faces_numpy,
vertex_match_eps=vertex_match_eps,
)
output_verts = torch.as_tensor(
output_vertices_numpy,
device=original_device,
dtype=original_verts_dtype,
)
output_faces = torch.as_tensor(
output_faces_numpy,
device=original_device,
dtype=original_faces_dtype,
)
if verbose:
tqdm.write(
f'Restored '
f'{len(preserved_degenerate_faces_numpy)} '
f'degenerate faces'
)
return output_verts, output_faces
# 如果所有输入面都是退化面,没有可补洞的有效表面
if faces.numel() == 0:
if keep_degenerate_faces:
return finalize_output(
verts[:0],
faces.reshape(0, 3),
)
return verts, faces
# ============================================================
# 只运行 PyMeshFix 补洞
# ============================================================
if only_fill_holes:
verts, faces = _run_meshfix_hole_filling(
verts=verts,
faces=faces,
max_hole_nbe=max_hole_nbe,
)
if verbose:
tqdm.write(
'Skip mincut and run MeshFix hole filling only'
)
return finalize_output(verts, faces)
device = verts.device
# ============================================================
# Construct cameras
# ============================================================
yaws = []
pitches = []
for view_index in range(num_views):
yaw, pitch = sphere_hammersley_sequence(
view_index,
num_views,
)
yaws.append(yaw)
pitches.append(pitch)
yaws = torch.as_tensor(
yaws,
device=device,
dtype=torch.float32,
)
pitches = torch.as_tensor(
pitches,
device=device,
dtype=torch.float32,
)
radius = 2.0
fov = torch.deg2rad(
torch.tensor(
40.0,
device=device,
dtype=torch.float32,
)
)
projection = utils3d.torch.perspective_from_fov_xy(
fov,
fov,
1,
3,
)
target = torch.zeros(
3,
device=device,
dtype=torch.float32,
)
up = torch.tensor(
[0.0, 0.0, 1.0],
device=device,
dtype=torch.float32,
)
views = []
for yaw, pitch in zip(yaws, pitches):
origin = torch.stack(
[
torch.sin(yaw) * torch.cos(pitch),
torch.cos(yaw) * torch.cos(pitch),
torch.sin(pitch),
],
dim=0,
).float() * radius
view = utils3d.torch.view_look_at(
origin,
target,
up,
)
views.append(view)
views = torch.stack(
views,
dim=0,
)
# ============================================================
# Rasterize
# ============================================================
visibility = torch.zeros(
faces.shape[0],
dtype=torch.int32,
device=device,
)
rastctx = utils3d.torch.RastContext(
backend='cuda'
)
for view_index in tqdm(
range(views.shape[0]),
total=views.shape[0],
disable=not verbose,
desc='Rasterizing',
):
view = views[view_index]
buffers = utils3d.torch.rasterize_triangle_faces(
rastctx,
verts[None],
faces,
resolution,
resolution,
view=view,
projection=projection,
)
valid_pixels = buffers['mask'][0] > 0.95
face_id = (
buffers['face_id'][0][valid_pixels] - 1
)
if face_id.numel() == 0:
continue
face_id = torch.unique(
face_id
).long()
# 避免异常背景 ID
face_id = face_id[
(face_id >= 0)
& (face_id < faces.shape[0])
]
if face_id.numel() > 0:
visibility[face_id] += 1
visibility = (
visibility.float()
/ float(num_views)
)
# ============================================================
# Mincut: construct outer faces
# ============================================================
(
edges,
face2edge,
edge_degrees,
) = utils3d.torch.compute_edges(faces)
boundary_edge_indices = torch.nonzero(
edge_degrees == 1,
as_tuple=False,
).reshape(-1)
connected_components = (
utils3d.torch.compute_connected_components(
faces,
edges,
face2edge,
)
)
outer_face_mask = torch.zeros(
faces.shape[0],
dtype=torch.bool,
device=faces.device,
)
for component in connected_components:
component_visibility = visibility[component]
if component_visibility.numel() == 0:
continue
visibility_threshold = min(
max(
component_visibility
.quantile(0.75)
.item(),
0.25,
),
0.5,
)
outer_face_mask[component] = (
component_visibility
> visibility_threshold
)
outer_face_indices = torch.nonzero(
outer_face_mask,
as_tuple=False,
).reshape(-1)
# ============================================================
# Construct inner faces
# ============================================================
inner_face_indices = torch.nonzero(
visibility == 0,
as_tuple=False,
).reshape(-1)
if verbose:
tqdm.write(
f'Found {inner_face_indices.shape[0]} '
f'invisible faces'
)
# 保持原函数行为:
# 若不存在不可见面,则不执行 mincut 和最终 MeshFix。
if inner_face_indices.shape[0] == 0:
return finalize_output(verts, faces)
# ============================================================
# Construct dual graph
# ============================================================
(
dual_edges,
dual_edge2edge,
) = utils3d.torch.compute_dual_graph(
face2edge
)
dual_edge_vertices = edges[
dual_edge2edge
]
dual_edges_weights = torch.linalg.vector_norm(
verts[dual_edge_vertices[:, 0]]
- verts[dual_edge_vertices[:, 1]],
dim=1,
)
if verbose:
tqdm.write(
f'Dual graph: {dual_edges.shape[0]} edges'
)
# ============================================================
# Solve mincut
# ============================================================
graph = igraph.Graph()
graph.add_vertices(
faces.shape[0]
)
if dual_edges.numel() > 0:
graph.add_edges(
dual_edges.detach().cpu().numpy()
)
graph.es['weight'] = (
dual_edges_weights
.detach()
.cpu()
.numpy()
.tolist()
)
else:
graph.es['weight'] = []
graph.add_vertex('s')
graph.add_vertex('t')
# Connect invisible faces to source
if inner_face_indices.numel() > 0:
graph.add_edges(
[
(int(face_index), 's')
for face_index
in inner_face_indices.tolist()
],
attributes={
'weight': np.ones(
inner_face_indices.shape[0],
dtype=np.float32,
)
},
)
# Connect outer faces to target
if outer_face_indices.numel() > 0:
graph.add_edges(
[
(int(face_index), 't')
for face_index
in outer_face_indices.tolist()
],
attributes={
'weight': np.ones(
outer_face_indices.shape[0],
dtype=np.float32,
)
},
)
mincut_weights = (
np.asarray(
graph.es['weight'],
dtype=np.float64,
)
* 1000.0
).tolist()
cut = graph.mincut(
's',
't',
mincut_weights,
)
remove_face_indices = torch.tensor(
[
vertex_id
for vertex_id in cut.partition[0]
if vertex_id < faces.shape[0]
],
dtype=torch.long,
device=faces.device,
)
if verbose:
tqdm.write(
'Mincut solved, start checking the cut'
)
# ============================================================
# Validate each connected component selected by mincut
# ============================================================
if remove_face_indices.numel() > 0:
to_remove_components = (
utils3d.torch.compute_connected_components(
faces[remove_face_indices]
)
)
else:
to_remove_components = []
if debug:
tqdm.write(
'Number of connected components of the cut: '
f'{len(to_remove_components)}'
)
valid_remove_components = []
cutting_edges = []
for component in to_remove_components:
selected_face_indices = remove_face_indices[
component
]
component_visibility_median = visibility[
selected_face_indices
].median()
if debug:
tqdm.write(
'visibility_median: '
f'{component_visibility_median.item()}'
)
# 可见性过高,不删除
if component_visibility_median > 0.25:
continue
# 检查切割形成的新边界环是否足够小
(
component_edge_indices,
component_edge_counts,
) = torch.unique(
face2edge[selected_face_indices],
return_counts=True,
)
component_boundary_edge_indices = (
component_edge_indices[
component_edge_counts == 1
]
)
component_new_boundary_edge_indices = (
component_boundary_edge_indices[
~torch.isin(
component_boundary_edge_indices,
boundary_edge_indices,
)
]
)
if component_new_boundary_edge_indices.numel() > 0:
new_boundary_components = (
utils3d.torch
.compute_edge_connected_components(
edges[
component_new_boundary_edge_indices
]
)
)
component_areas = []
for edge_component in new_boundary_components:
edge_indices = (
component_new_boundary_edge_indices[
edge_component
]
)
edge_vertex_indices = edges[
edge_indices
]
edge_center = verts[
edge_vertex_indices
].mean(dim=1).mean(dim=0)
edge_vector_1 = (
verts[edge_vertex_indices[:, 0]]
- edge_center
)
edge_vector_2 = (
verts[edge_vertex_indices[:, 1]]
- edge_center
)
loop_area = (
torch.linalg.vector_norm(
torch.cross(
edge_vector_1,
edge_vector_2,
dim=-1,
),
dim=1,
).sum()
* 0.5
)
component_areas.append(loop_area)
if debug:
cutting_edges.append(
component_new_boundary_edge_indices
)
tqdm.write(
'Area of the cutting loop: '
f'{[float(area.item()) for area in component_areas]}'
)
if any(
float(area.item()) > max_hole_size
for area in component_areas
):
continue
valid_remove_components.append(
component
)
# ============================================================
# Debug outputs
# ============================================================
if debug:
face_centers = (
verts[faces]
.mean(dim=1)
.detach()
.cpu()
.numpy()
)
visualization_dual_edges = (
dual_edges.detach().cpu().numpy()
)
visualization_colors = np.zeros(
(faces.shape[0], 3),
dtype=np.uint8,
)
visualization_colors[
inner_face_indices.detach().cpu().numpy()
] = [0, 0, 255]
visualization_colors[
outer_face_indices.detach().cpu().numpy()
] = [0, 255, 0]
visualization_colors[
remove_face_indices.detach().cpu().numpy()
] = [255, 0, 255]
if len(valid_remove_components) > 0:
valid_component_indices = torch.cat(
valid_remove_components
)
visualization_colors[
remove_face_indices[
valid_component_indices
].detach().cpu().numpy()
] = [255, 0, 0]
utils3d.io.write_ply(
'dbg_dual.ply',
face_centers,
edges=visualization_dual_edges,
vertex_colors=visualization_colors,
)
if len(cutting_edges) > 0:
debug_cutting_edge_indices = torch.cat(
cutting_edges
)
visualization_vertices = (
verts.detach().cpu().numpy()
)
visualization_edges = (
edges[debug_cutting_edge_indices]
.detach()
.cpu()
.numpy()
)
utils3d.io.write_ply(
'dbg_cut.ply',
visualization_vertices,
edges=visualization_edges,
)
# ============================================================
# Remove validated mincut components
# ============================================================
if len(valid_remove_components) > 0:
valid_component_indices = torch.cat(
valid_remove_components
)
remove_face_indices = remove_face_indices[
valid_component_indices
]
if remove_face_indices.numel() > 0:
keep_face_mask = torch.ones(
faces.shape[0],
dtype=torch.bool,
device=faces.device,
)
keep_face_mask[
remove_face_indices
] = False
faces = faces[
keep_face_mask
]
# 该函数只删除不再被任何面引用的顶点,
# 不会主动删除仍存在的面。
(
faces,
verts,
) = utils3d.torch.remove_unreferenced_vertices(
faces,
verts,
)
if verbose:
tqdm.write(
f'Removed '
f'{int((~keep_face_mask).sum().item())} '
f'faces by mincut'
)
elif verbose:
tqdm.write(
'Removed 0 faces by mincut'
)
elif verbose:
tqdm.write(
'Removed 0 faces by mincut'
)
# ============================================================
# MeshFix hole filling
# ============================================================
verts, faces = _run_meshfix_hole_filling(
verts=verts,
faces=faces,
max_hole_nbe=max_hole_nbe,
)
# 恢复提前保存的退化面
return finalize_output(
verts,
faces,
)
if __name__ == '__main__':
import trimesh
input_mesh_path = '/root/junkai_lin/mul_res_diffusion/pretrained_model/gobjaverse_fix3_octant_factorized_1_to_1024_ce_altpc_decoded_deg_w1_hg246/single_layer_ep28_res32to64_s20/000-002_21538150cde042bd87a0b0a75935c5a1_facenum_8572/pred_consensus.obj'
output_mesh_path = '/root/junkai_lin/mul_res_diffusion/pretrained_model/gobjaverse_fix3_octant_factorized_1_to_1024_ce_altpc_decoded_deg_w1_hg246/single_layer_ep28_res32to64_s20/000-002_21538150cde042bd87a0b0a75935c5a1_facenum_8572/pred_consensus_fix2.obj'
mesh = trimesh.load(input_mesh_path, force='mesh')
verts = torch.tensor(mesh.vertices, device='cuda', dtype=torch.float32)
faces = torch.tensor(mesh.faces, device='cuda', dtype=torch.int32)
new_verts, new_faces = _fill_holes(verts, faces, debug=True, verbose=True, only_fill_holes=True, keep_degenerate_faces=True)
new_mesh = trimesh.Trimesh(vertices=new_verts.cpu().numpy(), faces=new_faces.cpu().numpy())
new_mesh.export(output_mesh_path)
# from pathlib import Path
# import torch
# import trimesh
# input_dir = Path("/root/junkai_lin/experiment/render/result_control/t-flow-ab")
# output_dir = Path("/root/junkai_lin/experiment/render/result_control/t-flow-ab_fix_holes")
# output_dir.mkdir(parents=True, exist_ok=True)
# obj_paths = sorted(input_dir.glob("*.obj"))
# print(f"Found {len(obj_paths)} obj files in {input_dir}")
# for obj_path in obj_paths:
# print(f"\nProcessing: {obj_path.name}")
# try:
# mesh = trimesh.load(str(obj_path), force="mesh")
# verts = torch.tensor(mesh.vertices, device="cuda", dtype=torch.float32)
# faces = torch.tensor(mesh.faces, device="cuda", dtype=torch.int32)
# new_verts, new_faces = _fill_holes(
# verts,
# faces,
# debug=True,
# verbose=True,
# only_fill_holes=True,
# max_hole_nbe=64,
# resolution=256,
# max_hole_size=0.12,
# num_views=1000
# )
# new_mesh = trimesh.Trimesh(
# vertices=new_verts.detach().cpu().numpy(),
# faces=new_faces.detach().cpu().numpy(),
# process=False,
# )
# out_path = output_dir / f"{obj_path.stem}_fix2.obj"
# new_mesh.export(str(out_path))
# print(f"Saved: {out_path}")
# except Exception as e:
# print(f"Failed: {obj_path.name}")
# print(f"Error: {e}")