prithivMLmods commited on
Commit
9236fe1
Β·
verified Β·
1 Parent(s): 2179c65

update app

Browse files
Files changed (1) hide show
  1. app.py +1264 -0
app.py ADDED
@@ -0,0 +1,1264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import gc
8
+ import os
9
+ import shutil
10
+ import sys
11
+ import time
12
+ from datetime import datetime
13
+
14
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
15
+
16
+ import cv2
17
+ import gradio as gr
18
+ import numpy as np
19
+ import spaces
20
+ import torch
21
+ from PIL import Image
22
+ from pillow_heif import register_heif_opener
23
+
24
+ register_heif_opener()
25
+
26
+ import rerun as rr
27
+ try:
28
+ import rerun.blueprint as rrb
29
+ except ImportError:
30
+ rrb = None
31
+ from gradio_rerun import Rerun
32
+
33
+ sys.path.append("mapanything/")
34
+
35
+ from mapanything.utils.geometry import depthmap_to_world_frame, points_to_normals
36
+ from mapanything.utils.image import load_images, rgb
37
+
38
+ MAX_SEED = np.iinfo(np.int32).max
39
+
40
+ # ── MapAnything Configuration ──
41
+ high_level_config = {
42
+ "path": "configs/train.yaml",
43
+ "hf_model_name": "facebook/map-anything",
44
+ "model_str": "mapanything",
45
+ "config_overrides": [
46
+ "machine=aws", "model=mapanything", "model/task=images_only",
47
+ "model.encoder.uses_torch_hub=false",
48
+ ],
49
+ "checkpoint_name": "model.safetensors",
50
+ "config_name": "config.json",
51
+ "trained_with_amp": True,
52
+ "trained_with_amp_dtype": "bf16",
53
+ "data_norm_type": "dinov2",
54
+ "patch_size": 14,
55
+ "resolution": 518,
56
+ }
57
+
58
+ model = None
59
+
60
+
61
+ # ═══════════════════════════════════════════════════════════════════
62
+ # BACKEND FUNCTIONS
63
+ # ═══════════════════════════════════════════════════════════════════
64
+
65
+ def initialize_mapanything_model_fn(config, device):
66
+ from mapanything.utils.hf_utils.hf_helpers import initialize_mapanything_model
67
+ return initialize_mapanything_model(config, device)
68
+
69
+
70
+ @spaces.GPU(duration=120)
71
+ def run_model(target_dir, apply_mask=True, mask_edges=True,
72
+ filter_black_bg=False, filter_white_bg=False):
73
+ global model
74
+ import torch
75
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
76
+ if model is None:
77
+ model = initialize_mapanything_model_fn(high_level_config, device)
78
+ else:
79
+ model = model.to(device)
80
+ model.eval()
81
+
82
+ image_folder_path = os.path.join(target_dir, "images")
83
+ views = load_images(image_folder_path)
84
+ if len(views) == 0:
85
+ raise ValueError("No images found. Check your upload.")
86
+
87
+ outputs = model.infer(views, apply_mask=apply_mask, mask_edges=True,
88
+ memory_efficient_inference=False)
89
+
90
+ predictions = {}
91
+ extrinsic_list, intrinsic_list, world_points_list = [], [], []
92
+ depth_maps_list, images_list, final_mask_list = [], [], []
93
+
94
+ for pred in outputs:
95
+ depthmap_torch = pred["depth_z"][0].squeeze(-1)
96
+ intrinsics_torch = pred["intrinsics"][0]
97
+ camera_pose_torch = pred["camera_poses"][0]
98
+ pts3d_computed, valid_mask = depthmap_to_world_frame(
99
+ depthmap_torch, intrinsics_torch, camera_pose_torch)
100
+ mask = pred.get("mask")
101
+ if mask is not None:
102
+ mask = mask[0].squeeze(-1).cpu().numpy().astype(bool)
103
+ else:
104
+ mask = np.ones_like(depthmap_torch.cpu().numpy(), dtype=bool)
105
+ mask = mask & valid_mask.cpu().numpy()
106
+ image = pred["img_no_norm"][0].cpu().numpy()
107
+ extrinsic_list.append(camera_pose_torch.cpu().numpy())
108
+ intrinsic_list.append(intrinsics_torch.cpu().numpy())
109
+ world_points_list.append(pts3d_computed.cpu().numpy())
110
+ depth_maps_list.append(depthmap_torch.cpu().numpy())
111
+ images_list.append(image)
112
+ final_mask_list.append(mask)
113
+
114
+ predictions["extrinsic"] = np.stack(extrinsic_list, axis=0)
115
+ predictions["intrinsic"] = np.stack(intrinsic_list, axis=0)
116
+ predictions["world_points"] = np.stack(world_points_list, axis=0)
117
+ depth_maps = np.stack(depth_maps_list, axis=0)
118
+ if len(depth_maps.shape) == 3:
119
+ depth_maps = depth_maps[..., np.newaxis]
120
+ predictions["depth"] = depth_maps
121
+ predictions["images"] = np.stack(images_list, axis=0)
122
+ predictions["final_mask"] = np.stack(final_mask_list, axis=0)
123
+
124
+ processed_data = process_predictions_for_visualization(
125
+ predictions, views, high_level_config, filter_black_bg, filter_white_bg)
126
+ torch.cuda.empty_cache()
127
+ return predictions, processed_data
128
+
129
+
130
+ def process_predictions_for_visualization(predictions, views, config,
131
+ filter_black_bg=False, filter_white_bg=False):
132
+ processed_data = {}
133
+ for view_idx, view in enumerate(views):
134
+ image = rgb(view["img"], norm_type=config["data_norm_type"])
135
+ pred_pts3d = predictions["world_points"][view_idx]
136
+ view_data = {"image": image[0], "points3d": pred_pts3d,
137
+ "depth": None, "normal": None, "mask": None}
138
+ mask = predictions["final_mask"][view_idx].copy()
139
+ if filter_black_bg:
140
+ vc = image[0] * 255 if image[0].max() <= 1.0 else image[0]
141
+ mask = mask & (vc.sum(axis=2) >= 16)
142
+ if filter_white_bg:
143
+ vc = image[0] * 255 if image[0].max() <= 1.0 else image[0]
144
+ mask = mask & ~((vc[:,:,0]>240)&(vc[:,:,1]>240)&(vc[:,:,2]>240))
145
+ view_data["mask"] = mask
146
+ view_data["depth"] = predictions["depth"][view_idx].squeeze()
147
+ normals, _ = points_to_normals(pred_pts3d, mask=view_data["mask"])
148
+ view_data["normal"] = normals
149
+ processed_data[view_idx] = view_data
150
+ return processed_data
151
+
152
+
153
+ def colorize_depth(depth_map, mask=None):
154
+ if depth_map is None:
155
+ return None
156
+ import matplotlib.pyplot as plt
157
+ d = depth_map.copy()
158
+ valid = d > 0
159
+ if mask is not None:
160
+ valid = valid & mask
161
+ if valid.sum() > 0:
162
+ vd = d[valid]
163
+ p5, p95 = np.percentile(vd, 5), np.percentile(vd, 95)
164
+ d[valid] = (d[valid] - p5) / (p95 - p5)
165
+ colored = (plt.cm.turbo_r(d)[:, :, :3] * 255).astype(np.uint8)
166
+ colored[~valid] = [255, 255, 255]
167
+ return colored
168
+
169
+
170
+ def colorize_normal(normal_map, mask=None):
171
+ if normal_map is None:
172
+ return None
173
+ nv = normal_map.copy()
174
+ if mask is not None:
175
+ nv[~mask] = [0, 0, 0]
176
+ nv = ((nv + 1.0) / 2.0 * 255).astype(np.uint8)
177
+ return nv
178
+
179
+
180
+ def get_view_data_by_index(processed_data, view_index):
181
+ if processed_data is None or len(processed_data) == 0:
182
+ return None
183
+ keys = list(processed_data.keys())
184
+ if view_index < 0 or view_index >= len(keys):
185
+ view_index = 0
186
+ return processed_data[keys[view_index]]
187
+
188
+
189
+ def update_depth_view(processed_data, view_index):
190
+ vd = get_view_data_by_index(processed_data, view_index)
191
+ if vd is None or vd["depth"] is None:
192
+ return None
193
+ return colorize_depth(vd["depth"], mask=vd.get("mask"))
194
+
195
+
196
+ def update_normal_view(processed_data, view_index):
197
+ vd = get_view_data_by_index(processed_data, view_index)
198
+ if vd is None or vd["normal"] is None:
199
+ return None
200
+ return colorize_normal(vd["normal"], mask=vd.get("mask"))
201
+
202
+
203
+ def update_measure_view(processed_data, view_index):
204
+ vd = get_view_data_by_index(processed_data, view_index)
205
+ if vd is None:
206
+ return None, []
207
+ image = vd["image"].copy()
208
+ if image.dtype != np.uint8:
209
+ image = (image * 255).astype(np.uint8) if image.max() <= 1.0 else image.astype(np.uint8)
210
+ if vd["mask"] is not None:
211
+ inv = ~vd["mask"]
212
+ if inv.any():
213
+ oc = np.array([255, 220, 220], dtype=np.uint8)
214
+ for c in range(3):
215
+ image[:,:,c] = np.where(inv, (0.5*image[:,:,c]+0.5*oc[c]), image[:,:,c]).astype(np.uint8)
216
+ return image, []
217
+
218
+
219
+ def navigate_depth_view(processed_data, current_sel, direction):
220
+ if processed_data is None or len(processed_data) == 0:
221
+ return "View 1", None
222
+ try: cur = int(current_sel.split()[1]) - 1
223
+ except: cur = 0
224
+ nv = (cur + direction) % len(processed_data)
225
+ return f"View {nv+1}", update_depth_view(processed_data, nv)
226
+
227
+
228
+ def navigate_normal_view(processed_data, current_sel, direction):
229
+ if processed_data is None or len(processed_data) == 0:
230
+ return "View 1", None
231
+ try: cur = int(current_sel.split()[1]) - 1
232
+ except: cur = 0
233
+ nv = (cur + direction) % len(processed_data)
234
+ return f"View {nv+1}", update_normal_view(processed_data, nv)
235
+
236
+
237
+ def navigate_measure_view(processed_data, current_sel, direction):
238
+ if processed_data is None or len(processed_data) == 0:
239
+ return "View 1", None, []
240
+ try: cur = int(current_sel.split()[1]) - 1
241
+ except: cur = 0
242
+ nv = (cur + direction) % len(processed_data)
243
+ img, pts = update_measure_view(processed_data, nv)
244
+ return f"View {nv+1}", img, pts
245
+
246
+
247
+ def update_view_selectors(processed_data):
248
+ if processed_data is None or len(processed_data) == 0:
249
+ choices = ["View 1"]
250
+ else:
251
+ choices = [f"View {i+1}" for i in range(len(processed_data))]
252
+ return (gr.Dropdown(choices=choices, value=choices[0]),
253
+ gr.Dropdown(choices=choices, value=choices[0]),
254
+ gr.Dropdown(choices=choices, value=choices[0]))
255
+
256
+
257
+ def populate_visualization_tabs(processed_data):
258
+ if processed_data is None or len(processed_data) == 0:
259
+ return None, None, None, []
260
+ return (update_depth_view(processed_data, 0),
261
+ update_normal_view(processed_data, 0),
262
+ update_measure_view(processed_data, 0)[0], [])
263
+
264
+
265
+ def handle_uploads(unified_upload, s_time_interval=1.0):
266
+ gc.collect()
267
+ torch.cuda.empty_cache()
268
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
269
+ target_dir = f"input_images_{timestamp}"
270
+ target_dir_images = os.path.join(target_dir, "images")
271
+ if os.path.exists(target_dir):
272
+ shutil.rmtree(target_dir)
273
+ os.makedirs(target_dir_images)
274
+ image_paths = []
275
+ if unified_upload is not None:
276
+ for file_data in unified_upload:
277
+ file_path = file_data["name"] if isinstance(file_data, dict) and "name" in file_data else str(file_data)
278
+ file_ext = os.path.splitext(file_path)[1].lower()
279
+ video_exts = [".mp4",".avi",".mov",".mkv",".wmv",".flv",".webm",".m4v",".3gp"]
280
+ if file_ext in video_exts:
281
+ vs = cv2.VideoCapture(file_path)
282
+ fps = vs.get(cv2.CAP_PROP_FPS)
283
+ frame_interval = int(fps * s_time_interval)
284
+ count, vfn = 0, 0
285
+ while True:
286
+ gotit, frame = vs.read()
287
+ if not gotit: break
288
+ count += 1
289
+ if count % frame_interval == 0:
290
+ bn = os.path.splitext(os.path.basename(file_path))[0]
291
+ ip = os.path.join(target_dir_images, f"{bn}_{vfn:06}.png")
292
+ cv2.imwrite(ip, frame)
293
+ image_paths.append(ip)
294
+ vfn += 1
295
+ vs.release()
296
+ elif file_ext in [".heic", ".heif"]:
297
+ try:
298
+ with Image.open(file_path) as img:
299
+ if img.mode not in ("RGB", "L"): img = img.convert("RGB")
300
+ bn = os.path.splitext(os.path.basename(file_path))[0]
301
+ dp = os.path.join(target_dir_images, f"{bn}.jpg")
302
+ img.save(dp, "JPEG", quality=95)
303
+ image_paths.append(dp)
304
+ except:
305
+ dp = os.path.join(target_dir_images, os.path.basename(file_path))
306
+ shutil.copy(file_path, dp)
307
+ image_paths.append(dp)
308
+ else:
309
+ dp = os.path.join(target_dir_images, os.path.basename(file_path))
310
+ shutil.copy(file_path, dp)
311
+ image_paths.append(dp)
312
+ return target_dir, sorted(image_paths)
313
+
314
+
315
+ def log_3d_to_rerun(predictions, frame_filter="All", show_cam=True,
316
+ filter_black_bg=False, filter_white_bg=False):
317
+ """Convert predictions to Rerun RRD stream."""
318
+ rr.init("mapanything_scene")
319
+ rec = rr.memory_recording()
320
+
321
+ num_views = predictions["world_points"].shape[0]
322
+ all_indices = list(range(num_views))
323
+
324
+ if frame_filter != "All":
325
+ try:
326
+ idx = int(frame_filter.split(":")[0])
327
+ all_indices = [idx]
328
+ except:
329
+ pass
330
+
331
+ for i in all_indices:
332
+ pts = predictions["world_points"][i]
333
+ mask = predictions["final_mask"][i]
334
+ imgs = predictions["images"][i]
335
+
336
+ if filter_black_bg:
337
+ vc = imgs * 255 if imgs.max() <= 1.0 else imgs
338
+ mask = mask & (vc.sum(axis=2) >= 16)
339
+ if filter_white_bg:
340
+ vc = imgs * 255 if imgs.max() <= 1.0 else imgs
341
+ mask = mask & ~((vc[:,:,0]>240)&(vc[:,:,1]>240)&(vc[:,:,2]>240))
342
+
343
+ valid_pts = pts[mask]
344
+ valid_colors = imgs[mask]
345
+ if valid_colors.max() <= 1.0:
346
+ valid_colors = (valid_colors * 255).astype(np.uint8)
347
+ else:
348
+ valid_colors = valid_colors.astype(np.uint8)
349
+
350
+ rr.log(f"world/points/view_{i}",
351
+ rr.Points3D(valid_pts, colors=valid_colors, radii=0.005))
352
+
353
+ if show_cam:
354
+ ext = predictions["extrinsic"][i]
355
+ pos = ext[:3, 3]
356
+ rr.log(f"world/cameras/view_{i}",
357
+ rr.Points3D([pos], colors=[[0, 255, 0]], radii=0.03))
358
+
359
+ return rec.storage
360
+
361
+
362
+ def get_scene_info(examples_dir):
363
+ import glob
364
+ scenes = []
365
+ if not os.path.exists(examples_dir):
366
+ return scenes
367
+ for sf in sorted(os.listdir(examples_dir)):
368
+ sp = os.path.join(examples_dir, sf)
369
+ if os.path.isdir(sp):
370
+ exts = ["*.jpg","*.jpeg","*.png","*.bmp","*.tiff","*.tif"]
371
+ ifs = []
372
+ for ext in exts:
373
+ ifs.extend(glob.glob(os.path.join(sp, ext)))
374
+ ifs.extend(glob.glob(os.path.join(sp, ext.upper())))
375
+ if ifs:
376
+ ifs = sorted(ifs)
377
+ scenes.append({"name": sf, "path": sp, "thumbnail": ifs[0],
378
+ "num_images": len(ifs), "image_files": ifs})
379
+ return scenes
380
+
381
+
382
+ def load_example_scene(scene_name, examples_dir="examples"):
383
+ scenes = get_scene_info(examples_dir)
384
+ sel = next((s for s in scenes if s["name"] == scene_name), None)
385
+ if sel is None:
386
+ return None, None, None, "Scene not found"
387
+ target_dir, image_paths = handle_uploads(sel["image_files"], 1.0)
388
+ return None, target_dir, image_paths, f"Loaded scene '{scene_name}' with {sel['num_images']} images. Click Reconstruct to begin."
389
+
390
+
391
+ @spaces.GPU(duration=120)
392
+ def gradio_demo(target_dir, frame_filter="All", show_cam=True,
393
+ filter_black_bg=False, filter_white_bg=False,
394
+ apply_mask=True, show_mesh=True):
395
+ if not target_dir or not os.path.isdir(target_dir) or target_dir == "None":
396
+ return None, "No valid directory. Please upload first.", None, None, None, None, None, "", \
397
+ gr.Dropdown(choices=["View 1"], value="View 1"), \
398
+ gr.Dropdown(choices=["View 1"], value="View 1"), \
399
+ gr.Dropdown(choices=["View 1"], value="View 1")
400
+
401
+ gc.collect()
402
+ torch.cuda.empty_cache()
403
+ target_dir_images = os.path.join(target_dir, "images")
404
+ all_files = sorted(os.listdir(target_dir_images)) if os.path.isdir(target_dir_images) else []
405
+ all_files_labeled = [f"{i}: {fn}" for i, fn in enumerate(all_files)]
406
+ frame_filter_choices = ["All"] + all_files_labeled
407
+
408
+ with torch.no_grad():
409
+ predictions, processed_data = run_model(target_dir, apply_mask)
410
+
411
+ np.savez(os.path.join(target_dir, "predictions.npz"), **predictions)
412
+ if frame_filter is None:
413
+ frame_filter = "All"
414
+
415
+ # Build Rerun data
416
+ rerun_data = log_3d_to_rerun(predictions, frame_filter, show_cam,
417
+ filter_black_bg, filter_white_bg)
418
+
419
+ del predictions
420
+ gc.collect()
421
+ torch.cuda.empty_cache()
422
+
423
+ log_msg = f"Reconstruction Success ({len(all_files)} frames)."
424
+ depth_vis, normal_vis, measure_img, measure_pts = populate_visualization_tabs(processed_data)
425
+ ds, ns, ms = update_view_selectors(processed_data)
426
+
427
+ return (rerun_data, log_msg,
428
+ gr.Dropdown(choices=frame_filter_choices, value=frame_filter, interactive=True),
429
+ processed_data, depth_vis, normal_vis, measure_img, "", ds, ns, ms)
430
+
431
+
432
+ def update_visualization(target_dir, frame_filter, show_cam, is_example,
433
+ filter_black_bg=False, filter_white_bg=False, show_mesh=True):
434
+ if is_example == "True":
435
+ return gr.update(), "No reconstruction available."
436
+ if not target_dir or target_dir == "None" or not os.path.isdir(target_dir):
437
+ return gr.update(), "No reconstruction available."
438
+ pp = os.path.join(target_dir, "predictions.npz")
439
+ if not os.path.exists(pp):
440
+ return gr.update(), "No reconstruction available. Run Reconstruct first."
441
+ loaded = np.load(pp, allow_pickle=True)
442
+ predictions = {k: loaded[k] for k in loaded.keys()}
443
+ rerun_data = log_3d_to_rerun(predictions, frame_filter, show_cam,
444
+ filter_black_bg, filter_white_bg)
445
+ return rerun_data, "Visualization updated."
446
+
447
+
448
+ def update_all_views_on_filter_change(target_dir, filter_black_bg, filter_white_bg,
449
+ processed_data, ds, ns, ms):
450
+ if not target_dir or target_dir == "None" or not os.path.isdir(target_dir):
451
+ return processed_data, None, None, None, []
452
+ pp = os.path.join(target_dir, "predictions.npz")
453
+ if not os.path.exists(pp):
454
+ return processed_data, None, None, None, []
455
+ try:
456
+ loaded = np.load(pp, allow_pickle=True)
457
+ predictions = {k: loaded[k] for k in loaded.keys()}
458
+ views = load_images(os.path.join(target_dir, "images"))
459
+ new_pd = process_predictions_for_visualization(
460
+ predictions, views, high_level_config, filter_black_bg, filter_white_bg)
461
+ di = int(ds.split()[1])-1 if ds else 0
462
+ ni = int(ns.split()[1])-1 if ns else 0
463
+ mi = int(ms.split()[1])-1 if ms else 0
464
+ return (new_pd, update_depth_view(new_pd, di),
465
+ update_normal_view(new_pd, ni),
466
+ update_measure_view(new_pd, mi)[0], [])
467
+ except:
468
+ return processed_data, None, None, None, []
469
+
470
+
471
+ def measure(processed_data, measure_points, current_view_selector, event: gr.SelectData):
472
+ try:
473
+ if processed_data is None or len(processed_data) == 0:
474
+ return None, [], "No data available"
475
+ try: cvi = int(current_view_selector.split()[1]) - 1
476
+ except: cvi = 0
477
+ if cvi < 0 or cvi >= len(processed_data): cvi = 0
478
+ vkeys = list(processed_data.keys())
479
+ cv = processed_data[vkeys[cvi]]
480
+ if cv is None:
481
+ return None, [], "No view data"
482
+ pt = event.index[0], event.index[1]
483
+ if cv["mask"] is not None and 0<=pt[1]<cv["mask"].shape[0] and 0<=pt[0]<cv["mask"].shape[1]:
484
+ if not cv["mask"][pt[1], pt[0]]:
485
+ mi, _ = update_measure_view(processed_data, cvi)
486
+ return mi, measure_points, 'Cannot measure on masked areas (grey regions)'
487
+ measure_points.append(pt)
488
+ image, _ = update_measure_view(processed_data, cvi)
489
+ if image is None:
490
+ return None, [], "No image"
491
+ image = image.copy()
492
+ pts3d = cv["points3d"]
493
+ if image.dtype != np.uint8:
494
+ image = (image*255).astype(np.uint8) if image.max()<=1.0 else image.astype(np.uint8)
495
+ for p in measure_points:
496
+ if 0<=p[0]<image.shape[1] and 0<=p[1]<image.shape[0]:
497
+ image = cv2.circle(image, p, radius=5, color=(255,0,0), thickness=2)
498
+ depth_text = ""
499
+ for i, p in enumerate(measure_points):
500
+ if cv["depth"] is not None and 0<=p[1]<cv["depth"].shape[0] and 0<=p[0]<cv["depth"].shape[1]:
501
+ depth_text += f"P{i+1} depth: {cv['depth'][p[1],p[0]]:.2f}m. "
502
+ if len(measure_points) == 2:
503
+ p1, p2 = measure_points
504
+ if all(0<=v<s for v,s in [(p1[0],image.shape[1]),(p1[1],image.shape[0]),
505
+ (p2[0],image.shape[1]),(p2[1],image.shape[0])]):
506
+ image = cv2.line(image, p1, p2, color=(255,0,0), thickness=2)
507
+ dist_text = "Distance: N/A"
508
+ if pts3d is not None:
509
+ try:
510
+ d = np.linalg.norm(pts3d[p1[1],p1[0]] - pts3d[p2[1],p2[0]])
511
+ dist_text = f"Distance: {d:.2f}m"
512
+ except: pass
513
+ return [image, [], depth_text + dist_text]
514
+ return [image, measure_points, depth_text]
515
+ except Exception as e:
516
+ return None, [], f"Error: {e}"
517
+
518
+
519
+ def reset_measure(processed_data):
520
+ if processed_data is None or len(processed_data) == 0:
521
+ return None, [], ""
522
+ return list(processed_data.values())[0]["image"], [], ""
523
+
524
+
525
+ def clear_fields():
526
+ return None
527
+
528
+
529
+ def update_log():
530
+ return "Loading and Reconstructing..."
531
+
532
+
533
+ def update_gallery_on_unified_upload(files, interval):
534
+ if not files:
535
+ return None, None, None
536
+ target_dir, image_paths = handle_uploads(files, interval)
537
+ return target_dir, image_paths, "Upload complete. Click Reconstruct to begin 3D processing."
538
+
539
+
540
+ def show_resample_button(files):
541
+ if not files:
542
+ return gr.update(visible=False)
543
+ video_exts = [".mp4",".avi",".mov",".mkv",".wmv",".flv",".webm",".m4v",".3gp"]
544
+ for fd in files:
545
+ fp = fd["name"] if isinstance(fd, dict) and "name" in fd else str(fd)
546
+ if os.path.splitext(fp)[1].lower() in video_exts:
547
+ return gr.update(visible=True)
548
+ return gr.update(visible=False)
549
+
550
+
551
+ def resample_video(files, new_interval, current_target_dir):
552
+ if not files:
553
+ return current_target_dir, None, "No files.", gr.update(visible=False)
554
+ if current_target_dir and current_target_dir != "None" and os.path.exists(current_target_dir):
555
+ shutil.rmtree(current_target_dir)
556
+ td, ip = handle_uploads(files, new_interval)
557
+ return td, ip, f"Resampled at {new_interval}s interval.", gr.update(visible=False)
558
+
559
+
560
+ # ═══════════════════════════════════════════════════════════════════
561
+ # PLACEHOLDER -- CSS, HTML, JS, and Gradio Blocks below
562
+ # ═══════════════════════════════════════════════════════════════════
563
+
564
+ css = r"""
565
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
566
+ *{box-sizing:border-box;margin:0;padding:0}
567
+ body,.gradio-container{background:#0f0f13!important;font-family:'Inter',system-ui,sans-serif!important;font-size:14px!important;color:#e4e4e7!important;min-height:100vh}
568
+ .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
569
+ footer{display:none!important}
570
+ .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
571
+ .app-shell{background:#18181b;border:1px solid #27272a;border-radius:16px;margin:12px auto;max-width:1500px;overflow:hidden;box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03)}
572
+ .app-header{background:linear-gradient(135deg,#18181b 0%,#1e1e24 100%);border-bottom:1px solid #27272a;padding:14px 24px;display:flex;align-items:center;justify-content:space-between}
573
+ .app-header-left{display:flex;align-items:center;gap:12px}
574
+ .app-logo{width:36px;height:36px;background:linear-gradient(135deg,#6366f1,#8b5cf6,#a78bfa);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:800;color:#fff;box-shadow:0 4px 12px rgba(99,102,241,.35)}
575
+ .app-title{font-size:18px;font-weight:700;background:linear-gradient(135deg,#e4e4e7,#a1a1aa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px}
576
+ .app-badge{font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;background:rgba(99,102,241,.15);color:#818cf8;border:1px solid rgba(99,102,241,.25);letter-spacing:.3px}
577
+ .app-toolbar{background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}
578
+ .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px}
579
+ .modern-tb-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-width:32px;height:34px;background:transparent;border:1px solid transparent;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;padding:0 12px;font-family:'Inter',sans-serif;color:#fff!important;transition:all .15s ease}
580
+ .modern-tb-btn:hover{background:rgba(99,102,241,.15);border-color:rgba(99,102,241,.3)}
581
+ .modern-tb-btn:active,.modern-tb-btn.active{background:rgba(99,102,241,.25);border-color:rgba(99,102,241,.45)}
582
+ .modern-tb-btn .tb-icon{font-size:15px;line-height:1;color:#fff!important}
583
+ .modern-tb-btn .tb-label{font-size:13px;color:#fff!important;font-weight:600}
584
+ .btn-primary-tb{background:linear-gradient(135deg,#6366f1,#7c3aed)!important;border:none!important;box-shadow:0 2px 8px rgba(99,102,241,.3);color:#fff!important}
585
+ .btn-primary-tb:hover{background:linear-gradient(135deg,#7c7cf5,#8b5cf6)!important;box-shadow:0 4px 16px rgba(99,102,241,.45)}
586
+ .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
587
+ .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
588
+ .app-main-right{width:460px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b;overflow-y:auto;max-height:calc(100vh - 120px)}
589
+ .upload-zone{position:relative;background:#09090b;min-height:200px;display:flex;align-items:center;justify-content:center;border-bottom:1px solid #27272a}
590
+ .upload-click-area{display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;padding:36px 44px;border:2px dashed #3f3f46;border-radius:16px;background:rgba(99,102,241,.03);transition:all .2s ease}
591
+ .upload-click-area:hover{background:rgba(99,102,241,.08);border-color:#6366f1;transform:scale(1.02)}
592
+ .upload-click-area svg{width:64px;height:64px}
593
+ .upload-click-area span{margin-top:12px;font-size:13px;color:#71717a}
594
+ .gallery-zone{background:#09090b;border-bottom:1px solid #27272a;padding:12px;min-height:100px;display:none}
595
+ .gallery-zone.has-images{display:block}
596
+ .gallery-grid{display:flex;flex-wrap:wrap;gap:8px}
597
+ .gallery-grid img{width:80px;height:80px;object-fit:cover;border-radius:8px;border:1px solid #27272a;cursor:pointer;transition:border-color .15s}
598
+ .gallery-grid img:hover{border-color:#6366f1}
599
+ .gallery-info{margin-top:8px;font-size:12px;color:#71717a;font-family:'JetBrains Mono',monospace}
600
+ .tab-bar{display:flex;background:#18181b;border-bottom:1px solid #27272a;padding:0}
601
+ .tab-btn{padding:10px 20px;font-size:13px;font-weight:600;font-family:'Inter',sans-serif;color:#71717a;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s}
602
+ .tab-btn:hover{color:#a1a1aa;background:rgba(99,102,241,.05)}
603
+ .tab-btn.active{color:#c7d2fe;border-bottom-color:#6366f1;background:rgba(99,102,241,.08)}
604
+ .tab-content{display:none;flex:1;flex-direction:column;min-height:0}
605
+ .tab-content.active{display:flex}
606
+ .view-nav{display:flex;align-items:center;gap:8px;padding:8px 16px;background:rgba(24,24,27,.5);border-bottom:1px solid #27272a}
607
+ .nav-btn{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;background:transparent;border:1px solid #27272a;border-radius:6px;color:#a1a1aa;font-size:12px;font-weight:500;font-family:'Inter',sans-serif;cursor:pointer;transition:all .15s}
608
+ .nav-btn:hover{background:rgba(99,102,241,.1);border-color:rgba(99,102,241,.3);color:#c7d2fe}
609
+ .view-label{flex:1;text-align:center;font-size:12px;font-weight:600;color:#818cf8;font-family:'JetBrains Mono',monospace}
610
+ .view-body{flex:1;background:#09090b;display:flex;align-items:center;justify-content:center;overflow:hidden;min-height:300px;position:relative}
611
+ .view-body img{max-width:100%;max-height:500px;image-rendering:auto}
612
+ .view-placeholder{color:#3f3f46;font-size:13px;text-align:center;padding:20px}
613
+ .rerun-container{flex:1;min-height:450px;background:#09090b}
614
+ .measure-info{padding:12px 16px;background:#18181b;border-top:1px solid #27272a;font-size:13px;color:#a1a1aa;min-height:40px}
615
+ .panel-card{border-bottom:1px solid #27272a}
616
+ .panel-card-title{padding:10px 20px;font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6)}
617
+ .panel-card-body{padding:14px 20px;display:flex;flex-direction:column;gap:10px}
618
+ .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}
619
+ .settings-group-title{font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5)}
620
+ .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:10px}
621
+ .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
622
+ .checkbox-row input[type="checkbox"]{accent-color:#6366f1;width:16px;height:16px;cursor:pointer}
623
+ .checkbox-row label{color:#a1a1aa;font-size:13px;cursor:pointer}
624
+ .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
625
+ .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:100px;flex-shrink:0}
626
+ .slider-row input[type="range"]{flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;border-radius:3px;outline:none}
627
+ .slider-row input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#6366f1,#7c3aed);border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(99,102,241,.4)}
628
+ .slider-row .slider-val{min-width:44px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;padding:3px 8px;background:#09090b;border:1px solid #27272a;border-radius:6px;color:#a1a1aa}
629
+ .modern-select{width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;padding:8px 12px;font-family:'Inter',sans-serif;font-size:13px;color:#e4e4e7;outline:none;cursor:pointer}
630
+ .modern-select:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
631
+ .modern-select option{background:#18181b;color:#e4e4e7}
632
+ .modern-loader{display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px)}
633
+ .modern-loader.active{display:flex}
634
+ .modern-loader .loader-spinner{width:36px;height:36px;border:3px solid #27272a;border-top-color:#6366f1;border-radius:50%;animation:spin .8s linear infinite}
635
+ @keyframes spin{to{transform:rotate(360deg)}}
636
+ .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500}
637
+ .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
638
+ .loader-bar-fill{height:100%;background:linear-gradient(90deg,#6366f1,#8b5cf6,#6366f1);background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px}
639
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
640
+ .app-statusbar{background:#18181b;border-top:1px solid #27272a;padding:6px 20px;display:flex;gap:12px;height:34px;align-items:center;font-size:12px}
641
+ .app-statusbar .sb-section{padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap}
642
+ .app-statusbar .sb-section.sb-fixed{flex:0 0 auto;min-width:90px;text-align:center;justify-content:center;padding:3px 12px;background:rgba(99,102,241,.08);border-radius:6px;color:#818cf8;font-weight:500}
643
+ .log-panel{padding:10px 20px;font-size:13px;color:#a1a1aa;border-bottom:1px solid #27272a;min-height:36px;font-family:'JetBrains Mono',monospace;background:rgba(9,9,11,.5)}
644
+ .examples-grid{display:flex;flex-wrap:wrap;gap:12px;padding:16px}
645
+ .example-card{width:140px;cursor:pointer;border:1px solid #27272a;border-radius:10px;overflow:hidden;background:#18181b;transition:all .2s}
646
+ .example-card:hover{border-color:#6366f1;transform:translateY(-2px);box-shadow:0 4px 12px rgba(99,102,241,.2)}
647
+ .example-card img{width:100%;height:100px;object-fit:cover}
648
+ .example-card .example-label{padding:6px 10px;font-size:11px;font-weight:600;color:#a1a1aa;text-align:center}
649
+ .out-download-btn{display:none;align-items:center;justify-content:center;background:rgba(99,102,241,.1);border:1px solid rgba(99,102,241,.2);border-radius:6px;cursor:pointer;padding:3px 10px;font-size:11px;font-weight:500;color:#c7d2fe!important;gap:4px;height:24px;transition:all .15s}
650
+ .out-download-btn:hover{background:rgba(99,102,241,.2);border-color:rgba(99,102,241,.35);color:#fff!important}
651
+ .out-download-btn.visible{display:inline-flex}
652
+ .out-download-btn svg{width:12px;height:12px;fill:#c7d2fe}
653
+ .toast-notification{position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif;font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none}
654
+ .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
655
+ .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
656
+ .toast-notification.info{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:1px solid rgba(255,255,255,.15)}
657
+ #gradio-run-btn{position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;opacity:.01;pointer-events:none;overflow:hidden}
658
+ ::-webkit-scrollbar{width:8px;height:8px}
659
+ ::-webkit-scrollbar-track{background:#09090b}
660
+ ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
661
+ ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
662
+ @media(max-width:900px){.app-main-row{flex-direction:column}.app-main-right{width:100%}.app-main-left{border-right:none;border-bottom:1px solid #27272a}}
663
+ """
664
+
665
+ DOWNLOAD_SVG = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 16l-5-5h3V4h4v7h3l-5 5z"/><path d="M20 18H4v2h16v-2z"/></svg>'
666
+
667
+ main_js = r"""
668
+ () => {
669
+ function init() {
670
+ if (window.__maInitDone) return;
671
+ const fileInput = document.getElementById('ma-file-input');
672
+ const uploadZone = document.getElementById('ma-upload-zone');
673
+ const uploadArea = document.getElementById('ma-upload-click');
674
+ const galleryZone = document.getElementById('ma-gallery-zone');
675
+ const galleryGrid = document.getElementById('ma-gallery-grid');
676
+ const galleryInfo = document.getElementById('ma-gallery-info');
677
+ const tabBtns = document.querySelectorAll('.tab-btn');
678
+ const tabContents = document.querySelectorAll('.tab-content');
679
+ const logPanel = document.getElementById('ma-log');
680
+ const statusFixed = document.getElementById('ma-status-fixed');
681
+ const statusInfo = document.getElementById('ma-status-info');
682
+
683
+ if (!fileInput || !uploadZone) { setTimeout(init, 300); return; }
684
+ window.__maInitDone = true;
685
+
686
+ let imageFiles = [];
687
+ let toastTimer = null;
688
+
689
+ function showToast(msg, type) {
690
+ let t = document.getElementById('app-toast');
691
+ if (!t) { t=document.createElement('div'); t.id='app-toast'; t.className='toast-notification';
692
+ t.innerHTML='<span class="toast-icon"></span><span class="toast-text"></span>'; document.body.appendChild(t); }
693
+ t.className='toast-notification '+(type||'error');
694
+ t.querySelector('.toast-icon').textContent = type==='info'?'\u2139':'\u2717';
695
+ t.querySelector('.toast-text').textContent = msg;
696
+ if(toastTimer) clearTimeout(toastTimer);
697
+ void t.offsetWidth; t.classList.add('visible');
698
+ toastTimer = setTimeout(()=>t.classList.remove('visible'), 3500);
699
+ }
700
+
701
+ function setGradioValue(cid, val) {
702
+ const c = document.getElementById(cid);
703
+ if (!c) return;
704
+ c.querySelectorAll('input,textarea').forEach(el => {
705
+ if(el.type==='file'||el.type==='range'||el.type==='checkbox') return;
706
+ const p = el.tagName==='TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
707
+ const ns = Object.getOwnPropertyDescriptor(p,'value');
708
+ if(ns&&ns.set){ns.set.call(el,val);el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));}
709
+ });
710
+ }
711
+
712
+ // Tab switching
713
+ tabBtns.forEach(btn => {
714
+ btn.addEventListener('click', () => {
715
+ tabBtns.forEach(b=>b.classList.remove('active'));
716
+ tabContents.forEach(c=>c.classList.remove('active'));
717
+ btn.classList.add('active');
718
+ const target = document.getElementById('tab-'+btn.dataset.tab);
719
+ if(target) target.classList.add('active');
720
+ });
721
+ });
722
+
723
+ // Upload handling
724
+ uploadArea.addEventListener('click', ()=>fileInput.click());
725
+ document.getElementById('tb-upload-btn').addEventListener('click', ()=>fileInput.click());
726
+
727
+ fileInput.addEventListener('change', (e) => {
728
+ if(e.target.files.length) handleFiles(e.target.files);
729
+ e.target.value = '';
730
+ });
731
+
732
+ uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.style.outline='2px solid #6366f1'; });
733
+ uploadZone.addEventListener('dragleave', (e) => { e.preventDefault(); uploadZone.style.outline=''; });
734
+ uploadZone.addEventListener('drop', (e) => { e.preventDefault(); uploadZone.style.outline='';
735
+ if(e.dataTransfer.files.length) handleFiles(e.dataTransfer.files); });
736
+
737
+ function handleFiles(files) {
738
+ // Pass files to hidden Gradio file input
739
+ const gradioUpload = document.getElementById('gradio-upload');
740
+ if (!gradioUpload) return;
741
+ const gInput = gradioUpload.querySelector('input[type="file"]');
742
+ if (gInput) {
743
+ const dt = new DataTransfer();
744
+ for(let f of files) dt.items.add(f);
745
+ gInput.files = dt.files;
746
+ gInput.dispatchEvent(new Event('change', {bubbles:true}));
747
+ }
748
+ // Show preview
749
+ imageFiles = [];
750
+ galleryGrid.innerHTML = '';
751
+ for(let f of files) {
752
+ if(f.type.startsWith('image/')) {
753
+ imageFiles.push(f);
754
+ const url = URL.createObjectURL(f);
755
+ const img = document.createElement('img');
756
+ img.src = url;
757
+ galleryGrid.appendChild(img);
758
+ }
759
+ }
760
+ if(imageFiles.length > 0) {
761
+ galleryZone.classList.add('has-images');
762
+ galleryInfo.textContent = imageFiles.length + ' file(s) loaded';
763
+ uploadArea.parentElement.style.display = 'none';
764
+ }
765
+ statusInfo.textContent = files.length + ' file(s) uploaded';
766
+ }
767
+
768
+ // Reconstruct button
769
+ document.getElementById('tb-reconstruct-btn').addEventListener('click', () => {
770
+ const gradioBtn = document.getElementById('gradio-run-btn');
771
+ if(!gradioBtn) return;
772
+ statusFixed.textContent = 'Processing...';
773
+ showLoaders();
774
+ const btn = gradioBtn.querySelector('button');
775
+ if(btn) btn.click(); else gradioBtn.click();
776
+ });
777
+
778
+ // Clear button
779
+ document.getElementById('tb-clear-btn').addEventListener('click', () => {
780
+ galleryGrid.innerHTML = '';
781
+ galleryZone.classList.remove('has-images');
782
+ uploadArea.parentElement.style.display = '';
783
+ imageFiles = [];
784
+ statusInfo.textContent = 'Cleared';
785
+ statusFixed.textContent = 'Ready';
786
+ });
787
+
788
+ // Settings checkboxes sync
789
+ function syncCheckbox(customId, gradioId) {
790
+ const cb = document.getElementById(customId);
791
+ if(!cb) return;
792
+ cb.addEventListener('change', () => {
793
+ const gc = document.getElementById(gradioId);
794
+ if(!gc) return;
795
+ const gcb = gc.querySelector('input[type="checkbox"]');
796
+ if(gcb && gcb.checked !== cb.checked) gcb.click();
797
+ });
798
+ }
799
+ syncCheckbox('custom-show-cam', 'gradio-show-cam');
800
+ syncCheckbox('custom-show-mesh', 'gradio-show-mesh');
801
+ syncCheckbox('custom-filter-black', 'gradio-filter-black');
802
+ syncCheckbox('custom-filter-white', 'gradio-filter-white');
803
+ syncCheckbox('custom-apply-mask', 'gradio-apply-mask');
804
+
805
+ // Slider sync
806
+ function syncSlider(customId, gradioId) {
807
+ const s = document.getElementById(customId);
808
+ const v = document.getElementById(customId+'-val');
809
+ if(!s) return;
810
+ s.addEventListener('input', () => {
811
+ if(v) v.textContent = s.value;
812
+ const gc = document.getElementById(gradioId);
813
+ if(!gc) return;
814
+ gc.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
815
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value');
816
+ if(ns&&ns.set){ns.set.call(el,s.value);el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));}
817
+ });
818
+ });
819
+ }
820
+ syncSlider('custom-interval', 'gradio-interval');
821
+
822
+ // Frame filter select sync
823
+ const frameSelect = document.getElementById('custom-frame-filter');
824
+ if(frameSelect) {
825
+ frameSelect.addEventListener('change', () => {
826
+ setGradioValue('gradio-frame-filter', frameSelect.value);
827
+ });
828
+ }
829
+
830
+ // Navigation buttons
831
+ function navBtn(btnId, gradioId) {
832
+ const b = document.getElementById(btnId);
833
+ if(!b) return;
834
+ b.addEventListener('click', () => {
835
+ const gb = document.getElementById(gradioId);
836
+ if(!gb) { const btn = gb.querySelector('button'); if(btn) btn.click(); }
837
+ });
838
+ }
839
+
840
+ // Loader helpers
841
+ function showLoaders() {
842
+ document.querySelectorAll('.modern-loader').forEach(l=>l.classList.add('active'));
843
+ }
844
+ function hideLoaders() {
845
+ document.querySelectorAll('.modern-loader').forEach(l=>l.classList.remove('active'));
846
+ statusFixed.textContent = 'Done';
847
+ }
848
+ window.__showLoaders = showLoaders;
849
+ window.__hideLoaders = hideLoaders;
850
+
851
+ // Watch outputs for images
852
+ function watchOutputs() {
853
+ const containers = {
854
+ 'gradio-depth-out': 'depth-view-body',
855
+ 'gradio-normal-out': 'normal-view-body',
856
+ 'gradio-measure-out': 'measure-view-body'
857
+ };
858
+ for(const [gid, vid] of Object.entries(containers)) {
859
+ const gc = document.getElementById(gid);
860
+ const vb = document.getElementById(vid);
861
+ if(!gc||!vb) continue;
862
+ const gimg = gc.querySelector('img');
863
+ if(gimg && gimg.src) {
864
+ let existing = vb.querySelector('img.view-img');
865
+ if(!existing) { existing=document.createElement('img'); existing.className='view-img'; vb.appendChild(existing); }
866
+ if(existing.src !== gimg.src) {
867
+ existing.src = gimg.src;
868
+ const ph = vb.querySelector('.view-placeholder');
869
+ if(ph) ph.style.display='none';
870
+ }
871
+ }
872
+ }
873
+ // Watch log
874
+ const logGradio = document.getElementById('gradio-log');
875
+ if(logGradio && logPanel) {
876
+ const md = logGradio.querySelector('.prose, p, span');
877
+ if(md && md.textContent.trim()) {
878
+ logPanel.textContent = md.textContent.trim();
879
+ if(md.textContent.includes('Success')) { hideLoaders(); statusFixed.textContent='Done'; }
880
+ }
881
+ }
882
+ // Watch view selectors
883
+ ['depth','normal','measure'].forEach(t => {
884
+ const gc = document.getElementById('gradio-'+t+'-selector');
885
+ const lbl = document.getElementById(t+'-view-label');
886
+ if(!gc||!lbl) return;
887
+ const sel = gc.querySelector('input');
888
+ if(sel && sel.value) lbl.textContent = sel.value;
889
+ });
890
+ // Watch frame filter
891
+ const ff = document.getElementById('gradio-frame-filter');
892
+ if(ff && frameSelect) {
893
+ const opts = ff.querySelectorAll('option');
894
+ if(opts.length > 1 && frameSelect.options.length <= 1) {
895
+ frameSelect.innerHTML = '';
896
+ opts.forEach(o => {
897
+ const no = document.createElement('option');
898
+ no.value = o.value; no.textContent = o.textContent;
899
+ frameSelect.appendChild(no);
900
+ });
901
+ }
902
+ }
903
+ }
904
+ setInterval(watchOutputs, 800);
905
+
906
+ // Watch measure text
907
+ function watchMeasure() {
908
+ const gc = document.getElementById('gradio-measure-text');
909
+ const mt = document.getElementById('measure-text-display');
910
+ if(!gc||!mt) return;
911
+ const md = gc.querySelector('.prose, p, span');
912
+ if(md) mt.innerHTML = md.innerHTML || md.textContent;
913
+ }
914
+ setInterval(watchMeasure, 500);
915
+
916
+ statusFixed.textContent = 'Ready';
917
+ }
918
+ init();
919
+ }
920
+ """
921
+
922
+ # Build HTML layout
923
+ def build_html():
924
+ return f"""
925
+ <div class="app-shell">
926
+ <div class="app-header">
927
+ <div class="app-header-left">
928
+ <div class="app-logo">M</div>
929
+ <span class="app-title">MapAnything</span>
930
+ <span class="app-badge">3D Reconstruction</span>
931
+ </div>
932
+ </div>
933
+
934
+ <div class="app-toolbar">
935
+ <button id="tb-upload-btn" class="modern-tb-btn" title="Upload files">
936
+ <span class="tb-label">Upload</span>
937
+ </button>
938
+ <button id="tb-reconstruct-btn" class="modern-tb-btn btn-primary-tb" title="Run reconstruction">
939
+ <span class="tb-label">Reconstruct</span>
940
+ </button>
941
+ <button id="tb-clear-btn" class="modern-tb-btn" title="Clear all">
942
+ <span class="tb-label">Clear</span>
943
+ </button>
944
+ <div class="tb-sep"></div>
945
+ <button id="tb-resample-btn" class="modern-tb-btn" title="Resample video" style="display:none">
946
+ <span class="tb-label">Resample</span>
947
+ </button>
948
+ </div>
949
+
950
+ <div class="log-panel" id="ma-log">Upload images or video, then click Reconstruct.</div>
951
+
952
+ <div class="app-main-row">
953
+ <div class="app-main-left">
954
+ <div id="ma-upload-zone" class="upload-zone">
955
+ <div id="ma-upload-click" class="upload-click-area">
956
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
957
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#6366f1" stroke-width="2" stroke-dasharray="4 3"/>
958
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(99,102,241,0.15)" stroke="#6366f1" stroke-width="1.5"/>
959
+ <circle cx="28" cy="30" r="6" fill="rgba(99,102,241,0.2)" stroke="#6366f1" stroke-width="1.5"/>
960
+ </svg>
961
+ <span>Drop images or video here, or click to browse</span>
962
+ </div>
963
+ <input id="ma-file-input" type="file" accept="image/*,video/*" multiple style="display:none;" />
964
+ </div>
965
+
966
+ <div id="ma-gallery-zone" class="gallery-zone">
967
+ <div id="ma-gallery-grid" class="gallery-grid"></div>
968
+ <div id="ma-gallery-info" class="gallery-info"></div>
969
+ </div>
970
+
971
+ <div class="tab-bar">
972
+ <button class="tab-btn active" data-tab="3dview">3D View</button>
973
+ <button class="tab-btn" data-tab="depth">Depth</button>
974
+ <button class="tab-btn" data-tab="normal">Normal</button>
975
+ <button class="tab-btn" data-tab="measure">Measure</button>
976
+ </div>
977
+
978
+ <div id="tab-3dview" class="tab-content active">
979
+ <div class="rerun-container" id="rerun-mount"></div>
980
+ </div>
981
+
982
+ <div id="tab-depth" class="tab-content">
983
+ <div class="view-nav">
984
+ <button class="nav-btn" id="depth-prev-btn">Prev</button>
985
+ <span class="view-label" id="depth-view-label">View 1</span>
986
+ <button class="nav-btn" id="depth-next-btn">Next</button>
987
+ </div>
988
+ <div class="view-body" id="depth-view-body">
989
+ <div class="modern-loader" id="depth-loader">
990
+ <div class="loader-spinner"></div><div class="loader-text">Loading depth...</div>
991
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
992
+ </div>
993
+ <div class="view-placeholder">Depth map will appear after reconstruction</div>
994
+ </div>
995
+ </div>
996
+
997
+ <div id="tab-normal" class="tab-content">
998
+ <div class="view-nav">
999
+ <button class="nav-btn" id="normal-prev-btn">Prev</button>
1000
+ <span class="view-label" id="normal-view-label">View 1</span>
1001
+ <button class="nav-btn" id="normal-next-btn">Next</button>
1002
+ </div>
1003
+ <div class="view-body" id="normal-view-body">
1004
+ <div class="modern-loader" id="normal-loader">
1005
+ <div class="loader-spinner"></div><div class="loader-text">Loading normals...</div>
1006
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1007
+ </div>
1008
+ <div class="view-placeholder">Normal map will appear after reconstruction</div>
1009
+ </div>
1010
+ </div>
1011
+
1012
+ <div id="tab-measure" class="tab-content">
1013
+ <div class="view-nav">
1014
+ <button class="nav-btn" id="measure-prev-btn">Prev</button>
1015
+ <span class="view-label" id="measure-view-label">View 1</span>
1016
+ <button class="nav-btn" id="measure-next-btn">Next</button>
1017
+ </div>
1018
+ <div class="view-body" id="measure-view-body">
1019
+ <div class="modern-loader" id="measure-loader">
1020
+ <div class="loader-spinner"></div><div class="loader-text">Loading...</div>
1021
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1022
+ </div>
1023
+ <div class="view-placeholder">Measure view will appear after reconstruction</div>
1024
+ </div>
1025
+ <div class="measure-info">
1026
+ <div style="font-size:12px;color:#71717a;margin-bottom:4px">Click two points to measure distance. Grey areas have no depth data.</div>
1027
+ <div id="measure-text-display"></div>
1028
+ </div>
1029
+ </div>
1030
+ </div>
1031
+
1032
+ <div class="app-main-right">
1033
+ <div class="panel-card">
1034
+ <div class="panel-card-title">Frame Filter</div>
1035
+ <div class="panel-card-body">
1036
+ <select id="custom-frame-filter" class="modern-select">
1037
+ <option value="All">All</option>
1038
+ </select>
1039
+ </div>
1040
+ </div>
1041
+
1042
+ <div class="settings-group">
1043
+ <div class="settings-group-title">Pointcloud Options</div>
1044
+ <div class="settings-group-body">
1045
+ <div class="checkbox-row">
1046
+ <input type="checkbox" id="custom-show-cam" checked>
1047
+ <label for="custom-show-cam">Show Camera</label>
1048
+ </div>
1049
+ <div class="checkbox-row">
1050
+ <input type="checkbox" id="custom-show-mesh" checked>
1051
+ <label for="custom-show-mesh">Show Mesh</label>
1052
+ </div>
1053
+ <div class="checkbox-row">
1054
+ <input type="checkbox" id="custom-filter-black">
1055
+ <label for="custom-filter-black">Filter Black Background</label>
1056
+ </div>
1057
+ <div class="checkbox-row">
1058
+ <input type="checkbox" id="custom-filter-white">
1059
+ <label for="custom-filter-white">Filter White Background</label>
1060
+ </div>
1061
+ </div>
1062
+ </div>
1063
+
1064
+ <div class="settings-group">
1065
+ <div class="settings-group-title">Reconstruction Options</div>
1066
+ <div class="settings-group-body">
1067
+ <div class="checkbox-row">
1068
+ <input type="checkbox" id="custom-apply-mask" checked>
1069
+ <label for="custom-apply-mask">Apply mask for ambiguous depth and edges</label>
1070
+ </div>
1071
+ <div class="slider-row">
1072
+ <label>Video Interval</label>
1073
+ <input type="range" id="custom-interval" min="0.1" max="5.0" step="0.1" value="1.0">
1074
+ <span class="slider-val" id="custom-interval-val">1.0</span>
1075
+ </div>
1076
+ </div>
1077
+ </div>
1078
+ </div>
1079
+ </div>
1080
+
1081
+ <div class="app-statusbar">
1082
+ <div class="sb-section" id="ma-status-info">No files uploaded</div>
1083
+ <div class="sb-section sb-fixed" id="ma-status-fixed">Ready</div>
1084
+ </div>
1085
+ </div>
1086
+ """
1087
+
1088
+
1089
+ # ═══════════════════════════════════════════════════════════════════
1090
+ # GRADIO BLOCKS
1091
+ # ═══════════════════════════════════════════════════════════════════
1092
+
1093
+ with gr.Blocks() as demo:
1094
+ # Hidden state
1095
+ is_example = gr.Textbox(label="is_example", visible=False, value="None")
1096
+ processed_data_state = gr.State(value=None)
1097
+ measure_points_state = gr.State(value=[])
1098
+ target_dir_output = gr.Textbox(label="Target Dir", visible=False, value="None",
1099
+ elem_id="gradio-target-dir")
1100
+
1101
+ # Hidden Gradio inputs
1102
+ unified_upload = gr.File(file_count="multiple", label="Upload",
1103
+ interactive=True, file_types=["image","video"],
1104
+ elem_id="gradio-upload", elem_classes="hidden-input")
1105
+ s_time_interval = gr.Slider(minimum=0.1, maximum=5.0, value=1.0, step=0.1,
1106
+ elem_id="gradio-interval", elem_classes="hidden-input")
1107
+ image_gallery = gr.Gallery(label="Preview", elem_classes="hidden-input",
1108
+ elem_id="gradio-gallery")
1109
+ frame_filter = gr.Dropdown(choices=["All"], value="All",
1110
+ elem_id="gradio-frame-filter", elem_classes="hidden-input")
1111
+ show_cam = gr.Checkbox(value=True, elem_id="gradio-show-cam", elem_classes="hidden-input")
1112
+ show_mesh = gr.Checkbox(value=True, elem_id="gradio-show-mesh", elem_classes="hidden-input")
1113
+ filter_black_bg = gr.Checkbox(value=False, elem_id="gradio-filter-black", elem_classes="hidden-input")
1114
+ filter_white_bg = gr.Checkbox(value=False, elem_id="gradio-filter-white", elem_classes="hidden-input")
1115
+ apply_mask_checkbox = gr.Checkbox(value=True, elem_id="gradio-apply-mask", elem_classes="hidden-input")
1116
+ log_output = gr.Markdown("Ready", elem_id="gradio-log", elem_classes="hidden-input")
1117
+
1118
+ # Hidden outputs for depth/normal/measure
1119
+ depth_map = gr.Image(type="numpy", format="png", interactive=False,
1120
+ elem_id="gradio-depth-out", elem_classes="hidden-input")
1121
+ normal_map = gr.Image(type="numpy", format="png", interactive=False,
1122
+ elem_id="gradio-normal-out", elem_classes="hidden-input")
1123
+ measure_image = gr.Image(type="numpy", format="webp", interactive=False,
1124
+ sources=[], elem_id="gradio-measure-out", elem_classes="hidden-input")
1125
+ measure_text = gr.Markdown("", elem_id="gradio-measure-text", elem_classes="hidden-input")
1126
+
1127
+ # Hidden view selectors
1128
+ depth_view_selector = gr.Dropdown(choices=["View 1"], value="View 1",
1129
+ elem_id="gradio-depth-selector", elem_classes="hidden-input")
1130
+ normal_view_selector = gr.Dropdown(choices=["View 1"], value="View 1",
1131
+ elem_id="gradio-normal-selector", elem_classes="hidden-input")
1132
+ measure_view_selector = gr.Dropdown(choices=["View 1"], value="View 1",
1133
+ elem_id="gradio-measure-selector", elem_classes="hidden-input")
1134
+
1135
+ # Hidden navigation buttons
1136
+ prev_depth_btn = gr.Button("prev", elem_id="gradio-depth-prev", elem_classes="hidden-input")
1137
+ next_depth_btn = gr.Button("next", elem_id="gradio-depth-next", elem_classes="hidden-input")
1138
+ prev_normal_btn = gr.Button("prev", elem_id="gradio-normal-prev", elem_classes="hidden-input")
1139
+ next_normal_btn = gr.Button("next", elem_id="gradio-normal-next", elem_classes="hidden-input")
1140
+ prev_measure_btn = gr.Button("prev", elem_id="gradio-measure-prev", elem_classes="hidden-input")
1141
+ next_measure_btn = gr.Button("next", elem_id="gradio-measure-next", elem_classes="hidden-input")
1142
+
1143
+ # Rerun viewer (visible, placed inside tab via JS)
1144
+ rerun_output = Rerun(label="Rerun 3D Viewer", elem_id="gradio-rerun")
1145
+
1146
+ # Main HTML layout
1147
+ gr.HTML(build_html())
1148
+
1149
+ # Hidden run button
1150
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1151
+
1152
+ # Load JS
1153
+ demo.load(fn=None, js=main_js)
1154
+
1155
+ # ── Event Wiring ──
1156
+
1157
+ # Reconstruct pipeline
1158
+ run_btn.click(fn=clear_fields, inputs=[], outputs=[rerun_output]).then(
1159
+ fn=update_log, inputs=[], outputs=[log_output]
1160
+ ).then(
1161
+ fn=gradio_demo,
1162
+ inputs=[target_dir_output, frame_filter, show_cam, filter_black_bg,
1163
+ filter_white_bg, apply_mask_checkbox, show_mesh],
1164
+ outputs=[rerun_output, log_output, frame_filter, processed_data_state,
1165
+ depth_map, normal_map, measure_image, measure_text,
1166
+ depth_view_selector, normal_view_selector, measure_view_selector],
1167
+ ).then(fn=lambda: "False", inputs=[], outputs=[is_example])
1168
+
1169
+ # Upload handling
1170
+ unified_upload.change(
1171
+ fn=update_gallery_on_unified_upload,
1172
+ inputs=[unified_upload, s_time_interval],
1173
+ outputs=[target_dir_output, image_gallery, log_output],
1174
+ )
1175
+
1176
+ # Visualization updates
1177
+ frame_filter.change(
1178
+ update_visualization,
1179
+ [target_dir_output, frame_filter, show_cam, is_example,
1180
+ filter_black_bg, filter_white_bg, show_mesh],
1181
+ [rerun_output, log_output])
1182
+
1183
+ show_cam.change(update_visualization,
1184
+ [target_dir_output, frame_filter, show_cam, is_example],
1185
+ [rerun_output, log_output])
1186
+
1187
+ show_mesh.change(update_visualization,
1188
+ [target_dir_output, frame_filter, show_cam, is_example,
1189
+ filter_black_bg, filter_white_bg, show_mesh],
1190
+ [rerun_output, log_output])
1191
+
1192
+ filter_black_bg.change(update_visualization,
1193
+ [target_dir_output, frame_filter, show_cam, is_example,
1194
+ filter_black_bg, filter_white_bg, show_mesh],
1195
+ [rerun_output, log_output]).then(
1196
+ fn=update_all_views_on_filter_change,
1197
+ inputs=[target_dir_output, filter_black_bg, filter_white_bg,
1198
+ processed_data_state, depth_view_selector,
1199
+ normal_view_selector, measure_view_selector],
1200
+ outputs=[processed_data_state, depth_map, normal_map,
1201
+ measure_image, measure_points_state])
1202
+
1203
+ filter_white_bg.change(update_visualization,
1204
+ [target_dir_output, frame_filter, show_cam, is_example,
1205
+ filter_black_bg, filter_white_bg, show_mesh],
1206
+ [rerun_output, log_output]).then(
1207
+ fn=update_all_views_on_filter_change,
1208
+ inputs=[target_dir_output, filter_black_bg, filter_white_bg,
1209
+ processed_data_state, depth_view_selector,
1210
+ normal_view_selector, measure_view_selector],
1211
+ outputs=[processed_data_state, depth_map, normal_map,
1212
+ measure_image, measure_points_state])
1213
+
1214
+ # Navigation: Depth
1215
+ prev_depth_btn.click(
1216
+ fn=lambda pd, cs: navigate_depth_view(pd, cs, -1),
1217
+ inputs=[processed_data_state, depth_view_selector],
1218
+ outputs=[depth_view_selector, depth_map])
1219
+ next_depth_btn.click(
1220
+ fn=lambda pd, cs: navigate_depth_view(pd, cs, 1),
1221
+ inputs=[processed_data_state, depth_view_selector],
1222
+ outputs=[depth_view_selector, depth_map])
1223
+ depth_view_selector.change(
1224
+ fn=lambda pd, sv: update_depth_view(pd, int(sv.split()[1])-1) if sv else None,
1225
+ inputs=[processed_data_state, depth_view_selector],
1226
+ outputs=[depth_map])
1227
+
1228
+ # Navigation: Normal
1229
+ prev_normal_btn.click(
1230
+ fn=lambda pd, cs: navigate_normal_view(pd, cs, -1),
1231
+ inputs=[processed_data_state, normal_view_selector],
1232
+ outputs=[normal_view_selector, normal_map])
1233
+ next_normal_btn.click(
1234
+ fn=lambda pd, cs: navigate_normal_view(pd, cs, 1),
1235
+ inputs=[processed_data_state, normal_view_selector],
1236
+ outputs=[normal_view_selector, normal_map])
1237
+ normal_view_selector.change(
1238
+ fn=lambda pd, sv: update_normal_view(pd, int(sv.split()[1])-1) if sv else None,
1239
+ inputs=[processed_data_state, normal_view_selector],
1240
+ outputs=[normal_map])
1241
+
1242
+ # Navigation: Measure
1243
+ prev_measure_btn.click(
1244
+ fn=lambda pd, cs: navigate_measure_view(pd, cs, -1),
1245
+ inputs=[processed_data_state, measure_view_selector],
1246
+ outputs=[measure_view_selector, measure_image, measure_points_state])
1247
+ next_measure_btn.click(
1248
+ fn=lambda pd, cs: navigate_measure_view(pd, cs, 1),
1249
+ inputs=[processed_data_state, measure_view_selector],
1250
+ outputs=[measure_view_selector, measure_image, measure_points_state])
1251
+ measure_view_selector.change(
1252
+ fn=lambda pd, sv: update_measure_view(pd, int(sv.split()[1])-1) if sv else (None,[]),
1253
+ inputs=[processed_data_state, measure_view_selector],
1254
+ outputs=[measure_image, measure_points_state])
1255
+
1256
+ # Measure click
1257
+ measure_image.select(
1258
+ fn=measure,
1259
+ inputs=[processed_data_state, measure_points_state, measure_view_selector],
1260
+ outputs=[measure_image, measure_points_state, measure_text])
1261
+
1262
+
1263
+ if __name__ == "__main__":
1264
+ demo.queue(max_size=50).launch(css=css, show_error=True, share=True, ssr_mode=False)