Spaces:
Running on Zero
Running on Zero
Rawal Khirodkar commited on
Commit Β·
bece70e
1
Parent(s): 409a27b
Pointmap: switch back to spheres (KHR_materials_unlit injection makes them flat-coloured)
Browse files
app.py
CHANGED
|
@@ -236,93 +236,52 @@ 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 |
-
|
| 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 |
-
#
|
| 276 |
-
#
|
| 277 |
-
# raw KD-tree NN distance is biased too small (adjacent pixels often map
|
| 278 |
-
# to nearly-coincident 3D points, pulling the median way below the actual
|
| 279 |
-
# visual spacing). Factor 1.5 gives clear overlap so the surface reads
|
| 280 |
-
# as solid skin rather than a sparse tile pattern.
|
| 281 |
if len(pts) >= 2:
|
| 282 |
extent_diag = float(np.linalg.norm(np.ptp(pts, axis=0)))
|
| 283 |
r = max(extent_diag / np.sqrt(len(pts)) * 1.5, 1e-4)
|
| 284 |
else:
|
| 285 |
r = 0.005
|
| 286 |
|
| 287 |
-
#
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
parallel = (u_norm.squeeze(-1) < 1e-4)
|
| 293 |
-
if parallel.any():
|
| 294 |
-
ref = np.array([1.0, 0.0, 0.0], dtype=np.float32)
|
| 295 |
-
u[parallel] = np.cross(nrm[parallel], ref)
|
| 296 |
-
u_norm[parallel] = np.linalg.norm(u[parallel], axis=1, keepdims=True).clip(min=1e-8)
|
| 297 |
-
u = u / u_norm.clip(min=1e-8)
|
| 298 |
-
v = np.cross(nrm, u) # (N, 3) β already unit
|
| 299 |
-
|
| 300 |
-
# 4 corners per surfel (CCW when viewed from +normal):
|
| 301 |
-
# 0 = P β r*u β r*v
|
| 302 |
-
# 1 = P + r*u β r*v
|
| 303 |
-
# 2 = P + r*u + r*v
|
| 304 |
-
# 3 = P β r*u + r*v
|
| 305 |
-
ru, rv = r * u, r * v
|
| 306 |
-
c0 = pts - ru - rv
|
| 307 |
-
c1 = pts + ru - rv
|
| 308 |
-
c2 = pts + ru + rv
|
| 309 |
-
c3 = pts - ru + rv
|
| 310 |
-
verts = np.stack([c0, c1, c2, c3], axis=1).reshape(-1, 3).astype(np.float32)
|
| 311 |
-
|
| 312 |
-
# 2 triangles per surfel: (0,1,2) and (0,2,3), offset by 4 per surfel.
|
| 313 |
N = len(pts)
|
| 314 |
-
base = (np.arange(N, dtype=np.int64) * 4)[:, None, None]
|
| 315 |
-
tri = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int64)[None, :, :]
|
| 316 |
-
faces = (base + tri).reshape(-1, 3)
|
| 317 |
|
| 318 |
-
#
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
| 320 |
vc[..., :3] = cols[:, None, :]
|
| 321 |
vc[..., 3] = 255
|
| 322 |
vertex_colors = vc.reshape(-1, 4)
|
| 323 |
|
| 324 |
-
# Plain vertex colors only β no PBR material customization, no normals,
|
| 325 |
-
# nothing that would inject lighting variation between surfels.
|
| 326 |
mesh = trimesh.Trimesh(
|
| 327 |
vertices=verts, faces=faces,
|
| 328 |
vertex_colors=vertex_colors,
|
|
@@ -330,7 +289,7 @@ def _make_glb(image_pil_texture: Image.Image, pointmap_hwc: np.ndarray,
|
|
| 330 |
)
|
| 331 |
out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".glb").name
|
| 332 |
mesh.export(out_path)
|
| 333 |
-
_glb_inject_unlit(out_path) # β
|
| 334 |
return out_path
|
| 335 |
|
| 336 |
|
|
|
|
| 236 |
|
| 237 |
|
| 238 |
def _make_glb(image_pil_texture: Image.Image, pointmap_hwc: np.ndarray,
|
| 239 |
+
mask_hw: np.ndarray, max_points: int = 25_000) -> str:
|
| 240 |
+
"""Render the pointmap as a cloud of small icospheres β one per pixel,
|
| 241 |
+
coloured by the source image. With KHR_materials_unlit injected post-
|
| 242 |
+
export, each ball renders as a flat-coloured 3D primitive (no lighting
|
| 243 |
+
variation), giving a clean "candy ball cloud" surface."""
|
|
|
|
|
|
|
|
|
|
| 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 |
if len(yy) > max_points:
|
| 249 |
idx = np.random.default_rng(0).choice(len(yy), max_points, replace=False)
|
| 250 |
yy, xx = yy[idx], xx[idx]
|
| 251 |
|
| 252 |
pts = pointmap_hwc[yy, xx].astype(np.float32)
|
|
|
|
| 253 |
image_native = np.asarray(image_pil_texture.resize((W, H), Image.LANCZOS))
|
| 254 |
cols = image_native[yy, xx].astype(np.uint8)
|
| 255 |
|
| 256 |
flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
|
| 257 |
pts = pts * flip
|
|
|
|
| 258 |
centroid = pts.mean(axis=0).astype(np.float32) if len(pts) else np.zeros(3, np.float32)
|
| 259 |
pts = pts - centroid
|
| 260 |
|
| 261 |
+
# Sphere radius: surface-spacing estimate = bbox diagonal / sqrt(N).
|
| 262 |
+
# Factor 1.0 β balls just kiss neighbours; 1.5 β distinct overlap.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
if len(pts) >= 2:
|
| 264 |
extent_diag = float(np.linalg.norm(np.ptp(pts, axis=0)))
|
| 265 |
r = max(extent_diag / np.sqrt(len(pts)) * 1.5, 1e-4)
|
| 266 |
else:
|
| 267 |
r = 0.005
|
| 268 |
|
| 269 |
+
# Smooth icosphere template (subdivisions=1 β 42 verts, 80 faces).
|
| 270 |
+
sphere = trimesh.creation.icosphere(subdivisions=1, radius=r)
|
| 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,
|
|
|
|
| 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 |
|