Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,250 +1,248 @@
|
|
| 1 |
-
# app.py
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import datetime as dt
|
| 5 |
-
import io
|
| 6 |
import os
|
| 7 |
-
import
|
| 8 |
-
import
|
| 9 |
-
import
|
| 10 |
-
import
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
from typing import List, Optional, Tuple
|
| 13 |
|
| 14 |
import gradio as gr
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
#
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def run_pipeline(
|
| 55 |
-
files: List[gr.File],
|
| 56 |
-
max_px: int,
|
| 57 |
-
match_mode: str,
|
| 58 |
-
use_gpu_sift: bool,
|
| 59 |
-
voxel: float,
|
| 60 |
-
depth: int,
|
| 61 |
-
tris: int,
|
| 62 |
-
):
|
| 63 |
-
logs: List[str] = []
|
| 64 |
-
try:
|
| 65 |
-
if not files:
|
| 66 |
-
return None, [], "Please upload 3–30 images.", gr.update(visible=False)
|
| 67 |
-
|
| 68 |
-
if not _ensure_tool("colmap"):
|
| 69 |
-
return None, [], "COLMAP not found. Make sure `packages.txt` contains `colmap`.", gr.update(visible=False)
|
| 70 |
-
|
| 71 |
-
run_id = dt.datetime.now().strftime("run_%Y%m%d_%H%M%S_") + uuid.uuid4().hex[:8]
|
| 72 |
-
run_dir = Path("runs") / run_id
|
| 73 |
-
imgs_dir = run_dir / "images"
|
| 74 |
-
db = run_dir / "db.db"
|
| 75 |
-
sparse_dir = run_dir / "sparse"
|
| 76 |
-
dense_dir = run_dir / "dense"
|
| 77 |
-
run_dir.mkdir(parents=True, exist_ok=True)
|
| 78 |
-
|
| 79 |
-
logs.append(f"Workspace: {run_dir}")
|
| 80 |
-
_save_images(files, imgs_dir, max_px)
|
| 81 |
-
img_count = len(list(imgs_dir.glob("*")))
|
| 82 |
-
logs.append(f"Ingested {img_count} image(s). Max side capped at {max_px}px")
|
| 83 |
-
|
| 84 |
-
# 1) Features
|
| 85 |
-
feat_cmd = [
|
| 86 |
-
"colmap", "feature_extractor",
|
| 87 |
-
"--database_path", str(db),
|
| 88 |
-
"--image_path", str(imgs_dir),
|
| 89 |
-
"--ImageReader.single_camera", "1",
|
| 90 |
-
"--SiftExtraction.use_gpu", "1" if (use_gpu_sift and _ensure_tool("nvidia-smi")) else "0",
|
| 91 |
-
]
|
| 92 |
-
code, out = _run(feat_cmd, cwd=run_dir)
|
| 93 |
-
logs.append("[feature_extractor]\n" + out)
|
| 94 |
-
if code != 0:
|
| 95 |
-
raise RuntimeError("COLMAP feature extraction failed.")
|
| 96 |
-
|
| 97 |
-
# 2) Matching
|
| 98 |
-
if match_mode == "sequential":
|
| 99 |
-
match_cmd = ["colmap", "sequential_matcher", "--database_path", str(db)]
|
| 100 |
-
elif match_mode == "exhaustive":
|
| 101 |
-
match_cmd = ["colmap", "exhaustive_matcher", "--database_path", str(db)]
|
| 102 |
else:
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
)
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
depth = gr.Slider(6, 12, value=9, step=1, label="Poisson depth (higher → more detail)")
|
| 224 |
-
tris = gr.Slider(0, 500_000, value=150_000, step=10_000, label="Target triangles (0 = keep)")
|
| 225 |
-
|
| 226 |
-
run = gr.Button("▶ Reconstruct 3D", variant="primary")
|
| 227 |
-
|
| 228 |
-
with gr.Column(scale=1):
|
| 229 |
-
preview = gr.Model3D(label="Preview (PLY/OBJ)", visible=False)
|
| 230 |
-
outputs = gr.Files(label="Downloads")
|
| 231 |
-
logs = gr.Markdown("Logs will appear here…")
|
| 232 |
-
|
| 233 |
-
run.click(
|
| 234 |
-
run_pipeline,
|
| 235 |
-
inputs=[images, max_px, match_mode, use_gpu_sift, voxel, depth, tris],
|
| 236 |
-
outputs=[preview, outputs, logs, preview],
|
| 237 |
-
queue=True,
|
| 238 |
-
)
|
| 239 |
-
|
| 240 |
-
gr.Markdown(
|
| 241 |
-
"""
|
| 242 |
-
### Notes & Scaling
|
| 243 |
-
- Results are in **arbitrary units** (SfM scale). For metric scale, align in GIS/CAD with known distances.
|
| 244 |
-
- Outdoor scenes with repetitive textures (glass/trees) can be challenging—add more oblique views if possible.
|
| 245 |
-
"""
|
| 246 |
-
)
|
| 247 |
-
return demo
|
| 248 |
|
| 249 |
if __name__ == "__main__":
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
+
import io
|
| 3 |
+
import tempfile
|
| 4 |
+
import numpy as np
|
| 5 |
+
from PIL import Image
|
|
|
|
|
|
|
| 6 |
|
| 7 |
import gradio as gr
|
| 8 |
+
import torch
|
| 9 |
+
from transformers import GLPNForDepthEstimation, GLPNImageProcessor
|
| 10 |
+
|
| 11 |
+
import open3d as o3d
|
| 12 |
+
|
| 13 |
+
# ------------------------------
|
| 14 |
+
# Model setup (loaded once)
|
| 15 |
+
# ------------------------------
|
| 16 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 17 |
+
FE = GLPNImageProcessor.from_pretrained("vinvino02/glpn-nyu")
|
| 18 |
+
MODEL = GLPNForDepthEstimation.from_pretrained("vinvino02/glpn-nyu").to(DEVICE)
|
| 19 |
+
MODEL.eval()
|
| 20 |
+
|
| 21 |
+
# ------------------------------
|
| 22 |
+
# Utilities
|
| 23 |
+
# ------------------------------
|
| 24 |
+
def _resize_to_mult32(img: Image.Image, max_h=480):
|
| 25 |
+
"""Resize keeping aspect, cap height to max_h, and make both dims multiple of 32."""
|
| 26 |
+
new_h = min(max_h, img.height)
|
| 27 |
+
new_h -= new_h % 32
|
| 28 |
+
new_w = int(new_h * img.width / img.height)
|
| 29 |
+
diff = new_w % 32
|
| 30 |
+
new_w = new_w - diff if diff < 16 else new_w + (32 - diff)
|
| 31 |
+
return img.resize((new_w, new_h), Image.BICUBIC)
|
| 32 |
+
|
| 33 |
+
def predict_depth(image_pil: Image.Image):
|
| 34 |
+
"""Run GLPN and return cropped RGB (as PIL) + raw depth (float32 numpy)."""
|
| 35 |
+
img = _resize_to_mult32(image_pil.convert("RGB"))
|
| 36 |
+
inputs = FE(images=img, return_tensors="pt").to(DEVICE)
|
| 37 |
+
|
| 38 |
+
with torch.no_grad():
|
| 39 |
+
outputs = MODEL(**inputs)
|
| 40 |
+
pred = outputs.predicted_depth # (1, 1, H, W)
|
| 41 |
+
|
| 42 |
+
# remove padding GLPN expects around borders (pad=16)
|
| 43 |
+
pad = 16
|
| 44 |
+
depth = pred.squeeze().float().cpu().numpy() * 1000.0 # scale for nicer contrast
|
| 45 |
+
depth = depth[pad:-pad, pad:-pad]
|
| 46 |
+
|
| 47 |
+
rgb = img.crop((pad, pad, img.width - pad, img.height - pad))
|
| 48 |
+
return rgb, depth
|
| 49 |
+
|
| 50 |
+
def depth_to_colormap(depth: np.ndarray):
|
| 51 |
+
"""Return a PIL image (plasma colormap) from depth for preview."""
|
| 52 |
+
import matplotlib
|
| 53 |
+
matplotlib.use("Agg")
|
| 54 |
+
import matplotlib.pyplot as plt
|
| 55 |
+
|
| 56 |
+
d = depth.copy()
|
| 57 |
+
d -= d.min()
|
| 58 |
+
if d.max() > 0:
|
| 59 |
+
d /= d.max()
|
| 60 |
+
d8 = (d * 255).astype(np.uint8)
|
| 61 |
+
|
| 62 |
+
# Make a small PNG buffer
|
| 63 |
+
import matplotlib.cm as cm
|
| 64 |
+
cmap = cm.get_cmap("plasma")
|
| 65 |
+
colored = (cmap(d8)[:, :, :3] * 255).astype(np.uint8)
|
| 66 |
+
return Image.fromarray(colored)
|
| 67 |
+
|
| 68 |
+
def rgbd_to_pointcloud(rgb_pil: Image.Image, depth: np.ndarray):
|
| 69 |
+
"""Create an Open3D point cloud from RGB + relative depth."""
|
| 70 |
+
# Normalize depth to 0..1 then to 0..255 uint8 for Open3D RGBD convenience
|
| 71 |
+
d = depth.copy()
|
| 72 |
+
d -= d.min()
|
| 73 |
+
if d.max() > 0:
|
| 74 |
+
d /= d.max()
|
| 75 |
+
depth_u8 = (d * 255).astype(np.uint8)
|
| 76 |
+
|
| 77 |
+
rgb_np = np.array(rgb_pil) # H, W, 3 (uint8)
|
| 78 |
+
|
| 79 |
+
depth_o3d = o3d.geometry.Image(depth_u8)
|
| 80 |
+
color_o3d = o3d.geometry.Image(rgb_np)
|
| 81 |
+
|
| 82 |
+
rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
|
| 83 |
+
color_o3d, depth_o3d, convert_rgb_to_intensity=False
|
| 84 |
+
)
|
| 85 |
|
| 86 |
+
h, w = rgb_np.shape[:2]
|
| 87 |
+
intr = o3d.camera.PinholeCameraIntrinsic()
|
| 88 |
+
intr.set_intrinsics(w, h, 500.0, 500.0, w / 2.0, h / 2.0)
|
| 89 |
+
|
| 90 |
+
pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intr)
|
| 91 |
+
|
| 92 |
+
# Clean & orient normals
|
| 93 |
+
if len(pcd.points) > 0:
|
| 94 |
+
_, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
|
| 95 |
+
pcd = pcd.select_by_index(ind)
|
| 96 |
+
if len(pcd.points) > 0:
|
| 97 |
+
pcd.estimate_normals()
|
| 98 |
+
pcd.orient_normals_to_align_with_direction()
|
| 99 |
+
return pcd
|
| 100 |
+
|
| 101 |
+
def pointcloud_to_mesh(pcd: o3d.geometry.PointCloud, depth=10):
|
| 102 |
+
if len(pcd.points) == 0:
|
| 103 |
+
return None
|
| 104 |
+
mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
|
| 105 |
+
pcd, depth=depth, n_threads=1
|
| 106 |
)
|
| 107 |
+
# Rotate 180° around x for typical camera convention
|
| 108 |
+
R = mesh.get_rotation_matrix_from_xyz((np.pi, 0, 0))
|
| 109 |
+
mesh.rotate(R, center=(0, 0, 0))
|
| 110 |
+
mesh.compute_vertex_normals()
|
| 111 |
+
return mesh
|
| 112 |
+
|
| 113 |
+
def save_o3d(obj, path):
|
| 114 |
+
ext = os.path.splitext(path)[1].lower()
|
| 115 |
+
if isinstance(obj, o3d.geometry.PointCloud):
|
| 116 |
+
if ext == ".ply":
|
| 117 |
+
o3d.io.write_point_cloud(path, obj)
|
| 118 |
+
else:
|
| 119 |
+
raise ValueError("Point cloud: please save as .ply")
|
| 120 |
+
elif isinstance(obj, o3d.geometry.TriangleMesh):
|
| 121 |
+
if ext == ".obj":
|
| 122 |
+
o3d.io.write_triangle_mesh(path, obj)
|
| 123 |
+
elif ext == ".ply":
|
| 124 |
+
o3d.io.write_triangle_mesh(path, obj)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
else:
|
| 126 |
+
raise ValueError("Mesh: use .obj or .ply")
|
| 127 |
+
else:
|
| 128 |
+
raise ValueError("Unsupported type for saving")
|
| 129 |
+
|
| 130 |
+
def render_mesh_image(mesh: o3d.geometry.TriangleMesh, width=640, height=480):
|
| 131 |
+
"""
|
| 132 |
+
Try offscreen render for a preview PNG. If it fails (e.g., no EGL/OSMesa),
|
| 133 |
+
we return None and rely on the Model3D viewer + downloads.
|
| 134 |
+
"""
|
| 135 |
+
try:
|
| 136 |
+
from open3d.visualization import rendering
|
| 137 |
+
|
| 138 |
+
# Make sure mesh has vertex colors or a default material
|
| 139 |
+
if not mesh.has_vertex_colors():
|
| 140 |
+
mesh.paint_uniform_color([0.8, 0.8, 0.85])
|
| 141 |
+
|
| 142 |
+
renderer = rendering.OffscreenRenderer(width, height)
|
| 143 |
+
mat = rendering.MaterialRecord()
|
| 144 |
+
mat.shader = "defaultLit"
|
| 145 |
+
|
| 146 |
+
scene = renderer.scene
|
| 147 |
+
scene.set_background([1, 1, 1, 1])
|
| 148 |
+
scene.add_geometry("mesh", mesh, mat)
|
| 149 |
+
|
| 150 |
+
bbox = mesh.get_axis_aligned_bounding_box()
|
| 151 |
+
center = bbox.get_center()
|
| 152 |
+
extent = bbox.get_extent()
|
| 153 |
+
radius = np.linalg.norm(extent) * 0.8 + 1e-6
|
| 154 |
+
|
| 155 |
+
# Camera looking at center from +z
|
| 156 |
+
cam = scene.camera
|
| 157 |
+
cam.look_at(center, center + [0, 0, radius], [0, 1, 0])
|
| 158 |
+
|
| 159 |
+
img_o3d = renderer.render_to_image()
|
| 160 |
+
img = np.asarray(img_o3d)
|
| 161 |
+
return Image.fromarray(img)
|
| 162 |
+
except Exception:
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
+
# ------------------------------
|
| 166 |
+
# Gradio pipeline
|
| 167 |
+
# ------------------------------
|
| 168 |
+
def run_pipeline(image: Image.Image, poisson_depth: int = 10):
|
| 169 |
+
"""
|
| 170 |
+
Main function wired to Gradio:
|
| 171 |
+
returns (depth_preview_image, mesh_preview_png, pcd_ply_path, mesh_obj_path)
|
| 172 |
+
"""
|
| 173 |
+
if image is None:
|
| 174 |
+
return None, None, None, None
|
| 175 |
+
|
| 176 |
+
# 1) depth
|
| 177 |
+
rgb, depth = predict_depth(image)
|
| 178 |
+
depth_vis = depth_to_colormap(depth)
|
| 179 |
+
|
| 180 |
+
# 2) point cloud
|
| 181 |
+
pcd = rgbd_to_pointcloud(rgb, depth)
|
| 182 |
+
if len(pcd.points) == 0:
|
| 183 |
+
return depth_vis, None, None, None
|
| 184 |
+
|
| 185 |
+
# 3) mesh
|
| 186 |
+
mesh = pointcloud_to_mesh(pcd, depth=poisson_depth)
|
| 187 |
+
if mesh is None:
|
| 188 |
+
# At least return PCD
|
| 189 |
+
with tempfile.NamedTemporaryFile(suffix=".ply", delete=False) as fpcd:
|
| 190 |
+
save_o3d(pcd, fpcd.name)
|
| 191 |
+
pcd_path = fpcd.name
|
| 192 |
+
return depth_vis, None, pcd_path, None
|
| 193 |
+
|
| 194 |
+
# 4) save artifacts
|
| 195 |
+
with tempfile.NamedTemporaryFile(suffix=".ply", delete=False) as fpcd:
|
| 196 |
+
save_o3d(pcd, fpcd.name)
|
| 197 |
+
pcd_path = fpcd.name
|
| 198 |
+
|
| 199 |
+
# Save mesh in OBJ (works with Gradio Model3D)
|
| 200 |
+
with tempfile.NamedTemporaryFile(suffix=".obj", delete=False) as fmesh:
|
| 201 |
+
save_o3d(mesh, fmesh.name)
|
| 202 |
+
mesh_obj_path = fmesh.name
|
| 203 |
+
|
| 204 |
+
# 5) mesh preview (best effort)
|
| 205 |
+
preview = render_mesh_image(mesh, 768, 512)
|
| 206 |
+
|
| 207 |
+
return depth_vis, preview, pcd_path, mesh_obj_path
|
| 208 |
+
|
| 209 |
+
# ------------------------------
|
| 210 |
+
# Interface
|
| 211 |
+
# ------------------------------
|
| 212 |
+
TITLE = "Monocular Depth → Point Cloud → Poisson Mesh (GLPN + Open3D)"
|
| 213 |
+
DESC = """
|
| 214 |
+
Upload an image. We estimate relative depth (GLPN), build a point cloud, and reconstruct
|
| 215 |
+
a mesh (Poisson). Outputs: depth preview, mesh preview (if renderer available),
|
| 216 |
+
and downloads for .ply (point cloud) and .obj (mesh).
|
| 217 |
+
**Note:** monocular depth lacks absolute scale; this is for visualization/demo purposes.
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
with gr.Blocks(title="2D → 3D Reconstruction") as demo:
|
| 221 |
+
gr.Markdown(f"# {TITLE}")
|
| 222 |
+
gr.Markdown(DESC)
|
| 223 |
+
|
| 224 |
+
with gr.Row():
|
| 225 |
+
with gr.Column():
|
| 226 |
+
in_img = gr.Image(type="pil", label="Input Image")
|
| 227 |
+
poisson_depth = gr.Slider(5, 12, value=10, step=1, label="Poisson depth (mesh detail)")
|
| 228 |
+
run_btn = gr.Button("Reconstruct 3D", variant="primary")
|
| 229 |
+
|
| 230 |
+
with gr.Column():
|
| 231 |
+
depth_out = gr.Image(label="Depth Map (colormap)")
|
| 232 |
+
mesh_preview = gr.Image(label="Mesh Preview (offscreen render)", visible=True)
|
| 233 |
+
|
| 234 |
+
with gr.Row():
|
| 235 |
+
pcd_file = gr.File(label="Download Point Cloud (.ply)")
|
| 236 |
+
mesh_obj_view = gr.Model3D(label="Mesh Viewer (.obj)")
|
| 237 |
+
mesh_obj_file = gr.File(label="Download Mesh (.obj)")
|
| 238 |
+
|
| 239 |
+
run_btn.click(
|
| 240 |
+
fn=run_pipeline,
|
| 241 |
+
inputs=[in_img, poisson_depth],
|
| 242 |
+
outputs=[depth_out, mesh_preview, pcd_file, mesh_obj_view]
|
| 243 |
+
)
|
| 244 |
+
# Also expose mesh file separately (same path as viewer output)
|
| 245 |
+
mesh_obj_view.change(lambda p: p, inputs=mesh_obj_view, outputs=mesh_obj_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
if __name__ == "__main__":
|
| 248 |
+
demo.launch()
|