Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
-
#
|
| 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
|
| 13 |
os.environ.setdefault("OMP_NUM_THREADS", "1")
|
| 14 |
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 15 |
|
| 16 |
-
#
|
| 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 |
-
#
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 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
|
| 86 |
if len(pcd.points) == 0: return o3d.geometry.TriangleMesh()
|
| 87 |
-
|
| 88 |
-
|
| 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 |
-
|
| 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 |
-
#
|
| 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)
|
| 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)
|
| 134 |
rgbd = _rgbd_for_open3d(image, d_m, far=5.0)
|
| 135 |
-
pcd
|
| 136 |
-
pcd
|
| 137 |
-
|
| 138 |
-
logs.append("4) Meshing + rotateβ¦")
|
| 139 |
-
mesh = _mesh_from_pcd(pcd)
|
| 140 |
|
| 141 |
-
# Save
|
| 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 |
-
#
|
| 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 |
-
|
|
|
|
| 155 |
|
| 156 |
except Exception as e:
|
| 157 |
tb = traceback.format_exc()
|
| 158 |
logs.append(f"[ERROR] {e}\n{tb}")
|
| 159 |
-
return None, None, None,
|
| 160 |
|
| 161 |
-
#
|
| 162 |
-
with gr.Blocks(title="2D β 3D (GLPN + Open3D) β
|
| 163 |
-
gr.Markdown("### 2D β 3D
|
| 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="
|
| 174 |
-
|
| 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,
|
| 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)
|