Rawal Khirodkar commited on
Commit
056d8b0
·
1 Parent(s): 0b515f7

Pointmap mesh polish: UV-textured PBR (image as albedo) + vertex normals + tighter max_edge=0.02m

Browse files
Files changed (1) hide show
  1. app.py +38 -32
app.py CHANGED
@@ -211,68 +211,74 @@ def _floor_ring(radius: float = 1.5, n_points: int = 360, y: float = 0.0,
211
  return verts, cols
212
 
213
 
214
- def _triangulate_grid(pointmap_hwc: np.ndarray, image_rgb: np.ndarray,
215
- mask_hw: np.ndarray, max_edge: float = 0.05):
216
  """Build a triangulated mesh from the (H, W) pointmap grid.
217
 
218
- Each valid pixel becomes a vertex; adjacent valid pixels form quads
219
- (two triangles). Triangles whose edges exceed `max_edge` (meters) are
220
- dropped so we don't stretch skin between the subject and the background.
 
221
  """
222
  H, W = pointmap_hwc.shape[:2]
223
  z = pointmap_hwc[:, :, 2]
224
  valid = mask_hw & np.isfinite(pointmap_hwc).all(axis=2) & (z > 0.05) & (z < 25.0)
225
 
226
- # Vertex index per pixel; -1 if invalid.
227
  idx_map = np.full((H, W), -1, dtype=np.int64)
228
  yy, xx = np.where(valid)
229
  idx_map[yy, xx] = np.arange(len(yy))
230
 
231
- verts = pointmap_hwc[yy, xx] # (N, 3) float32
232
- cols = image_rgb[yy, xx] # (N, 3) uint8
 
233
 
234
- # Quad corners
235
- a = idx_map[:-1, :-1] # top-left
236
- b = idx_map[:-1, 1:] # top-right
237
- c = idx_map[1:, :-1] # bottom-left
238
- d = idx_map[1:, 1:] # bottom-right
239
  quad_valid = (a != -1) & (b != -1) & (c != -1) & (d != -1)
240
-
241
  a_v, b_v, c_v, d_v = a[quad_valid], b[quad_valid], c[quad_valid], d[quad_valid]
242
- tri1 = np.stack([a_v, c_v, b_v], axis=1) # (M, 3)
243
- tri2 = np.stack([b_v, c_v, d_v], axis=1) # (M, 3)
244
- faces = np.concatenate([tri1, tri2], axis=0) # (2M, 3)
245
-
246
- # Drop triangles with any edge longer than max_edge kills stretched skins.
247
- p0 = verts[faces[:, 0]]
248
- p1 = verts[faces[:, 1]]
249
- p2 = verts[faces[:, 2]]
250
  e01 = np.linalg.norm(p1 - p0, axis=1)
251
  e12 = np.linalg.norm(p2 - p1, axis=1)
252
  e20 = np.linalg.norm(p0 - p2, axis=1)
253
  keep = (e01 < max_edge) & (e12 < max_edge) & (e20 < max_edge)
254
- faces = faces[keep]
255
-
256
- return verts.astype(np.float32), cols.astype(np.uint8), faces.astype(np.int64)
257
 
258
 
259
  def _make_glb(image_pil_native: Image.Image, pointmap_hwc: np.ndarray,
260
- mask_hw: np.ndarray) -> str:
261
  h, w = pointmap_hwc.shape[:2]
262
- image_rgb = np.asarray(image_pil_native.resize((w, h), Image.LANCZOS))
263
 
264
- verts, cols_rgb, faces = _triangulate_grid(pointmap_hwc, image_rgb, mask_hw)
265
 
266
  # Y-up flip so the viewer's default orientation matches photographic intuition.
267
  flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
268
  verts = verts * flip
269
 
270
- cols_rgba = np.concatenate(
271
- [cols_rgb, np.full((len(cols_rgb), 1), 255, dtype=np.uint8)], axis=1
 
 
 
 
 
 
 
 
 
272
  )
273
- mesh = trimesh.Trimesh(vertices=verts, faces=faces, vertex_colors=cols_rgba, process=False)
 
 
 
 
274
 
275
- # Scene aids as separate point clouds.
276
  aids_v, aids_c = [], []
277
  for fn in (_camera_marker, _xyz_axes, _floor_ring):
278
  v, c = fn()
 
211
  return verts, cols
212
 
213
 
214
+ def _triangulate_grid(pointmap_hwc: np.ndarray, mask_hw: np.ndarray,
215
+ max_edge: float = 0.02):
216
  """Build a triangulated mesh from the (H, W) pointmap grid.
217
 
218
+ Returns (verts, uvs, faces). Each valid pixel becomes a vertex; adjacent
219
+ valid pixels form quads (2 tris). Triangles with any edge longer than
220
+ `max_edge` (meters) are dropped to kill stretched skin at depth jumps.
221
+ UVs are pixel-aligned for direct texturing with the input image.
222
  """
223
  H, W = pointmap_hwc.shape[:2]
224
  z = pointmap_hwc[:, :, 2]
225
  valid = mask_hw & np.isfinite(pointmap_hwc).all(axis=2) & (z > 0.05) & (z < 25.0)
226
 
 
227
  idx_map = np.full((H, W), -1, dtype=np.int64)
228
  yy, xx = np.where(valid)
229
  idx_map[yy, xx] = np.arange(len(yy))
230
 
231
+ verts = pointmap_hwc[yy, xx].astype(np.float32) # (N, 3)
232
+ uvs = np.stack([xx / max(W - 1, 1), yy / max(H - 1, 1)],
233
+ axis=1).astype(np.float32) # (N, 2)
234
 
235
+ a = idx_map[:-1, :-1]; b = idx_map[:-1, 1:]
236
+ c = idx_map[1:, :-1]; d = idx_map[1:, 1:]
 
 
 
237
  quad_valid = (a != -1) & (b != -1) & (c != -1) & (d != -1)
 
238
  a_v, b_v, c_v, d_v = a[quad_valid], b[quad_valid], c[quad_valid], d[quad_valid]
239
+ tri1 = np.stack([a_v, c_v, b_v], axis=1)
240
+ tri2 = np.stack([b_v, c_v, d_v], axis=1)
241
+ faces = np.concatenate([tri1, tri2], axis=0)
242
+
243
+ p0 = verts[faces[:, 0]]; p1 = verts[faces[:, 1]]; p2 = verts[faces[:, 2]]
 
 
 
244
  e01 = np.linalg.norm(p1 - p0, axis=1)
245
  e12 = np.linalg.norm(p2 - p1, axis=1)
246
  e20 = np.linalg.norm(p0 - p2, axis=1)
247
  keep = (e01 < max_edge) & (e12 < max_edge) & (e20 < max_edge)
248
+ faces = faces[keep].astype(np.int64)
249
+ return verts, uvs, faces
 
250
 
251
 
252
  def _make_glb(image_pil_native: Image.Image, pointmap_hwc: np.ndarray,
253
+ mask_hw: np.ndarray, max_edge: float = 0.02) -> str:
254
  h, w = pointmap_hwc.shape[:2]
255
+ image_native = image_pil_native.resize((w, h), Image.LANCZOS)
256
 
257
+ verts, uvs, faces = _triangulate_grid(pointmap_hwc, mask_hw, max_edge=max_edge)
258
 
259
  # Y-up flip so the viewer's default orientation matches photographic intuition.
260
  flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
261
  verts = verts * flip
262
 
263
+ # MoGe-style: V flipped (image origin is top-left, GL UVs origin bottom-left).
264
+ uvs = uvs * np.array([1.0, -1.0], dtype=np.float32) + np.array([0.0, 1.0], dtype=np.float32)
265
+
266
+ # PBR material with the input image as the albedo texture — much sharper than
267
+ # vertex colors because each triangle interior is sampled from the image at
268
+ # the correct pixel, not bilinearly between 3 corner colors.
269
+ material = trimesh.visual.material.PBRMaterial(
270
+ baseColorTexture=image_native,
271
+ metallicFactor=0.0,
272
+ roughnessFactor=1.0,
273
+ doubleSided=True,
274
  )
275
+ visual = trimesh.visual.texture.TextureVisuals(uv=uvs, material=material)
276
+
277
+ mesh = trimesh.Trimesh(vertices=verts, faces=faces, visual=visual, process=False)
278
+ # Compute and attach per-vertex normals → enables shading in Three.js viewer.
279
+ _ = mesh.vertex_normals # triggers lazy compute; trimesh exports them in glb
280
 
281
+ # Scene aids (camera marker, XYZ axes, floor ring) as a single point primitive.
282
  aids_v, aids_c = [], []
283
  for fn in (_camera_marker, _xyz_axes, _floor_ring):
284
  v, c = fn()