Download robotryon/image/ar_keyframes.py from RyanL22/RoboTryOn: direct link, hf CLI and curl.
- Browser
- Download file 8.48 kB
-
https://huggingface.co/spaces/RyanL22/RoboTryOn/resolve/main/robotryon/image/ar_keyframes.py
- Command line
-
hf download hf://spaces/RyanL22/RoboTryOn/robotryon/image/ar_keyframes.py
-
curl -L -o ar_keyframes.py https://huggingface.co/spaces/RyanL22/RoboTryOn/resolve/main/robotryon/image/ar_keyframes.py
8.48 kB
| """Autoregressive keyframe translation (paper Eq. 3.2, App. B.2.3). | |
| Human keyframes are translated in temporal order. Keyframe t_j is conditioned on | |
| * the conditioned human frame Phi(o^h; M^h, S^h): the human arm mask (convex hull per arm, | |
| fixed dilation, eroded object removed) blacked out and the retargeted robot-hand skeleton drawn | |
| on top, plus the mask itself as an input channel; | |
| * the reference: for the first keyframe, the encoded robot-only reference image supplied by the | |
| user (it selects the embodiment); for every later keyframe, the DENOISED LATENT TOKENS of the | |
| previous keyframe, reused directly as reference tokens (no decode / re-encode). | |
| ``ref_skeleton: true`` replaces latent reuse by a pixel-space reference: the previous keyframe is | |
| decoded, the current keyframe's skeleton is painted on it, and the result is re-encoded (the first | |
| keyframe's reference image is painted the same way). | |
| Stereo episodes are translated jointly (both views in one token sequence) by default; ``views: | |
| mono`` translates each view as its own chain. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from PIL import Image | |
| from ..masks import MaskAug, inference_mask | |
| from ..skeleton import condition_frame, draw_skeleton, paint | |
| from .episodes import Episode, FrameReader, load_view_frame | |
| from .fill_channels import pack | |
| from .sampler import LatentCodec, ViewCondition, denoise | |
| from ..captions import HandStateCuts, hand_state_arrays, hand_state_prompt, load_hand_state, words_at | |
| class TranslatedKeyframe: | |
| image: np.ndarray # uint8 (H, W, 3) robot keyframe | |
| cond: np.ndarray # uint8 (H, W, 3) conditioned human frame | |
| mask: np.ndarray # uint8 (H, W) {0, 1} inference mask | |
| tokens: torch.Tensor # (1, h*w, 128) denoised latent tokens (next keyframe's reference) | |
| class KeyframeTranslator: | |
| def __init__(self, pipe, cfg, device="cuda"): | |
| self.pipe, self.cfg, self.device = pipe, cfg, device | |
| self.codec = LatentCodec(pipe.vae, device) | |
| self.reader = FrameReader() | |
| self.dilate = MaskAug.from_config(cfg.masks).infer_dilate() | |
| self.ref_skeleton = bool(cfg.get("ref_skeleton", False)) | |
| self._prompts: dict[str, tuple] = {} | |
| def prompt_fn(self, ep: Episode, episode_dir: Path): | |
| """``prompt(t)``: the instruction and each hand's closure / facing words at keyframe t | |
| (``robotryon.captions``), read from the episode's ``hand_state_<view>.npz`` (or computed | |
| from ``hands.npz`` when absent).""" | |
| cuts = HandStateCuts.from_config(self.cfg.caption.get("cuts")) | |
| states = [] | |
| for i, v in enumerate(ep.views): | |
| f = episode_dir / f"hand_state_{v}.npz" | |
| if f.exists(): | |
| states.append(load_hand_state(f)) | |
| if not states: | |
| z = np.load(episode_dir / "hands.npz") | |
| states = [hand_state_arrays(z["joints3d"][i], z["valid"][i].astype(bool)) | |
| for i in range(z["joints3d"].shape[0])] | |
| return lambda t: hand_state_prompt(*words_at(states, int(t), cuts)) | |
| def encode_prompt(self, text: str): | |
| if text not in self._prompts: | |
| self._prompts[text] = self.pipe.encode_prompt(prompt=text, device=self.device) | |
| return self._prompts[text] | |
| def condition(self, ep: Episode, view: str, t: int): | |
| """(frame, conditioned frame, mask, skeleton image) of one human keyframe at model resolution.""" | |
| c = self.cfg | |
| f = load_view_frame(self.reader, ep, view, t, int(c.image.long_edge), | |
| tuple(c.image.aspect) if c.image.get("aspect") else None, | |
| c.image.get("crop_anchor", "bottom")) | |
| H, W = f.image.shape[:2] | |
| mask = inference_mask(f.hulls, W, H, self.dilate, f.object_mask, int(c.masks.obj_erode)) | |
| skel = draw_skeleton(f.kp, f.vis, W, H) | |
| cond = condition_frame(f.image, mask, skel) | |
| return f.image, cond, mask, skel | |
| def reference(self, view: str, prev: dict, references: dict, skel: np.ndarray) -> torch.Tensor: | |
| """Reference tokens of one view for the current keyframe.""" | |
| if view in prev and not self.ref_skeleton: | |
| return prev[view].tokens # latent reuse | |
| if view in prev: | |
| img = prev[view].image | |
| else: | |
| H, W = skel.shape[:2] | |
| img = np.asarray(Image.fromarray(references[view]).resize((W, H), Image.LANCZOS)) | |
| return pack(self.codec.encode(paint(img, skel) if self.ref_skeleton else img)) | |
| def translate_chain(self, views: list[str], keyframes: list[int], references: dict[str, np.ndarray], | |
| prompt, conditions) -> dict: | |
| """One chain over ``keyframes``; ``views`` are denoised jointly. | |
| ``prompt`` is a string or ``prompt(t)`` giving each keyframe its own prompt. | |
| ``conditions(view, t)`` returns ``(frame, conditioned frame, mask, skeleton image)``. | |
| Returns ``{view: {t: TranslatedKeyframe}}``. | |
| """ | |
| c = self.cfg | |
| prompt_at = prompt if callable(prompt) else (lambda t: prompt) | |
| neg, neg_ids = self.encode_prompt("") if float(c.guidance_scale) > 1.0 else (None, None) | |
| prev: dict[str, TranslatedKeyframe] = {} | |
| out: dict[str, dict] = {v: {} for v in views} | |
| for t in keyframes: | |
| conds, frames = [], [] | |
| for v in views: | |
| _, cond, mask, skel = conditions(v, t) | |
| mask_t = torch.from_numpy(mask.astype(np.float32))[None, None].to(self.device) | |
| conds.append(ViewCondition(self.codec.encode(cond), mask_t, | |
| self.reference(v, prev, references, skel))) | |
| frames.append((cond, mask)) | |
| pos, pos_ids = self.encode_prompt(prompt_at(t)) | |
| gen = torch.Generator(device=self.device).manual_seed(int(c.seed) + int(t)) | |
| tokens = denoise(self.pipe.transformer, self.pipe.scheduler, conds, pos, pos_ids, neg, neg_ids, | |
| steps=int(c.steps), guidance_scale=float(c.guidance_scale), generator=gen) | |
| for v, tok, (cond, mask) in zip(views, tokens, frames): | |
| H, W = mask.shape | |
| img = self.codec.decode_tokens(tok, H // 16, W // 16) | |
| out[v][t] = prev[v] = TranslatedKeyframe(img, cond, mask, tok) | |
| return out | |
| def translate_episode(self, episode_dir: str | Path, references: dict[str, np.ndarray], | |
| out_dir: str | Path) -> Path: | |
| """Translate every keyframe of a preprocessed human episode and write the results.""" | |
| c = self.cfg | |
| ep = Episode.load(episode_dir) | |
| kf = json.loads((Path(episode_dir) / "keyframes.json").read_text())["indices"] | |
| keyframes = sorted({int(i) for i in kf} | {0}) | |
| stereo = len(ep.views) >= 2 and c.views in ("auto", "stereo") | |
| if c.views == "stereo" and not stereo: | |
| raise ValueError(f"{episode_dir}: views=stereo needs two camera views, found {ep.views}") | |
| chains = [ep.views[:2]] if stereo else [[v] for v in ep.views] | |
| missing = [v for v in ep.views if v not in references] | |
| if missing: | |
| raise ValueError(f"no reference image for view(s) {missing}") | |
| prompt_at = self.prompt_fn(ep, Path(episode_dir)) | |
| prompts = {int(t): prompt_at(t) for t in keyframes} | |
| out = Path(out_dir) / ep.path.name | |
| out.mkdir(parents=True, exist_ok=True) | |
| for views in chains: | |
| res = self.translate_chain(views, keyframes, references, prompt_at, | |
| lambda v, t: self.condition(ep, v, t)) | |
| for v, per_t in res.items(): | |
| for t, k in per_t.items(): | |
| Image.fromarray(k.image).save(out / f"keyframe_{v}_{t:06d}.png") | |
| Image.fromarray(k.cond).save(out / f"cond_{v}_{t:06d}.png") | |
| Image.fromarray(k.mask * 255).save(out / f"mask_{v}_{t:06d}.png") | |
| (out / "keyframes.json").write_text(json.dumps({ | |
| "indices": keyframes, "views": ep.views, "joint_stereo": stereo, "prompts": prompts, | |
| "ref_skeleton": self.ref_skeleton, "steps": int(c.steps), | |
| "guidance_scale": float(c.guidance_scale), | |
| "seed": int(c.seed)}, indent=1)) | |
| return out | |