Spaces:
Runtime error
Runtime error
Rawal Khirodkar commited on
Commit Β·
bed3d5d
1
Parent(s): 14f2b6a
Pointmap: switch from spheres to surfels (oriented quads aligned to surface normal)
Browse files
app.py
CHANGED
|
@@ -236,62 +236,86 @@ 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, max_points: int =
|
| 240 |
-
"""Render the pointmap as
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
-
|
| 243 |
-
pixels. Each pixel becomes an icosphere instance colored by its image
|
| 244 |
-
pixel, radius scaled to the typical inter-pixel distance in 3D so the cloud
|
| 245 |
-
looks coherent without huge overlaps.
|
| 246 |
-
|
| 247 |
-
`image_pil_texture` is sampled (downscaled) to native pixel positions for
|
| 248 |
-
color picking β high-res isn't useful here since we sample point-wise.
|
| 249 |
"""
|
| 250 |
H, W = pointmap_hwc.shape[:2]
|
| 251 |
z = pointmap_hwc[:, :, 2]
|
| 252 |
valid = mask_hw & np.isfinite(pointmap_hwc).all(axis=2) & (z > 0.05) & (z < 25.0)
|
| 253 |
-
yy, xx = np.where(valid)
|
| 254 |
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
if len(yy) > max_points:
|
| 257 |
-
idx =
|
| 258 |
yy, xx = yy[idx], xx[idx]
|
| 259 |
|
| 260 |
-
pts = pointmap_hwc[yy, xx].astype(np.float32)
|
| 261 |
-
|
| 262 |
-
|
|
|
|
| 263 |
|
| 264 |
-
# Y-up flip + recenter on centroid for a tight default view.
|
| 265 |
flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
|
| 266 |
pts = pts * flip
|
|
|
|
| 267 |
centroid = pts.mean(axis=0).astype(np.float32) if len(pts) else np.zeros(3, np.float32)
|
| 268 |
pts = pts - centroid
|
| 269 |
|
| 270 |
-
#
|
| 271 |
-
#
|
| 272 |
-
#
|
| 273 |
-
# Factor 0.6 β adjacent balls overlap slightly, forming a solid surface
|
| 274 |
-
# without big gaps and without obvious overlap blobs.
|
| 275 |
if len(pts) >= 2:
|
| 276 |
from scipy.spatial import cKDTree
|
| 277 |
-
nn_dist = cKDTree(pts).query(pts, k=2)[0][:, 1]
|
| 278 |
-
|
| 279 |
else:
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
#
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
N = len(pts)
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
#
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
faces = (sf[None, :, :] + face_off).reshape(-1, 3) # (N*F, 3)
|
| 293 |
-
vc = np.empty((N, V, 4), dtype=np.uint8)
|
| 294 |
-
vc[..., :3] = cols[:, None, :] # broadcast color to all 12 verts
|
| 295 |
vc[..., 3] = 255
|
| 296 |
vertex_colors = vc.reshape(-1, 4)
|
| 297 |
|
|
@@ -300,6 +324,10 @@ def _make_glb(image_pil_texture: Image.Image, pointmap_hwc: np.ndarray,
|
|
| 300 |
vertex_colors=vertex_colors,
|
| 301 |
process=True,
|
| 302 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".glb").name
|
| 304 |
mesh.export(out_path)
|
| 305 |
return out_path
|
|
|
|
| 236 |
|
| 237 |
|
| 238 |
def _make_glb(image_pil_texture: Image.Image, pointmap_hwc: np.ndarray,
|
| 239 |
+
mask_hw: np.ndarray, max_points: int = 80_000) -> str:
|
| 240 |
+
"""Render the pointmap as **surfels** β each pixel becomes a small flat
|
| 241 |
+
quad oriented along the surface normal at that point. No triangulation
|
| 242 |
+
BETWEEN points (so no triangle artifacts), but each surfel still tiles
|
| 243 |
+
the surface, giving a smooth "skin" appearance instead of granular balls.
|
| 244 |
|
| 245 |
+
4 verts + 2 faces per surfel β much lighter than spheres (~1/10 the verts).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
"""
|
| 247 |
H, W = pointmap_hwc.shape[:2]
|
| 248 |
z = pointmap_hwc[:, :, 2]
|
| 249 |
valid = mask_hw & np.isfinite(pointmap_hwc).all(axis=2) & (z > 0.05) & (z < 25.0)
|
|
|
|
| 250 |
|
| 251 |
+
# Smooth per-pixel normals from the pointmap's spatial gradient.
|
| 252 |
+
px = np.zeros_like(pointmap_hwc, dtype=np.float32)
|
| 253 |
+
py = np.zeros_like(pointmap_hwc, dtype=np.float32)
|
| 254 |
+
px[:, 1:-1] = (pointmap_hwc[:, 2:] - pointmap_hwc[:, :-2]) * 0.5
|
| 255 |
+
py[1:-1, :] = (pointmap_hwc[2:, :] - pointmap_hwc[:-2, :]) * 0.5
|
| 256 |
+
n_grid = np.cross(px, py)
|
| 257 |
+
n_grid /= np.linalg.norm(n_grid, axis=2, keepdims=True).clip(min=1e-8)
|
| 258 |
+
|
| 259 |
+
yy, xx = np.where(valid)
|
| 260 |
if len(yy) > max_points:
|
| 261 |
+
idx = np.random.default_rng(0).choice(len(yy), max_points, replace=False)
|
| 262 |
yy, xx = yy[idx], xx[idx]
|
| 263 |
|
| 264 |
+
pts = pointmap_hwc[yy, xx].astype(np.float32)
|
| 265 |
+
nrm = n_grid[yy, xx].astype(np.float32)
|
| 266 |
+
image_native = np.asarray(image_pil_texture.resize((W, H), Image.LANCZOS))
|
| 267 |
+
cols = image_native[yy, xx].astype(np.uint8)
|
| 268 |
|
|
|
|
| 269 |
flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
|
| 270 |
pts = pts * flip
|
| 271 |
+
nrm = nrm * flip
|
| 272 |
centroid = pts.mean(axis=0).astype(np.float32) if len(pts) else np.zeros(3, np.float32)
|
| 273 |
pts = pts - centroid
|
| 274 |
|
| 275 |
+
# Surfel size from KD-tree median nearest-neighbour distance.
|
| 276 |
+
# Factor 0.6 β adjacent surfels overlap slightly along the surface so
|
| 277 |
+
# there are no visible gaps between tiles.
|
|
|
|
|
|
|
| 278 |
if len(pts) >= 2:
|
| 279 |
from scipy.spatial import cKDTree
|
| 280 |
+
nn_dist = cKDTree(pts).query(pts, k=2)[0][:, 1]
|
| 281 |
+
r = max(float(np.median(nn_dist)) * 0.6, 1e-4)
|
| 282 |
else:
|
| 283 |
+
r = 0.005
|
| 284 |
+
|
| 285 |
+
# Tangent basis (u, v) per surfel, perpendicular to that surfel's normal.
|
| 286 |
+
up = np.array([0.0, 1.0, 0.0], dtype=np.float32)
|
| 287 |
+
u = np.cross(nrm, up) # (N, 3)
|
| 288 |
+
u_norm = np.linalg.norm(u, axis=1, keepdims=True)
|
| 289 |
+
# Fallback for any normals nearly parallel to "up".
|
| 290 |
+
parallel = (u_norm.squeeze(-1) < 1e-4)
|
| 291 |
+
if parallel.any():
|
| 292 |
+
ref = np.array([1.0, 0.0, 0.0], dtype=np.float32)
|
| 293 |
+
u[parallel] = np.cross(nrm[parallel], ref)
|
| 294 |
+
u_norm[parallel] = np.linalg.norm(u[parallel], axis=1, keepdims=True).clip(min=1e-8)
|
| 295 |
+
u = u / u_norm.clip(min=1e-8)
|
| 296 |
+
v = np.cross(nrm, u) # (N, 3) β already unit
|
| 297 |
+
|
| 298 |
+
# 4 corners per surfel (CCW when viewed from +normal):
|
| 299 |
+
# 0 = P β r*u β r*v
|
| 300 |
+
# 1 = P + r*u β r*v
|
| 301 |
+
# 2 = P + r*u + r*v
|
| 302 |
+
# 3 = P β r*u + r*v
|
| 303 |
+
ru, rv = r * u, r * v
|
| 304 |
+
c0 = pts - ru - rv
|
| 305 |
+
c1 = pts + ru - rv
|
| 306 |
+
c2 = pts + ru + rv
|
| 307 |
+
c3 = pts - ru + rv
|
| 308 |
+
verts = np.stack([c0, c1, c2, c3], axis=1).reshape(-1, 3).astype(np.float32)
|
| 309 |
+
|
| 310 |
+
# 2 triangles per surfel: (0,1,2) and (0,2,3), offset by 4 per surfel.
|
| 311 |
N = len(pts)
|
| 312 |
+
base = (np.arange(N, dtype=np.int64) * 4)[:, None, None]
|
| 313 |
+
tri = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int64)[None, :, :]
|
| 314 |
+
faces = (base + tri).reshape(-1, 3)
|
| 315 |
|
| 316 |
+
# Uniform colour across each surfel's 4 verts.
|
| 317 |
+
vc = np.empty((N, 4, 4), dtype=np.uint8)
|
| 318 |
+
vc[..., :3] = cols[:, None, :]
|
|
|
|
|
|
|
|
|
|
| 319 |
vc[..., 3] = 255
|
| 320 |
vertex_colors = vc.reshape(-1, 4)
|
| 321 |
|
|
|
|
| 324 |
vertex_colors=vertex_colors,
|
| 325 |
process=True,
|
| 326 |
)
|
| 327 |
+
# Force double-sided so surfels are visible regardless of normal flip after the Y-flip.
|
| 328 |
+
if hasattr(mesh.visual, "material") and hasattr(mesh.visual.material, "doubleSided"):
|
| 329 |
+
mesh.visual.material.doubleSided = True
|
| 330 |
+
|
| 331 |
out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".glb").name
|
| 332 |
mesh.export(out_path)
|
| 333 |
return out_path
|