Zhaoting123 commited on
Commit
e80f0b4
·
verified ·
1 Parent(s): ca76c1e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +183 -359
app.py CHANGED
@@ -1,38 +1,37 @@
1
- """
2
- Standalone Hugging Face Space viewer for TrajectoryBuffer-style HDF5 files.
3
-
4
- Best-practice version:
5
- - No dependency on your local TrajectoryBuffer Python class.
6
- - Dataset preset + custom dataset support.
7
- - Robust HDF5 schema detection for root-level episode_XXXX groups.
8
- - Auto-detect image keys from each trajectory's observation group.
9
- - Avoids fragile multiline f-strings in UI status text.
10
- - Uses slider.release() for timestep rendering to reduce image flicker.
11
-
12
- requirements.txt:
13
- gradio
14
- huggingface_hub
15
- h5py
16
- numpy
17
- pillow
18
- matplotlib
19
-
20
- Optional:
21
- opencv-python-headless
22
- """
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  import re
 
25
  from functools import lru_cache
26
 
27
  import gradio as gr
28
  import h5py
29
  import matplotlib
30
-
31
  matplotlib.use("Agg")
32
  import matplotlib.pyplot as plt
33
  import numpy as np
34
  from huggingface_hub import hf_hub_download
35
- from PIL import Image
 
 
 
 
 
36
 
37
  try:
38
  import cv2
@@ -40,9 +39,6 @@ except Exception:
40
  cv2 = None
41
 
42
 
43
- # -----------------------------------------------------------------------------
44
- # Dataset presets
45
- # -----------------------------------------------------------------------------
46
  DATASET_PRESETS = {
47
  "Robosuite Square 20260409": {
48
  "repo_id": "Zhaoting123/Robosuite_Square_image_abs_with_state",
@@ -67,7 +63,6 @@ DATASET_PRESETS = {
67
  DEFAULT_PRESET = "Robosuite Square 20260409"
68
  REPO_TYPE = "dataset"
69
  DEFAULT_CHUNK_LEN = 16
70
-
71
  PREFERRED_IMAGE_KEYS = [
72
  "image1",
73
  "image2",
@@ -76,16 +71,11 @@ PREFERRED_IMAGE_KEYS = [
76
  "front_image",
77
  "wrist_image",
78
  ]
79
-
80
  IMAGE_KEY_HINTS = ["rgb", "image", "img", "camera", "cam"]
81
 
82
 
83
- # -----------------------------------------------------------------------------
84
- # Dataset resolution and cache helpers
85
- # -----------------------------------------------------------------------------
86
  def resolve_dataset(preset_name, custom_repo_id=None, custom_filename=None):
87
  preset_name = preset_name or DEFAULT_PRESET
88
-
89
  if preset_name == "Custom":
90
  repo_id = str(custom_repo_id or "").strip()
91
  filename = str(custom_filename or "").strip()
@@ -93,20 +83,13 @@ def resolve_dataset(preset_name, custom_repo_id=None, custom_filename=None):
93
  raise ValueError("For Custom mode, provide both repo_id and HDF5 filename/path.")
94
  return repo_id, filename
95
 
96
- if preset_name not in DATASET_PRESETS:
97
- preset_name = DEFAULT_PRESET
98
-
99
- item = DATASET_PRESETS[preset_name]
100
  return item["repo_id"], item["filename"]
101
 
102
 
103
  @lru_cache(maxsize=8)
104
  def get_local_hdf5_path(repo_id, filename):
105
- return hf_hub_download(
106
- repo_id=repo_id,
107
- filename=filename,
108
- repo_type=REPO_TYPE,
109
- )
110
 
111
 
112
  def _natural_sort_key(name):
@@ -118,39 +101,21 @@ def _natural_sort_key(name):
118
 
119
  @lru_cache(maxsize=8)
120
  def get_trajectory_keys(repo_id, filename):
121
- """Return ordered trajectory group paths."""
122
  path = get_local_hdf5_path(repo_id, filename)
123
-
124
  with h5py.File(path, "r") as f:
125
- # Your TrajectoryBuffer format:
126
- # /episode_0000
127
- # /episode_0001
128
  root_episode_keys = [
129
- key
130
- for key in f.keys()
131
  if isinstance(f[key], h5py.Group) and str(key).startswith("episode_")
132
  ]
133
  if root_episode_keys:
134
  return tuple(sorted(root_episode_keys, key=_natural_sort_key))
135
 
136
- # Robomimic-style fallback:
137
- # /data/demo_0
138
- # /data/demo_1
139
  if "data" in f and isinstance(f["data"], h5py.Group):
140
  data_group = f["data"]
141
- keys = [
142
- key
143
- for key in data_group.keys()
144
- if isinstance(data_group[key], h5py.Group)
145
- ]
146
  return tuple("data/" + key for key in sorted(keys, key=_natural_sort_key))
147
 
148
- # Generic root-level group fallback.
149
- keys = [
150
- key
151
- for key in f.keys()
152
- if isinstance(f[key], h5py.Group)
153
- ]
154
  return tuple(sorted(keys, key=_natural_sort_key))
155
 
156
 
@@ -169,9 +134,7 @@ def inspect_hdf5_tree(preset_name, custom_repo_id, custom_filename, max_lines=18
169
  if len(lines) >= max_lines:
170
  return
171
  if isinstance(obj, h5py.Dataset):
172
- lines.append(
173
- "DATASET {} shape={} dtype={}".format(name, obj.shape, obj.dtype)
174
- )
175
  elif isinstance(obj, h5py.Group):
176
  lines.append("GROUP {}".format(name))
177
 
@@ -179,15 +142,9 @@ def inspect_hdf5_tree(preset_name, custom_repo_id, custom_filename, max_lines=18
179
 
180
  if len(lines) >= max_lines:
181
  lines.append("...")
182
-
183
- if not lines:
184
- return "No HDF5 contents found."
185
- return chr(10).join(lines)
186
 
187
 
188
- # -----------------------------------------------------------------------------
189
- # HDF5 loading helpers
190
- # -----------------------------------------------------------------------------
191
  def _read_dataset_value(dataset):
192
  value = dataset[()]
193
  if isinstance(value, bytes):
@@ -213,7 +170,6 @@ def _find_first_key(mapping, candidate_keys):
213
 
214
 
215
  def _infer_time_length(data):
216
- """Infer trajectory length from common TrajectoryBuffer fields."""
217
  for key in ["timesteps", "dones", "robot_actions", "teacher_actions", "actions"]:
218
  if key in data:
219
  arr = np.asarray(data[key])
@@ -235,7 +191,6 @@ def _infer_time_length(data):
235
  if lengths:
236
  values, counts = np.unique(lengths, return_counts=True)
237
  return int(values[np.argmax(counts)])
238
-
239
  return 1
240
 
241
 
@@ -248,7 +203,6 @@ def _slice_time(value, t, T):
248
 
249
  @lru_cache(maxsize=64)
250
  def load_traj(repo_id, filename, traj_id):
251
- """Load one trajectory as list[dict]."""
252
  traj_keys = get_trajectory_keys(repo_id, filename)
253
  if not traj_keys:
254
  return []
@@ -258,8 +212,7 @@ def load_traj(repo_id, filename, traj_id):
258
  path = get_local_hdf5_path(repo_id, filename)
259
 
260
  with h5py.File(path, "r") as f:
261
- group = f[traj_key]
262
- data = _read_group_recursive(group)
263
 
264
  T = _infer_time_length(data)
265
 
@@ -281,66 +234,41 @@ def load_traj(repo_id, filename, traj_id):
281
 
282
  traj = []
283
  for t in range(T):
284
- obs_t = {}
285
- for key, value in obs_all.items():
286
- obs_t[key] = _slice_time(value, t, T)
287
 
288
  default_action = np.zeros(1, dtype=np.float32)
289
  if action_key is not None:
290
  default_action = _slice_time(data[action_key], t, T)
291
 
292
- teacher_action = default_action
293
- if teacher_key is not None:
294
- teacher_action = _slice_time(data[teacher_key], t, T)
295
-
296
- robot_action = default_action
297
- if robot_key is not None:
298
- robot_action = _slice_time(data[robot_key], t, T)
299
-
300
- no_teacher = False
301
- if no_teacher_key is not None:
302
- no_teacher = _slice_time(data[no_teacher_key], t, T)
303
-
304
- no_robot = False
305
- if no_robot_key is not None:
306
- no_robot = _slice_time(data[no_robot_key], t, T)
307
-
308
- done = False
309
- if done_key is not None:
310
- done = _slice_time(data[done_key], t, T)
311
 
312
  timestep = t
313
  if timestep_key is not None:
314
  timestep_arr = _slice_time(data[timestep_key], t, T)
315
  timestep = int(np.asarray(timestep_arr).reshape(-1)[0])
316
 
317
- if_success = False
318
- if success_key is not None:
319
- if_success = _slice_time(data[success_key], t, T)
320
-
321
- traj.append(
322
- {
323
- "obs": obs_t,
324
- "robot_action": np.asarray(robot_action),
325
- "teacher_action": np.asarray(teacher_action),
326
- "done": bool(np.asarray(done).reshape(-1)[0]),
327
- "timestep": timestep,
328
- "no_robot_action": bool(np.asarray(no_robot).reshape(-1)[0]),
329
- "no_teacher_action": bool(np.asarray(no_teacher).reshape(-1)[0]),
330
- "episode_id": traj_key,
331
- "if_success": bool(np.asarray(if_success).reshape(-1)[0]),
332
- }
333
- )
334
 
335
  return traj
336
 
337
 
338
- # -----------------------------------------------------------------------------
339
- # Image and plotting helpers
340
- # -----------------------------------------------------------------------------
341
  def _extract_latest_obs_value(value):
342
  arr = np.asarray(value)
343
- # Per-timestep stacked observation commonly has shape [obs_T, C, H, W].
344
  if arr.ndim >= 1 and arr.shape[0] in (1, 2, 3, 4):
345
  return arr[-1]
346
  return arr
@@ -365,8 +293,6 @@ def _float_img_to_uint8(img):
365
  arr_min = float(np.nanmin(arr))
366
  arr_max = float(np.nanmax(arr))
367
 
368
- # TrajectoryBuffer saves float images originally in [-1, 1] as uint8.
369
- # But for compatibility, handle float [-1, 1], [0, 1], and [0, 255].
370
  if arr_min >= -1.01 and arr_max <= 1.01:
371
  if arr_min < 0.0:
372
  arr = (arr + 1.0) * 0.5
@@ -395,16 +321,10 @@ def _extract_display_image(value, reverse_channels=False):
395
  if img.ndim != 3:
396
  raise ValueError("Unsupported image shape: {}".format(img.shape))
397
 
398
- if img.dtype == np.uint8:
399
- out = img.copy()
400
- else:
401
- out = _float_img_to_uint8(img)
402
 
403
- # Browser display expects RGB. Your current data appears RGB already,
404
- # so default reverse_channels=False.
405
  if reverse_channels and out.shape[-1] == 3:
406
  out = out[..., ::-1]
407
-
408
  return out
409
 
410
 
@@ -437,7 +357,6 @@ def _extract_mixed_action_chunk(traj, start_idx, chunk_length):
437
 
438
  if not chunk:
439
  return None, ""
440
-
441
  return np.stack(chunk, axis=0), "".join(sources)
442
 
443
 
@@ -451,7 +370,6 @@ def _extract_robot_action_chunk(traj, start_idx, chunk_length):
451
 
452
  if not chunk:
453
  return None
454
-
455
  return np.stack(chunk, axis=0)
456
 
457
 
@@ -505,25 +423,8 @@ def _make_action_chunk_plot(mixed_chunk, robot_chunk):
505
  return image
506
 
507
 
508
- # -----------------------------------------------------------------------------
509
- # Frame-level render cache
510
- # -----------------------------------------------------------------------------
511
  @lru_cache(maxsize=8192)
512
- def get_cached_gallery_items(
513
- repo_id,
514
- filename,
515
- traj_id,
516
- timestep,
517
- image_keys_tuple,
518
- display_scale,
519
- reverse_channels,
520
- ):
521
- """Cache decoded/resized observation images for one frame.
522
-
523
- Gradio may briefly clear the image component while a callback is running.
524
- This cache makes the callback fast after preloading, which largely removes
525
- the black-frame effect when scrubbing.
526
- """
527
  traj = load_traj(repo_id, filename, int(traj_id))
528
  timestep = int(np.clip(int(timestep), 0, len(traj) - 1))
529
  obs = traj[timestep].get("obs", {})
@@ -535,10 +436,7 @@ def get_cached_gallery_items(
535
  warnings.append("Missing image key: {}".format(key))
536
  continue
537
  try:
538
- img = _extract_display_image(
539
- obs[key],
540
- reverse_channels=bool(reverse_channels),
541
- )
542
  img = _resize_image_for_display(img, float(display_scale))
543
  gallery_items.append((img, key))
544
  except Exception as exc:
@@ -556,17 +454,7 @@ def get_cached_action_plot(repo_id, filename, traj_id, timestep, chunk_len):
556
  return _make_action_chunk_plot(mixed_chunk, robot_chunk), source_mask
557
 
558
 
559
- def preload_current_trajectory(
560
- preset_name,
561
- custom_repo_id,
562
- custom_filename,
563
- traj_id,
564
- image_keys,
565
- chunk_len,
566
- display_scale,
567
- reverse_channels,
568
- ):
569
- """Pre-render all selected observation frames for the current trajectory."""
570
  repo_id, filename = resolve_dataset(preset_name, custom_repo_id, custom_filename)
571
  n_traj = get_num_trajectories(repo_id, filename)
572
  if n_traj == 0:
@@ -585,27 +473,106 @@ def preload_current_trajectory(
585
 
586
  total = len(traj)
587
  for t in range(total):
588
- get_cached_gallery_items(
589
- repo_id,
590
- filename,
591
- traj_id,
592
- t,
593
- image_keys_tuple,
594
- float(display_scale),
595
- bool(reverse_channels),
596
- )
597
- # The action plot is usually the slowest part. Preload it too.
598
  get_cached_action_plot(repo_id, filename, traj_id, t, int(chunk_len))
599
 
600
  status = "Preloaded trajectory {}".format(traj_id)
601
- status += chr(10) + "Frames cached: {}".format(total)
602
- status += chr(10) + "Image keys: {}".format(", ".join(image_keys_tuple) if image_keys_tuple else "none")
603
  return status
604
 
605
 
606
- # -----------------------------------------------------------------------------
607
- # Gradio callbacks
608
- # -----------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  def get_available_image_keys(repo_id, filename, traj_id):
610
  n_traj = get_num_trajectories(repo_id, filename)
611
  if n_traj == 0:
@@ -641,7 +608,7 @@ def update_after_dataset_change(preset_name, custom_repo_id, custom_filename):
641
 
642
  if n_traj == 0:
643
  status = "Loaded `{}` / `{}`".format(repo_id, filename)
644
- status = status + chr(10) + "Detected trajectories: 0"
645
  return (
646
  gr.update(maximum=1, value=0),
647
  gr.update(maximum=1, value=0),
@@ -653,7 +620,7 @@ def update_after_dataset_change(preset_name, custom_repo_id, custom_filename):
653
  traj = load_traj(repo_id, filename, 0)
654
 
655
  status = "Loaded `{}` / `{}`".format(repo_id, filename)
656
- status = status + chr(10) + "Detected trajectories: {}".format(n_traj)
657
 
658
  return (
659
  gr.update(maximum=max(n_traj - 1, 1), value=0),
@@ -679,17 +646,7 @@ def update_after_traj_change(preset_name, custom_repo_id, custom_filename, traj_
679
  )
680
 
681
 
682
- def render_frame(
683
- preset_name,
684
- custom_repo_id,
685
- custom_filename,
686
- traj_id,
687
- timestep,
688
- image_keys,
689
- chunk_len,
690
- display_scale,
691
- reverse_channels,
692
- ):
693
  repo_id, filename = resolve_dataset(preset_name, custom_repo_id, custom_filename)
694
  n_traj = get_num_trajectories(repo_id, filename)
695
 
@@ -711,27 +668,14 @@ def render_frame(
711
  image_keys = [image_keys]
712
 
713
  step = traj[timestep]
714
- obs = step.get("obs", {})
715
-
716
  image_keys_tuple = tuple(image_keys)
 
717
  gallery_items, warnings_tuple = get_cached_gallery_items(
718
- repo_id,
719
- filename,
720
- traj_id,
721
- timestep,
722
- image_keys_tuple,
723
- display_scale,
724
- bool(reverse_channels),
725
  )
726
  warnings = list(warnings_tuple)
727
 
728
- action_plot, source_mask = get_cached_action_plot(
729
- repo_id,
730
- filename,
731
- traj_id,
732
- timestep,
733
- chunk_len,
734
- )
735
 
736
  info_lines = [
737
  "dataset: {} / {}".format(repo_id, filename),
@@ -756,12 +700,9 @@ def render_frame(
756
  info_lines.append("Image warnings:")
757
  info_lines.extend(warnings)
758
 
759
- return gallery_items, action_plot, chr(10).join(info_lines)
760
 
761
 
762
- # -----------------------------------------------------------------------------
763
- # App
764
- # -----------------------------------------------------------------------------
765
  def build_app():
766
  repo_id, filename = resolve_dataset(DEFAULT_PRESET)
767
 
@@ -774,8 +715,7 @@ def build_app():
774
  first_keys = []
775
  startup_warning = repr(exc)
776
 
777
- default_status = "Loaded default dataset" + chr(10)
778
- default_status += "Detected trajectories: {}".format(n_traj)
779
 
780
  with gr.Blocks(title="HDF5 Trajectory Viewer") as demo:
781
  gr.Markdown(
@@ -792,82 +732,31 @@ def build_app():
792
  value=DEFAULT_PRESET,
793
  label="Dataset preset",
794
  )
795
- custom_repo_id = gr.Textbox(
796
- value="",
797
- label="Custom repo_id, e.g. Zhaoting123/InsertT",
798
- visible=False,
799
- )
800
- custom_filename = gr.Textbox(
801
- value="",
802
- label="Custom HDF5 path in repo",
803
- visible=False,
804
- )
805
 
806
- dataset_status = gr.Textbox(
807
- label="Dataset status",
808
- lines=2,
809
- value=default_status,
810
- interactive=False,
811
- )
812
 
813
  with gr.Row():
814
- traj_slider = gr.Slider(
815
- minimum=0,
816
- maximum=max(n_traj - 1, 1),
817
- value=0,
818
- step=1,
819
- label="Trajectory index",
820
- )
821
- timestep_slider = gr.Slider(
822
- minimum=0,
823
- maximum=1,
824
- value=0,
825
- step=1,
826
- label="Timestep",
827
- )
828
 
829
  with gr.Row():
830
- image_keys = gr.CheckboxGroup(
831
- choices=first_keys,
832
- value=first_keys[:2],
833
- label="Image keys",
834
- )
835
- chunk_len = gr.Slider(
836
- minimum=1,
837
- maximum=64,
838
- value=DEFAULT_CHUNK_LEN,
839
- step=1,
840
- label="Action chunk length",
841
- )
842
- display_scale = gr.Slider(
843
- minimum=1,
844
- maximum=10,
845
- value=4,
846
- step=1,
847
- label="Image display scale",
848
- )
849
- reverse_channels = gr.Checkbox(
850
- value=False,
851
- label="Reverse channels BGR↔RGB",
852
- )
853
 
854
  with gr.Row():
855
  render_btn = gr.Button("Render frame", variant="primary")
856
  preload_btn = gr.Button("Preload current trajectory")
 
 
857
 
858
- preload_status = gr.Textbox(
859
- label="Preload status",
860
- lines=3,
861
- value="Not preloaded yet.",
862
- interactive=False,
863
- )
864
 
865
- gallery = gr.Gallery(
866
- label="Observation images",
867
- columns=2,
868
- height="auto",
869
- object_fit="contain",
870
- )
871
  action_plot = gr.Image(label="Action chunk plot", type="numpy")
872
  info = gr.Textbox(label="Frame info", lines=16)
873
 
@@ -875,7 +764,6 @@ def build_app():
875
  inspect_btn = gr.Button("Inspect HDF5 structure")
876
  hdf5_tree = gr.Textbox(lines=24, label="HDF5 tree")
877
 
878
- # Dataset selection and custom-field visibility.
879
  preset.change(
880
  fn=update_custom_visibility,
881
  inputs=preset,
@@ -886,17 +774,7 @@ def build_app():
886
  outputs=[traj_slider, timestep_slider, image_keys, dataset_status],
887
  ).then(
888
  fn=render_frame,
889
- inputs=[
890
- preset,
891
- custom_repo_id,
892
- custom_filename,
893
- traj_slider,
894
- timestep_slider,
895
- image_keys,
896
- chunk_len,
897
- display_scale,
898
- reverse_channels,
899
- ],
900
  outputs=[gallery, action_plot, info],
901
  )
902
 
@@ -917,85 +795,41 @@ def build_app():
917
  outputs=[timestep_slider, image_keys],
918
  ).then(
919
  fn=render_frame,
920
- inputs=[
921
- preset,
922
- custom_repo_id,
923
- custom_filename,
924
- traj_slider,
925
- timestep_slider,
926
- image_keys,
927
- chunk_len,
928
- display_scale,
929
- reverse_channels,
930
- ],
931
  outputs=[gallery, action_plot, info],
932
  )
933
 
934
- # Render only after releasing the timestep slider, reducing flicker.
935
  timestep_slider.release(
936
  fn=render_frame,
937
- inputs=[
938
- preset,
939
- custom_repo_id,
940
- custom_filename,
941
- traj_slider,
942
- timestep_slider,
943
- image_keys,
944
- chunk_len,
945
- display_scale,
946
- reverse_channels,
947
- ],
948
  outputs=[gallery, action_plot, info],
949
  )
950
 
951
  for widget in [image_keys, chunk_len, display_scale, reverse_channels]:
952
  widget.change(
953
  fn=render_frame,
954
- inputs=[
955
- preset,
956
- custom_repo_id,
957
- custom_filename,
958
- traj_slider,
959
- timestep_slider,
960
- image_keys,
961
- chunk_len,
962
- display_scale,
963
- reverse_channels,
964
- ],
965
  outputs=[gallery, action_plot, info],
966
  )
967
 
968
  render_btn.click(
969
  fn=render_frame,
970
- inputs=[
971
- preset,
972
- custom_repo_id,
973
- custom_filename,
974
- traj_slider,
975
- timestep_slider,
976
- image_keys,
977
- chunk_len,
978
- display_scale,
979
- reverse_channels,
980
- ],
981
  outputs=[gallery, action_plot, info],
982
  )
983
 
984
  preload_btn.click(
985
  fn=preload_current_trajectory,
986
- inputs=[
987
- preset,
988
- custom_repo_id,
989
- custom_filename,
990
- traj_slider,
991
- image_keys,
992
- chunk_len,
993
- display_scale,
994
- reverse_channels,
995
- ],
996
  outputs=preload_status,
997
  )
998
 
 
 
 
 
 
 
999
  inspect_btn.click(
1000
  fn=inspect_hdf5_tree,
1001
  inputs=[preset, custom_repo_id, custom_filename],
@@ -1008,17 +842,7 @@ def build_app():
1008
  outputs=[traj_slider, timestep_slider, image_keys, dataset_status],
1009
  ).then(
1010
  fn=render_frame,
1011
- inputs=[
1012
- preset,
1013
- custom_repo_id,
1014
- custom_filename,
1015
- traj_slider,
1016
- timestep_slider,
1017
- image_keys,
1018
- chunk_len,
1019
- display_scale,
1020
- reverse_channels,
1021
- ],
1022
  outputs=[gallery, action_plot, info],
1023
  )
1024
 
@@ -1026,4 +850,4 @@ def build_app():
1026
 
1027
 
1028
  if __name__ == "__main__":
1029
- build_app().launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ # Standalone Hugging Face Space viewer for TrajectoryBuffer-style HDF5 files.
3
+ #
4
+ # requirements.txt:
5
+ # gradio
6
+ # huggingface_hub
7
+ # h5py
8
+ # numpy
9
+ # pillow
10
+ # matplotlib
11
+ # imageio
12
+ # imageio-ffmpeg
13
+ #
14
+ # Optional:
15
+ # opencv-python-headless
16
+
17
+ import os
18
  import re
19
+ import tempfile
20
  from functools import lru_cache
21
 
22
  import gradio as gr
23
  import h5py
24
  import matplotlib
 
25
  matplotlib.use("Agg")
26
  import matplotlib.pyplot as plt
27
  import numpy as np
28
  from huggingface_hub import hf_hub_download
29
+ from PIL import Image, ImageDraw
30
+
31
+ try:
32
+ import imageio.v2 as imageio
33
+ except Exception:
34
+ imageio = None
35
 
36
  try:
37
  import cv2
 
39
  cv2 = None
40
 
41
 
 
 
 
42
  DATASET_PRESETS = {
43
  "Robosuite Square 20260409": {
44
  "repo_id": "Zhaoting123/Robosuite_Square_image_abs_with_state",
 
63
  DEFAULT_PRESET = "Robosuite Square 20260409"
64
  REPO_TYPE = "dataset"
65
  DEFAULT_CHUNK_LEN = 16
 
66
  PREFERRED_IMAGE_KEYS = [
67
  "image1",
68
  "image2",
 
71
  "front_image",
72
  "wrist_image",
73
  ]
 
74
  IMAGE_KEY_HINTS = ["rgb", "image", "img", "camera", "cam"]
75
 
76
 
 
 
 
77
  def resolve_dataset(preset_name, custom_repo_id=None, custom_filename=None):
78
  preset_name = preset_name or DEFAULT_PRESET
 
79
  if preset_name == "Custom":
80
  repo_id = str(custom_repo_id or "").strip()
81
  filename = str(custom_filename or "").strip()
 
83
  raise ValueError("For Custom mode, provide both repo_id and HDF5 filename/path.")
84
  return repo_id, filename
85
 
86
+ item = DATASET_PRESETS.get(preset_name, DATASET_PRESETS[DEFAULT_PRESET])
 
 
 
87
  return item["repo_id"], item["filename"]
88
 
89
 
90
  @lru_cache(maxsize=8)
91
  def get_local_hdf5_path(repo_id, filename):
92
+ return hf_hub_download(repo_id=repo_id, filename=filename, repo_type=REPO_TYPE)
 
 
 
 
93
 
94
 
95
  def _natural_sort_key(name):
 
101
 
102
  @lru_cache(maxsize=8)
103
  def get_trajectory_keys(repo_id, filename):
 
104
  path = get_local_hdf5_path(repo_id, filename)
 
105
  with h5py.File(path, "r") as f:
 
 
 
106
  root_episode_keys = [
107
+ key for key in f.keys()
 
108
  if isinstance(f[key], h5py.Group) and str(key).startswith("episode_")
109
  ]
110
  if root_episode_keys:
111
  return tuple(sorted(root_episode_keys, key=_natural_sort_key))
112
 
 
 
 
113
  if "data" in f and isinstance(f["data"], h5py.Group):
114
  data_group = f["data"]
115
+ keys = [key for key in data_group.keys() if isinstance(data_group[key], h5py.Group)]
 
 
 
 
116
  return tuple("data/" + key for key in sorted(keys, key=_natural_sort_key))
117
 
118
+ keys = [key for key in f.keys() if isinstance(f[key], h5py.Group)]
 
 
 
 
 
119
  return tuple(sorted(keys, key=_natural_sort_key))
120
 
121
 
 
134
  if len(lines) >= max_lines:
135
  return
136
  if isinstance(obj, h5py.Dataset):
137
+ lines.append("DATASET {} shape={} dtype={}".format(name, obj.shape, obj.dtype))
 
 
138
  elif isinstance(obj, h5py.Group):
139
  lines.append("GROUP {}".format(name))
140
 
 
142
 
143
  if len(lines) >= max_lines:
144
  lines.append("...")
145
+ return "\n".join(lines) if lines else "No HDF5 contents found."
 
 
 
146
 
147
 
 
 
 
148
  def _read_dataset_value(dataset):
149
  value = dataset[()]
150
  if isinstance(value, bytes):
 
170
 
171
 
172
  def _infer_time_length(data):
 
173
  for key in ["timesteps", "dones", "robot_actions", "teacher_actions", "actions"]:
174
  if key in data:
175
  arr = np.asarray(data[key])
 
191
  if lengths:
192
  values, counts = np.unique(lengths, return_counts=True)
193
  return int(values[np.argmax(counts)])
 
194
  return 1
195
 
196
 
 
203
 
204
  @lru_cache(maxsize=64)
205
  def load_traj(repo_id, filename, traj_id):
 
206
  traj_keys = get_trajectory_keys(repo_id, filename)
207
  if not traj_keys:
208
  return []
 
212
  path = get_local_hdf5_path(repo_id, filename)
213
 
214
  with h5py.File(path, "r") as f:
215
+ data = _read_group_recursive(f[traj_key])
 
216
 
217
  T = _infer_time_length(data)
218
 
 
234
 
235
  traj = []
236
  for t in range(T):
237
+ obs_t = {key: _slice_time(value, t, T) for key, value in obs_all.items()}
 
 
238
 
239
  default_action = np.zeros(1, dtype=np.float32)
240
  if action_key is not None:
241
  default_action = _slice_time(data[action_key], t, T)
242
 
243
+ teacher_action = _slice_time(data[teacher_key], t, T) if teacher_key else default_action
244
+ robot_action = _slice_time(data[robot_key], t, T) if robot_key else default_action
245
+ no_teacher = _slice_time(data[no_teacher_key], t, T) if no_teacher_key else False
246
+ no_robot = _slice_time(data[no_robot_key], t, T) if no_robot_key else False
247
+ done = _slice_time(data[done_key], t, T) if done_key else False
248
+ if_success = _slice_time(data[success_key], t, T) if success_key else False
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
  timestep = t
251
  if timestep_key is not None:
252
  timestep_arr = _slice_time(data[timestep_key], t, T)
253
  timestep = int(np.asarray(timestep_arr).reshape(-1)[0])
254
 
255
+ traj.append({
256
+ "obs": obs_t,
257
+ "robot_action": np.asarray(robot_action),
258
+ "teacher_action": np.asarray(teacher_action),
259
+ "done": bool(np.asarray(done).reshape(-1)[0]),
260
+ "timestep": timestep,
261
+ "no_robot_action": bool(np.asarray(no_robot).reshape(-1)[0]),
262
+ "no_teacher_action": bool(np.asarray(no_teacher).reshape(-1)[0]),
263
+ "episode_id": traj_key,
264
+ "if_success": bool(np.asarray(if_success).reshape(-1)[0]),
265
+ })
 
 
 
 
 
 
266
 
267
  return traj
268
 
269
 
 
 
 
270
  def _extract_latest_obs_value(value):
271
  arr = np.asarray(value)
 
272
  if arr.ndim >= 1 and arr.shape[0] in (1, 2, 3, 4):
273
  return arr[-1]
274
  return arr
 
293
  arr_min = float(np.nanmin(arr))
294
  arr_max = float(np.nanmax(arr))
295
 
 
 
296
  if arr_min >= -1.01 and arr_max <= 1.01:
297
  if arr_min < 0.0:
298
  arr = (arr + 1.0) * 0.5
 
321
  if img.ndim != 3:
322
  raise ValueError("Unsupported image shape: {}".format(img.shape))
323
 
324
+ out = img.copy() if img.dtype == np.uint8 else _float_img_to_uint8(img)
 
 
 
325
 
 
 
326
  if reverse_channels and out.shape[-1] == 3:
327
  out = out[..., ::-1]
 
328
  return out
329
 
330
 
 
357
 
358
  if not chunk:
359
  return None, ""
 
360
  return np.stack(chunk, axis=0), "".join(sources)
361
 
362
 
 
370
 
371
  if not chunk:
372
  return None
 
373
  return np.stack(chunk, axis=0)
374
 
375
 
 
423
  return image
424
 
425
 
 
 
 
426
  @lru_cache(maxsize=8192)
427
+ def get_cached_gallery_items(repo_id, filename, traj_id, timestep, image_keys_tuple, display_scale, reverse_channels):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  traj = load_traj(repo_id, filename, int(traj_id))
429
  timestep = int(np.clip(int(timestep), 0, len(traj) - 1))
430
  obs = traj[timestep].get("obs", {})
 
436
  warnings.append("Missing image key: {}".format(key))
437
  continue
438
  try:
439
+ img = _extract_display_image(obs[key], reverse_channels=bool(reverse_channels))
 
 
 
440
  img = _resize_image_for_display(img, float(display_scale))
441
  gallery_items.append((img, key))
442
  except Exception as exc:
 
454
  return _make_action_chunk_plot(mixed_chunk, robot_chunk), source_mask
455
 
456
 
457
+ def preload_current_trajectory(preset_name, custom_repo_id, custom_filename, traj_id, image_keys, chunk_len, display_scale, reverse_channels):
 
 
 
 
 
 
 
 
 
 
458
  repo_id, filename = resolve_dataset(preset_name, custom_repo_id, custom_filename)
459
  n_traj = get_num_trajectories(repo_id, filename)
460
  if n_traj == 0:
 
473
 
474
  total = len(traj)
475
  for t in range(total):
476
+ get_cached_gallery_items(repo_id, filename, traj_id, t, image_keys_tuple, float(display_scale), bool(reverse_channels))
 
 
 
 
 
 
 
 
 
477
  get_cached_action_plot(repo_id, filename, traj_id, t, int(chunk_len))
478
 
479
  status = "Preloaded trajectory {}".format(traj_id)
480
+ status += "\nFrames cached: {}".format(total)
481
+ status += "\nImage keys: {}".format(", ".join(image_keys_tuple) if image_keys_tuple else "none")
482
  return status
483
 
484
 
485
+ def _compose_video_frame(gallery_items, frame_label):
486
+ if not gallery_items:
487
+ canvas = Image.new("RGB", (640, 360), color=(20, 20, 20))
488
+ draw = ImageDraw.Draw(canvas)
489
+ draw.text((16, 16), "No selected image keys", fill=(255, 255, 255))
490
+ return np.asarray(canvas)
491
+
492
+ pil_images = []
493
+ for img, label in gallery_items:
494
+ pil_img = Image.fromarray(np.asarray(img, dtype=np.uint8)).convert("RGB")
495
+ label_h = 24
496
+ panel = Image.new("RGB", (pil_img.width, pil_img.height + label_h), color=(0, 0, 0))
497
+ panel.paste(pil_img, (0, label_h))
498
+ draw = ImageDraw.Draw(panel)
499
+ draw.text((6, 4), str(label), fill=(255, 255, 255))
500
+ pil_images.append(panel)
501
+
502
+ gap = 8
503
+ top_h = 28
504
+ width = sum(im.width for im in pil_images) + gap * max(len(pil_images) - 1, 0)
505
+ height = max(im.height for im in pil_images) + top_h
506
+ canvas = Image.new("RGB", (width, height), color=(0, 0, 0))
507
+ draw = ImageDraw.Draw(canvas)
508
+ draw.text((8, 6), frame_label, fill=(255, 255, 255))
509
+
510
+ x = 0
511
+ for im in pil_images:
512
+ canvas.paste(im, (x, top_h))
513
+ x += im.width + gap
514
+
515
+ pad_w = int(np.ceil(canvas.width / 16.0) * 16)
516
+ pad_h = int(np.ceil(canvas.height / 16.0) * 16)
517
+ if pad_w != canvas.width or pad_h != canvas.height:
518
+ padded = Image.new("RGB", (pad_w, pad_h), color=(0, 0, 0))
519
+ padded.paste(canvas, (0, 0))
520
+ canvas = padded
521
+
522
+ return np.asarray(canvas)
523
+
524
+
525
+ def build_current_trajectory_video(preset_name, custom_repo_id, custom_filename, traj_id, image_keys, display_scale, reverse_channels, fps):
526
+ if imageio is None:
527
+ return None, "Video export requires imageio and imageio-ffmpeg in requirements.txt."
528
+
529
+ repo_id, filename = resolve_dataset(preset_name, custom_repo_id, custom_filename)
530
+ n_traj = get_num_trajectories(repo_id, filename)
531
+ if n_traj == 0:
532
+ return None, "No trajectories found."
533
+
534
+ traj_id = int(np.clip(int(traj_id), 0, n_traj - 1))
535
+ traj = load_traj(repo_id, filename, traj_id)
536
+ if not traj:
537
+ return None, "Trajectory could not be loaded."
538
+
539
+ if image_keys is None:
540
+ image_keys = []
541
+ if isinstance(image_keys, str):
542
+ image_keys = [image_keys]
543
+ image_keys_tuple = tuple(image_keys)
544
+
545
+ safe_repo = re.sub(r"[^A-Za-z0-9_.-]+", "_", repo_id)
546
+ safe_file = re.sub(r"[^A-Za-z0-9_.-]+", "_", filename)[-80:]
547
+ out_path = os.path.join(
548
+ tempfile.gettempdir(),
549
+ "trajectory_{}_{}_traj{:04d}_fps{}.mp4".format(safe_repo, safe_file, traj_id, int(fps)),
550
+ )
551
+
552
+ writer = imageio.get_writer(out_path, fps=float(fps), codec="libx264", quality=8)
553
+ try:
554
+ for t in range(len(traj)):
555
+ gallery_items, _warnings = get_cached_gallery_items(
556
+ repo_id,
557
+ filename,
558
+ traj_id,
559
+ t,
560
+ image_keys_tuple,
561
+ float(display_scale),
562
+ bool(reverse_channels),
563
+ )
564
+ label = "trajectory {} | frame {}/{}".format(traj_id, t, len(traj) - 1)
565
+ frame = _compose_video_frame(gallery_items, label)
566
+ writer.append_data(frame)
567
+ finally:
568
+ writer.close()
569
+
570
+ status = "Built trajectory video"
571
+ status += "\nTrajectory: {}".format(traj_id)
572
+ status += "\nFrames: {} | FPS: {}".format(len(traj), fps)
573
+ return out_path, status
574
+
575
+
576
  def get_available_image_keys(repo_id, filename, traj_id):
577
  n_traj = get_num_trajectories(repo_id, filename)
578
  if n_traj == 0:
 
608
 
609
  if n_traj == 0:
610
  status = "Loaded `{}` / `{}`".format(repo_id, filename)
611
+ status += "\nDetected trajectories: 0"
612
  return (
613
  gr.update(maximum=1, value=0),
614
  gr.update(maximum=1, value=0),
 
620
  traj = load_traj(repo_id, filename, 0)
621
 
622
  status = "Loaded `{}` / `{}`".format(repo_id, filename)
623
+ status += "\nDetected trajectories: {}".format(n_traj)
624
 
625
  return (
626
  gr.update(maximum=max(n_traj - 1, 1), value=0),
 
646
  )
647
 
648
 
649
+ def render_frame(preset_name, custom_repo_id, custom_filename, traj_id, timestep, image_keys, chunk_len, display_scale, reverse_channels):
 
 
 
 
 
 
 
 
 
 
650
  repo_id, filename = resolve_dataset(preset_name, custom_repo_id, custom_filename)
651
  n_traj = get_num_trajectories(repo_id, filename)
652
 
 
668
  image_keys = [image_keys]
669
 
670
  step = traj[timestep]
 
 
671
  image_keys_tuple = tuple(image_keys)
672
+
673
  gallery_items, warnings_tuple = get_cached_gallery_items(
674
+ repo_id, filename, traj_id, timestep, image_keys_tuple, display_scale, bool(reverse_channels)
 
 
 
 
 
 
675
  )
676
  warnings = list(warnings_tuple)
677
 
678
+ action_plot, source_mask = get_cached_action_plot(repo_id, filename, traj_id, timestep, chunk_len)
 
 
 
 
 
 
679
 
680
  info_lines = [
681
  "dataset: {} / {}".format(repo_id, filename),
 
700
  info_lines.append("Image warnings:")
701
  info_lines.extend(warnings)
702
 
703
+ return gallery_items, action_plot, "\n".join(info_lines)
704
 
705
 
 
 
 
706
  def build_app():
707
  repo_id, filename = resolve_dataset(DEFAULT_PRESET)
708
 
 
715
  first_keys = []
716
  startup_warning = repr(exc)
717
 
718
+ default_status = "Loaded default dataset\nDetected trajectories: {}".format(n_traj)
 
719
 
720
  with gr.Blocks(title="HDF5 Trajectory Viewer") as demo:
721
  gr.Markdown(
 
732
  value=DEFAULT_PRESET,
733
  label="Dataset preset",
734
  )
735
+ custom_repo_id = gr.Textbox(value="", label="Custom repo_id, e.g. Zhaoting123/InsertT", visible=False)
736
+ custom_filename = gr.Textbox(value="", label="Custom HDF5 path in repo", visible=False)
 
 
 
 
 
 
 
 
737
 
738
+ dataset_status = gr.Textbox(label="Dataset status", lines=2, value=default_status, interactive=False)
 
 
 
 
 
739
 
740
  with gr.Row():
741
+ traj_slider = gr.Slider(minimum=0, maximum=max(n_traj - 1, 1), value=0, step=1, label="Trajectory index")
742
+ timestep_slider = gr.Slider(minimum=0, maximum=1, value=0, step=1, label="Timestep")
 
 
 
 
 
 
 
 
 
 
 
 
743
 
744
  with gr.Row():
745
+ image_keys = gr.CheckboxGroup(choices=first_keys, value=first_keys[:2], label="Image keys")
746
+ chunk_len = gr.Slider(minimum=1, maximum=64, value=DEFAULT_CHUNK_LEN, step=1, label="Action chunk length")
747
+ display_scale = gr.Slider(minimum=1, maximum=10, value=4, step=1, label="Image display scale")
748
+ reverse_channels = gr.Checkbox(value=False, label="Reverse channels BGR↔RGB")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
 
750
  with gr.Row():
751
  render_btn = gr.Button("Render frame", variant="primary")
752
  preload_btn = gr.Button("Preload current trajectory")
753
+ video_btn = gr.Button("Build trajectory video")
754
+ video_fps = gr.Slider(minimum=1, maximum=30, value=10, step=1, label="Video FPS")
755
 
756
+ preload_status = gr.Textbox(label="Preload / video status", lines=4, value="Not preloaded yet.", interactive=False)
757
+ trajectory_video = gr.Video(label="Trajectory video: smooth browser-side playback")
 
 
 
 
758
 
759
+ gallery = gr.Gallery(label="Observation images", columns=2, height="auto", object_fit="contain")
 
 
 
 
 
760
  action_plot = gr.Image(label="Action chunk plot", type="numpy")
761
  info = gr.Textbox(label="Frame info", lines=16)
762
 
 
764
  inspect_btn = gr.Button("Inspect HDF5 structure")
765
  hdf5_tree = gr.Textbox(lines=24, label="HDF5 tree")
766
 
 
767
  preset.change(
768
  fn=update_custom_visibility,
769
  inputs=preset,
 
774
  outputs=[traj_slider, timestep_slider, image_keys, dataset_status],
775
  ).then(
776
  fn=render_frame,
777
+ inputs=[preset, custom_repo_id, custom_filename, traj_slider, timestep_slider, image_keys, chunk_len, display_scale, reverse_channels],
 
 
 
 
 
 
 
 
 
 
778
  outputs=[gallery, action_plot, info],
779
  )
780
 
 
795
  outputs=[timestep_slider, image_keys],
796
  ).then(
797
  fn=render_frame,
798
+ inputs=[preset, custom_repo_id, custom_filename, traj_slider, timestep_slider, image_keys, chunk_len, display_scale, reverse_channels],
 
 
 
 
 
 
 
 
 
 
799
  outputs=[gallery, action_plot, info],
800
  )
801
 
 
802
  timestep_slider.release(
803
  fn=render_frame,
804
+ inputs=[preset, custom_repo_id, custom_filename, traj_slider, timestep_slider, image_keys, chunk_len, display_scale, reverse_channels],
 
 
 
 
 
 
 
 
 
 
805
  outputs=[gallery, action_plot, info],
806
  )
807
 
808
  for widget in [image_keys, chunk_len, display_scale, reverse_channels]:
809
  widget.change(
810
  fn=render_frame,
811
+ inputs=[preset, custom_repo_id, custom_filename, traj_slider, timestep_slider, image_keys, chunk_len, display_scale, reverse_channels],
 
 
 
 
 
 
 
 
 
 
812
  outputs=[gallery, action_plot, info],
813
  )
814
 
815
  render_btn.click(
816
  fn=render_frame,
817
+ inputs=[preset, custom_repo_id, custom_filename, traj_slider, timestep_slider, image_keys, chunk_len, display_scale, reverse_channels],
 
 
 
 
 
 
 
 
 
 
818
  outputs=[gallery, action_plot, info],
819
  )
820
 
821
  preload_btn.click(
822
  fn=preload_current_trajectory,
823
+ inputs=[preset, custom_repo_id, custom_filename, traj_slider, image_keys, chunk_len, display_scale, reverse_channels],
 
 
 
 
 
 
 
 
 
824
  outputs=preload_status,
825
  )
826
 
827
+ video_btn.click(
828
+ fn=build_current_trajectory_video,
829
+ inputs=[preset, custom_repo_id, custom_filename, traj_slider, image_keys, display_scale, reverse_channels, video_fps],
830
+ outputs=[trajectory_video, preload_status],
831
+ )
832
+
833
  inspect_btn.click(
834
  fn=inspect_hdf5_tree,
835
  inputs=[preset, custom_repo_id, custom_filename],
 
842
  outputs=[traj_slider, timestep_slider, image_keys, dataset_status],
843
  ).then(
844
  fn=render_frame,
845
+ inputs=[preset, custom_repo_id, custom_filename, traj_slider, timestep_slider, image_keys, chunk_len, display_scale, reverse_channels],
 
 
 
 
 
 
 
 
 
 
846
  outputs=[gallery, action_plot, info],
847
  )
848
 
 
850
 
851
 
852
  if __name__ == "__main__":
853
+ build_app().launch()