| 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]], |
| output_dir: str, |
| fig_w: float = 12.0, |
| fig_h: float = 4.0, |
| dpi: int = 100, |
| fps: int = 30, |
| metric_only: bool = False, |
| ): |
| """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] |
| |
| 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) |
|
|
| |
| views = img_list[idx] |
| titles = ["Base Frame", "Wrist Left", "Wrist Right"] |
| |
| titles = titles[:len(views)] |
|
|
| for ax_img, view, title in zip(axes[1:], views, titles): |
| |
| 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() |
|
|
| |
| 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 |
|
|
| tensor = image_tools.resize_with_pad_torch(tensor, 224, 224) |
|
|
| |
| tensor = tensor.permute(2, 0, 1) |
|
|
| |
| tensor = tensor.unsqueeze(0) |
| |
| |
| return tensor |
|
|
|
|
| def main( |
| config_name: str, |
| ckpt_dir: str, |
| split: str = "val_tasks", |
| metric_only: bool = False, |
| output_video_dir: str = "./visualizations", |
| |
| |
| |
| ): |
| """Main function to run value prediction and visualization.""" |
| |
| global all_pred |
| global all_tgt |
|
|
| |
| config = _config.get_config(config_name) |
| checkpoint_dir = download.maybe_download(ckpt_dir) |
|
|
|
|
| |
| 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() |
| 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) |
| |
| |
| |
| 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, |
| ) |
| |
|
|
| |
| 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) |
| |
| if img.shape[0] == 3: |
| img = np.transpose(img, (1, 2, 0)) |
| return img |
|
|
| value_frames = [] |
| raw_frames = [] |
| |
| fps = 30 |
|
|
| |
| 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) |
|
|
|
|
|
|
| |
| image_folder_root = "path/to/your/images" |
| 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":{} |
| } |
|
|
| |
|
|
|
|
| tokens, token_masks = tokenizer.tokenize(prompt, state=None) |
| tokens = tokens[np.newaxis, :] |
| token_masks = token_masks[np.newaxis, :] |
|
|
| 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(): |
| |
| val_arr = model.sample_values(device, observation) |
| 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] |
| 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) |
|
|