Spaces:
Running on Zero
Running on Zero
Rawal Khirodkar commited on
Commit ·
a90c10a
1
Parent(s): b10b215
Pointmap: switch back to triangulated mesh — MoGe-2 recipe (UV-textured PBR, smooth normals via lazy compute)
Browse files
app.py
CHANGED
|
@@ -236,60 +236,70 @@ def _triangulate_grid(pointmap_hwc: np.ndarray, mask_hw: np.ndarray,
|
|
| 236 |
|
| 237 |
|
| 238 |
def _make_glb(image_pil_texture: Image.Image, pointmap_hwc: np.ndarray,
|
| 239 |
-
mask_hw: np.ndarray,
|
| 240 |
-
"""
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
H, W = pointmap_hwc.shape[:2]
|
|
|
|
|
|
|
|
|
|
| 245 |
z = pointmap_hwc[:, :, 2]
|
| 246 |
valid = mask_hw & np.isfinite(pointmap_hwc).all(axis=2) & (z > 0.05) & (z < 25.0)
|
|
|
|
| 247 |
yy, xx = np.where(valid)
|
| 248 |
-
|
| 249 |
-
idx = np.random.default_rng(0).choice(len(yy), max_points, replace=False)
|
| 250 |
-
yy, xx = yy[idx], xx[idx]
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
cols = image_native[yy, xx].astype(np.uint8)
|
| 255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
|
| 257 |
-
|
| 258 |
-
centroid =
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
sv = sphere.vertices.astype(np.float32)
|
| 272 |
-
sf = sphere.faces.astype(np.int64)
|
| 273 |
-
V, F = len(sv), len(sf)
|
| 274 |
-
N = len(pts)
|
| 275 |
-
|
| 276 |
-
# Vectorized instancing: place a sphere at every point.
|
| 277 |
-
verts = (sv[None, :, :] + pts[:, None, :]).reshape(-1, 3)
|
| 278 |
-
face_off = (np.arange(N, dtype=np.int64) * V)[:, None, None]
|
| 279 |
-
faces = (sf[None, :, :] + face_off).reshape(-1, 3)
|
| 280 |
-
vc = np.empty((N, V, 4), dtype=np.uint8)
|
| 281 |
-
vc[..., :3] = cols[:, None, :]
|
| 282 |
-
vc[..., 3] = 255
|
| 283 |
-
vertex_colors = vc.reshape(-1, 4)
|
| 284 |
|
| 285 |
mesh = trimesh.Trimesh(
|
| 286 |
-
vertices=verts, faces=faces,
|
| 287 |
-
vertex_colors=vertex_colors,
|
| 288 |
-
process=False,
|
| 289 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".glb").name
|
| 291 |
mesh.export(out_path)
|
| 292 |
-
_glb_inject_unlit(out_path) # → flat-coloured balls, no shading
|
| 293 |
return out_path
|
| 294 |
|
| 295 |
|
|
|
|
| 236 |
|
| 237 |
|
| 238 |
def _make_glb(image_pil_texture: Image.Image, pointmap_hwc: np.ndarray,
|
| 239 |
+
mask_hw: np.ndarray, max_edge: float = 0.04) -> str:
|
| 240 |
+
"""Triangulated mesh, UV-textured with the input image — MoGe-2's recipe.
|
| 241 |
+
|
| 242 |
+
Each valid pixel = vertex; adjacent valid pixels form quads → 2 triangles.
|
| 243 |
+
Triangles whose edges exceed `max_edge` (meters) are dropped to kill
|
| 244 |
+
stretched skin at depth jumps. The input image is used as the GLB's albedo
|
| 245 |
+
texture (per-triangle PBR sampling), and trimesh's lazy vertex_normals get
|
| 246 |
+
exported so Three.js applies smooth shading instead of flat facets.
|
| 247 |
+
"""
|
| 248 |
H, W = pointmap_hwc.shape[:2]
|
| 249 |
+
image_native = image_pil_texture.resize((W, H), Image.LANCZOS)
|
| 250 |
+
|
| 251 |
+
# Triangulate the (H, W) grid over the foreground mask.
|
| 252 |
z = pointmap_hwc[:, :, 2]
|
| 253 |
valid = mask_hw & np.isfinite(pointmap_hwc).all(axis=2) & (z > 0.05) & (z < 25.0)
|
| 254 |
+
idx_map = np.full((H, W), -1, dtype=np.int64)
|
| 255 |
yy, xx = np.where(valid)
|
| 256 |
+
idx_map[yy, xx] = np.arange(len(yy))
|
|
|
|
|
|
|
| 257 |
|
| 258 |
+
verts = pointmap_hwc[yy, xx].astype(np.float32)
|
| 259 |
+
uvs = np.stack([xx / max(W - 1, 1), yy / max(H - 1, 1)], axis=1).astype(np.float32)
|
|
|
|
| 260 |
|
| 261 |
+
a = idx_map[:-1, :-1]; b = idx_map[:-1, 1:]
|
| 262 |
+
c = idx_map[1:, :-1]; d = idx_map[1:, 1:]
|
| 263 |
+
quad_valid = (a != -1) & (b != -1) & (c != -1) & (d != -1)
|
| 264 |
+
a_v, b_v, c_v, d_v = a[quad_valid], b[quad_valid], c[quad_valid], d[quad_valid]
|
| 265 |
+
tri1 = np.stack([a_v, c_v, b_v], axis=1)
|
| 266 |
+
tri2 = np.stack([b_v, c_v, d_v], axis=1)
|
| 267 |
+
faces = np.concatenate([tri1, tri2], axis=0)
|
| 268 |
+
|
| 269 |
+
p0 = verts[faces[:, 0]]; p1 = verts[faces[:, 1]]; p2 = verts[faces[:, 2]]
|
| 270 |
+
e01 = np.linalg.norm(p1 - p0, axis=1)
|
| 271 |
+
e12 = np.linalg.norm(p2 - p1, axis=1)
|
| 272 |
+
e20 = np.linalg.norm(p0 - p2, axis=1)
|
| 273 |
+
keep = (e01 < max_edge) & (e12 < max_edge) & (e20 < max_edge)
|
| 274 |
+
faces = faces[keep].astype(np.int64)
|
| 275 |
+
|
| 276 |
+
# MoGe-2: y/z flip on positions, v-flip on UVs.
|
| 277 |
flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
|
| 278 |
+
verts = verts * flip
|
| 279 |
+
centroid = verts.mean(axis=0).astype(np.float32) if len(verts) else np.zeros(3, np.float32)
|
| 280 |
+
verts = verts - centroid
|
| 281 |
+
|
| 282 |
+
uvs = uvs * np.array([1.0, -1.0], dtype=np.float32) + np.array([0.0, 1.0], dtype=np.float32)
|
| 283 |
+
|
| 284 |
+
# PBR with image as albedo texture — MoGe's exact material settings.
|
| 285 |
+
material = trimesh.visual.material.PBRMaterial(
|
| 286 |
+
baseColorTexture=image_native,
|
| 287 |
+
metallicFactor=0.5,
|
| 288 |
+
roughnessFactor=1.0,
|
| 289 |
+
doubleSided=True,
|
| 290 |
+
)
|
| 291 |
+
visual = trimesh.visual.texture.TextureVisuals(uv=uvs, material=material)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
mesh = trimesh.Trimesh(
|
| 294 |
+
vertices=verts, faces=faces, visual=visual, process=False,
|
|
|
|
|
|
|
| 295 |
)
|
| 296 |
+
# Touch vertex_normals so trimesh caches them; the GLB exporter reads
|
| 297 |
+
# the cached normals and writes them into the file → smooth shading
|
| 298 |
+
# in Three.js (instead of the per-face fallback that looked like facets).
|
| 299 |
+
_ = mesh.vertex_normals
|
| 300 |
+
|
| 301 |
out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".glb").name
|
| 302 |
mesh.export(out_path)
|
|
|
|
| 303 |
return out_path
|
| 304 |
|
| 305 |
|