sattwik21 commited on
Commit
4ee543e
ยท
verified ยท
1 Parent(s): fbe207f

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +46 -0
  2. app.py +711 -0
  3. requirements.txt +12 -0
README.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: "AnyTraverse Studio ๐Ÿšœ"
3
+ emoji: "๐Ÿšœ"
4
+ colorFrom: "slate"
5
+ colorTo: "blue"
6
+ sdk: "gradio"
7
+ sdk_version: "6.22.0"
8
+ app_file: "app.py"
9
+ pinned: false
10
+ python_version: "3.12"
11
+ short_description: "Live off-road traversability evaluation dashboard with Human-in-the-Loop (AnyTraverse)"
12
+ tags:
13
+ - computer-vision
14
+ - robotics
15
+ - segmentation
16
+ - vlm
17
+ ---
18
+
19
+ # ๐Ÿšœ AnyTraverse Studio โ€” Live Evaluation & HITL Dashboard
20
+
21
+ A live Gradio dashboard for evaluating the **AnyTraverse** zero-shot off-road
22
+ traversability framework ([paper](https://arxiv.org/abs/2506.16826),
23
+ [PyPI](https://pypi.org/project/anytraverse/)).
24
+
25
+ ## Workflow
26
+ 1. **Upload a video** of an off-road scene.
27
+ 2. Set the traversability preferences (ฯ„), scene-similarity threshold,
28
+ ROI-uncertainty threshold, ROI bounds and the VLM inference **frame skip**.
29
+ 3. Press **โ–ถ๏ธ Go / Reset**.
30
+ 4. Watch per frame:
31
+ - raw image + ROI box ยท traversability map ยท uncertainty map ยท ROI crop
32
+ - **attention maps for all prompts** (live, not in the exported video)
33
+ - live dual-metric **ROI traversability + uncertainty** line plot
34
+ - horizontal **0โ€“1 gauge bars** for the two ROI scores
35
+ - the traversal state (`ok` / `unknown_scene` / `unknown_object`)
36
+ 5. The run **halts** whenever `traversal_state != OK`. Provide an operator
37
+ update like `mud: -0.7; gravel: 0.6` (or just `ok`) and press **Resume**.
38
+
39
+ Thresholds and ROI bounds can be edited **live** during a run โ€” they are
40
+ applied directly to the running pipeline.
41
+
42
+ ## Notes
43
+ - The VLM weights (CLIPSeg + CLIP) download on first run and are cached.
44
+ - The composed analysis video is exported as an H.264 `.mp4` (bundled
45
+ `imageio-ffmpeg`, no system ffmpeg required).
46
+ - GPU is recommended; the space is configured for a **T4** accelerator.
app.py ADDED
@@ -0,0 +1,711 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import time
6
+
7
+ import cv2
8
+ import gradio as gr
9
+ import numpy as np
10
+ import pandas as pd
11
+ import torch
12
+
13
+ from PIL import Image as PILImage
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # AnyTraverse API surface
17
+ # ---------------------------------------------------------------------------
18
+ ANYTRAVERSE_AVAILABLE = False
19
+ try:
20
+ from anytraverse import build_pipeline_from_paper
21
+ from anytraverse.utils.state import TraversalState
22
+
23
+ ANYTRAVERSE_AVAILABLE = True
24
+ except Exception as _e: # pragma: no cover - depends on environment
25
+ print(f"[app] anytraverse not importable ({_e}); running in SIMULATION mode.")
26
+
27
+ class TraversalState:
28
+ """Stand-in so the dashboard is testable without the package."""
29
+ OK = object()
30
+ UNKNOWN_SCENE = object()
31
+ UNKOWN_OBJ = object() # source spelling (missing N) kept for parity
32
+
33
+
34
+ # Human-readable HOC labels requested by the user.
35
+ HOC_LABELS = {
36
+ TraversalState.OK: "ok",
37
+ TraversalState.UNKNOWN_SCENE: "unknown_scene",
38
+ TraversalState.UNKOWN_OBJ: "unknown_object",
39
+ }
40
+
41
+
42
+ def get_vlm_device():
43
+ if torch.cuda.is_available():
44
+ return f"CUDA:0 ({torch.cuda.get_device_name(0)})"
45
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
46
+ return "Apple MPS"
47
+ return "CPU"
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Small helpers. NOTE: all maps / grids are kept in BGR internally and only
52
+ # converted to RGB at the very end, so the raw image is not color-swapped.
53
+ # ---------------------------------------------------------------------------
54
+ def to_numpy(val):
55
+ if isinstance(val, torch.Tensor):
56
+ return val.detach().cpu().numpy()
57
+ if isinstance(val, (list, tuple)):
58
+ return np.asarray(val[0])
59
+ return np.asarray(val)
60
+
61
+
62
+ def to_float(val, default=0.0):
63
+ if val is None:
64
+ return default
65
+ if isinstance(val, torch.Tensor):
66
+ return float(val.detach().cpu().item())
67
+ return float(val)
68
+
69
+
70
+ def colorize(arr, target_w, target_h, colormap=cv2.COLORMAP_INFERNO):
71
+ """Normalize a 2D map and apply a color map, resized to target dims (BGR)."""
72
+ arr = to_numpy(arr)
73
+ if arr.ndim == 3:
74
+ arr = arr.reshape(arr.shape[-2:])
75
+ if arr.size == 0:
76
+ arr = np.zeros((2, 2))
77
+ lo, hi = float(arr.min()), float(arr.max())
78
+ if hi - lo < 1e-9:
79
+ norm = np.zeros(arr.shape, dtype=np.uint8)
80
+ else:
81
+ norm = ((arr - lo) / (hi - lo) * 255.0).astype(np.uint8)
82
+ return cv2.resize(cv2.applyColorMap(norm, colormap), (int(target_w), int(target_h)))
83
+
84
+
85
+ def add_caption(img_bgr, text):
86
+ cv2.putText(img_bgr, str(text), (6, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.7,
87
+ (255, 255, 255), 2)
88
+ cv2.putText(img_bgr, str(text), (6, 24), cv2.FONT_HERSHEY_SIMPLEX, 0.7,
89
+ (0, 0, 0), 1)
90
+ return img_bgr
91
+
92
+
93
+ def _ffmpeg_bin():
94
+ """Locate an ffmpeg binary: bundled (imageio-ffmpeg) first, else system."""
95
+ try:
96
+ import imageio_ffmpeg
97
+ return imageio_ffmpeg.get_ffmpeg_exe()
98
+ except Exception:
99
+ pass
100
+ return shutil.which("ffmpeg") or "ffmpeg"
101
+
102
+
103
+ def convert_to_h264(in_path, out_path):
104
+ """FFmpeg wrapper producing a browser-playable H.264 video (no audio)."""
105
+ if not in_path or not os.path.exists(in_path):
106
+ return None
107
+ try:
108
+ subprocess.run(
109
+ [_ffmpeg_bin(), "-y", "-i", in_path, "-vcodec", "libx264",
110
+ "-pix_fmt", "yuv420p", "-preset", "fast", "-an", out_path],
111
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True,
112
+ )
113
+ return out_path
114
+ except Exception:
115
+ return None
116
+
117
+
118
+ def bounds_box(rx_min, rx_max, ry_min, ry_max, w, h):
119
+ return ((int(rx_min * w), int(ry_min * h)), (int(rx_max * w), int(ry_max * h)))
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Session: shared mutable state read by the streaming generator + button events
124
+ # ---------------------------------------------------------------------------
125
+ class AppSession:
126
+ def __init__(self):
127
+ self.pipeline = None
128
+ self.cap = None
129
+ self.writer = None
130
+ self.video_path = None
131
+ self.frame_idx = 0
132
+ self.fps = 30
133
+ self.vw = 0
134
+ self.vh = 0
135
+ self.is_paused = False
136
+ self.resume_requested = False
137
+ self.simulate_hoc_requested = False
138
+ self.is_running = False
139
+ self.last_grid = None
140
+ self.last_attn = None
141
+ self.traversal = TraversalState.OK
142
+ self.preferences = {"road": 1.0, "grass": 0.5, "bush": -0.8, "rock": -0.6}
143
+ self.uncert_thresh = 0.4
144
+ self.sim_thresh = 0.8
145
+ self.skip = 2
146
+ self.telemetry = []
147
+ self.raw_out = "raw_opencv_temp.mp4"
148
+ self.h264_out = "anytraverse_h264_output.mp4"
149
+ self.device = get_vlm_device()
150
+
151
+
152
+ session = AppSession()
153
+
154
+ TEL_COLUMNS = ["Frame", "ROI Trav", "ROI Unc", "Ref Sim", "State"]
155
+ EMPTY_DF = pd.DataFrame(columns=TEL_COLUMNS)
156
+ PLOT_COLUMNS = ["Frame", "ROI Trav", "ROI Unc", "Uncert Thresh"]
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Frame-state unpacking / simulation
161
+ # ---------------------------------------------------------------------------
162
+ def unpack_state(state_obj, bgr):
163
+ """Map a real anytraverse AnyTraverseState to the dashboard's shado dict."""
164
+ prompts = list(state_obj.traversability_preferences.keys())
165
+ attn_maps = [(p, m) for p, m in zip(prompts, list(state_obj.attention_maps))]
166
+ return {
167
+ "raw_bgr": bgr,
168
+ "roi_bbox": state_obj.roi_bbox,
169
+ "trav": to_numpy(state_obj.traversability_map),
170
+ "uncert": to_numpy(state_obj.uncertainty_map),
171
+ "attn_maps": attn_maps,
172
+ "roi_trav": to_float(state_obj.roi_traversability),
173
+ "roi_uncert": to_float(state_obj.roi_uncertainty),
174
+ "sim": to_float(state_obj.ref_scene_similarity),
175
+ "state": state_obj.traversal_state,
176
+ }
177
+
178
+
179
+ def _simulate_state(bgr, prefs, uncert_thresh, frame_idx):
180
+ """Deterministic fake state so the UI is testable without the package."""
181
+ h, w, _ = bgr.shape
182
+ roi_u = float(np.clip(0.18 + 0.45 * np.sin(frame_idx / 7.0), 0.0, 1.0))
183
+ trav = float(np.clip(0.65 + 0.35 * np.sin(frame_idx / 9.0), 0.05, 0.95))
184
+ sim = float(np.clip(0.95 - frame_idx * 0.002, 0.2, 1.0))
185
+
186
+ att = []
187
+ phase = np.linspace(0, np.pi, w, dtype=np.float32)
188
+ for k, p in enumerate(prefs.keys()):
189
+ base = np.full((h, w), 0.5, dtype=np.float32)
190
+ base[h // 2:, :] += (0.25 * np.sin(phase + k))[None, :]
191
+ base[0:h // 2, :] = 0.9
192
+ att.append((p, base))
193
+
194
+ box = bounds_box(0.333, 0.667, 0.6, 0.95, w, h)
195
+ if roi_u > uncert_thresh:
196
+ st = TraversalState.UNKOWN_OBJ
197
+ elif (frame_idx // 40) % 4 == 2:
198
+ st = TraversalState.UNKNOWN_SCENE
199
+ else:
200
+ st = TraversalState.OK
201
+
202
+ return {
203
+ "raw_bgr": bgr, "roi_bbox": box,
204
+ "trav": np.full((h, w), trav, dtype=np.float32),
205
+ "uncert": np.full((h, w), roi_u, dtype=np.float32),
206
+ "attn_maps": att, "roi_trav": trav, "roi_uncert": roi_u,
207
+ "sim": sim, "state": st,
208
+ }
209
+
210
+
211
+ def seed_state(bgr, box):
212
+ h, w, _ = bgr.shape
213
+ return {
214
+ "raw_bgr": bgr, "roi_bbox": box,
215
+ "trav": np.full((h, w), 0.5, np.float32),
216
+ "uncert": np.zeros((h, w), np.float32),
217
+ "attn_maps": [], "roi_trav": 0.5, "roi_uncert": 0.0, "sim": 1.0,
218
+ "state": TraversalState.OK,
219
+ }
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # Rendering (no matplotlib anywhere)
224
+ # ---------------------------------------------------------------------------
225
+ def build_grid(p):
226
+ """2x2 grid: [raw+ROI | traversability] / [uncertainty | ROI crop]."""
227
+ h, w, _ = p["raw_bgr"].shape
228
+ raw = p["raw_bgr"].copy()
229
+ (x0, y0), (x1, y1) = p["roi_bbox"]
230
+ cv2.rectangle(raw, (x0, y0), (x1, y1), (0, 255, 255), 2) # BGR yellow
231
+
232
+ trav_img = colorize(p["trav"], w, h)
233
+ uncert_img = colorize(p["uncert"], w, h)
234
+
235
+ xa, xb = max(x0, 0), min(x1, w)
236
+ ya, yb = max(y0, 0), min(y1, h)
237
+ roi_crop = raw[ya:yb + 1, xa:xb + 1]
238
+ if roi_crop.size == 0:
239
+ roi_crop = raw
240
+ roi_crop = cv2.resize(roi_crop, (w, h))
241
+
242
+ row1 = np.hstack([raw, trav_img])
243
+ row2 = np.hstack([uncert_img, roi_crop])
244
+ grid = np.vstack([row1, row2])
245
+ return cv2.cvtColor(grid, cv2.COLOR_BGR2RGB)
246
+
247
+
248
+ def build_attn_strip(p):
249
+ """All prompt attention maps as one labeled strip (not written to the video)."""
250
+ raw = p["raw_bgr"]
251
+ h, w, _ = raw.shape
252
+ att = p["attn_maps"]
253
+ if not att:
254
+ return np.zeros((h, w, 3), dtype=np.uint8)
255
+ cell_w = max(int(w // len(att)), 80)
256
+ cells = [add_caption(colorize(m, cell_w, h).copy(), name) for name, m in att]
257
+ strip = np.hstack(cells)
258
+ return cv2.cvtColor(strip, cv2.COLOR_BGR2RGB)
259
+
260
+
261
+ def bars_html(trav, unc, thresh):
262
+ """Two horizontal 0..1 gauge bars (pure HTML/CSS, no matplotlib)."""
263
+ t = int(round(max(0.0, min(1.0, trav)) * 100))
264
+ u = int(round(max(0.0, min(1.0, unc)) * 100))
265
+ th = max(0.0, min(1.0, thresh)) * 100
266
+ return (
267
+ f"<div class='prog'><div style='display:flex;justify-content:space-between'>"
268
+ f"<span style='font-weight:600'>ROI Traversability</span><span>{trav:.3f}</span></div>"
269
+ f"<div style='position:relative;height:16px;background:#e9ecef;border-radius:8px;border:1px solid #ced4da'>"
270
+ f"<div style='position:absolute;left:0;top:0;height:100%;width:{t}%;background:#2ca02c;border-radius:8px'></div></div></div>"
271
+ f"<div class='bar' style='margin-top:10px'><div style='display:flex;justify-content:space-between'>"
272
+ f"<span style='font-weight:600'>ROI Uncertainty</span><span>{unc:.3f}</span></div>"
273
+ f"<div style='position:relative;height:16px;background:#e9ecef;border-radius:8px;border:1px solid #ced4da'>"
274
+ f"<div style='position:absolute;left:0;top:0;height:100%;width:{u}%;background:#d62728;border-radius:8px'></div>"
275
+ f"<div title='threshold' style='position:absolute;left:{th}%;top:-3px;bottom:-3px;width:2px;background:#343a40'></div>"
276
+ f"</div></div>")
277
+
278
+
279
+ def lineplot_df():
280
+ if not session.telemetry:
281
+ return pd.DataFrame(columns=PLOT_COLUMNS)
282
+ rows = []
283
+ for t in session.telemetry:
284
+ rows.append({"Frame": t["frame"], "ROI Trav": t["roi_trav"],
285
+ "ROI Unc": t["roi_uncert"], "Uncert Thresh": session.uncert_thresh})
286
+ return pd.DataFrame(rows, columns=PLOT_COLUMNS)
287
+
288
+
289
+ def df_table():
290
+ if not session.telemetry:
291
+ return EMPTY_DF
292
+ return pd.DataFrame(
293
+ [{"Frame": t["frame"], "ROI Trav": t["roi_trav"], "ROI Unc": t["roi_uncert"],
294
+ "Ref Sim": t["sim"], "State": t["state"]} for t in session.telemetry]
295
+ )
296
+
297
+
298
+ # Order of the generator outputs (must mirror the `outputs` list).
299
+ def render(grid, status, op_visible, attn, m_frame, m_skip, m_state, m_trav,
300
+ m_unc, m_sim, m_fps, m_lat, plot, bars, table, video=None):
301
+ return (grid, status, gr.update(visible=op_visible), attn, str(m_frame),
302
+ str(m_skip), str(m_state), f"{m_trav:.3f}", f"{m_unc:.3f}",
303
+ f"{m_sim:.3f}", str(m_fps), f"{m_lat} ms", plot, bars, table, video)
304
+
305
+
306
+ def initial_render(msg):
307
+ return (None, msg, gr.update(visible=False), None, "0", str(session.skip),
308
+ "ok", "0.000", "0.000", "0.000", "0", "0 ms",
309
+ lineplot_df(), bars_html(0.0, 0.0, session.uncert_thresh),
310
+ EMPTY_DF, None)
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # Main streaming worker. Restarted on "Go / Reset" and on "Resume".
315
+ # ---------------------------------------------------------------------------
316
+ def run_evaluation(video_file, pref_json, sim_thresh, uncert_thresh,
317
+ rx_min, rx_max, ry_min, ry_max, frame_skip):
318
+ if session.is_running and not session.is_paused and not session.resume_requested:
319
+ yield initial_render("โณ A live evaluation is already running.")
320
+ return
321
+
322
+
323
+ fresh = not session.resume_requested
324
+ if fresh:
325
+ session.telemetry = []
326
+ session.uncert_thresh = float(uncert_thresh)
327
+ session.sim_thresh = float(sim_thresh)
328
+ session.skip = int(frame_skip) if frame_skip else 1
329
+ session.is_paused = False
330
+ session.simulate_hoc_requested = False
331
+ session.is_running = True
332
+ session.resume_requested = False
333
+
334
+ if video_file:
335
+ session.video_path = (video_file if isinstance(video_file, str)
336
+ else getattr(video_file, "name", str(video_file)))
337
+
338
+ if fresh:
339
+ try:
340
+ session.preferences = json.loads(pref_json) or session.preferences
341
+ except Exception:
342
+ pass
343
+ if ANYTRAVERSE_AVAILABLE:
344
+ yield initial_render(
345
+ "๐Ÿ”„ Building AnyTraverse pipeline (first run may download models)โ€ฆ")
346
+ session.pipeline = build_pipeline_from_paper(
347
+ init_traversabilty_preferences=session.preferences,
348
+ ref_scene_similarity_threshold=float(sim_thresh),
349
+ roi_uncertainty_threshold=float(uncert_thresh),
350
+ roi_x_bounds=(float(rx_min), float(rx_max)),
351
+ roi_y_bounds=(float(ry_min), float(ry_max)),
352
+ )
353
+ else:
354
+ session.pipeline = None
355
+
356
+ if not session.video_path:
357
+ session.is_running = False
358
+ yield initial_render("โš  Please upload a video first.")
359
+ return
360
+
361
+ if session.cap is None or not session.cap.isOpened():
362
+ session.cap = cv2.VideoCapture(session.video_path)
363
+ session.fps = int(session.cap.get(cv2.CAP_PROP_FPS)) or 30
364
+ session.vw = int(session.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
365
+ session.vh = int(session.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
366
+ if session.vw == 0 or session.vh == 0:
367
+ session.is_running = False
368
+ session.cap = None
369
+ yield initial_render("โŒ Could not read the uploaded video file.")
370
+ return
371
+ session.writer = cv2.VideoWriter(session.raw_out,
372
+ cv2.VideoWriter_fourcc(*"mp4v"),
373
+ session.fps, (session.vw * 2, session.vh * 2))
374
+ session.frame_idx = 0
375
+
376
+ box = bounds_box(float(rx_min), float(rx_max),
377
+ float(ry_min), float(ry_max), session.vw, session.vh)
378
+ skip = session.skip
379
+ last_state = None
380
+
381
+ try:
382
+ while session.cap.isOpened():
383
+ # -------- operator pause handling -------------------------------------
384
+ if session.is_paused:
385
+ if session.resume_requested:
386
+ session.resume_requested = False
387
+ session.is_paused = False
388
+ else:
389
+ last = session.telemetry[-1] if session.telemetry else None
390
+ yield render(
391
+ session.last_grid, f"๐Ÿšจ **HALTED at frame {session.frame_idx}** "
392
+ f"โ€” traversal state **`{HOC_LABELS.get(session.traversal,'?')}`**. "
393
+ "Enter a ฯ„ update (or `ok`) and press **Resume**.",
394
+ True, session.last_attn, session.frame_idx, skip,
395
+ HOC_LABELS.get(session.traversal, "?"),
396
+ last["roi_trav"] if last else 0.0,
397
+ last["roi_uncert"] if last else 0.0,
398
+ last["sim"] if last else 0.0, 0, 0,
399
+ lineplot_df(), bars_html(
400
+ last["roi_trav"] if last else 0.0,
401
+ last["roi_uncert"] if last else 0.0,
402
+ session.uncert_thresh), df_table(), video=None)
403
+ return
404
+
405
+ # -------- simulated operator call (manual test trigger) ---------------
406
+ if session.simulate_hoc_requested:
407
+ session.simulate_hoc_requested = False
408
+ session.is_paused = True
409
+ session.traversal = TraversalState.UNKOWN_OBJ
410
+ last = session.telemetry[-1] if session.telemetry else None
411
+ yield render(
412
+ session.last_grid,
413
+ "๐Ÿšจ **SIMULATED HUMAN-OPERATOR-CALL** โ€” live loop paused. "
414
+ "Provide a ฯ„ update (or you can resume) and press **Resume**.",
415
+ True, session.last_attn, session.frame_idx,
416
+ skip, "unknown_object",
417
+ last["roi_trav"] if last else 0.0,
418
+ last["roi_uncert"] if last else 0.0,
419
+ last["sim"] if last else 0.0, 0, 0,
420
+ lineplot_df(), bars_html(
421
+ last["roi_trav"] if last else 0.0,
422
+ last["roi_uncert"] if last else 0.0,
423
+ session.uncert_thresh), df_table(), video=None)
424
+ return
425
+
426
+ # -------- read + process a display frame --------------------------------
427
+ t0 = time.time()
428
+ ret, frame_bgr = session.cap.read()
429
+ if not ret:
430
+ break
431
+ session.frame_idx += 1
432
+ rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
433
+
434
+ run_infer = ((session.frame_idx - 1) % skip == 0) or (last_state is None)
435
+ if run_infer:
436
+ if ANYTRAVERSE_AVAILABLE and session.pipeline is not None:
437
+ st = session.pipeline.step(image=PILImage.fromarray(rgb))
438
+ p = unpack_state(st, frame_bgr)
439
+ else:
440
+ p = _simulate_state(frame_bgr, session.preferences,
441
+ session.uncert_thresh, session.frame_idx)
442
+ last_state = p
443
+ else:
444
+ p = dict(last_state) if last_state else seed_state(frame_bgr, box)
445
+ p["raw_bgr"] = frame_bgr
446
+ session.traversal = p["state"]
447
+
448
+ fps = round(1.0 / max(time.time() - t0, 1e-3), 1)
449
+ lat = round((time.time() - t0) * 1000, 1)
450
+ lbl = HOC_LABELS.get(p["state"], "ok")
451
+
452
+ # telemetry / chart row
453
+ session.telemetry.append({
454
+ "frame": session.frame_idx,
455
+ "roi_trav": round(float(p["roi_trav"]), 4),
456
+ "roi_uncert": round(float(p["roi_uncert"]), 4),
457
+ "sim": round(float(p["sim"]), 4),
458
+ "state": lbl,
459
+ })
460
+
461
+ grid = build_grid(p)
462
+ attn = build_attn_strip(p)
463
+ session.last_grid = grid
464
+ session.last_attn = attn
465
+ if session.writer is not None:
466
+ session.writer.write(cv2.cvtColor(grid, cv2.COLOR_RGB2BGR))
467
+
468
+ status = f"Frame {session.frame_idx} ยท state **`{lbl}`**" + (
469
+ "" if run_infer else " ยท (inference skipped, reusing last maps)")
470
+
471
+ yield render(
472
+ grid, status, False, attn, session.frame_idx, skip, lbl,
473
+ p["roi_trav"], p["roi_uncert"], p["sim"], fps, lat,
474
+ lineplot_df(), bars_html(p["roi_trav"], p["roi_uncert"],
475
+ session.uncert_thresh), df_table(),
476
+ video=None)
477
+
478
+ if lbl != "ok":
479
+ session.is_paused = True
480
+ yield render(
481
+ grid, f"๐Ÿšจ **HOC TRIGGERED at frame {session.frame_idx}** โ†’ "
482
+ f"**`{lbl}`**. Provide a ฯ„ update (or just `ok`) and **Resume**.",
483
+ True, attn, session.frame_idx, skip, lbl, p["roi_trav"],
484
+ p["roi_uncert"], p["sim"], fps, lat, lineplot_df(),
485
+ bars_html(p["roi_trav"], p["roi_uncert"],
486
+ session.uncert_thresh), df_table(), video=None)
487
+ return
488
+
489
+ # -------- normal completion -------------------------------------------------
490
+ if session.writer is not None:
491
+ session.writer.release()
492
+ session.writer = None
493
+ final_video = convert_to_h264(session.raw_out, session.h264_out)
494
+ last = session.telemetry[-1] if session.telemetry else None
495
+ yield render(
496
+ session.last_grid, "๐ŸŽ‰ **Evaluation complete.** Download the composed video below.",
497
+ False, session.last_attn, session.frame_idx, skip,
498
+ last["state"] if last else "ok",
499
+ last["roi_trav"] if last else 0.0,
500
+ last["roi_uncert"] if last else 0.0,
501
+ last["sim"] if last else 0.0, 0, 0, lineplot_df(),
502
+ bars_html(last["roi_trav"] if last else 0.0,
503
+ last["roi_uncert"] if last else 0.0, session.uncert_thresh),
504
+ df_table(), video=final_video)
505
+ finally:
506
+ if not session.is_paused:
507
+ if session.writer is not None:
508
+ session.writer.release()
509
+ session.writer = None
510
+ if session.cap is not None:
511
+ session.cap.release()
512
+ session.cap = None
513
+ session.is_running = False
514
+
515
+
516
+ # ---------------------------------------------------------------------------
517
+ # Operator intervention (Resume), Simulate-HOC, and live pipeline updates
518
+ # ---------------------------------------------------------------------------
519
+ def handle_operator_resume(operator_text):
520
+ text = (operator_text or "").strip()
521
+ if session.pipeline is not None:
522
+ if text and text.lower() != "ok":
523
+ session.pipeline.human_call(human_input=text)
524
+ session.preferences = dict(session.pipeline.traversability_preferences)
525
+ msg = f"โœ… Applied operator ฯ„ update `{text}` โ€” resuming."
526
+ else:
527
+ session.pipeline.register_scene()
528
+ msg = "โœ… Scene registered (no ฯ„ change) โ€” resuming."
529
+ else:
530
+ if text and text.lower() != "ok":
531
+ try:
532
+ for pw in text.split(";"):
533
+ if ":" in pw:
534
+ k, v = pw.split(":", 1)
535
+ session.preferences[k.strip()] = float(v)
536
+ except Exception:
537
+ pass
538
+ msg = "โœ… (Simulation) resuming."
539
+ session.resume_requested = True
540
+ session.is_paused = False
541
+ session.simulate_hoc_requested = False
542
+ return msg, gr.update(visible=False), json.dumps(session.preferences, indent=2)
543
+
544
+
545
+ def simulate_hoc():
546
+ session.simulate_hoc_requested = True
547
+ session.is_paused = False
548
+ session.resume_requested = False
549
+ return "โธ Simulate-HOC requested โ€” the live loop will pause on its next frame."
550
+
551
+
552
+ def live_pipeline_update(sim, unc, rxmin, rxmax, rymin, rymax):
553
+ """Apply threshold / ROI changes to the running pipeline object on the fly."""
554
+ pipe = session.pipeline
555
+ if pipe is not None:
556
+ try:
557
+ pipe._threshold.ref_scene_similarity = float(sim)
558
+ pipe._threshold.roi_uncertainty = float(unc)
559
+ pipe._roi._x_bounds = (float(rxmin), float(rxmax))
560
+ pipe._roi._y_bounds = (float(rymin), float(rymax))
561
+ except Exception:
562
+ return "โš  live update failed"
563
+ session.uncert_thresh = float(unc)
564
+ session.sim_thresh = float(sim)
565
+ return (f"Live cfg: sim={float(sim):.2f}, unc={float(unc):.2f}, "
566
+ f"ROI x=({float(rxmin):.2f},{float(rxmax):.2f}) "
567
+ f"y=({float(rymin):.2f},{float(rymax):.2f})")
568
+
569
+
570
+ # ---------------------------------------------------------------------------
571
+ # Gradio UI
572
+ # ---------------------------------------------------------------------------
573
+ MONO = [gr.themes.GoogleFont("IBM Plex Mono"), "DejaVu Sans Mono", "monospace"]
574
+
575
+ THEME = gr.themes.Base(
576
+ primary_hue=gr.themes.colors.slate,
577
+ secondary_hue=gr.themes.colors.gray,
578
+ neutral_hue=gr.themes.colors.gray,
579
+ font=MONO,
580
+ font_mono=MONO,
581
+ radius_size=gr.themes.sizes.radius_sm,
582
+ spacing_size=gr.themes.sizes.spacing_sm,
583
+ ).set(
584
+ body_background_fill="#0e1013",
585
+ body_text_color="#d7dce4",
586
+ block_background_fill="#141920",
587
+ block_border_color="#242b36",
588
+ block_title_background_fill="#0e1013",
589
+ block_title_text_color="#9fb0c3",
590
+ input_background_fill="#0e1116",
591
+ input_border_color="#2a3240",
592
+ button_primary_background_fill="#1f6feb",
593
+ button_primary_background_fill_hover="#2f7bf5",
594
+ button_primary_text_color="#ffffff",
595
+ button_secondary_background_fill="#1c232d",
596
+ button_secondary_text_color="#c7d2de",
597
+ )
598
+
599
+ CUSTOM_CSS = """
600
+ .prose h1, .prose h2, .prose h3, .prose p, .prose li, .prose code {
601
+ font-family: 'IBM Plex Mono', 'DejaVu Sans Mono', monospace;
602
+ }
603
+ :root { --body-font: 'IBM Plex Mono', 'DejaVu Sans Mono', monospace; }
604
+ footer { display: none !important; }
605
+ #status-banner { border-left: 4px solid #1f6feb; padding-left: 12px; }
606
+ """
607
+
608
+ with gr.Blocks(title="AnyTraverse Studio") as demo:
609
+ gr.Markdown("# ๐Ÿšœ AnyTraverse Studio โ€” Live Evaluation & HITL Dashboard")
610
+
611
+ with gr.Row():
612
+ with gr.Column(scale=3):
613
+ # -------- main view --------
614
+ live_view = gr.Image(label="Raw+ROI (TL) | Traversability (TR) | "
615
+ "Uncertainty (BL) | ROI crop (BR)",
616
+ height=360)
617
+ attn_view = gr.Image(label="Attention maps (all prompts) โ€” live only",
618
+ height=120)
619
+ status_banner = gr.Markdown(
620
+ "### Status: ready โ€” upload a video and press โ–ถ๏ธ Go.",
621
+ elem_id="status-banner")
622
+
623
+ with gr.Row():
624
+ live_plot = gr.LinePlot(x="Frame", y=["ROI Trav", "ROI Unc"],
625
+ title="Live ROI Metrics (ROI Trav & ROI "
626
+ "Uncert vs threshold)",
627
+ height=260, )
628
+ metric_bars = gr.HTML(value=bars_html(0.0, 0.0, session.uncert_thresh),
629
+ label="ROI Score Gauges")
630
+
631
+ with gr.Column(scale=2):
632
+ # -------- Controls --------
633
+ video_in = gr.File(label="๐Ÿ“น Upload Off-Road Video (.mp4, .mov, .avi)",
634
+ file_count="single")
635
+ pref_input = gr.Code(value=json.dumps(session.preferences, indent=2),
636
+ language="json", label="Traversability Preferences (ฯ„)")
637
+ with gr.Row():
638
+ sim_thresh = gr.Slider(0.05, 1.0, value=0.8, step=0.05,
639
+ label="Ref Scene Sim. Threshold")
640
+ uncert_thresh = gr.Slider(0.05, 1.0, value=0.4, step=0.05,
641
+ label="ROI Uncertainty Threshold")
642
+ frame_skip = gr.Slider(1, 10, value=2, step=1,
643
+ label="Frame Skip (VLM inference interval)")
644
+ gr.Markdown("#### ROI (normalized) โ€” editable live")
645
+ with gr.Row():
646
+ rx_min = gr.Number(value=0.333, label="ROI X Min", step=0.01)
647
+ rx_max = gr.Number(value=0.667, label="ROI X Max", step=0.01)
648
+ with gr.Row():
649
+ ry_min = gr.Number(value=0.600, label="ROI Y Min", step=0.01)
650
+ ry_max = gr.Number(value=0.950, label="ROI Y Max", step=0.01)
651
+ cfg_status = gr.Markdown("_Live-threshold / ROI edits apply to the "
652
+ "running pipeline instantly._")
653
+ with gr.Row():
654
+ run_btn = gr.Button("โ–ถ๏ธ Go / Reset", variant="primary")
655
+ sim_btn = gr.Button("โธ Simulate HOC", variant="secondary")
656
+
657
+ with gr.Group(visible=False) as operator_box:
658
+ gr.Markdown("### ๐Ÿšจ HUMAN OPERATOR CALL")
659
+ gr.Markdown(
660
+ "Enter ฯ„ updates as `prompt`: `weight; prompt: weight`, e.g. "
661
+ "`mud: -0.7; gravel: 0.6`. Type **ok** (or leave blank) to "
662
+ "resume without changing preferences (registers the scene).")
663
+ operator_text = gr.Textbox(label="Operator ฯ„ update / ok",
664
+ placeholder="mud: -0.7; gravel: 0.6")
665
+ resume_btn = gr.Button("โœ… Apply & Resume", variant="primary")
666
+
667
+ gr.Markdown("#### Per-frame outputs")
668
+ with gr.Row():
669
+ m_frame = gr.Textbox(label="Frame", value="0", interactive=False)
670
+ m_skip = gr.Textbox(label="Skip", value="2", interactive=False)
671
+ m_state = gr.Textbox(label="State", value="ok", interactive=False)
672
+ with gr.Row():
673
+ m_trav = gr.Textbox(label="ROI Trav", value="0.000", interactive=False)
674
+ m_unc = gr.Textbox(label="ROI Unc", value="0.000", interactive=False)
675
+ m_sim = gr.Textbox(label="Ref Sim", value="0.000", interactive=False)
676
+ with gr.Row():
677
+ m_fps = gr.Textbox(label="FPS", value="0", interactive=False)
678
+ m_lat = gr.Textbox(label="Latency", value="0 ms", interactive=False)
679
+ m_dev = gr.Textbox(label="Device", value=session.device, interactive=False)
680
+
681
+ with gr.Row():
682
+ log_table = gr.DataFrame(headers=TEL_COLUMNS, interactive=False,
683
+ label="Telemetry")
684
+ download_out = gr.DownloadButton(label="โฌ‡ Download composed video (.mp4)",
685
+ value=None, variant="primary")
686
+
687
+ # ---------------- Event wiring ----------------
688
+ inputs = [video_in, pref_input, sim_thresh, uncert_thresh, rx_min, rx_max,
689
+ ry_min, ry_max, frame_skip]
690
+ outputs = [live_view, status_banner, operator_box, attn_view, m_frame, m_skip,
691
+ m_state, m_trav, m_unc, m_sim, m_fps, m_lat, live_plot, metric_bars,
692
+ log_table, download_out]
693
+
694
+ cfg_inputs = [sim_thresh, uncert_thresh, rx_min, rx_max, ry_min, ry_max]
695
+ for ctl in (sim_thresh, uncert_thresh, rx_min, rx_max, ry_min, ry_max):
696
+ ctl.change(live_pipeline_update, inputs=cfg_inputs, outputs=[cfg_status])
697
+
698
+ run_btn.click(run_evaluation, inputs=inputs, outputs=outputs)
699
+ sim_btn.click(simulate_hoc, outputs=[status_banner])
700
+ resume_btn.click(
701
+ handle_operator_resume, inputs=[operator_text],
702
+ outputs=[status_banner, operator_box, pref_input],
703
+ ).then(run_evaluation, inputs=inputs, outputs=outputs)
704
+
705
+
706
+ if __name__ == "__main__":
707
+ demo.queue().launch(
708
+ share=True, theme=THEME, css=CUSTOM_CSS,
709
+ allowed_paths=["."],
710
+ server_name="0.0.0.0",
711
+ )
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate>=1.14.0
2
+ anytraverse>=1.0.9
3
+ einops>=0.8.2
4
+ ffmpeg>=1.4
5
+ gradio==6.22.0
6
+ imageio-ffmpeg>=0.6.0
7
+ opencv-python>=5.0.0.93
8
+ pandas>=3.0.5
9
+ pillow>=12.3.0
10
+ torch>=2.13.0
11
+ torchvision>=0.28.0
12
+ transformers>=5.14.1