Tohru127 commited on
Commit
e4b3e88
·
verified ·
1 Parent(s): adae5c1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +230 -175
app.py CHANGED
@@ -1,194 +1,249 @@
1
- # ===== Minimal app wrapper around your script logic =====
2
- # Steps: GLPN depth -> RGBD -> point cloud -> Poisson mesh -> save to outputs/
3
- # No extra options, no video. Matches a standard single-image pipeline.
 
4
 
5
- # ---- keep libgomp quiet (optional) ----
6
- import os, multiprocessing
 
 
7
  os.environ.pop("OMP_NUM_THREADS", None)
8
  os.environ.setdefault("OMP_NUM_THREADS", "1")
9
  os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
10
  os.environ.setdefault("MKL_NUM_THREADS", "1")
11
  os.environ.setdefault("NUMEXPR_NUM_THREADS", "1")
12
-
13
- # ---- imports ----
14
- import io, time, traceback
15
- from pathlib import Path
16
- from typing import Tuple, Optional
17
-
18
- import numpy as np
19
- from PIL import Image
20
- import torch
21
- import gradio as gr
22
- import open3d as o3d
23
- from matplotlib import pyplot as plt
24
- from matplotlib.figure import Figure
25
- from transformers import GLPNForDepthEstimation, GLPNImageProcessor
26
-
27
- # ---- device & model ----
28
- DEVICE = torch.device(
29
- "mps" if torch.backends.mps.is_available()
30
- else ("cuda" if torch.cuda.is_available() else "cpu")
31
- )
32
- MODEL_ID = "vinvino02/glpn-nyu"
33
- _PROCESSOR: Optional[GLPNImageProcessor] = None
34
- _MODEL: Optional[GLPNForDepthEstimation] = None
35
-
36
- def _load_model_once() -> Tuple[GLPNImageProcessor, GLPNForDepthEstimation]:
37
- global _PROCESSOR, _MODEL
38
- if _PROCESSOR is None or _MODEL is None:
39
- _PROCESSOR = GLPNImageProcessor.from_pretrained(MODEL_ID)
40
- _MODEL = GLPNForDepthEstimation.from_pretrained(MODEL_ID).to(DEVICE)
41
- _MODEL.eval()
42
- return _PROCESSOR, _MODEL
43
-
44
- # ---- helpers mirroring script behavior ----
45
- def _depth_from_glpn(pil_img: Image.Image) -> np.ndarray:
46
- """Return depth normalized to [0,1], upsampled to original size (matches typical GLPN usage)."""
47
- processor, model = _load_model_once()
48
-
49
- # your script-style resize: height <= 480 and multiple of 32
50
- new_h = min(pil_img.height, 480)
51
- new_h -= (new_h % 32)
52
- new_w = int(new_h * pil_img.width / pil_img.height)
53
- resized = pil_img.resize((new_w, new_h))
54
-
55
- inputs = processor(images=resized, return_tensors="pt")
56
- inputs = {k: v.to(DEVICE) for k, v in inputs.items()}
57
-
58
- with torch.no_grad():
59
- outputs = model(**inputs) # (1, h, w)
60
- pred = outputs.predicted_depth
61
- # upsample logits back to original image size
62
- pred = torch.nn.functional.interpolate(
63
- pred.unsqueeze(1), size=pil_img.size[::-1],
64
- mode="bicubic", align_corners=False
65
- ).squeeze(1)
66
- depth = pred[0].detach().float().cpu().numpy()
67
- # normalize like a typical script
68
- depth -= depth.min()
69
- if depth.max() > 0:
70
- depth /= depth.max()
71
- return depth.astype(np.float32)
72
-
73
- def _depth_preview_img(depth01: np.ndarray) -> Image.Image:
74
- fig: Figure = plt.figure(figsize=(5, 5), dpi=120)
75
- ax = fig.add_subplot(111)
76
- ax.axis("off")
77
- ax.imshow(depth01, cmap="viridis")
78
- buf = io.BytesIO()
79
- plt.savefig(buf, format="png", bbox_inches="tight", pad_inches=0)
80
- plt.close(fig)
81
- buf.seek(0)
82
- return Image.open(buf)
83
-
84
- def _make_rgbd(pil_img: Image.Image, depth_norm01: np.ndarray) -> o3d.geometry.RGBDImage:
85
- # map normalized depth to a metric-like range (like a simple fixed scale in scripts)
86
- depth_mm = (depth_norm01 * 3000.0).astype(np.float32) # ~0–3 m span; matches common simple pipelines
87
- depth_o3d = o3d.geometry.Image(depth_mm)
88
- color_o3d = o3d.geometry.Image(np.array(pil_img.convert("RGB")))
89
- return o3d.geometry.RGBDImage.create_from_color_and_depth(
90
- color_o3d, depth_o3d,
91
- convert_rgb_to_intensity=False,
92
- depth_scale=1000.0, # 1000 units == 1 meter
93
- depth_trunc=10.0, # ignore far depths
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  )
95
 
96
- def _pcd_from_rgbd(rgbd: o3d.geometry.RGBDImage) -> o3d.geometry.PointCloud:
97
- h = np.asarray(rgbd.depth).shape[0]
98
- w = np.asarray(rgbd.depth).shape[1]
99
- # simple pinhole intrinsics (script-style constants)
100
- fx = fy = 575.0
101
- cx, cy = w / 2.0, h / 2.0
102
- intr = o3d.camera.PinholeCameraIntrinsic(w, h, fx, fy, cx, cy)
103
- pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intr)
104
- # flip to a more natural orientation (Open3D default is +Z forward)
105
- pcd.transform([[1,0,0,0],[0,-1,0,0],[0,0,-1,0],[0,0,0,1]])
106
- return pcd
107
-
108
- def _pcd_clean_and_normals(pcd: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud:
109
- if len(pcd.points) == 0:
110
- return pcd
111
- # lightweight clean like a typical script
112
- pcd, _ = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
113
- pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.05, max_nn=30))
114
- pcd.orient_normals_consistent_tangent_plane(10)
115
- return pcd
116
-
117
- def _poisson_mesh(pcd: o3d.geometry.PointCloud, depth: int = 8) -> o3d.geometry.TriangleMesh:
118
- mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd, depth=depth)
119
- mesh.remove_duplicated_vertices()
120
- mesh.remove_degenerate_triangles()
121
- mesh.remove_non_manifold_edges()
122
- mesh.remove_unreferenced_vertices()
123
- mesh.compute_vertex_normals()
124
- return mesh
125
-
126
- # ---- core pipeline (1:1 with a standard single-image script) ----
127
- def pipeline(image: Image.Image):
128
- t0 = time.time()
129
- out_dir = Path("outputs")
130
- out_dir.mkdir(parents=True, exist_ok=True)
131
-
132
- # 1) depth
133
- depth01 = _depth_from_glpn(image)
134
- depth_png = _depth_preview_img(depth01)
135
- depth_png_path = out_dir / "depth_preview.png"
136
- depth_png.save(depth_png_path)
137
-
138
- # 2) rgbd
139
- rgbd = _make_rgbd(image, depth01)
140
-
141
- # 3) point cloud
142
- pcd = _pcd_from_rgbd(rgbd)
143
- pcd = _pcd_clean_and_normals(pcd)
144
- pcd_path = out_dir / "point_cloud.ply"
145
- o3d.io.write_point_cloud(str(pcd_path), pcd)
146
-
147
- # 4) mesh (Poisson)
148
- mesh = _poisson_mesh(pcd, depth=8) # matches common default in script examples
149
- # try OBJ first for viewer compatibility; fallback to PLY
150
- mesh_obj = out_dir / "mesh.obj"
151
- if not o3d.io.write_triangle_mesh(str(mesh_obj), mesh):
152
- mesh_obj = out_dir / "mesh.ply"
153
- o3d.io.write_triangle_mesh(str(mesh_obj), mesh)
154
-
155
- elapsed = time.time() - t0
156
- log_text = f"Done in {elapsed:.1f}s\nSaved:\n- {depth_png_path}\n- {pcd_path}\n- {mesh_obj}"
157
- return depth_png, str(mesh_obj), str(pcd_path), str(mesh_obj), log_text
158
-
159
- # ---- very small UI, just like running the script ----
160
- with gr.Blocks(title="2D → 3D (Script-Exact)") as demo:
161
- gr.Markdown("### 2D → 3D Reconstruction — matches your Python script\nUpload an image → Depth • Point Cloud • Poisson Mesh (saved in `outputs/`).")
162
  with gr.Row():
163
  with gr.Column(scale=1):
164
- img = gr.Image(type="pil", label="Input image", interactive=True)
165
- run_btn = gr.Button("Run", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  with gr.Column(scale=2):
167
  with gr.Tabs():
168
  with gr.Tab("Depth"):
169
- depth_img = gr.Image(type="pil", label="Depth (preview)")
170
- with gr.Tab("3D Mesh"):
171
- viewer = gr.Model3D(label="Interactive Mesh (OBJ/PLY)")
172
  with gr.Tab("Downloads"):
173
- pcd_file = gr.File(label="point_cloud.ply")
174
- mesh_file = gr.File(label="mesh (OBJ/PLY)")
175
  with gr.Tab("Logs"):
176
- logs = gr.Textbox(label="Log", lines=8)
177
 
178
- def _on_run(image):
179
- if image is None:
180
- return None, None, None, "Please upload an image."
181
- try:
182
- depth_im, model3d_path, pcd_path, mesh_path, log = pipeline(image)
183
- return depth_im, model3d_path, pcd_path, mesh_path, log
184
- except Exception as e:
185
- tb = traceback.format_exc()
186
- return None, None, None, None, f"ERROR: {e}\n\n{tb}"
187
-
188
- run_btn.click(_on_run, inputs=[img], outputs=[depth_img, viewer, pcd_file, mesh_file, logs])
189
 
190
- # Optional queue to keep the session stable on small CPUs
191
- demo.queue(concurrency_count=1, max_size=4, status_update_rate=1.0)
192
 
193
  if __name__ == "__main__":
194
- demo.launch(show_error=True, server_keepalive_timeout=180)
 
1
+ # Run-your-script (dynamic) HF Spaces wrapper for main.py with user inputs
2
+ import os, sys, io, time, glob, json, shlex, subprocess
3
+ from pathlib import Path
4
+ from typing import Optional, Tuple
5
 
6
+ import gradio as gr
7
+ from PIL import Image
8
+
9
+ # Keep CPU runtimes stable
10
  os.environ.pop("OMP_NUM_THREADS", None)
11
  os.environ.setdefault("OMP_NUM_THREADS", "1")
12
  os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
13
  os.environ.setdefault("MKL_NUM_THREADS", "1")
14
  os.environ.setdefault("NUMEXPR_NUM_THREADS", "1")
15
+ os.environ.setdefault("MPLBACKEND", "Agg")
16
+
17
+ REPO_ROOT = Path(".").resolve()
18
+
19
+ # ---- Defaults: adjust to match your script, or override in the UI ----
20
+ DEFAULT_SCRIPT = str(REPO_ROOT / "main.py")
21
+ DEFAULT_INPUT_PATH = str(REPO_ROOT / "ROOM.jpg") # where we'll save the uploaded image
22
+ DEFAULT_WORKDIR = str(REPO_ROOT)
23
+ DEFAULT_OUTPUT_DIR = str(REPO_ROOT / "outputs") # where your script writes results
24
+
25
+ # ---------- helpers ----------
26
+ def _save_image(img: Image.Image, path: str) -> str:
27
+ p = Path(path); p.parent.mkdir(parents=True, exist_ok=True)
28
+ img.convert("RGB").save(p, format="JPEG", quality=95)
29
+ return str(p)
30
+
31
+ def _pick_latest(patterns):
32
+ newest = None; mt = -1
33
+ for pat in patterns:
34
+ for fp in glob.glob(pat):
35
+ try:
36
+ sz = os.path.getsize(fp)
37
+ if sz <= 0: continue
38
+ m = os.path.getmtime(fp)
39
+ if m > mt:
40
+ newest, mt = fp, m
41
+ except Exception:
42
+ pass
43
+ return newest
44
+
45
+ def _scan_outputs(output_dir: str):
46
+ od = Path(output_dir)
47
+ depth = _pick_latest([
48
+ str(od / "depth_preview.*"),
49
+ str(od / "*depth*.png"),
50
+ str(od / "*depth*.jpg"),
51
+ str(REPO_ROOT / "depth_preview.*"),
52
+ ])
53
+ pcd = _pick_latest([
54
+ str(od / "point_cloud.ply"),
55
+ str(od / "*.ply"),
56
+ ])
57
+ mesh = _pick_latest([
58
+ str(od / "mesh.obj"),
59
+ str(od / "*.obj"),
60
+ str(od / "mesh.ply"),
61
+ str(od / "*mesh*.ply"),
62
+ str(od / "*.glb"),
63
+ str(od / "*.gltf"),
64
+ ])
65
+ return depth, pcd, mesh
66
+
67
+ def _compose_cli(script_path: str, base_args: str, kv_pairs: str):
68
+ """
69
+ base_args: free-form CLI string (e.g., "--poisson_depth 10 --out outputs")
70
+ kv_pairs: JSON or 'key=value key2=value2' becomes '--key value --key2 value2'
71
+ """
72
+ args = [sys.executable, script_path]
73
+
74
+ # Add free-form args (if provided)
75
+ if base_args and base_args.strip():
76
+ args.extend(shlex.split(base_args.strip()))
77
+
78
+ # Add key=value pairs
79
+ if kv_pairs and kv_pairs.strip():
80
+ # try JSON first
81
+ as_json = None
82
+ try:
83
+ as_json = json.loads(kv_pairs)
84
+ except Exception:
85
+ pass
86
+ if isinstance(as_json, dict):
87
+ for k, v in as_json.items():
88
+ if k.startswith("--"): args.append(k)
89
+ else: args.append(f"--{k}")
90
+ if v is not True and v is not None:
91
+ args.append(str(v))
92
+ else:
93
+ # fallback: split by spaces, accept k=v tokens
94
+ for token in shlex.split(kv_pairs.strip()):
95
+ if "=" in token:
96
+ k, v = token.split("=", 1)
97
+ if k.startswith("--"): args.append(k)
98
+ else: args.append(f"--{k}")
99
+ args.append(v)
100
+ else:
101
+ # allow plain flags like --use_poisson
102
+ args.append(token)
103
+
104
+ return args
105
+
106
+ # ---------- streaming runner ----------
107
+ def _run_streaming(
108
+ image,
109
+ script_path,
110
+ input_path,
111
+ workdir,
112
+ output_dir,
113
+ freeform_args, # raw CLI string
114
+ kv_args, # k=v pairs or JSON
115
+ extra_env_json # ENV as JSON (optional)
116
+ ):
117
+ depth_path = None; pcd_path = None; mesh_path = None
118
+ viewer_path = None
119
+ log_buf = []
120
+
121
+ if image is None:
122
+ yield None, None, None, None, "Please upload an image."
123
+ return
124
+
125
+ # Save input where the script expects it
126
+ try:
127
+ saved = _save_image(image, input_path)
128
+ log_buf.append(f"[app] Saved input → {saved}")
129
+ except Exception as e:
130
+ yield None, None, None, None, f"[Save error] {e}"
131
+ return
132
+
133
+ # Compose CLI
134
+ try:
135
+ args = _compose_cli(script_path, freeform_args, kv_args)
136
+ # If the script expects a positional image path, add it here (uncomment if needed):
137
+ # args.extend([saved])
138
+ log_buf.append(f"[app] Running: {' '.join(shlex.quote(a) for a in args)}")
139
+ except Exception as e:
140
+ yield None, None, None, None, f"[Args error] {e}"
141
+ return
142
+
143
+ # Build environment
144
+ env = os.environ.copy()
145
+ if extra_env_json and extra_env_json.strip():
146
+ try:
147
+ env.update(json.loads(extra_env_json))
148
+ except Exception as e:
149
+ yield None, None, None, None, f"[ENV JSON parse error] {e}"
150
+ return
151
+
152
+ # Launch process, stream logs
153
+ try:
154
+ proc = subprocess.Popen(
155
+ args, cwd=workdir, env=env,
156
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
157
+ text=True, bufsize=1
158
+ )
159
+ except Exception as e:
160
+ yield None, None, None, None, f"[Run error] {e}"
161
+ return
162
+
163
+ last_yield = time.time()
164
+ for line in iter(proc.stdout.readline, ""):
165
+ log_buf.append(line.rstrip("\n"))
166
+ if time.time() - last_yield > 1.0:
167
+ d, p, m = _scan_outputs(output_dir)
168
+ depth_path = depth_path or d
169
+ pcd_path = pcd_path or p
170
+ mesh_path = mesh_path or m
171
+ viewer_path = mesh_path or pcd_path
172
+ yield depth_path, viewer_path, pcd_path, mesh_path, "\n".join(log_buf[-800:])
173
+ last_yield = time.time()
174
+
175
+ proc.wait()
176
+
177
+ # Final scan
178
+ d, p, m = _scan_outputs(output_dir)
179
+ depth_path = depth_path or d
180
+ pcd_path = pcd_path or p
181
+ mesh_path = mesh_path or m
182
+ viewer_path = mesh_path or pcd_path
183
+ log_buf.append(f"[app] Script finished with return code {proc.returncode}")
184
+
185
+ yield depth_path, viewer_path, pcd_path, mesh_path, "\n".join(log_buf[-2000:])
186
+
187
+ # ---------- UI ----------
188
+ with gr.Blocks(title="Run main.py — Dynamic Inputs") as demo:
189
+ gr.Markdown(
190
+ "## Run your `main.py` with dynamic user inputs\n"
191
+ "- Upload an image (we’ll save it to the path your script expects)\n"
192
+ "- Enter **CLI arguments** and/or **key=value** pairs (auto-converted to `--key value`)\n"
193
+ "- We stream stdout/stderr live and show any depth/PCD/mesh files your script writes\n"
194
  )
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  with gr.Row():
197
  with gr.Column(scale=1):
198
+ img = gr.Image(type="pil", label="Upload image", interactive=True)
199
+
200
+ with gr.Accordion("Script paths", open=False):
201
+ script_path = gr.Textbox(value=DEFAULT_SCRIPT, label="Script path (e.g., main.py)")
202
+ input_path = gr.Textbox(value=DEFAULT_INPUT_PATH, label="Save uploaded image to (path your script reads)")
203
+ workdir = gr.Textbox(value=DEFAULT_WORKDIR, label="Working directory")
204
+ output_dir = gr.Textbox(value=DEFAULT_OUTPUT_DIR, label="Output directory to scan")
205
+
206
+ with gr.Accordion("Arguments", open=True):
207
+ freeform_args = gr.Textbox(
208
+ value="",
209
+ placeholder="e.g., --poisson_depth 10 --out outputs",
210
+ label="CLI arguments (free-form)"
211
+ )
212
+ kv_args = gr.Textbox(
213
+ value="",
214
+ placeholder='JSON or k=v (space-separated). e.g., {"poisson_depth":10, "out":"outputs"} or poisson_depth=10 out=outputs',
215
+ label="Key=Value (auto → --key value)"
216
+ )
217
+
218
+ with gr.Accordion("Environment (optional)", open=False):
219
+ extra_env = gr.Textbox(
220
+ value="{}",
221
+ label="ENV as JSON",
222
+ placeholder='e.g., {"OMP_NUM_THREADS":"1"}'
223
+ )
224
+
225
+ run_btn = gr.Button("Run script", variant="primary")
226
+
227
  with gr.Column(scale=2):
228
  with gr.Tabs():
229
  with gr.Tab("Depth"):
230
+ depth_img = gr.Image(type="filepath", label="Depth preview (detected)")
231
+ with gr.Tab("3D Reconstruction"):
232
+ model3d = gr.Model3D(label="Mesh / Point Cloud (OBJ/PLY/GLB/GLTF)")
233
  with gr.Tab("Downloads"):
234
+ pcd_file = gr.File(label="Point cloud (PLY)")
235
+ mesh_file = gr.File(label="Mesh (OBJ/PLY/GLB/GLTF)")
236
  with gr.Tab("Logs"):
237
+ logs = gr.Textbox(label="Live logs", lines=20)
238
 
239
+ run_btn.click(
240
+ _run_streaming,
241
+ inputs=[img, script_path, input_path, workdir, output_dir, freeform_args, kv_args, extra_env],
242
+ outputs=[depth_img, model3d, pcd_file, mesh_file, logs]
243
+ )
 
 
 
 
 
 
244
 
245
+ # Keep long jobs alive & serialized
246
+ demo.queue(concurrency_count=1, max_size=8, status_update_rate=1.0)
247
 
248
  if __name__ == "__main__":
249
+ demo.launch(show_error=True, server_keepalive_timeout=180)