Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -68,6 +68,7 @@ DATASET_PRESETS = {
|
|
| 68 |
DEFAULT_PRESET = "Robosuite Square Correction"
|
| 69 |
REPO_TYPE = "dataset"
|
| 70 |
DEFAULT_CHUNK_LEN = 16
|
|
|
|
| 71 |
PREFERRED_IMAGE_KEYS = [
|
| 72 |
"image1",
|
| 73 |
"image2",
|
|
@@ -797,7 +798,58 @@ def get_cached_video_status_frame(repo_id, filename, traj_id, timestep, valid_wi
|
|
| 797 |
|
| 798 |
return np.asarray(img)
|
| 799 |
|
| 800 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
if imageio is None:
|
| 802 |
return None, "Video export requires imageio and imageio-ffmpeg in requirements.txt."
|
| 803 |
|
|
@@ -817,16 +869,34 @@ def build_current_trajectory_video(preset_name, custom_repo_id, custom_filename,
|
|
| 817 |
image_keys = [image_keys]
|
| 818 |
image_keys_tuple = tuple(image_keys)
|
| 819 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 820 |
safe_repo = re.sub(r"[^A-Za-z0-9_.-]+", "_", repo_id)
|
| 821 |
safe_file = re.sub(r"[^A-Za-z0-9_.-]+", "_", filename)[-80:]
|
| 822 |
out_path = os.path.join(
|
| 823 |
tempfile.gettempdir(),
|
| 824 |
-
"trajectory_{}_{}_traj{:04d}_fps{}.mp4".format(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 825 |
)
|
| 826 |
|
| 827 |
-
writer =
|
|
|
|
| 828 |
try:
|
| 829 |
-
for t in
|
|
|
|
|
|
|
| 830 |
gallery_items, _warnings = get_cached_gallery_items(
|
| 831 |
repo_id,
|
| 832 |
filename,
|
|
@@ -837,26 +907,22 @@ def build_current_trajectory_video(preset_name, custom_repo_id, custom_filename,
|
|
| 837 |
bool(reverse_channels),
|
| 838 |
)
|
| 839 |
label = "trajectory {} | frame {}/{}".format(traj_id, t, len(traj) - 1)
|
| 840 |
-
status_plot =
|
| 841 |
-
repo_id,
|
| 842 |
-
filename,
|
| 843 |
-
traj_id,
|
| 844 |
-
t,
|
| 845 |
-
int(valid_window_len),
|
| 846 |
-
)
|
| 847 |
frame = _compose_video_frame(gallery_items, label, status_plot=status_plot)
|
| 848 |
writer.append_data(frame)
|
|
|
|
| 849 |
finally:
|
| 850 |
writer.close()
|
| 851 |
|
| 852 |
-
|
|
|
|
| 853 |
status += "\nTrajectory: {}".format(traj_id)
|
| 854 |
-
status += "\
|
|
|
|
| 855 |
status += "\nValid-window length: {}".format(int(valid_window_len))
|
| 856 |
-
status += "\
|
| 857 |
return out_path, status
|
| 858 |
|
| 859 |
-
|
| 860 |
def get_available_image_keys(repo_id, filename, traj_id):
|
| 861 |
n_traj = get_num_trajectories(repo_id, filename)
|
| 862 |
if n_traj == 0:
|
|
@@ -1047,7 +1113,7 @@ def build_app():
|
|
| 1047 |
with gr.Row():
|
| 1048 |
image_keys = gr.CheckboxGroup(choices=first_keys, value=first_keys[:2], label="Image keys")
|
| 1049 |
chunk_len = gr.Slider(minimum=1, maximum=64, value=DEFAULT_CHUNK_LEN, step=1, label="Valid-window length")
|
| 1050 |
-
display_scale = gr.
|
| 1051 |
reverse_channels = gr.Checkbox(value=get_default_reverse_channels(DEFAULT_PRESET), label="Reverse channels BGR↔RGB")
|
| 1052 |
|
| 1053 |
with gr.Row():
|
|
@@ -1055,6 +1121,7 @@ def build_app():
|
|
| 1055 |
preload_btn = gr.Button("Preload current trajectory")
|
| 1056 |
video_btn = gr.Button("Build trajectory video")
|
| 1057 |
video_fps = gr.Slider(minimum=1, maximum=30, value=10, step=1, label="Video FPS")
|
|
|
|
| 1058 |
|
| 1059 |
preload_status = gr.Textbox(label="Preload / video status", lines=4, value="Not preloaded yet.", interactive=False)
|
| 1060 |
|
|
@@ -1121,7 +1188,7 @@ def build_app():
|
|
| 1121 |
outputs=[gallery, status_plot, info],
|
| 1122 |
)
|
| 1123 |
|
| 1124 |
-
for widget in [image_keys, chunk_len,
|
| 1125 |
widget.change(
|
| 1126 |
fn=render_frame,
|
| 1127 |
inputs=[preset, custom_repo_id, custom_filename, traj_slider, timestep_slider, image_keys, chunk_len, display_scale, reverse_channels],
|
|
@@ -1142,7 +1209,7 @@ def build_app():
|
|
| 1142 |
|
| 1143 |
video_btn.click(
|
| 1144 |
fn=build_current_trajectory_video,
|
| 1145 |
-
inputs=[preset, custom_repo_id, custom_filename, traj_slider, image_keys, display_scale, reverse_channels, video_fps, chunk_len],
|
| 1146 |
outputs=[trajectory_video, preload_status],
|
| 1147 |
)
|
| 1148 |
|
|
|
|
| 68 |
DEFAULT_PRESET = "Robosuite Square Correction"
|
| 69 |
REPO_TYPE = "dataset"
|
| 70 |
DEFAULT_CHUNK_LEN = 16
|
| 71 |
+
DEFAULT_DISPLAY_SCALE = 1
|
| 72 |
PREFERRED_IMAGE_KEYS = [
|
| 73 |
"image1",
|
| 74 |
"image2",
|
|
|
|
| 798 |
|
| 799 |
return np.asarray(img)
|
| 800 |
|
| 801 |
+
def _draw_status_cursor_on_base(base, bounds, total_steps, timestep):
|
| 802 |
+
"""Fast video status frame: copy one static Matplotlib image and draw cursor.
|
| 803 |
+
|
| 804 |
+
This avoids calling the lru-cached per-timestep status frame function during
|
| 805 |
+
video export. For long trajectories, caching thousands of status images can
|
| 806 |
+
consume a lot of memory and still requires PIL conversion for every frame.
|
| 807 |
+
"""
|
| 808 |
+
if base is None:
|
| 809 |
+
return None
|
| 810 |
+
|
| 811 |
+
total_steps = int(max(total_steps, 1))
|
| 812 |
+
timestep = int(np.clip(int(timestep), 0, total_steps - 1))
|
| 813 |
+
x0, y0, x1, y1 = [int(v) for v in bounds]
|
| 814 |
+
denom = max(total_steps - 1, 1)
|
| 815 |
+
x = int(round(x0 + (x1 - x0) * float(timestep) / float(denom)))
|
| 816 |
+
|
| 817 |
+
img = np.asarray(base, dtype=np.uint8).copy()
|
| 818 |
+
|
| 819 |
+
# Draw the vertical cursor directly with NumPy. This is much cheaper than
|
| 820 |
+
# creating a Matplotlib plot for every frame.
|
| 821 |
+
x_left = max(0, x - 2)
|
| 822 |
+
x_right = min(img.shape[1], x + 2)
|
| 823 |
+
y_top = max(0, y0)
|
| 824 |
+
y_bottom = min(img.shape[0], y1)
|
| 825 |
+
img[y_top:y_bottom, x_left:x_right, :] = 0
|
| 826 |
+
|
| 827 |
+
# Small text label. PIL is used only for the label, not for the whole plot.
|
| 828 |
+
pil_img = Image.fromarray(img).convert("RGB")
|
| 829 |
+
draw = ImageDraw.Draw(pil_img)
|
| 830 |
+
label = "step {}/{}".format(timestep, total_steps - 1)
|
| 831 |
+
draw.rectangle((x0 + 4, y0 + 4, x0 + 126, y0 + 24), fill=(255, 255, 255))
|
| 832 |
+
draw.text((x0 + 8, y0 + 7), label, fill=(0, 0, 0))
|
| 833 |
+
return np.asarray(pil_img)
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def _get_fast_video_writer(out_path, fps):
|
| 837 |
+
"""Use ffmpeg's ultrafast x264 preset for interactive Spaces exports."""
|
| 838 |
+
return imageio.get_writer(
|
| 839 |
+
out_path,
|
| 840 |
+
fps=float(fps),
|
| 841 |
+
codec="libx264",
|
| 842 |
+
macro_block_size=16,
|
| 843 |
+
ffmpeg_params=[
|
| 844 |
+
"-preset", "ultrafast",
|
| 845 |
+
"-crf", "28",
|
| 846 |
+
"-pix_fmt", "yuv420p",
|
| 847 |
+
"-movflags", "+faststart",
|
| 848 |
+
],
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
def build_current_trajectory_video(preset_name, custom_repo_id, custom_filename, traj_id, image_keys, display_scale, reverse_channels, fps, valid_window_len, video_stride=1):
|
| 853 |
if imageio is None:
|
| 854 |
return None, "Video export requires imageio and imageio-ffmpeg in requirements.txt."
|
| 855 |
|
|
|
|
| 869 |
image_keys = [image_keys]
|
| 870 |
image_keys_tuple = tuple(image_keys)
|
| 871 |
|
| 872 |
+
video_stride = int(max(1, int(video_stride)))
|
| 873 |
+
frame_indices = list(range(0, len(traj), video_stride))
|
| 874 |
+
if frame_indices and frame_indices[-1] != len(traj) - 1:
|
| 875 |
+
frame_indices.append(len(traj) - 1)
|
| 876 |
+
|
| 877 |
safe_repo = re.sub(r"[^A-Za-z0-9_.-]+", "_", repo_id)
|
| 878 |
safe_file = re.sub(r"[^A-Za-z0-9_.-]+", "_", filename)[-80:]
|
| 879 |
out_path = os.path.join(
|
| 880 |
tempfile.gettempdir(),
|
| 881 |
+
"trajectory_{}_{}_traj{:04d}_fps{}_stride{}.mp4".format(
|
| 882 |
+
safe_repo, safe_file, traj_id, int(fps), video_stride
|
| 883 |
+
),
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
# Build the static status plot once. During export, only draw the cursor.
|
| 887 |
+
status_base, status_bounds, total_steps = get_video_status_plot_base(
|
| 888 |
+
repo_id,
|
| 889 |
+
filename,
|
| 890 |
+
traj_id,
|
| 891 |
+
int(valid_window_len),
|
| 892 |
)
|
| 893 |
|
| 894 |
+
writer = _get_fast_video_writer(out_path, fps)
|
| 895 |
+
written = 0
|
| 896 |
try:
|
| 897 |
+
for t in frame_indices:
|
| 898 |
+
# Use the existing cached image extraction for correctness, but avoid
|
| 899 |
+
# cached per-timestep status images to reduce memory pressure.
|
| 900 |
gallery_items, _warnings = get_cached_gallery_items(
|
| 901 |
repo_id,
|
| 902 |
filename,
|
|
|
|
| 907 |
bool(reverse_channels),
|
| 908 |
)
|
| 909 |
label = "trajectory {} | frame {}/{}".format(traj_id, t, len(traj) - 1)
|
| 910 |
+
status_plot = _draw_status_cursor_on_base(status_base, status_bounds, total_steps, t)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
frame = _compose_video_frame(gallery_items, label, status_plot=status_plot)
|
| 912 |
writer.append_data(frame)
|
| 913 |
+
written += 1
|
| 914 |
finally:
|
| 915 |
writer.close()
|
| 916 |
|
| 917 |
+
approx_seconds = float(written) / float(max(float(fps), 1.0))
|
| 918 |
+
status = "Built trajectory video with optimized encoder and status rendering"
|
| 919 |
status += "\nTrajectory: {}".format(traj_id)
|
| 920 |
+
status += "\nOriginal timesteps: {} | Written frames: {} | Stride: {}".format(len(traj), written, video_stride)
|
| 921 |
+
status += "\nFPS: {} | Approx video duration: {:.1f}s".format(fps, approx_seconds)
|
| 922 |
status += "\nValid-window length: {}".format(int(valid_window_len))
|
| 923 |
+
status += "\nSpeedups: x264 ultrafast preset; static status plot rendered once; cursor drawn with NumPy/PIL"
|
| 924 |
return out_path, status
|
| 925 |
|
|
|
|
| 926 |
def get_available_image_keys(repo_id, filename, traj_id):
|
| 927 |
n_traj = get_num_trajectories(repo_id, filename)
|
| 928 |
if n_traj == 0:
|
|
|
|
| 1113 |
with gr.Row():
|
| 1114 |
image_keys = gr.CheckboxGroup(choices=first_keys, value=first_keys[:2], label="Image keys")
|
| 1115 |
chunk_len = gr.Slider(minimum=1, maximum=64, value=DEFAULT_CHUNK_LEN, step=1, label="Valid-window length")
|
| 1116 |
+
display_scale = gr.State(value=DEFAULT_DISPLAY_SCALE)
|
| 1117 |
reverse_channels = gr.Checkbox(value=get_default_reverse_channels(DEFAULT_PRESET), label="Reverse channels BGR↔RGB")
|
| 1118 |
|
| 1119 |
with gr.Row():
|
|
|
|
| 1121 |
preload_btn = gr.Button("Preload current trajectory")
|
| 1122 |
video_btn = gr.Button("Build trajectory video")
|
| 1123 |
video_fps = gr.Slider(minimum=1, maximum=30, value=10, step=1, label="Video FPS")
|
| 1124 |
+
video_stride = gr.Slider(minimum=1, maximum=10, value=1, step=1, label="Video frame stride")
|
| 1125 |
|
| 1126 |
preload_status = gr.Textbox(label="Preload / video status", lines=4, value="Not preloaded yet.", interactive=False)
|
| 1127 |
|
|
|
|
| 1188 |
outputs=[gallery, status_plot, info],
|
| 1189 |
)
|
| 1190 |
|
| 1191 |
+
for widget in [image_keys, chunk_len, reverse_channels]:
|
| 1192 |
widget.change(
|
| 1193 |
fn=render_frame,
|
| 1194 |
inputs=[preset, custom_repo_id, custom_filename, traj_slider, timestep_slider, image_keys, chunk_len, display_scale, reverse_channels],
|
|
|
|
| 1209 |
|
| 1210 |
video_btn.click(
|
| 1211 |
fn=build_current_trajectory_video,
|
| 1212 |
+
inputs=[preset, custom_repo_id, custom_filename, traj_slider, image_keys, display_scale, reverse_channels, video_fps, chunk_len, video_stride],
|
| 1213 |
outputs=[trajectory_video, preload_status],
|
| 1214 |
)
|
| 1215 |
|