samrobertsondev commited on
Commit
d42d85e
·
verified ·
1 Parent(s): 8ffa450

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +106 -72
  2. requirements.txt +2 -0
app.py CHANGED
@@ -6,21 +6,16 @@ import gradio as gr
6
  import numpy as np
7
  import torch
8
  import cv2
 
 
9
 
10
  from moge.model.v2 import MoGeModel
11
 
12
-
13
- # ---------- Model setup ----------
14
-
15
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
16
 
17
 
18
  @torch.no_grad()
19
  def load_model() -> MoGeModel:
20
- """
21
- Load MoGe from the HF repo 'Ruicheng/moge-2-vitl-normal'.
22
- This will download model.pt the first time and cache it.
23
- """
24
  print(f"Loading MoGe model on device: {DEVICE}")
25
  model = MoGeModel.from_pretrained("Ruicheng/moge-2-vitl-normal")
26
  model = model.to(DEVICE)
@@ -31,8 +26,6 @@ def load_model() -> MoGeModel:
31
  MODEL = load_model()
32
 
33
 
34
- # ---------- Helper: run MoGe & get point cloud ----------
35
-
36
  @torch.no_grad()
37
  def run_moge_on_image(image: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
38
  """
@@ -42,37 +35,31 @@ def run_moge_on_image(image: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
42
  points: (N, 3) float32 XYZ
43
  colors: (N, 3) uint8 RGB
44
  """
45
-
46
- # Convert to float tensor [0, 1], CHW, batch
47
  img = image.astype(np.float32) / 255.0
48
- tensor = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).to(DEVICE) # (1,3,H,W)
 
 
 
 
 
49
 
50
- # --- Run MoGe ---
51
  out = MODEL.infer(tensor)
52
 
53
- # --- DEBUG: log what MoGe actually returned ---
54
  print("MoGe output keys:", list(out.keys()))
55
- shaped = {}
56
- for k, v in out.items():
57
- if torch.is_tensor(v):
58
- shaped[k] = (v.shape, v.dtype, float(v.min()), float(v.max()))
59
- else:
60
- shaped[k] = type(v).__name__
61
- print("MoGe output summary:", shaped)
62
 
63
- # --- Try several common patterns ---
 
 
64
 
65
  points = None
66
  colors = None
67
 
68
- # 1) Single tensor with xyzrgb in last dim: (B, N, 6)
69
  if "pcd" in out:
70
  pcd = out["pcd"]
71
  if pcd.ndim == 3 and pcd.shape[-1] >= 3:
72
- # remove batch
73
  if pcd.shape[0] == 1:
74
  pcd = pcd[0]
75
- pcd_np = pcd.detach().cpu().float().numpy() # (N, C)
76
  points = pcd_np[:, :3]
77
  if pcd_np.shape[1] >= 6:
78
  cols = pcd_np[:, 3:6]
@@ -80,7 +67,6 @@ def run_moge_on_image(image: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
80
  cols = (cols * 255.0).clip(0, 255)
81
  colors = cols.astype(np.uint8)
82
 
83
- # 2) Separate "points" and "colors"/"rgb"
84
  if points is None:
85
  if "points" in out:
86
  pts = out["points"]
@@ -93,11 +79,8 @@ def run_moge_on_image(image: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
93
  if pts.ndim == 3 and pts.shape[0] == 1:
94
  pts = pts[0]
95
  pts_np = pts.detach().cpu().float().numpy()
96
- if pts_np.shape[-1] != 3:
97
- raise RuntimeError(f"Expected points last dim=3, got {pts_np.shape}")
98
  points = pts_np
99
 
100
- # colors
101
  col_tensor = None
102
  for k in ["colors", "rgb", "point_colors"]:
103
  if k in out:
@@ -112,40 +95,26 @@ def run_moge_on_image(image: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
112
  col_np = (col_np * 255.0).clip(0, 255)
113
  colors = col_np.astype(np.uint8)
114
 
115
- # 3) If still no colors, default to white
116
- if points is not None and colors is None:
117
- colors = np.full_like(points, 255, dtype=np.uint8)
118
-
119
  if points is None:
120
- raise RuntimeError(
121
- f"Could not find point cloud in MoGe output; keys: {list(out.keys())}"
122
- )
123
 
124
- # ensure 2D
125
  points = points.reshape(-1, 3)
126
- colors = colors.reshape(-1, 3)
 
 
 
127
 
128
  n = points.shape[0]
129
  print("MoGe point count:", n)
130
-
131
- # sanity check: bail if the model gave us basically nothing
132
  if n < 100:
133
- raise RuntimeError(f"MoGe returned too few points (N={n}), refusing to write bogus PLY.")
134
 
135
  return points, colors
136
 
137
- # ---------- Helper: write PLY into memory ----------
138
 
139
  def pointcloud_to_ply_bytes(points: np.ndarray, colors: np.ndarray) -> bytes:
140
- """
141
- Create an ASCII PLY file as bytes from points & colors.
142
-
143
- points: (N,3) float32
144
- colors: (N,3) uint8
145
- """
146
  n = points.shape[0]
147
- pts = points.reshape(-1, 3)
148
- cols = colors.reshape(-1, 3)
149
 
150
  header = f"""ply
151
  format ascii 1.0
@@ -160,53 +129,118 @@ end_header
160
  """
161
  lines = []
162
  for i in range(n):
163
- x, y, z = pts[i]
164
- r, g, b = cols[i]
165
  lines.append(f"{x:.6f} {y:.6f} {z:.6f} {int(r)} {int(g)} {int(b)}")
166
 
167
  body = "\n".join(lines) + "\n"
168
- ply_str = header + body
169
- return ply_str.encode("utf-8")
170
 
171
 
172
- # ---------- Gradio inference function ----------
173
-
174
- def infer_and_export_ply(image: np.ndarray) -> gr.File:
175
  """
176
- Gradio callable: takes an RGB image, returns a PLY file.
 
 
177
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  if image is None:
179
  raise gr.Error("Please upload an image.")
180
 
181
- # image arrives in HxWx3 RGB (uint8) from gr.Image
182
  points, colors = run_moge_on_image(image)
183
 
 
184
  ply_bytes = pointcloud_to_ply_bytes(points, colors)
185
-
186
- # Save to a temporary file path Gradio can return
187
- tmp_path = "output.ply"
188
- with open(tmp_path, "wb") as f:
189
  f.write(ply_bytes)
190
 
191
- return tmp_path
 
 
 
 
192
 
 
193
 
194
- # ---------- Gradio app ----------
195
 
196
- title = "MoGe 3D Reconstruction → Textured Point Cloud (PLY)"
197
  description = (
198
- "Upload an image and get a colored point cloud (PLY) reconstructed by MoGe. "
199
- "You can download the PLY and render it in Three.js or any 3D viewer."
200
  )
201
 
202
  demo = gr.Interface(
203
- fn=infer_and_export_ply,
204
  inputs=gr.Image(type="numpy", label="Input image"),
205
- outputs=gr.File(label="Download PLY"),
 
 
 
206
  title=title,
207
  description=description,
208
  )
209
 
210
- # Expose for HF Spaces
211
  if __name__ == "__main__":
212
- demo.launch()
 
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)
 
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
  """
 
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)
41
+ .permute(2, 0, 1)
42
+ .unsqueeze(0)
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]
 
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"]
 
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:
 
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
 
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
+ transfer colors from points to mesh vertices via nearest neighbor, and
144
+ export as GLB with vertex colors.
145
  """
146
+ print("Building mesh from point cloud for GLB export")
147
+ # Optional: downsample for speed
148
+ max_points = 50000
149
+ if points.shape[0] > max_points:
150
+ idx = np.random.choice(points.shape[0], max_points, replace=False)
151
+ pts_ds = points[idx]
152
+ cols_ds = colors[idx]
153
+ else:
154
+ pts_ds = points
155
+ cols_ds = colors
156
+
157
+ # Open3D point cloud
158
+ pcd = o3d.geometry.PointCloud()
159
+ pcd.points = o3d.utility.Vector3dVector(pts_ds.astype(np.float64))
160
+ pcd.colors = o3d.utility.Vector3dVector((cols_ds / 255.0).astype(np.float64))
161
+
162
+ # Poisson reconstruction
163
+ mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
164
+ pcd, depth=8
165
+ )
166
+
167
+ # Remove low-density vertices (optional cleanup)
168
+ densities = np.asarray(densities)
169
+ density_thresh = np.quantile(densities, 0.05)
170
+ vertices_to_keep = densities > density_thresh
171
+ mesh = mesh.select_by_index(np.where(vertices_to_keep)[0])
172
+
173
+ mesh.remove_duplicated_vertices()
174
+ mesh.remove_degenerate_triangles()
175
+ mesh.remove_duplicated_triangles()
176
+ mesh.remove_non_manifold_edges()
177
+
178
+ verts = np.asarray(mesh.vertices)
179
+ faces = np.asarray(mesh.triangles)
180
+ print("Mesh verts:", verts.shape, "faces:", faces.shape)
181
+
182
+ if verts.shape[0] == 0 or faces.shape[0] == 0:
183
+ raise RuntimeError("Mesh reconstruction failed; got empty mesh")
184
+
185
+ # Transfer colors from original (downsampled) cloud to mesh vertices
186
+ pcd_tree = o3d.geometry.KDTreeFlann(pcd)
187
+ vert_colors = []
188
+ for v in verts:
189
+ _, idx, _ = pcd_tree.search_knn_vector_3d(v, 1)
190
+ vert_colors.append(np.asarray(pcd.colors)[idx[0]])
191
+ vert_colors = np.stack(vert_colors, axis=0) # (V,3) in [0,1]
192
+
193
+ # Convert to trimesh for GLB export
194
+ tm = trimesh.Trimesh(
195
+ vertices=verts,
196
+ faces=faces,
197
+ vertex_colors=(vert_colors * 255.0).astype(np.uint8),
198
+ process=False,
199
+ )
200
+
201
+ glb_bytes = tm.export(file_type="glb")
202
+ if isinstance(glb_bytes, str):
203
+ glb_bytes = glb_bytes.encode("utf-8")
204
+ return glb_bytes
205
+
206
+
207
+ def infer_and_export_files(image: np.ndarray):
208
  if image is None:
209
  raise gr.Error("Please upload an image.")
210
 
 
211
  points, colors = run_moge_on_image(image)
212
 
213
+ # PLY
214
  ply_bytes = pointcloud_to_ply_bytes(points, colors)
215
+ ply_path = "output.ply"
216
+ with open(ply_path, "wb") as f:
 
 
217
  f.write(ply_bytes)
218
 
219
+ # GLB
220
+ glb_bytes = pointcloud_to_mesh_glb_bytes(points, colors)
221
+ glb_path = "output.glb"
222
+ with open(glb_path, "wb") as f:
223
+ f.write(glb_bytes)
224
 
225
+ return ply_path, glb_path
226
 
 
227
 
228
+ title = "MoGe 3D Reconstruction → PLY + GLB"
229
  description = (
230
+ "Upload an image. MoGe reconstructs a 3D point cloud, which is exported as PLY "
231
+ "and meshed into a colored GLB suitable for Three.js."
232
  )
233
 
234
  demo = gr.Interface(
235
+ fn=infer_and_export_files,
236
  inputs=gr.Image(type="numpy", label="Input image"),
237
+ outputs=[
238
+ gr.File(label="Download PLY (point cloud)"),
239
+ gr.File(label="Download GLB (colored mesh)"),
240
+ ],
241
  title=title,
242
  description=description,
243
  )
244
 
 
245
  if __name__ == "__main__":
246
+ demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
requirements.txt CHANGED
@@ -3,4 +3,6 @@ torchvision
3
  numpy
4
  opencv-python
5
  Pillow
 
 
6
  git+https://github.com/microsoft/MoGe.git
 
3
  numpy
4
  opencv-python
5
  Pillow
6
+ trimesh
7
+ open3d
8
  git+https://github.com/microsoft/MoGe.git