File size: 10,180 Bytes
688e1f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | import dataclasses
import os
import cv2
import numpy as np
import tqdm
import matplotlib
import io
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import tyro
import logging
import torch
import safetensors.torch
from openpi_value.training import config as _config
from openpi_value.shared import download
import openpi_value.training.data_loader as _data
from openpi_value.models_pytorch.pi0_pytorch import PI0Pytorch
import openpi_value.models.tokenizer as _tokenizer
from types import SimpleNamespace
from openpi_value.shared import image_tools
all_pred = []
all_tgt = []
def write_episode_video(
episode_id: int,
value_list: list[float],
img_list: list[list[np.ndarray]], # now list of image lists
output_dir: str,
fig_w: float = 12.0,
fig_h: float = 4.0,
dpi: int = 100,
fps: int = 30,
metric_only: bool = False, # * Skip visualizations
):
"""Writes a video for a single episode visualizing predicted values and image frames."""
if not value_list:
return
n_frames = len(value_list)
tgt_progress = np.linspace(0.0, 1.0, n_frames)
val_pred = np.array(value_list, dtype=np.float32)
global all_pred
global all_tgt
all_pred.extend(val_pred.tolist())
all_tgt.extend(tgt_progress.tolist())
if metric_only:
return
os.makedirs(output_dir, exist_ok=True)
out_path = os.path.join(output_dir, f"episode_{episode_id:03d}.mp4")
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
width_px = int(fig_w * dpi)
height_px = int(fig_h * dpi)
video_writer = cv2.VideoWriter(out_path, fourcc, fps, (width_px, height_px))
logging.info(f"Writing video for episode {episode_id} with {n_frames} frames...")
for idx in tqdm.tqdm(range(n_frames), desc=f"Episode {episode_id} Video"):
fig, axes = plt.subplots(1, 1 + len(img_list[idx]), figsize=(fig_w, fig_h), dpi=dpi)
ax_plot = axes[0]
# Plotting
x = np.arange(idx + 1)
y = np.array(value_list[: idx + 1], dtype=np.float32)
ax_plot.plot(x, y, linewidth=2, color="tab:blue")
ax_plot.set_xlim(0, n_frames)
ax_plot.set_ylim(0.0, 1.0)
ax_plot.set_xlabel("Frame")
ax_plot.set_ylabel("Predicted Value")
ax_plot.set_title("Value Prediction Over Time")
ax_plot.grid(True)
# Images: e.g., base, wrist_left, wrist_right
views = img_list[idx]
titles = ["Base Frame", "Wrist Left", "Wrist Right"]
titles = titles[:len(views)] # Adjust titles to match number of views
for ax_img, view, title in zip(axes[1:], views, titles):
# ax_img.imshow(cv2.cvtColor(view, cv2.COLOR_BGR2RGB))
ax_img.imshow(view)
ax_img.set_title(title)
ax_img.axis("off")
plt.tight_layout()
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
buf.seek(0)
png_bytes = np.frombuffer(buf.getvalue(), dtype=np.uint8)
buf.close()
plt.close(fig)
img_bgr = cv2.imdecode(png_bytes, cv2.IMREAD_COLOR)
h_b, w_b = img_bgr.shape[:2]
if (w_b, h_b) != (width_px, height_px):
img_bgr = cv2.resize(img_bgr, (width_px, height_px), interpolation=cv2.INTER_LINEAR)
video_writer.write(img_bgr)
video_writer.release()
# Encode with H.264 for size/speed
new_out_path = out_path.replace(".mp4", "_new.mp4")
os.system(f"ffmpeg -y -i {out_path} -c:v libx264 -crf 18 -preset veryfast {new_out_path} > /dev/null 2>&1")
logging.info(f"=> Episode {episode_id} generated to: {new_out_path}")
os.remove(out_path)
def process_and_convert(img: np.ndarray, target_size: int) -> torch.Tensor:
rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
tensor = torch.from_numpy(rgb_img).float() / 255.0
tensor = tensor * 2.0 - 1.0 # Normalize to [-1, 1]
tensor = image_tools.resize_with_pad_torch(tensor, 224, 224) # * Keep ratio
# HWC to CHW
tensor = tensor.permute(2, 0, 1)
# Batch Dimension: Change C, H, W to 1, C, H, W (the final shape)
tensor = tensor.unsqueeze(0)
# Final shape check
return tensor
def main(
config_name: str,
ckpt_dir: str,
split: str = "val_tasks",
metric_only: bool = False,
output_video_dir: str = "./visualizations",
# headview_only: bool = False,
# n_batches: int = 20000,
# fps_downsample: int = 1,
):
"""Main function to run value prediction and visualization."""
global all_pred
global all_tgt
# --- Config and Checkpoint Setup ---
config = _config.get_config(config_name)
checkpoint_dir = download.maybe_download(ckpt_dir)
# =============== Load Model =============================
new_model = config.model.__class__(**{**config.model.__dict__,
'p_mask_ego_state': 1,
})
config = dataclasses.replace(config, model=new_model)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = PI0Pytorch(new_model).to(device)
model.sample_values = torch.compile(model.sample_values, mode="reduce-overhead")
model.eval() # Set model to evaluation mode
model_path = os.path.join(checkpoint_dir, "model.safetensors")
logging.info(f"Loading weights from: {model_path}")
safetensors.torch.load_model(model, model_path, strict=True)
logging.info(f"Loaded PyTorch weights successfully.")
tokenizer = _tokenizer.PaligemmaTokenizer(new_model.max_token_len)
# =============== Load Model finished ======================
# --- Modify Config for Inference ---
assert split in ['all', 'val_tasks', 'heldout_tasks']
config = dataclasses.replace(
config,
batch_size=1,
is_train=False,
num_workers=8,
split=split,
preceding_skipping_ratio=0,
)
# --- Data Loader ---
# --- Helper for Image Processing ---
def process_view_torch(torch_arr):
"""Converts a single image tensor [-1, 1] to a numpy array [0, 255]."""
arr = torch_arr.cpu().numpy()
img = ((arr + 1.0) * 127.5).clip(0, 255).astype(np.uint8)
# Assuming CHW -> HWC
if img.shape[0] == 3:
img = np.transpose(img, (1, 2, 0))
return img
value_frames = [] # type: list[float]
raw_frames = [] # type: list[list[np.ndarray]]
fps = 30
# Setup output directory
output_dir = os.path.join(output_video_dir, config_name, os.path.basename(checkpoint_dir) + f"_{split}")
print(f">>>>>> Generating episode videos to: {output_dir} ...")
os.makedirs(output_dir, exist_ok=True)
# # * Load one video and draw visualization based on that
image_folder_root = "path/to/your/images" # * Change this to your image folder root
left_folder = os.path.join(image_folder_root, 'hand_left')
right_folder = os.path.join(image_folder_root, 'hand_right')
top_folder = os.path.join(image_folder_root, 'top_head')
left_images = sorted([os.path.join(left_folder, f) for f in os.listdir(left_folder) if f.endswith('.jpg')])
right_images = sorted([os.path.join(right_folder, f) for f in os.listdir(right_folder) if f.endswith('.jpg')])
top_images = sorted([os.path.join(top_folder, f) for f in os.listdir(top_folder) if f.endswith('.jpg')])
n_frames = min(len(left_images), len(right_images), len(top_images))
left_img_0 = cv2.imread(left_images[0])
right_img_0 = cv2.imread(right_images[0])
top_img_0 = cv2.imread(top_images[0])
top_torch_0 = process_and_convert(top_img_0, 224)
left_torch_0 = process_and_convert(left_img_0, 224)
right_torch_0 = process_and_convert(right_img_0, 224)
for i in range(1, n_frames):
left_img = cv2.imread(left_images[i])
right_img = cv2.imread(right_images[i])
top_img = cv2.imread(top_images[i])
top_torch = process_and_convert(top_img, 224)
left_torch = process_and_convert(left_img, 224)
right_torch = process_and_convert(right_img, 224)
prompt = "Insert the memory stick."
observation = {
"state": torch.zeros((1, 32), dtype=torch.float32).to(device),
"images": {
"base_-100_rgb": top_torch_0.to(device),
"left_wrist_-100_rgb": left_torch_0.to(device),
"right_wrist_-100_rgb": right_torch_0.to(device),
"base_0_rgb": top_torch.to(device),
"left_wrist_0_rgb": left_torch.to(device),
"right_wrist_0_rgb": right_torch.to(device),
},
"image_masks":{} # * Set empty
}
# * ['base_-100_rgb', 'left_wrist_-100_rgb', 'right_wrist_-100_rgb', 'base_0_rgb', 'left_wrist_0_rgb', 'right_wrist_0_rgb']
tokens, token_masks = tokenizer.tokenize(prompt, state=None)
tokens = tokens[np.newaxis, :] # Add batch dim
token_masks = token_masks[np.newaxis, :] # Add batch dim
observation = {**observation,
"tokenized_prompt": torch.from_numpy(tokens).to(device),
"tokenized_prompt_mask": torch.from_numpy(token_masks).to(device)}
observation = SimpleNamespace(**observation)
with torch.no_grad():
# Predict value
val_arr = model.sample_values(device, observation) # Shape=(1, 1)
val = float(val_arr[0, 0].item())
value_frames.append(val)
base_torch = observation.images["base_0_rgb"][0]
img_base = process_view_torch(base_torch)
current_views = [img_base] # Start with base view
raw_frames.append(current_views)
logging.info(f"Writing final episode {0}.")
write_episode_video(
episode_id=0,
value_list=value_frames,
img_list=raw_frames,
output_dir=output_dir,
fig_w=12.0,
fig_h=4.0,
dpi=100,
fps=fps,
metric_only=metric_only,
)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
tyro.cli(main)
|