Tohru127 commited on
Commit
1668a33
Β·
verified Β·
1 Parent(s): 5c57795

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +31 -57
app.py CHANGED
@@ -1,5 +1,4 @@
1
- # Minimal 2D -> 3D (GLPN + Open3D) β€” fixed geometry + reliable viewer
2
-
3
  import os, time, traceback
4
  from pathlib import Path
5
  import numpy as np
@@ -9,11 +8,11 @@ import gradio as gr
9
  import open3d as o3d
10
  from transformers import GLPNForDepthEstimation, GLPNImageProcessor
11
 
12
- # Quiet threads warnings in Spaces
13
  os.environ.setdefault("OMP_NUM_THREADS", "1")
14
  os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
15
 
16
- # -------- Model / device --------
17
  DEVICE = torch.device(
18
  "cuda" if torch.cuda.is_available()
19
  else ("mps" if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available() else "cpu")
@@ -22,7 +21,7 @@ MODEL_ID = "vinvino02/glpn-nyu"
22
  PROCESSOR = GLPNImageProcessor.from_pretrained(MODEL_ID)
23
  MODEL = GLPNForDepthEstimation.from_pretrained(MODEL_ID).to(DEVICE).eval()
24
 
25
- # -------- Helpers (simple; no knobs) --------
26
  def _resize_main(pil_img: Image.Image):
27
  new_h = max(32, min(pil_img.height, 480))
28
  new_h -= new_h % 32
@@ -31,32 +30,26 @@ def _resize_main(pil_img: Image.Image):
31
 
32
  @torch.inference_mode()
33
  def _depth_pred_float(pil_img: Image.Image) -> np.ndarray:
34
- """Predict depth, upsample to original size (float32, arbitrary scale)."""
35
  resized, (W, H) = _resize_main(pil_img)
36
  inputs = PROCESSOR(images=resized, return_tensors="pt")
37
  inputs = {k: v.to(DEVICE) for k, v in inputs.items()}
38
  out = MODEL(**inputs).predicted_depth # [1, h, w]
39
  up = torch.nn.functional.interpolate(out.unsqueeze(1), size=(H, W), mode="bicubic", align_corners=False).squeeze(1)
40
- d = up[0].detach().float().cpu().numpy()
41
- return d
42
 
43
  def _depth_preview_u8(d: np.ndarray) -> Image.Image:
44
  d = d - d.min()
45
  mx = float(d.max()) if d.size else 1.0
46
  if mx <= 0: mx = 1.0
47
- u8 = (255.0 * d / mx).astype(np.uint8)
48
- return Image.fromarray(u8)
49
 
50
  def _depth_to_metric_meters(d: np.ndarray, near=0.3, far=5.0) -> np.ndarray:
51
- """Map arbitrary depth to meters using robust normalization (2–98% percentile)."""
52
  lo, hi = np.percentile(d, [2.0, 98.0])
53
  d01 = np.clip((d - lo) / max(hi - lo, 1e-6), 0, 1).astype(np.float32)
54
- # 0..1 -> [near, far] meters
55
  return (near + d01 * (far - near)).astype(np.float32)
56
 
57
  def _rgbd_for_open3d(rgb: Image.Image, depth_m: np.ndarray, far=5.0) -> o3d.geometry.RGBDImage:
58
- # Open3D expects depth in the same units used by depth_scale denominator.
59
- depth_scale = 1000.0 # meters * 1000 (mm-like)
60
  depth_o3d = o3d.geometry.Image((depth_m * depth_scale).astype(np.float32))
61
  color_o3d = o3d.geometry.Image(np.array(rgb.convert("RGB")))
62
  return o3d.geometry.RGBDImage.create_from_color_and_depth(
@@ -67,11 +60,9 @@ def _rgbd_for_open3d(rgb: Image.Image, depth_m: np.ndarray, far=5.0) -> o3d.geom
67
  def _pcd_from_rgbd(rgbd: o3d.geometry.RGBDImage) -> o3d.geometry.PointCloud:
68
  h = np.asarray(rgbd.depth).shape[0]
69
  w = np.asarray(rgbd.depth).shape[1]
70
- # keep your original intrinsics spirit; fx=fy=500, cx=w/2, cy=h/2
71
  intr = o3d.camera.PinholeCameraIntrinsic(w, h, 500.0, 500.0, w/2.0, h/2.0)
72
  pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intr)
73
- # Make orientation sensible for web viewers
74
- pcd.transform([[1,0,0,0], [0,-1,0,0], [0,0,-1,0], [0,0,0,1]])
75
  return pcd
76
 
77
  def _clean_pcd(pcd: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud:
@@ -82,20 +73,10 @@ def _clean_pcd(pcd: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud:
82
  pcd.orient_normals_consistent_tangent_plane(10)
83
  return pcd
84
 
85
- def _mesh_from_pcd(pcd: o3d.geometry.PointCloud) -> o3d.geometry.TriangleMesh:
86
  if len(pcd.points) == 0: return o3d.geometry.TriangleMesh()
87
- # Ball-Pivoting first (robust), fallback to Poisson
88
- try:
89
- dists = np.asarray(pcd.compute_nearest_neighbor_distance())
90
- avg = float(np.mean(dists)) if dists.size else 0.01
91
- radii = [avg*2.0, avg*3.0, avg*4.0]
92
- mesh = o3d.geometry.TriangleMesh.create_from_point_cloud_ball_pivoting(
93
- pcd, o3d.utility.DoubleVector(radii)
94
- )
95
- except Exception:
96
- mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd, depth=10, n_threads=1)
97
- # Rotate like your main.py
98
- R = mesh.get_rotation_matrix_from_xyz((np.pi, 0.0, 0.0))
99
  mesh.rotate(R, center=(0,0,0))
100
  mesh.remove_degenerate_triangles(); mesh.remove_duplicated_vertices()
101
  mesh.remove_non_manifold_edges(); mesh.remove_unreferenced_vertices()
@@ -103,18 +84,16 @@ def _mesh_from_pcd(pcd: o3d.geometry.PointCloud) -> o3d.geometry.TriangleMesh:
103
  return mesh
104
 
105
  def _normalize_for_view(mesh: o3d.geometry.TriangleMesh) -> o3d.geometry.TriangleMesh:
106
- """Center + unit scale so it always shows up in the viewer."""
107
  if len(mesh.vertices) == 0: return mesh
108
  aabb = mesh.get_axis_aligned_bounding_box()
109
  c = aabb.get_center()
110
  mesh = mesh.translate(-c, relative=True)
111
- extent = aabb.get_extent()
112
- s = 1.0 / max(extent.max(), 1e-6)
113
  mesh = mesh.scale(s, center=(0,0,0))
114
  mesh.compute_vertex_normals()
115
  return mesh
116
 
117
- # -------- Pipeline (no options) --------
118
  def run(image: Image.Image):
119
  logs = []
120
  t0 = time.time()
@@ -125,42 +104,39 @@ def run(image: Image.Image):
125
  image = image.convert("RGB")
126
  logs.append("1) Predicting depth (GLPN)…")
127
  d_pred = _depth_pred_float(image)
128
- depth_preview = _depth_preview_u8(d_pred) # for UI only
129
 
130
  logs.append("2) Mapping to meters (0.3–5.0 m)…")
131
  d_m = _depth_to_metric_meters(d_pred, near=0.3, far=5.0)
132
 
133
- logs.append("3) Building RGBD and point cloud…")
134
  rgbd = _rgbd_for_open3d(image, d_m, far=5.0)
135
- pcd = _pcd_from_rgbd(rgbd)
136
- pcd = _clean_pcd(pcd)
137
-
138
- logs.append("4) Meshing + rotate…")
139
- mesh = _mesh_from_pcd(pcd)
140
 
141
- # Save raw assets (metric)
142
  out = Path("outputs"); out.mkdir(parents=True, exist_ok=True)
143
- pcd_path = str(out / "point_cloud.ply")
144
  mesh_path = str(out / "mesh.ply")
145
- o3d.io.write_point_cloud(pcd_path, pcd)
146
  o3d.io.write_triangle_mesh(mesh_path, mesh)
 
 
147
 
148
- # Save a viewer copy (normalized so it always displays)
149
  viewer_path = str(out / "mesh_viewer.ply")
150
  o3d.io.write_triangle_mesh(viewer_path, _normalize_for_view(mesh))
151
-
152
- logs.append("5) Saved outputs.")
153
  logs.append(f"Done in {time.time()-t0:.1f}s.")
154
- return depth_preview, viewer_path, pcd_path, mesh_path, "\n".join(logs)
 
155
 
156
  except Exception as e:
157
  tb = traceback.format_exc()
158
  logs.append(f"[ERROR] {e}\n{tb}")
159
- return None, None, None, None, "\n".join(logs)
160
 
161
- # -------- UI --------
162
- with gr.Blocks(title="2D β†’ 3D (GLPN + Open3D) β€” Minimal & Fixed") as demo:
163
- gr.Markdown("### 2D β†’ 3D (GLPN + Open3D)\nUpload β†’ Depth preview β†’ 3D mesh (always visible) β†’ Downloads.")
164
 
165
  with gr.Row():
166
  with gr.Column():
@@ -170,12 +146,10 @@ with gr.Blocks(title="2D β†’ 3D (GLPN + Open3D) β€” Minimal & Fixed") as demo:
170
 
171
  with gr.Column():
172
  depth_img = gr.Image(label="Depth (preview)")
173
- model3d = gr.Model3D(label="3D Mesh (normalized for viewing)", height=520)
174
- with gr.Row():
175
- pcd_file = gr.File(label="point_cloud.ply")
176
- mesh_file = gr.File(label="mesh.ply")
177
 
178
- btn.click(run, inputs=[inp], outputs=[depth_img, model3d, pcd_file, mesh_file, logs])
179
 
180
  demo.queue()
181
  demo.launch(ssr_mode=False)
 
1
+ # 2D -> 3D (GLPN + Open3D) β€” Mesh-only (Poisson) output
 
2
  import os, time, traceback
3
  from pathlib import Path
4
  import numpy as np
 
8
  import open3d as o3d
9
  from transformers import GLPNForDepthEstimation, GLPNImageProcessor
10
 
11
+ # Quiet HF threads warnings
12
  os.environ.setdefault("OMP_NUM_THREADS", "1")
13
  os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
14
 
15
+ # ---- Model / device ----
16
  DEVICE = torch.device(
17
  "cuda" if torch.cuda.is_available()
18
  else ("mps" if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available() else "cpu")
 
21
  PROCESSOR = GLPNImageProcessor.from_pretrained(MODEL_ID)
22
  MODEL = GLPNForDepthEstimation.from_pretrained(MODEL_ID).to(DEVICE).eval()
23
 
24
+ # ---- Helpers ----
25
  def _resize_main(pil_img: Image.Image):
26
  new_h = max(32, min(pil_img.height, 480))
27
  new_h -= new_h % 32
 
30
 
31
  @torch.inference_mode()
32
  def _depth_pred_float(pil_img: Image.Image) -> np.ndarray:
 
33
  resized, (W, H) = _resize_main(pil_img)
34
  inputs = PROCESSOR(images=resized, return_tensors="pt")
35
  inputs = {k: v.to(DEVICE) for k, v in inputs.items()}
36
  out = MODEL(**inputs).predicted_depth # [1, h, w]
37
  up = torch.nn.functional.interpolate(out.unsqueeze(1), size=(H, W), mode="bicubic", align_corners=False).squeeze(1)
38
+ return up[0].detach().float().cpu().numpy()
 
39
 
40
  def _depth_preview_u8(d: np.ndarray) -> Image.Image:
41
  d = d - d.min()
42
  mx = float(d.max()) if d.size else 1.0
43
  if mx <= 0: mx = 1.0
44
+ return Image.fromarray((255.0 * d / mx).astype(np.uint8))
 
45
 
46
  def _depth_to_metric_meters(d: np.ndarray, near=0.3, far=5.0) -> np.ndarray:
 
47
  lo, hi = np.percentile(d, [2.0, 98.0])
48
  d01 = np.clip((d - lo) / max(hi - lo, 1e-6), 0, 1).astype(np.float32)
 
49
  return (near + d01 * (far - near)).astype(np.float32)
50
 
51
  def _rgbd_for_open3d(rgb: Image.Image, depth_m: np.ndarray, far=5.0) -> o3d.geometry.RGBDImage:
52
+ depth_scale = 1000.0 # meters * 1000
 
53
  depth_o3d = o3d.geometry.Image((depth_m * depth_scale).astype(np.float32))
54
  color_o3d = o3d.geometry.Image(np.array(rgb.convert("RGB")))
55
  return o3d.geometry.RGBDImage.create_from_color_and_depth(
 
60
  def _pcd_from_rgbd(rgbd: o3d.geometry.RGBDImage) -> o3d.geometry.PointCloud:
61
  h = np.asarray(rgbd.depth).shape[0]
62
  w = np.asarray(rgbd.depth).shape[1]
 
63
  intr = o3d.camera.PinholeCameraIntrinsic(w, h, 500.0, 500.0, w/2.0, h/2.0)
64
  pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intr)
65
+ pcd.transform([[1,0,0,0],[0,-1,0,0],[0,0,-1,0],[0,0,0,1]]) # upright for web
 
66
  return pcd
67
 
68
  def _clean_pcd(pcd: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud:
 
73
  pcd.orient_normals_consistent_tangent_plane(10)
74
  return pcd
75
 
76
+ def _poisson_mesh_from_pcd(pcd: o3d.geometry.PointCloud) -> o3d.geometry.TriangleMesh:
77
  if len(pcd.points) == 0: return o3d.geometry.TriangleMesh()
78
+ mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd, depth=10, n_threads=1)
79
+ R = mesh.get_rotation_matrix_from_xyz((np.pi, 0.0, 0.0)) # match main.py
 
 
 
 
 
 
 
 
 
 
80
  mesh.rotate(R, center=(0,0,0))
81
  mesh.remove_degenerate_triangles(); mesh.remove_duplicated_vertices()
82
  mesh.remove_non_manifold_edges(); mesh.remove_unreferenced_vertices()
 
84
  return mesh
85
 
86
  def _normalize_for_view(mesh: o3d.geometry.TriangleMesh) -> o3d.geometry.TriangleMesh:
 
87
  if len(mesh.vertices) == 0: return mesh
88
  aabb = mesh.get_axis_aligned_bounding_box()
89
  c = aabb.get_center()
90
  mesh = mesh.translate(-c, relative=True)
91
+ s = 1.0 / max(aabb.get_extent().max(), 1e-6)
 
92
  mesh = mesh.scale(s, center=(0,0,0))
93
  mesh.compute_vertex_normals()
94
  return mesh
95
 
96
+ # ---- Pipeline (mesh-only output) ----
97
  def run(image: Image.Image):
98
  logs = []
99
  t0 = time.time()
 
104
  image = image.convert("RGB")
105
  logs.append("1) Predicting depth (GLPN)…")
106
  d_pred = _depth_pred_float(image)
107
+ depth_preview = _depth_preview_u8(d_pred)
108
 
109
  logs.append("2) Mapping to meters (0.3–5.0 m)…")
110
  d_m = _depth_to_metric_meters(d_pred, near=0.3, far=5.0)
111
 
112
+ logs.append("3) RGBD -> PCD -> Poisson mesh…")
113
  rgbd = _rgbd_for_open3d(image, d_m, far=5.0)
114
+ pcd = _pcd_from_rgbd(rgbd)
115
+ pcd = _clean_pcd(pcd)
116
+ mesh = _poisson_mesh_from_pcd(pcd)
 
 
117
 
118
+ # Save ONLY the mesh
119
  out = Path("outputs"); out.mkdir(parents=True, exist_ok=True)
 
120
  mesh_path = str(out / "mesh.ply")
 
121
  o3d.io.write_triangle_mesh(mesh_path, mesh)
122
+ logs.append(f"Saved mesh β†’ {mesh_path}")
123
+ logs.append(f"Mesh stats: Vertices={len(mesh.vertices):,} Triangles={len(mesh.triangles):,}")
124
 
125
+ # Viewer copy (normalized so it always shows)
126
  viewer_path = str(out / "mesh_viewer.ply")
127
  o3d.io.write_triangle_mesh(viewer_path, _normalize_for_view(mesh))
 
 
128
  logs.append(f"Done in {time.time()-t0:.1f}s.")
129
+
130
+ return depth_preview, viewer_path, mesh_path, "\n".join(logs)
131
 
132
  except Exception as e:
133
  tb = traceback.format_exc()
134
  logs.append(f"[ERROR] {e}\n{tb}")
135
+ return None, None, None, "\n".join(logs)
136
 
137
+ # ---- UI ----
138
+ with gr.Blocks(title="2D β†’ 3D (GLPN + Open3D) β€” Mesh Only") as demo:
139
+ gr.Markdown("### 2D β†’ 3D β€” Mesh Only (Poisson)\nUpload β†’ Depth preview β†’ **Triangle mesh** (viewer + PLY download).")
140
 
141
  with gr.Row():
142
  with gr.Column():
 
146
 
147
  with gr.Column():
148
  depth_img = gr.Image(label="Depth (preview)")
149
+ model3d = gr.Model3D(label="Triangle Mesh (normalized for viewing)", height=520)
150
+ mesh_file = gr.File(label="mesh.ply")
 
 
151
 
152
+ btn.click(run, inputs=[inp], outputs=[depth_img, model3d, mesh_file, logs])
153
 
154
  demo.queue()
155
  demo.launch(ssr_mode=False)