samrobertsondev commited on
Commit
fd9a8c1
·
verified ·
1 Parent(s): 3dc1399

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +93 -206
  2. requirements.txt +2 -3
app.py CHANGED
@@ -1,4 +1,3 @@
1
- import io
2
  import os
3
  from typing import Tuple
4
 
@@ -6,17 +5,26 @@ import gradio as gr
6
  import numpy as np
7
  import torch
8
  import cv2
9
- import open3d as o3d
10
- import trimesh
11
 
12
  from moge.model.v2 import MoGeModel
13
 
14
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
15
 
16
 
 
 
17
  @torch.no_grad()
18
  def load_model() -> MoGeModel:
 
 
 
 
 
 
 
 
19
  print(f"Loading MoGe model on device: {DEVICE}")
 
20
  model = MoGeModel.from_pretrained("Ruicheng/moge-2-vitl-normal")
21
  model = model.to(DEVICE)
22
  model.eval()
@@ -26,15 +34,18 @@ def load_model() -> MoGeModel:
26
  MODEL = load_model()
27
 
28
 
 
 
29
  @torch.no_grad()
30
- def run_moge_on_image(image: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
31
  """
32
  image: HxWx3 RGB uint8 numpy array.
33
 
34
  Returns:
35
- points: (N, 3) float32 XYZ
36
- colors: (N, 3) uint8 RGB
37
  """
 
 
38
  img = image.astype(np.float32) / 255.0
39
  tensor = (
40
  torch.from_numpy(img)
@@ -43,226 +54,102 @@ def run_moge_on_image(image: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
43
  .to(DEVICE) # (1,3,H,W)
44
  )
45
 
46
- out = MODEL.infer(tensor)
47
-
48
- print("MoGe output keys:", list(out.keys()))
49
-
50
- # You already have this part working;
51
- # keep your existing logic if it's different.
52
- # Here’s a generic version that assumes out["pcd"] (B,N,6) or out["points"]/out["colors"].
53
-
54
- points = None
55
- colors = None
56
-
57
- if "pcd" in out:
58
- pcd = out["pcd"]
59
- if pcd.ndim == 3 and pcd.shape[-1] >= 3:
60
- if pcd.shape[0] == 1:
61
- pcd = pcd[0]
62
- pcd_np = pcd.detach().cpu().float().numpy()
63
- points = pcd_np[:, :3]
64
- if pcd_np.shape[1] >= 6:
65
- cols = pcd_np[:, 3:6]
66
- if cols.max() <= 1.0:
67
- cols = (cols * 255.0).clip(0, 255)
68
- colors = cols.astype(np.uint8)
69
-
70
- if points is None:
71
- if "points" in out:
72
- pts = out["points"]
73
- elif "point_cloud" in out:
74
- pts = out["point_cloud"]
75
- else:
76
- pts = None
77
-
78
- if pts is not None:
79
- if pts.ndim == 3 and pts.shape[0] == 1:
80
- pts = pts[0]
81
- pts_np = pts.detach().cpu().float().numpy()
82
- points = pts_np
83
-
84
- col_tensor = None
85
- for k in ["colors", "rgb", "point_colors"]:
86
- if k in out:
87
- col_tensor = out[k]
88
- break
89
-
90
- if col_tensor is not None:
91
- if col_tensor.ndim == 3 and col_tensor.shape[0] == 1:
92
- col_tensor = col_tensor[0]
93
- col_np = col_tensor.detach().cpu().float().numpy()
94
- if col_np.max() <= 1.0:
95
- col_np = (col_np * 255.0).clip(0, 255)
96
- colors = col_np.astype(np.uint8)
97
-
98
- if points is None:
99
- raise RuntimeError(f"Could not find point cloud in MoGe output")
100
-
101
- points = points.reshape(-1, 3)
102
- if colors is None:
103
- colors = np.full_like(points, 255, dtype=np.uint8)
104
- else:
105
- colors = colors.reshape(-1, 3)
106
-
107
- n = points.shape[0]
108
- print("MoGe point count:", n)
109
- if n < 100:
110
- raise RuntimeError(f"Too few points (N={n}), refusing to export")
111
-
112
- return points, colors
113
-
114
-
115
- def pointcloud_to_ply_bytes(points: np.ndarray, colors: np.ndarray) -> bytes:
116
- n = points.shape[0]
117
- print("Writing PLY with", n, "points")
118
-
119
- header = f"""ply
120
- format ascii 1.0
121
- element vertex {n}
122
- property float x
123
- property float y
124
- property float z
125
- property uchar red
126
- property uchar green
127
- property uchar blue
128
- end_header
129
- """
130
- lines = []
131
- for i in range(n):
132
- x, y, z = points[i]
133
- r, g, b = colors[i]
134
- lines.append(f"{x:.6f} {y:.6f} {z:.6f} {int(r)} {int(g)} {int(b)}")
135
-
136
- body = "\n".join(lines) + "\n"
137
- return (header + body).encode("utf-8")
138
-
139
-
140
- def pointcloud_to_mesh_glb_bytes(points: np.ndarray, colors: np.ndarray) -> bytes:
141
- """
142
- Build a surface mesh from the point cloud using Poisson reconstruction,
143
- denoise it, transfer colors from points to mesh vertices via nearest neighbor,
144
- and export as GLB with vertex colors.
145
- """
146
- print("Building mesh from point cloud for GLB export")
147
-
148
- # Basic normalization: center the cloud to reduce numeric issues
149
- center = points.mean(axis=0, keepdims=True)
150
- pts_norm = points - center
151
-
152
- # Optional: downsample for speed
153
- pcd = o3d.geometry.PointCloud()
154
- pcd.points = o3d.utility.Vector3dVector(pts_norm.astype(np.float64))
155
- pcd.colors = o3d.utility.Vector3dVector((colors / 255.0).astype(np.float64))
156
-
157
- # Voxel downsample: tweak voxel_size depending on MoGe scale
158
- voxel_size = float(np.linalg.norm(pts_norm.max(axis=0) - pts_norm.min(axis=0)) / 128.0)
159
- print("Voxel size:", voxel_size)
160
- if voxel_size > 0:
161
- pcd = pcd.voxel_down_sample(voxel_size=voxel_size)
162
-
163
- print("After downsample:", np.asarray(pcd.points).shape[0], "points")
164
-
165
- # Remove obvious outliers (radius-based)
166
- print("Removing outliers...")
167
- try:
168
- pcd, _ = pcd.remove_radius_outlier(nb_points=20, radius=voxel_size * 3.0)
169
- except Exception as e:
170
- print("Outlier removal failed:", e)
171
-
172
- print("Estimating normals...")
173
- pcd.estimate_normals(
174
- search_param=o3d.geometry.KDTreeSearchParamKNN(knn=30)
175
  )
176
 
177
- # Orient normals consistently
178
- try:
179
- pcd.orient_normals_consistent_tangent_plane(30)
180
- except Exception as e:
181
- print("Normal orientation failed:", e)
182
-
183
- # Poisson reconstruction
184
- print("Running Poisson reconstruction...")
185
- mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
186
- pcd, depth=8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  )
188
 
189
- densities = np.asarray(densities)
190
- # Keep higher-density areas: cuts away wispy boundary fog
191
- density_thresh = np.quantile(densities, 0.1)
192
- vertices_to_keep = densities > density_thresh
193
- mesh = mesh.select_by_index(np.where(vertices_to_keep)[0])
194
-
195
- mesh.remove_duplicated_vertices()
196
- mesh.remove_degenerate_triangles()
197
- mesh.remove_duplicated_triangles()
198
- mesh.remove_non_manifold_edges()
199
-
200
- verts = np.asarray(mesh.vertices)
201
- faces = np.asarray(mesh.triangles)
202
- print("Mesh verts:", verts.shape, "faces:", faces.shape)
203
-
204
- if verts.shape[0] == 0 or faces.shape[0] == 0:
205
- raise RuntimeError("Mesh reconstruction failed; got empty mesh")
206
-
207
- # Transfer colors from filtered point cloud -> mesh vertices
208
- print("Transferring vertex colors...")
209
- pcd_tree = o3d.geometry.KDTreeFlann(pcd)
210
- pcd_colors_np = np.asarray(pcd.colors)
211
- vert_colors = []
212
- for v in verts:
213
- _, idx, _ = pcd_tree.search_knn_vector_3d(v, 1)
214
- vert_colors.append(pcd_colors_np[idx[0]])
215
- vert_colors = np.stack(vert_colors, axis=0) # (V,3) in [0,1]
216
-
217
- # Undo centering so the mesh is in original coordinates
218
- verts = verts + center
219
-
220
- # Convert to trimesh for GLB export
221
- tm = trimesh.Trimesh(
222
- vertices=verts,
223
- faces=faces,
224
- vertex_colors=(vert_colors * 255.0).astype(np.uint8),
225
- process=False,
226
- )
227
 
228
- glb_bytes = tm.export(file_type="glb")
229
- if isinstance(glb_bytes, str):
230
- glb_bytes = glb_bytes.encode("utf-8")
231
- return glb_bytes
232
- def infer_and_export_files(image: np.ndarray):
233
  if image is None:
234
  raise gr.Error("Please upload an image.")
235
 
236
- points, colors = run_moge_on_image(image)
237
-
238
- # PLY
239
- ply_bytes = pointcloud_to_ply_bytes(points, colors)
240
- ply_path = "output.ply"
241
- with open(ply_path, "wb") as f:
242
- f.write(ply_bytes)
243
 
244
- # GLB
245
- glb_bytes = pointcloud_to_mesh_glb_bytes(points, colors)
246
  glb_path = "output.glb"
247
  with open(glb_path, "wb") as f:
248
  f.write(glb_bytes)
249
 
250
- return ply_path, glb_path
 
251
 
 
252
 
253
- title = "MoGe 3D Reconstruction → PLY + GLB"
254
  description = (
255
- "Upload an image. MoGe reconstructs a 3D point cloud, which is exported as PLY "
256
- "and meshed into a colored GLB suitable for Three.js."
257
  )
258
 
259
  demo = gr.Interface(
260
- fn=infer_and_export_files,
261
  inputs=gr.Image(type="numpy", label="Input image"),
262
- outputs=[
263
- gr.File(label="Download PLY (point cloud)"),
264
- gr.File(label="Download GLB (colored mesh)"),
265
- ],
266
  title=title,
267
  description=description,
268
  )
 
 
1
  import os
2
  from typing import Tuple
3
 
 
5
  import numpy as np
6
  import torch
7
  import cv2
 
 
8
 
9
  from moge.model.v2 import MoGeModel
10
 
11
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
12
 
13
 
14
+ # ---------- Model setup ----------
15
+
16
  @torch.no_grad()
17
  def load_model() -> MoGeModel:
18
+ """
19
+ Load the mesh-capable MoGe model.
20
+
21
+ NOTE:
22
+ - If there is a dedicated mesh checkpoint (e.g. "Ruicheng/moge-2-vitl-mesh"),
23
+ use that ID here.
24
+ - If not, keep the normal one and use the mesh reconstruction API on it.
25
+ """
26
  print(f"Loading MoGe model on device: {DEVICE}")
27
+ # If there is a mesh-specific checkpoint, change this string accordingly.
28
  model = MoGeModel.from_pretrained("Ruicheng/moge-2-vitl-normal")
29
  model = model.to(DEVICE)
30
  model.eval()
 
34
  MODEL = load_model()
35
 
36
 
37
+ # ---------- Helper: run MoGe mesh reconstruction ----------
38
+
39
  @torch.no_grad()
40
+ def run_moge_mesh(image: np.ndarray) -> bytes:
41
  """
42
  image: HxWx3 RGB uint8 numpy array.
43
 
44
  Returns:
45
+ glb_bytes: binary GLB data with texture baked, resolution ~256.
 
46
  """
47
+
48
+ # Convert to float [0,1], CHW, batch
49
  img = image.astype(np.float32) / 255.0
50
  tensor = (
51
  torch.from_numpy(img)
 
54
  .to(DEVICE) # (1,3,H,W)
55
  )
56
 
57
+ # ---- IMPORTANT PART: call the mesh reconstruction API ----
58
+ #
59
+ # You need to adjust THIS CALL to match the actual MoGe code.
60
+ # Look for something like:
61
+ # - MODEL.reconstruct_mesh(...)
62
+ # - MODEL.mesh_reconstruct(...)
63
+ # - MODEL.infer_mesh(...)
64
+ #
65
+ # And for arguments, look for:
66
+ # - mesh_resolution / grid_resolution
67
+ # - texture_size / tex_size
68
+ # - enable_texture / with_texture
69
+ #
70
+ # Below is a TEMPLATE that you should modify once you've checked the repo.
71
+
72
+ # TEMPLATE call – this will almost certainly need renaming:
73
+ result = MODEL.reconstruct_mesh(
74
+ tensor,
75
+ mesh_resolution=256, # 256^3 grid or equivalent
76
+ texture_size=256, # 256x256 texture
77
+ enable_texture=True, # or with_texture=True, etc.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  )
79
 
80
+ # ---- Inspect result structure (one-time debugging) ----
81
+ # While debugging, you can keep these prints to see keys in Space logs:
82
+ print("MoGe mesh result keys:", list(result.keys()))
83
+
84
+ # Common patterns:
85
+ # 1) result["glb"] -> raw GLB bytes
86
+ # 2) result["mesh"] -> mesh object (trimesh / internal) with export method
87
+
88
+ # Case 1: GLB bytes directly
89
+ if "glb" in result:
90
+ glb_bytes = result["glb"]
91
+ if isinstance(glb_bytes, str):
92
+ glb_bytes = glb_bytes.encode("utf-8")
93
+ return glb_bytes
94
+
95
+ # Case 2: mesh object with export method
96
+ if "mesh" in result:
97
+ mesh = result["mesh"]
98
+ # If MoGe mesh exposes something like `to_glb(texture=..., texture_size=256)`:
99
+ if hasattr(mesh, "to_glb"):
100
+ tex = result.get("texture", None)
101
+ if tex is not None:
102
+ glb_bytes = mesh.to_glb(texture=tex, texture_size=256)
103
+ else:
104
+ glb_bytes = mesh.to_glb(texture_size=256)
105
+ if isinstance(glb_bytes, str):
106
+ glb_bytes = glb_bytes.encode("utf-8")
107
+ return glb_bytes
108
+
109
+ # Or if it expects file export:
110
+ if hasattr(mesh, "export"):
111
+ tmp_path = "output.glb"
112
+ tex = result.get("texture", None)
113
+ if tex is not None:
114
+ # This is pseudocode – adapt to the actual mesh.export signature.
115
+ mesh.export(tmp_path, texture=tex, texture_size=256)
116
+ else:
117
+ mesh.export(tmp_path)
118
+ with open(tmp_path, "rb") as f:
119
+ return f.read()
120
+
121
+ raise RuntimeError(
122
+ f"Unsupported MoGe mesh result structure: keys={list(result.keys())}"
123
  )
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ # ---------- Gradio inference function ----------
127
+
128
+ def infer_and_export_glb(image: np.ndarray):
 
 
129
  if image is None:
130
  raise gr.Error("Please upload an image.")
131
 
132
+ glb_bytes = run_moge_mesh(image)
 
 
 
 
 
 
133
 
 
 
134
  glb_path = "output.glb"
135
  with open(glb_path, "wb") as f:
136
  f.write(glb_bytes)
137
 
138
+ return glb_path
139
+
140
 
141
+ # ---------- Gradio app ----------
142
 
143
+ title = "MoGe 3D Reconstruction → Textured GLB (256)"
144
  description = (
145
+ "Upload an image. MoGe reconstructs a textured 3D mesh and exports it as a GLB "
146
+ "with a ~256x256 texture."
147
  )
148
 
149
  demo = gr.Interface(
150
+ fn=infer_and_export_glb,
151
  inputs=gr.Image(type="numpy", label="Input image"),
152
+ outputs=gr.File(label="Download GLB (textured mesh)"),
 
 
 
153
  title=title,
154
  description=description,
155
  )
requirements.txt CHANGED
@@ -3,6 +3,5 @@ torchvision
3
  numpy
4
  opencv-python
5
  Pillow
6
- trimesh
7
- open3d
8
- git+https://github.com/microsoft/MoGe.git
 
3
  numpy
4
  opencv-python
5
  Pillow
6
+ git+https://github.com/microsoft/MoGe.git
7
+ gradio