171 lines
5.6 KiB
Python
171 lines
5.6 KiB
Python
import torch
|
|
import numpy as np
|
|
import trimesh
|
|
import os
|
|
import sys
|
|
|
|
current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
dap_core_path = os.path.join(current_dir, "dap_core")
|
|
if dap_core_path not in sys.path:
|
|
sys.path.insert(0, dap_core_path)
|
|
|
|
|
|
def spherical_uv_to_directions_np(uv: np.ndarray):
|
|
theta = (1.0 - uv[..., 0]) * (2.0 * np.pi)
|
|
phi = uv[..., 1] * np.pi
|
|
directions = np.stack(
|
|
[np.sin(phi) * np.cos(theta), np.sin(phi) * np.sin(theta), np.cos(phi)], axis=-1
|
|
)
|
|
return directions
|
|
|
|
|
|
class DAP_Panoramic_Mesh:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"depth": ("IMAGE",),
|
|
"mesh_scale": (
|
|
"FLOAT",
|
|
{"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.1},
|
|
),
|
|
"downsample": ("INT", {"default": 1, "min": 1, "max": 8, "step": 1}),
|
|
},
|
|
"optional": {
|
|
"image": ("IMAGE",),
|
|
"mask": ("MASK",),
|
|
"remove_long_edges": (
|
|
"FLOAT",
|
|
{"default": 0.10, "min": 0.0, "max": 2.0, "step": 0.01},
|
|
),
|
|
"max_depth_cutoff": (
|
|
"FLOAT",
|
|
{"default": 0.99, "min": 0.1, "max": 1.0, "step": 0.01},
|
|
),
|
|
"stitch_seam": ("BOOLEAN", {"default": True}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("TRIMESH",)
|
|
RETURN_NAMES = ("mesh",)
|
|
FUNCTION = "generate_mesh"
|
|
CATEGORY = "DAP/Geometry"
|
|
|
|
def generate_mesh(
|
|
self,
|
|
depth,
|
|
mesh_scale,
|
|
downsample,
|
|
image=None,
|
|
mask=None,
|
|
remove_long_edges=0.10,
|
|
max_depth_cutoff=0.99,
|
|
stitch_seam=True,
|
|
):
|
|
d_tensor = depth[0]
|
|
H, W, C = d_tensor.shape
|
|
d_np = d_tensor.cpu().numpy()
|
|
if C > 1:
|
|
d_np = np.mean(d_np, axis=2)
|
|
else:
|
|
d_np = d_np.squeeze()
|
|
|
|
d_np = np.clip(d_np, 0.01, 1.0)
|
|
|
|
if downsample > 1:
|
|
d_np = d_np[::downsample, ::downsample]
|
|
|
|
h_new, w_new = d_np.shape
|
|
|
|
# Pole Snapping: Average depth at poles
|
|
d_np[0, :] = np.mean(d_np[0, :])
|
|
d_np[-1, :] = np.mean(d_np[-1, :])
|
|
|
|
y, x = np.mgrid[0:h_new, 0:w_new]
|
|
u = x / (w_new - 1)
|
|
v = y / (h_new - 1)
|
|
uv = np.stack([u, v], axis=-1)
|
|
dirs = spherical_uv_to_directions_np(uv)
|
|
vertices = (dirs * d_np[..., None] * mesh_scale).reshape(-1, 3)
|
|
|
|
indices = np.arange(h_new * w_new).reshape(h_new, w_new)
|
|
|
|
# Grid faces
|
|
f1 = np.stack([indices[:-1, :-1], indices[:-1, 1:], indices[1:, :-1]], axis=-1)
|
|
f2 = np.stack([indices[:-1, 1:], indices[1:, 1:], indices[1:, :-1]], axis=-1)
|
|
faces_list = [f1.reshape(-1, 3), f2.reshape(-1, 3)]
|
|
|
|
# Seam Stitching (Right column back to Left column)
|
|
if stitch_seam:
|
|
s_f1 = np.stack(
|
|
[indices[:-1, -1], indices[:-1, 0], indices[1:, -1]], axis=-1
|
|
)
|
|
s_f2 = np.stack([indices[:-1, 0], indices[1:, 0], indices[1:, -1]], axis=-1)
|
|
faces_list.append(s_f1.reshape(-1, 3))
|
|
faces_list.append(s_f2.reshape(-1, 3))
|
|
|
|
faces = np.vstack(faces_list)
|
|
|
|
vertex_colors = None
|
|
if image is not None:
|
|
i_np = image[0].cpu().numpy()
|
|
if downsample > 1:
|
|
i_np = i_np[::downsample, ::downsample]
|
|
vertex_colors = (i_np.reshape(-1, 3) * 255).astype(np.uint8)
|
|
|
|
# Build initial mesh
|
|
mesh = trimesh.Trimesh(
|
|
vertices=vertices, faces=faces, vertex_colors=vertex_colors, process=False
|
|
)
|
|
|
|
# Filter by Depth Cutoff (Sky Removal)
|
|
if max_depth_cutoff < 1.0:
|
|
# Flatten depth to match vertices
|
|
d_flat = d_np.flatten()
|
|
# Identify valid vertices
|
|
valid_verts = d_flat <= max_depth_cutoff
|
|
|
|
# Keep faces where ALL vertices are valid (conservative)
|
|
# or ANY vertex is valid? Usually ALL prevents sky connection.
|
|
face_mask = valid_verts[mesh.faces].all(axis=1)
|
|
mesh.update_faces(face_mask)
|
|
mesh.remove_unreferenced_vertices()
|
|
|
|
# Filter by Mask
|
|
if mask is not None:
|
|
m_np = mask[0].cpu().numpy()
|
|
if downsample > 1:
|
|
m_np = m_np[::downsample, ::downsample]
|
|
m_flat = m_np.flatten()
|
|
|
|
# Remove faces where ANY vertex is masked (value < 0.5)
|
|
face_mask = (m_flat[mesh.faces] > 0.5).all(axis=1)
|
|
mesh.update_faces(face_mask)
|
|
mesh.remove_unreferenced_vertices()
|
|
|
|
# Remove Long Edges (Streaks)
|
|
if remove_long_edges > 0:
|
|
edges = mesh.vertices[mesh.edges_unique]
|
|
edge_lengths = np.linalg.norm(edges[:, 0] - edges[:, 1], axis=1)
|
|
# Find vertices involved in long edges and remove faces?
|
|
# Better: find faces with long edges
|
|
v0 = mesh.vertices[mesh.faces[:, 0]]
|
|
v1 = mesh.vertices[mesh.faces[:, 1]]
|
|
v2 = mesh.vertices[mesh.faces[:, 2]]
|
|
|
|
e1 = np.linalg.norm(v0 - v1, axis=1)
|
|
e2 = np.linalg.norm(v1 - v2, axis=1)
|
|
e3 = np.linalg.norm(v2 - v0, axis=1)
|
|
|
|
# Threshold relative to average edge or fixed
|
|
# 0.5 is huge for a 1.0 radius sphere
|
|
bad_faces = (
|
|
(e1 > remove_long_edges)
|
|
| (e2 > remove_long_edges)
|
|
| (e3 > remove_long_edges)
|
|
)
|
|
mesh.update_faces(~bad_faces)
|
|
mesh.remove_unreferenced_vertices()
|
|
|
|
return (mesh,)
|