Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- README.md +23 -8
- app.py +219 -0
- examples/bouncing/00.png +0 -0
- examples/bouncing/01.png +0 -0
- examples/bouncing/02.png +0 -0
- examples/collision/00.png +0 -0
- examples/collision/01.png +0 -0
- examples/collision/02.png +0 -0
- examples/looming/00.png +0 -0
- examples/looming/01.png +0 -0
- examples/looming/02.png +0 -0
- examples/parabola/00.png +0 -0
- examples/parabola/01.png +0 -0
- examples/parabola/02.png +0 -0
- examples/uniform/00.png +0 -0
- examples/uniform/01.png +0 -0
- examples/uniform/02.png +0 -0
- examples/uniform_pikachu/00.png +0 -0
- examples/uniform_pikachu/01.png +0 -0
- examples/uniform_pikachu/02.png +0 -0
- examples/uniform_soccer/00.png +0 -0
- examples/uniform_soccer/01.png +0 -0
- examples/uniform_soccer/02.png +0 -0
- ldr/__init__.py +4 -0
- ldr/metrics.py +326 -0
- ldr/model.py +455 -0
- requirements.txt +6 -0
README.md
CHANGED
|
@@ -1,13 +1,28 @@
|
|
| 1 |
---
|
| 2 |
-
title: Latent Dynamics Reasoning
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Latent Dynamics Reasoning (LDR)
|
| 3 |
+
emoji: 🎬
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.15.1
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Extrapolate physical video from 3 conditioning frames
|
| 10 |
+
python_version: "3.10"
|
| 11 |
+
startup_duration_timeout: 30m
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Latent Dynamics Reasoning (LDR)
|
| 15 |
+
|
| 16 |
+
Extrapolative video world model that rolls out future video frames from three
|
| 17 |
+
initial conditioning images, reasoning about latent physical dynamics.
|
| 18 |
+
|
| 19 |
+
Upload three frames of a physical scene, pick the task type (uniform,
|
| 20 |
+
parabola, collision, looming, bouncing), and the model generates ~29 future
|
| 21 |
+
frames of predicted motion.
|
| 22 |
+
|
| 23 |
+
Based on: [Learning How the World Evolves: Extrapolative Video World Models
|
| 24 |
+
via Latent Dynamics Reasoning](https://lat-dyn-reason.github.io/)
|
| 25 |
+
|
| 26 |
+
- [Model](https://huggingface.co/haodongli/LDR) (Apache-2.0)
|
| 27 |
+
- [Code](https://github.com/Lat-Dyn-Reason/Lat-Dyn-Reason)
|
| 28 |
+
- [Project Page](https://lat-dyn-reason.github.io/)
|
app.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LDR — Extrapolative Video World Models via Latent Dynamics Reasoning.
|
| 2 |
+
|
| 3 |
+
Upload three conditioning frames, pick a task type, and the model rolls out
|
| 4 |
+
~29 future frames of physical motion (uniform, parabola, collision, looming,
|
| 5 |
+
or bouncing) and returns them as a video.
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 9 |
+
|
| 10 |
+
import spaces # MUST be before torch
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
import numpy as np
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import tempfile
|
| 17 |
+
import imageio
|
| 18 |
+
import imageio.v3 as iio
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from huggingface_hub import hf_hub_download
|
| 21 |
+
|
| 22 |
+
from ldr import build_ldr
|
| 23 |
+
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
# Model loading (module scope — ZeroGPU intercepts .to("cuda"))
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
|
| 28 |
+
MODEL_ID = "haodongli/LDR"
|
| 29 |
+
|
| 30 |
+
# The joint-5task checkpoint handles all five tasks in one model.
|
| 31 |
+
# Single-task checkpoints are smaller but only work for one task.
|
| 32 |
+
# We load both 256x256 single-task and joint so users can pick.
|
| 33 |
+
CKPT_CONFIGS = {
|
| 34 |
+
"uniform": ("256x256/single_task/uniform.pt", 256),
|
| 35 |
+
"parabola": ("256x256/single_task/parabola.pt", 256),
|
| 36 |
+
"collision": ("256x256/single_task/collision.pt", 256),
|
| 37 |
+
"looming": ("256x256/single_task/looming.pt", 256),
|
| 38 |
+
"bouncing": ("256x256/single_task/bouncing.pt", 256),
|
| 39 |
+
"joint": ("256x256/joint_task/joint_5task.pt", 256),
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
OURS_ARCH = dict(num_pred=29, width=256, accel_scale=0.5, n_kp=16,
|
| 43 |
+
warp_flow_res=64, kappa_init=0.15)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _load_checkpoint(ckpt_path, img_size):
|
| 47 |
+
"""Load a .pt checkpoint into an LDR model (mirrors eval.build_model)."""
|
| 48 |
+
ck = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
| 49 |
+
arch = dict(OURS_ARCH)
|
| 50 |
+
cargs = ck.get("args") if isinstance(ck, dict) else None
|
| 51 |
+
if cargs:
|
| 52 |
+
arch.update(width=cargs.get("accel_width", 256),
|
| 53 |
+
n_kp=cargs.get("n_kp", 16),
|
| 54 |
+
accel_scale=cargs.get("accel_scale", 0.5),
|
| 55 |
+
kappa_init=cargs.get("kappa_init", cargs.get("accel_init_damp", 0.15)))
|
| 56 |
+
model = build_ldr(**arch).to("cuda").eval()
|
| 57 |
+
sd = ck["model"] if (isinstance(ck, dict) and "model" in ck) else ck
|
| 58 |
+
sd = {k[7:] if k.startswith("module.") else k: v for k, v in sd.items()}
|
| 59 |
+
model.load_state_dict(sd)
|
| 60 |
+
return model
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
# Pre-download all checkpoints at module scope so they are on disk for the
|
| 64 |
+
# worker to stream.
|
| 65 |
+
_ckpt_cache = {} # task_name -> (model, img_size)
|
| 66 |
+
for _task, (_rel, _img) in CKPT_CONFIGS.items():
|
| 67 |
+
_path = hf_hub_download(MODEL_ID, _rel, repo_type="model")
|
| 68 |
+
_model = _load_checkpoint(_path, _img)
|
| 69 |
+
_ckpt_cache[_task] = (_model, _img)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _get_model(task):
|
| 73 |
+
return _ckpt_cache[task]
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# ---------------------------------------------------------------------------
|
| 77 |
+
# Frame utilities (ported from infer.py)
|
| 78 |
+
# ---------------------------------------------------------------------------
|
| 79 |
+
|
| 80 |
+
def _resize(ft, fr, img_size):
|
| 81 |
+
"""Resize a frame tensor (N, 3, H, W) and its numpy companion to img_size."""
|
| 82 |
+
if ft.shape[-1] != img_size:
|
| 83 |
+
ft = nn.functional.interpolate(ft, size=(img_size, img_size),
|
| 84 |
+
mode="bilinear", align_corners=False)
|
| 85 |
+
fr = (((ft + 1) * 127.5).clamp(0, 255).byte().permute(0, 2, 3, 1).numpy())
|
| 86 |
+
return ft, fr
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _load_frames(images, img_size):
|
| 90 |
+
"""Load a list of PIL images → (float_tensor [-1,1], numpy uint8 frames)."""
|
| 91 |
+
fr = np.stack([np.asarray(img.convert("RGB")) for img in images], 0)
|
| 92 |
+
ft = (torch.from_numpy(fr.astype(np.float32)).permute(0, 3, 1, 2) / 127.5 - 1.0)
|
| 93 |
+
return _resize(ft, fr, img_size)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _gen(model, frames_t, frames_np, nc):
|
| 97 |
+
"""Run the model and concatenate conditioning frames with predictions."""
|
| 98 |
+
cond_t = frames_t[:nc].to("cuda")
|
| 99 |
+
with torch.no_grad():
|
| 100 |
+
pred = model(cond_t.unsqueeze(0), nc, full=False,
|
| 101 |
+
cond_img=cond_t[nc - 1:nc])
|
| 102 |
+
pf = ((pred.squeeze(0).clamp(-1, 1) + 1) * 127.5).byte().cpu() \
|
| 103 |
+
.permute(0, 2, 3, 1).numpy()
|
| 104 |
+
return np.concatenate([frames_np[:nc], pf], axis=0)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# ---------------------------------------------------------------------------
|
| 108 |
+
# Inference function
|
| 109 |
+
# ---------------------------------------------------------------------------
|
| 110 |
+
|
| 111 |
+
@spaces.GPU(duration=120)
|
| 112 |
+
def generate(frame1, frame2, frame3, task="uniform",
|
| 113 |
+
fps=8, progress=gr.Progress(track_tqdm=True)):
|
| 114 |
+
"""Roll out future video frames from three conditioning images.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
frame1: First conditioning frame (earliest in the sequence).
|
| 118 |
+
frame2: Second conditioning frame.
|
| 119 |
+
frame3: Third conditioning frame (latest in the sequence).
|
| 120 |
+
task: Physics task type — uniform, parabola, collision, looming, or bouncing.
|
| 121 |
+
Use "joint" for the model trained on all five tasks.
|
| 122 |
+
fps: Output video frames per second.
|
| 123 |
+
Returns:
|
| 124 |
+
Path to the generated MP4 video file.
|
| 125 |
+
"""
|
| 126 |
+
model, img_size = _get_model(task)
|
| 127 |
+
images = [frame1, frame2, frame3]
|
| 128 |
+
ft, fr = _load_frames(images, img_size)
|
| 129 |
+
all_frames = _gen(model, ft, fr, num_cond=3)
|
| 130 |
+
|
| 131 |
+
# Write to a unique temp file (concurrency-safe)
|
| 132 |
+
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False, dir="/tmp")
|
| 133 |
+
tmp.close()
|
| 134 |
+
imageio.mimwrite(tmp.name, list(all_frames), fps=fps, codec="libx264",
|
| 135 |
+
quality=8, output_params=["-pix_fmt", "yuv420p"])
|
| 136 |
+
return tmp.name
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# ---------------------------------------------------------------------------
|
| 140 |
+
# Gradio UI
|
| 141 |
+
# ---------------------------------------------------------------------------
|
| 142 |
+
|
| 143 |
+
CSS = """
|
| 144 |
+
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 145 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
|
| 149 |
+
gr.Markdown("""
|
| 150 |
+
# Latent Dynamics Reasoning (LDR)
|
| 151 |
+
Upload three conditioning frames of a physical scene and the model
|
| 152 |
+
extrapolates ~29 future frames as video. Pick the physics task type that
|
| 153 |
+
matches your input (uniform motion, parabola, collision, looming, or
|
| 154 |
+
bouncing), or use the **joint** model trained on all five.
|
| 155 |
+
|
| 156 |
+
[Model](https://huggingface.co/haodongli/LDR) ·
|
| 157 |
+
[Code](https://github.com/Lat-Dyn-Reason/Lat-Dyn-Reason) ·
|
| 158 |
+
[Project Page](https://lat-dyn-reason.github.io/)
|
| 159 |
+
""")
|
| 160 |
+
|
| 161 |
+
with gr.Row():
|
| 162 |
+
with gr.Column(scale=3):
|
| 163 |
+
frame1 = gr.Image(label="Frame 1 (t=0)", type="pil",
|
| 164 |
+
height=200)
|
| 165 |
+
frame2 = gr.Image(label="Frame 2 (t=1)", type="pil",
|
| 166 |
+
height=200)
|
| 167 |
+
frame3 = gr.Image(label="Frame 3 (t=2)", type="pil",
|
| 168 |
+
height=200)
|
| 169 |
+
with gr.Column(scale=2):
|
| 170 |
+
task = gr.Dropdown(
|
| 171 |
+
choices=["uniform", "parabola", "collision", "looming",
|
| 172 |
+
"bouncing", "joint"],
|
| 173 |
+
value="uniform",
|
| 174 |
+
label="Task / Checkpoint",
|
| 175 |
+
info="Match the physics type of your scene. "
|
| 176 |
+
"'joint' handles all five but slightly lower quality.")
|
| 177 |
+
fps = gr.Slider(4, 16, value=8, step=1, label="Output FPS")
|
| 178 |
+
run = gr.Button("Generate Video", variant="primary")
|
| 179 |
+
|
| 180 |
+
output = gr.Video(label="Generated Video (conditioning + prediction)")
|
| 181 |
+
|
| 182 |
+
with gr.Accordion("Examples", open=True):
|
| 183 |
+
gr.Examples(
|
| 184 |
+
examples=[
|
| 185 |
+
# [frame1, frame2, frame3, task, fps]
|
| 186 |
+
["examples/uniform/00.png", "examples/uniform/01.png",
|
| 187 |
+
"examples/uniform/02.png", "uniform", 8],
|
| 188 |
+
["examples/parabola/00.png", "examples/parabola/01.png",
|
| 189 |
+
"examples/parabola/02.png", "parabola", 8],
|
| 190 |
+
["examples/collision/00.png", "examples/collision/01.png",
|
| 191 |
+
"examples/collision/02.png", "collision", 8],
|
| 192 |
+
["examples/looming/00.png", "examples/looming/01.png",
|
| 193 |
+
"examples/looming/02.png", "looming", 8],
|
| 194 |
+
["examples/bouncing/00.png", "examples/bouncing/01.png",
|
| 195 |
+
"examples/bouncing/02.png", "bouncing", 8],
|
| 196 |
+
["examples/uniform_pikachu/00.png",
|
| 197 |
+
"examples/uniform_pikachu/01.png",
|
| 198 |
+
"examples/uniform_pikachu/02.png", "uniform", 8],
|
| 199 |
+
["examples/uniform_soccer/00.png",
|
| 200 |
+
"examples/uniform_soccer/01.png",
|
| 201 |
+
"examples/uniform_soccer/02.png", "uniform", 8],
|
| 202 |
+
],
|
| 203 |
+
inputs=[frame1, frame2, frame3, task, fps],
|
| 204 |
+
outputs=output,
|
| 205 |
+
fn=generate,
|
| 206 |
+
cache_examples=True,
|
| 207 |
+
cache_mode="lazy",
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
run.click(
|
| 211 |
+
fn=generate,
|
| 212 |
+
inputs=[frame1, frame2, frame3, task, fps],
|
| 213 |
+
outputs=output,
|
| 214 |
+
api_name="generate",
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
if __name__ == "__main__":
|
| 219 |
+
demo.launch(mcp_server=True)
|
examples/bouncing/00.png
ADDED
|
examples/bouncing/01.png
ADDED
|
examples/bouncing/02.png
ADDED
|
examples/collision/00.png
ADDED
|
examples/collision/01.png
ADDED
|
examples/collision/02.png
ADDED
|
examples/looming/00.png
ADDED
|
examples/looming/01.png
ADDED
|
examples/looming/02.png
ADDED
|
examples/parabola/00.png
ADDED
|
examples/parabola/01.png
ADDED
|
examples/parabola/02.png
ADDED
|
examples/uniform/00.png
ADDED
|
examples/uniform/01.png
ADDED
|
examples/uniform/02.png
ADDED
|
examples/uniform_pikachu/00.png
ADDED
|
examples/uniform_pikachu/01.png
ADDED
|
examples/uniform_pikachu/02.png
ADDED
|
examples/uniform_soccer/00.png
ADDED
|
examples/uniform_soccer/01.png
ADDED
|
examples/uniform_soccer/02.png
ADDED
|
ldr/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Latent Dynamics Reasoning (LDR). See ldr/rollout.md for the rollout derivation."""
|
| 2 |
+
from .model import build_ldr, LDR, PerceptualPyramidLoss
|
| 3 |
+
|
| 4 |
+
__all__ = ["build_ldr", "LDR", "PerceptualPyramidLoss"]
|
ldr/metrics.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PhyWorld white-box parser: extract each ball's center/radius from predicted frames and score vs GT."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
|
| 5 |
+
left_color = (255, 0, 0)
|
| 6 |
+
right_color = (0, 0, 255)
|
| 7 |
+
COLORS = [left_color, right_color]
|
| 8 |
+
WORLD_SCALE = 10.0
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def parse_state_from_image(image_rgb, default_y, default_r1, thres=0.15, color=0):
|
| 12 |
+
image_copy = image_rgb.copy()
|
| 13 |
+
all_scaled_circles = []
|
| 14 |
+
if color == 0:
|
| 15 |
+
circle_mask = (image_copy[:, :, 0] > 127) & (image_copy[:, :, 1] < 127) & (image_copy[:, :, 2] < 127)
|
| 16 |
+
elif color == 1:
|
| 17 |
+
circle_mask = (image_copy[:, :, 0] < 127) & (image_copy[:, :, 1] < 127) & (image_copy[:, :, 2] > 127)
|
| 18 |
+
else:
|
| 19 |
+
raise ValueError("Invalid color")
|
| 20 |
+
|
| 21 |
+
area = np.sum(circle_mask)
|
| 22 |
+
radius = np.sqrt(area / np.pi)
|
| 23 |
+
scaled_radius = radius / image_copy.shape[1] * WORLD_SCALE
|
| 24 |
+
if area > thres:
|
| 25 |
+
center = (
|
| 26 |
+
np.mean(np.nonzero(circle_mask)[1]),
|
| 27 |
+
np.mean(np.nonzero(circle_mask)[0]),
|
| 28 |
+
)
|
| 29 |
+
scaled_center = (
|
| 30 |
+
center[0] / image_copy.shape[1] * WORLD_SCALE,
|
| 31 |
+
center[1] / image_copy.shape[1] * WORLD_SCALE,
|
| 32 |
+
)
|
| 33 |
+
else:
|
| 34 |
+
scaled_center = (WORLD_SCALE, default_y)
|
| 35 |
+
scaled_radius = default_r1
|
| 36 |
+
|
| 37 |
+
all_scaled_circles.append([*scaled_center, scaled_radius])
|
| 38 |
+
return np.array(all_scaled_circles)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def parse_state_from_image_collision(image_rgb, default_y, default_r1, default_r2, thres=0.15):
|
| 42 |
+
image_copy = image_rgb.copy()
|
| 43 |
+
all_scaled_circles = []
|
| 44 |
+
for ball_id, color in enumerate(COLORS):
|
| 45 |
+
if color == (255, 0, 0):
|
| 46 |
+
circle_mask = (image_copy[:, :, 0] > 127) & (image_copy[:, :, 1] < 127) & (image_copy[:, :, 2] < 127)
|
| 47 |
+
elif color == (0, 0, 255):
|
| 48 |
+
circle_mask = (image_copy[:, :, 0] < 127) & (image_copy[:, :, 1] < 127) & (image_copy[:, :, 2] > 127)
|
| 49 |
+
else:
|
| 50 |
+
raise ValueError("Invalid color")
|
| 51 |
+
|
| 52 |
+
area = np.sum(circle_mask)
|
| 53 |
+
radius = np.sqrt(area / np.pi)
|
| 54 |
+
scaled_radius = radius / image_copy.shape[1] * WORLD_SCALE
|
| 55 |
+
if area > thres:
|
| 56 |
+
center = (
|
| 57 |
+
np.mean(np.nonzero(circle_mask)[1]),
|
| 58 |
+
np.mean(np.nonzero(circle_mask)[0]),
|
| 59 |
+
)
|
| 60 |
+
scaled_center = (
|
| 61 |
+
center[0] / image_copy.shape[1] * WORLD_SCALE,
|
| 62 |
+
center[1] / image_copy.shape[1] * WORLD_SCALE,
|
| 63 |
+
)
|
| 64 |
+
else:
|
| 65 |
+
if ball_id == 0:
|
| 66 |
+
scaled_center = (0, default_y)
|
| 67 |
+
scaled_radius = default_r1
|
| 68 |
+
else:
|
| 69 |
+
scaled_center = (WORLD_SCALE, default_y)
|
| 70 |
+
scaled_radius = default_r2
|
| 71 |
+
|
| 72 |
+
all_scaled_circles.append([*scaled_center, scaled_radius])
|
| 73 |
+
return np.array(all_scaled_circles)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def get_last_ema(values_list, span):
|
| 77 |
+
series = pd.Series(values_list)
|
| 78 |
+
ema = series.ewm(span=span, adjust=False).mean()
|
| 79 |
+
return ema.iloc[-1]
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def xy_metrics(list_a, list_b):
|
| 83 |
+
distances_x, distances_y = [], []
|
| 84 |
+
assert len(list_a) == len(list_b) == 1
|
| 85 |
+
for elem_a, elem_b in zip(list_a, list_b):
|
| 86 |
+
if not np.any(np.isnan(elem_a)) and not np.any(np.isnan(elem_b)):
|
| 87 |
+
distances_x.append(np.abs(elem_a[0] - elem_b[0]))
|
| 88 |
+
distances_y.append(np.abs(elem_a[1] - elem_b[1]))
|
| 89 |
+
x_error_avg = np.mean(distances_x) if distances_x else np.nan
|
| 90 |
+
y_error_avg = np.mean(distances_y) if distances_y else np.nan
|
| 91 |
+
return x_error_avg, y_error_avg
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def evaluate_xy(rollout_frames, gt_features, init, mode, gamma=0.98, sample_freq=1, pred_states=None):
|
| 95 |
+
assert sample_freq == 1, 'there may be some bugs if it is greater than 1'
|
| 96 |
+
|
| 97 |
+
# keep only frames where the ball is fully in view
|
| 98 |
+
left_ball_r = init[0]
|
| 99 |
+
left_ball_init_v = init[1]
|
| 100 |
+
left_ball_m = init[0]**2
|
| 101 |
+
index = []
|
| 102 |
+
CONDITION_FRAMES = 4
|
| 103 |
+
for i, state in enumerate(gt_features):
|
| 104 |
+
if i < CONDITION_FRAMES-1:
|
| 105 |
+
continue
|
| 106 |
+
left_ball_x, left_ball_y = state[0], state[1]
|
| 107 |
+
if left_ball_x - left_ball_r >= 0 and left_ball_x + left_ball_r <= WORLD_SCALE \
|
| 108 |
+
and left_ball_y - left_ball_r >= 0 and left_ball_y + left_ball_r <= WORLD_SCALE:
|
| 109 |
+
index.append(i)
|
| 110 |
+
|
| 111 |
+
if rollout_frames is not None:
|
| 112 |
+
assert len(rollout_frames) == len(gt_features), f'{len(rollout_frames)}, {len(gt_features)}'
|
| 113 |
+
rollout_frames = rollout_frames[index]
|
| 114 |
+
gt_features = gt_features[index]
|
| 115 |
+
|
| 116 |
+
x_error_list, y_error_list, r_list = [], [], []
|
| 117 |
+
x_pos_list, y_pos_list = [], []
|
| 118 |
+
default_y, default_r1 = np.nan, np.nan
|
| 119 |
+
for rollout_frame, gt_feature in zip(rollout_frames[sample_freq-1::sample_freq], gt_features[sample_freq-1::sample_freq]):
|
| 120 |
+
parsed_state = parse_state_from_image(rollout_frame, default_y, default_r1, color=init[2] if len(init) >= 3 else 0)
|
| 121 |
+
default_y, default_r1 = parsed_state[0][1], parsed_state[0][2]
|
| 122 |
+
x_pos_list.append(parsed_state[:, 0])
|
| 123 |
+
y_pos_list.append(parsed_state[:, 1])
|
| 124 |
+
r_list.append(parsed_state[:, 2])
|
| 125 |
+
parsed_state = parsed_state[:, :2]
|
| 126 |
+
x_error, y_error = xy_metrics([gt_feature], parsed_state)
|
| 127 |
+
x_error_list.append(x_error)
|
| 128 |
+
y_error_list.append(y_error)
|
| 129 |
+
span = len(rollout_frames[sample_freq-1::sample_freq])
|
| 130 |
+
ema_x_error = get_last_ema(x_error_list, span=span)
|
| 131 |
+
ema_y_error = get_last_ema(y_error_list, span=span)
|
| 132 |
+
|
| 133 |
+
else:
|
| 134 |
+
assert pred_states is not None and len(pred_states) == len(gt_features), f'{len(pred_states)}, {len(gt_features)}'
|
| 135 |
+
pred_states = pred_states[index]
|
| 136 |
+
gt_features = gt_features[index]
|
| 137 |
+
x_pos_list = pred_states[:, 0]
|
| 138 |
+
y_pos_list = pred_states[:, 1]
|
| 139 |
+
r_list = pred_states[:, 2]
|
| 140 |
+
|
| 141 |
+
r1_list = r_list
|
| 142 |
+
r1_min, r1_max = min(r1_list), max(r1_list)
|
| 143 |
+
delta_r = r1_max - r1_min
|
| 144 |
+
|
| 145 |
+
gt_x_pos = gt_features[:, 0]
|
| 146 |
+
gt_x_vel = np.diff(gt_x_pos, axis=0) / (0.1 * sample_freq)
|
| 147 |
+
gt_y_pos = gt_features[:, 1]
|
| 148 |
+
gt_y_vel = np.diff(gt_y_pos, axis=0) / (0.1 * sample_freq)
|
| 149 |
+
|
| 150 |
+
x_pos = np.array(x_pos_list)
|
| 151 |
+
y_pos = np.array(y_pos_list)
|
| 152 |
+
if x_pos.shape[-1] == 1:
|
| 153 |
+
x_pos = np.squeeze(x_pos, -1)
|
| 154 |
+
y_pos = np.squeeze(y_pos, -1)
|
| 155 |
+
|
| 156 |
+
x_vel = np.diff(x_pos, axis=0) / (0.1 * sample_freq)
|
| 157 |
+
y_vel = np.diff(y_pos, axis=0) / (0.1 * sample_freq)
|
| 158 |
+
|
| 159 |
+
x_err_avg = np.mean(np.abs(x_pos - gt_x_pos))
|
| 160 |
+
y_err_avg = np.mean(np.abs(y_pos - gt_y_pos))
|
| 161 |
+
|
| 162 |
+
x_vel_err_avg = np.mean(np.abs(x_vel - gt_x_vel))
|
| 163 |
+
y_vel_err_avg = np.mean(np.abs(y_vel - gt_y_vel))
|
| 164 |
+
|
| 165 |
+
if mode == 'all':
|
| 166 |
+
return {
|
| 167 |
+
'init': init,
|
| 168 |
+
'x_pos': x_pos,
|
| 169 |
+
'y_pos': y_pos,
|
| 170 |
+
'x_vel': x_vel,
|
| 171 |
+
'y_vel': y_vel,
|
| 172 |
+
'r': r_list,
|
| 173 |
+
'gt_x_pos': gt_x_pos,
|
| 174 |
+
'gt_y_pos': gt_y_pos,
|
| 175 |
+
'abs_x_err_avg': x_err_avg,
|
| 176 |
+
'abs_y_err_avg': y_err_avg,
|
| 177 |
+
'delta_r': delta_r,
|
| 178 |
+
'abs_x_vel_err_avg': x_vel_err_avg,
|
| 179 |
+
'abs_y_vel_err_avg': y_vel_err_avg,
|
| 180 |
+
}
|
| 181 |
+
else:
|
| 182 |
+
return ema_x_error, ema_y_error
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def evaluate_xy_collision(rollout_frames, gt_features, init, mode, gamma=0.98, sample_freq=1, pred_states=None):
|
| 186 |
+
assert sample_freq == 1, 'there may be some bugs if it is greater than 1'
|
| 187 |
+
|
| 188 |
+
left_ball_r, right_ball_r = init[:2]
|
| 189 |
+
left_ball_init_v, right_ball_init_v = init[2:4]
|
| 190 |
+
left_ball_m, right_ball_m = init[:2]**2
|
| 191 |
+
index = []
|
| 192 |
+
CONDITION_FRAMES = 4
|
| 193 |
+
for i, state in enumerate(gt_features):
|
| 194 |
+
if i < CONDITION_FRAMES-1:
|
| 195 |
+
continue
|
| 196 |
+
left_ball_x = state[0, 0]
|
| 197 |
+
right_ball_x = state[1, 0]
|
| 198 |
+
if left_ball_x - left_ball_r >= 0 and right_ball_x + right_ball_r <= WORLD_SCALE:
|
| 199 |
+
index.append(i)
|
| 200 |
+
|
| 201 |
+
if rollout_frames is not None:
|
| 202 |
+
assert len(rollout_frames) == len(gt_features), f'{len(rollout_frames)}, {len(gt_features)}'
|
| 203 |
+
rollout_frames = rollout_frames[index]
|
| 204 |
+
gt_features = gt_features[index]
|
| 205 |
+
|
| 206 |
+
r_list = []
|
| 207 |
+
x_pos_list = []
|
| 208 |
+
y_pos_list = []
|
| 209 |
+
default_y, default_r1, default_r2 = np.nan, np.nan, np.nan
|
| 210 |
+
for rollout_frame, gt_feature in zip(rollout_frames[sample_freq-1::sample_freq], gt_features[sample_freq-1::sample_freq]):
|
| 211 |
+
parsed_state = parse_state_from_image_collision(rollout_frame, default_y, default_r1, default_r2)
|
| 212 |
+
default_y, default_r1, default_r2 = parsed_state[0][1], parsed_state[0][2], parsed_state[1][2]
|
| 213 |
+
x_pos_list.append(parsed_state[:, 0])
|
| 214 |
+
y_pos_list.append(parsed_state[:, 1])
|
| 215 |
+
r_list.append(parsed_state[:, 2])
|
| 216 |
+
parsed_state = parsed_state[:, :2]
|
| 217 |
+
|
| 218 |
+
else:
|
| 219 |
+
assert pred_states is not None and len(pred_states) == len(gt_features), f'{len(pred_states)}, {len(gt_features)}'
|
| 220 |
+
pred_states = pred_states[index]
|
| 221 |
+
gt_features = gt_features[index]
|
| 222 |
+
x_pos_list = pred_states[:, :2]
|
| 223 |
+
y_pos_list = pred_states[:, 2:4]
|
| 224 |
+
r_list = pred_states[:, 4:]
|
| 225 |
+
|
| 226 |
+
# only use frames before collision to avoid out-of-vision region
|
| 227 |
+
MIN_POST_FRAMES = 8
|
| 228 |
+
r_list = r_list[:(MIN_POST_FRAMES-CONDITION_FRAMES+1)//sample_freq]
|
| 229 |
+
r1_list = [x[0] for x in r_list]
|
| 230 |
+
r2_list = [x[1] for x in r_list]
|
| 231 |
+
r1_min, r1_max = min(r1_list), max(r1_list)
|
| 232 |
+
r2_min, r2_max = min(r2_list), max(r2_list)
|
| 233 |
+
delta_r = max(r1_max - r1_min, r2_max - r2_min)
|
| 234 |
+
|
| 235 |
+
gt_x_pos = gt_features[:, :, 0]
|
| 236 |
+
gt_y_pos = gt_features[:, :, 1]
|
| 237 |
+
gt_x_vel = np.diff(gt_x_pos, axis=0) / (0.1 * sample_freq)
|
| 238 |
+
|
| 239 |
+
# collision frame = first large change in GT x-velocity
|
| 240 |
+
collision_index = len(gt_x_vel) - 1
|
| 241 |
+
for i in range(1, len(gt_x_vel)):
|
| 242 |
+
if np.abs(gt_x_vel[i, 0] - gt_x_vel[i-1, 0]) > 0.1 or np.abs(gt_x_vel[i, 1] - gt_x_vel[i-1, 1]) > 0.1:
|
| 243 |
+
collision_index = i
|
| 244 |
+
break
|
| 245 |
+
|
| 246 |
+
x_pos = np.array(x_pos_list)
|
| 247 |
+
x_vel = np.diff(x_pos, axis=0) / (0.1 * sample_freq)
|
| 248 |
+
|
| 249 |
+
pre_x_err_avg = np.mean(np.abs(x_pos[:collision_index+1] - gt_x_pos[:collision_index+1]))
|
| 250 |
+
post_x_err_avg = np.mean(np.abs(x_pos[collision_index+1:] - gt_x_pos[collision_index+1:]))
|
| 251 |
+
|
| 252 |
+
y_pos = np.array(y_pos_list)
|
| 253 |
+
pre_y_err_avg = np.mean(np.abs(y_pos[:collision_index+1] - gt_y_pos[:collision_index+1]))
|
| 254 |
+
post_y_err_avg = np.mean(np.abs(y_pos[collision_index+1:] - gt_y_pos[collision_index+1:]))
|
| 255 |
+
|
| 256 |
+
pre_vel_err_avg = np.mean(np.abs(x_vel[:collision_index] - gt_x_vel[:collision_index]))
|
| 257 |
+
post_vel_err_avg = np.mean(np.abs(x_vel[collision_index+1:] - gt_x_vel[collision_index+1:]))
|
| 258 |
+
|
| 259 |
+
momentum = left_ball_m * x_vel[:, 0] + right_ball_m * x_vel[:, 1]
|
| 260 |
+
gt_momentum = left_ball_m * left_ball_init_v - right_ball_m * right_ball_init_v
|
| 261 |
+
pre_momentum_error_avg = np.mean(np.abs(momentum[:collision_index] - gt_momentum))
|
| 262 |
+
post_momentum_error_avg = np.mean(np.abs(momentum[collision_index+1:] - gt_momentum))
|
| 263 |
+
|
| 264 |
+
energy = left_ball_m * x_vel[:, 0]**2 / 2 + right_ball_m * x_vel[:, 1]**2 / 2
|
| 265 |
+
gt_energy = left_ball_m * left_ball_init_v**2 / 2 + right_ball_m * right_ball_init_v**2 / 2
|
| 266 |
+
pre_energy_error_avg = np.mean(np.abs(energy[:collision_index] - gt_energy))
|
| 267 |
+
post_energy_error_avg = np.mean(np.abs(energy[collision_index+1:] - gt_energy))
|
| 268 |
+
|
| 269 |
+
return {
|
| 270 |
+
'init': init,
|
| 271 |
+
'collision_index': int(collision_index),
|
| 272 |
+
'pre_x_vel': x_vel[:collision_index],
|
| 273 |
+
'gt_pre_x_vel': gt_x_vel[:collision_index],
|
| 274 |
+
'post_x_vel': x_vel[collision_index+1:],
|
| 275 |
+
'gt_post_x_vel': gt_x_vel[collision_index+1:],
|
| 276 |
+
'x_pos': x_pos,
|
| 277 |
+
'gt_x_pos': gt_x_pos,
|
| 278 |
+
'x_vel': x_vel,
|
| 279 |
+
'abs_x_err_avg': float(np.mean(np.abs(x_pos - gt_x_pos))),
|
| 280 |
+
'abs_y_err_avg': float(np.mean(np.abs(y_pos - gt_y_pos))),
|
| 281 |
+
'y_pos': y_pos,
|
| 282 |
+
'gt_y_pos': gt_y_pos,
|
| 283 |
+
'delta_r': delta_r,
|
| 284 |
+
'pre_x_err_avg': pre_x_err_avg,
|
| 285 |
+
'post_x_err_avg': post_x_err_avg,
|
| 286 |
+
'pre_y_err_avg': pre_y_err_avg,
|
| 287 |
+
'post_y_err_avg': post_y_err_avg,
|
| 288 |
+
'pre_vel_err_avg': pre_vel_err_avg,
|
| 289 |
+
'post_vel_err_avg': post_vel_err_avg,
|
| 290 |
+
'pre_momentum_error_avg': pre_momentum_error_avg,
|
| 291 |
+
'post_momentum_error_avg': post_momentum_error_avg,
|
| 292 |
+
'pre_energy_error_avg': pre_energy_error_avg,
|
| 293 |
+
'post_energy_error_avg': post_energy_error_avg,
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def evaluate_xy_looming(rollout_frames, gt_pos, gt_radius, init, mode='all', sample_freq=1):
|
| 298 |
+
"""Looming (scale dynamics): parsed ball radius r(t) vs GT radius, plus x/y position; primary axis = radius."""
|
| 299 |
+
CONDITION_FRAMES = 4
|
| 300 |
+
m = min(len(rollout_frames), len(gt_pos), len(gt_radius))
|
| 301 |
+
idx = []
|
| 302 |
+
for i in range(m):
|
| 303 |
+
if i < CONDITION_FRAMES - 1:
|
| 304 |
+
continue
|
| 305 |
+
x, y, r = float(gt_pos[i, 0]), float(gt_pos[i, 1]), float(gt_radius[i])
|
| 306 |
+
if x - r >= 0 and x + r <= WORLD_SCALE and y - r >= 0 and y + r <= WORLD_SCALE:
|
| 307 |
+
idx.append(i)
|
| 308 |
+
if len(idx) == 0:
|
| 309 |
+
return {'abs_r_err_avg': np.nan, 'abs_x_err_avg': np.nan, 'abs_y_err_avg': np.nan, 'abs_r_vel_err_avg': np.nan}
|
| 310 |
+
xs, ys, rs = [], [], []
|
| 311 |
+
default_y, default_r = np.nan, np.nan
|
| 312 |
+
for i in idx:
|
| 313 |
+
ps = parse_state_from_image(rollout_frames[i], default_y, default_r, color=0)
|
| 314 |
+
default_y, default_r = ps[0][1], ps[0][2]
|
| 315 |
+
xs.append(float(ps[0][0])); ys.append(float(ps[0][1])); rs.append(float(ps[0][2]))
|
| 316 |
+
xs, ys, rs = np.array(xs), np.array(ys), np.array(rs)
|
| 317 |
+
gx = np.array([float(gt_pos[i, 0]) for i in idx]); gy = np.array([float(gt_pos[i, 1]) for i in idx])
|
| 318 |
+
gr = np.array([float(gt_radius[i]) for i in idx])
|
| 319 |
+
r_vel_err = float(np.mean(np.abs(np.diff(rs) - np.diff(gr)) / (0.1 * sample_freq))) if len(rs) > 1 else np.nan
|
| 320 |
+
return {
|
| 321 |
+
'init': init, 'r': rs, 'gt_r': gr, 'x_pos': xs, 'y_pos': ys,
|
| 322 |
+
'abs_r_err_avg': float(np.mean(np.abs(rs - gr))),
|
| 323 |
+
'abs_x_err_avg': float(np.mean(np.abs(xs - gx))),
|
| 324 |
+
'abs_y_err_avg': float(np.mean(np.abs(ys - gy))),
|
| 325 |
+
'abs_r_vel_err_avg': r_vel_err,
|
| 326 |
+
}
|
ldr/model.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LDR model: structured-latent encoder, kinematic-integration rollout, warp-render decoder, perceptual loss."""
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class _KeypointEnc(nn.Module):
|
| 8 |
+
"""Frame (N,3,H,W) -> K heatmaps at H/8 -> marginal soft-argmax -> structured latent (N,K,3)=(mu_x,mu_y,sigma)."""
|
| 9 |
+
def __init__(self, n_kp, gn=8):
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.n_kp = n_kp
|
| 12 |
+
self.net = nn.Sequential(
|
| 13 |
+
nn.Conv2d(3, 32, 7, 1, 3), nn.GroupNorm(gn, 32), nn.SiLU(),
|
| 14 |
+
nn.Conv2d(32, 64, 3, 2, 1), nn.GroupNorm(gn, 64), nn.SiLU(),
|
| 15 |
+
nn.Conv2d(64, 64, 3, 1, 1), nn.GroupNorm(gn, 64), nn.SiLU(),
|
| 16 |
+
nn.Conv2d(64, 96, 3, 2, 1), nn.GroupNorm(gn, 96), nn.SiLU(),
|
| 17 |
+
nn.Conv2d(96, 96, 3, 1, 1), nn.GroupNorm(gn, 96), nn.SiLU(),
|
| 18 |
+
nn.Conv2d(96, 128, 3, 2, 1), nn.GroupNorm(gn, 128), nn.SiLU(),
|
| 19 |
+
nn.Conv2d(128, 128, 3, 1, 1), nn.GroupNorm(gn, 128), nn.SiLU(),
|
| 20 |
+
nn.Conv2d(128, n_kp, 1))
|
| 21 |
+
|
| 22 |
+
def forward(self, x):
|
| 23 |
+
hm = self.net(x)
|
| 24 |
+
H, W = hm.shape[-2:]
|
| 25 |
+
gx = torch.linspace(-1, 1, W, device=x.device).view(1, 1, W)
|
| 26 |
+
gy = torch.linspace(-1, 1, H, device=x.device).view(1, 1, H)
|
| 27 |
+
px = torch.softmax(hm.mean(2), dim=-1)
|
| 28 |
+
py = torch.softmax(hm.mean(3), dim=-1)
|
| 29 |
+
cx = (px * gx).sum(-1); cy = (py * gy).sum(-1) # centroid mu
|
| 30 |
+
vx = (px * (gx - cx.unsqueeze(-1)) ** 2).sum(-1)
|
| 31 |
+
vy = (py * (gy - cy.unsqueeze(-1)) ** 2).sum(-1)
|
| 32 |
+
s = torch.sqrt(0.5 * (vx + vy) + 1e-6) # extent sigma
|
| 33 |
+
return torch.stack([cx, cy, s], dim=-1)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _make_coord_grid(h, w, device, dtype=torch.float32):
|
| 37 |
+
y = torch.linspace(-1, 1, h, device=device, dtype=dtype)
|
| 38 |
+
x = torch.linspace(-1, 1, w, device=device, dtype=dtype)
|
| 39 |
+
xx = x.view(1, w).expand(h, w)
|
| 40 |
+
yy = y.view(h, 1).expand(h, w)
|
| 41 |
+
return torch.stack([xx, yy], dim=-1)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _region_gaussian(centers, stds, res, device):
|
| 45 |
+
grid = _make_coord_grid(res, res, device).view(1, 1, res, res, 2)
|
| 46 |
+
c = centers.view(*centers.shape[:-1], 1, 1, 2)
|
| 47 |
+
var = (stds ** 2).view(*stds.shape, 1, 1).clamp(min=1e-6)
|
| 48 |
+
d2 = ((grid - c) ** 2).sum(-1)
|
| 49 |
+
return torch.exp(-0.5 * d2 / var)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class _GNSameBlock(nn.Module):
|
| 53 |
+
def __init__(self, cin, cout, kernel_size=3, padding=1, gn=8):
|
| 54 |
+
super().__init__()
|
| 55 |
+
self.conv = nn.Conv2d(cin, cout, kernel_size, padding=padding)
|
| 56 |
+
self.norm = nn.GroupNorm(gn, cout)
|
| 57 |
+
|
| 58 |
+
def forward(self, x):
|
| 59 |
+
return F.relu(self.norm(self.conv(x)))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class _GNDownBlock(nn.Module):
|
| 63 |
+
def __init__(self, cin, cout, gn=8):
|
| 64 |
+
super().__init__()
|
| 65 |
+
self.conv = nn.Conv2d(cin, cout, 3, padding=1)
|
| 66 |
+
self.norm = nn.GroupNorm(gn, cout)
|
| 67 |
+
self.pool = nn.AvgPool2d(2)
|
| 68 |
+
|
| 69 |
+
def forward(self, x):
|
| 70 |
+
return self.pool(F.relu(self.norm(self.conv(x))))
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class _GNUpBlock(nn.Module):
|
| 74 |
+
def __init__(self, cin, cout, gn=8):
|
| 75 |
+
super().__init__()
|
| 76 |
+
self.conv = nn.Conv2d(cin, cout, 3, padding=1)
|
| 77 |
+
self.norm = nn.GroupNorm(gn, cout)
|
| 78 |
+
|
| 79 |
+
def forward(self, x):
|
| 80 |
+
return F.relu(self.norm(self.conv(F.interpolate(x, scale_factor=2))))
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class _GNResBlock(nn.Module):
|
| 84 |
+
def __init__(self, c, gn=8):
|
| 85 |
+
super().__init__()
|
| 86 |
+
self.n1 = nn.GroupNorm(gn, c); self.c1 = nn.Conv2d(c, c, 3, padding=1)
|
| 87 |
+
self.n2 = nn.GroupNorm(gn, c); self.c2 = nn.Conv2d(c, c, 3, padding=1)
|
| 88 |
+
|
| 89 |
+
def forward(self, x):
|
| 90 |
+
out = self.c1(F.relu(self.n1(x)))
|
| 91 |
+
out = self.c2(F.relu(self.n2(out)))
|
| 92 |
+
return out + x
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class _HGEncoder(nn.Module):
|
| 96 |
+
def __init__(self, block_expansion, in_features, num_blocks, max_features, gn=8):
|
| 97 |
+
super().__init__()
|
| 98 |
+
blocks = []
|
| 99 |
+
for i in range(num_blocks):
|
| 100 |
+
cin = in_features if i == 0 else min(max_features, block_expansion * (2 ** i))
|
| 101 |
+
cout = min(max_features, block_expansion * (2 ** (i + 1)))
|
| 102 |
+
blocks.append(_GNDownBlock(cin, cout, gn=gn))
|
| 103 |
+
self.blocks = nn.ModuleList(blocks)
|
| 104 |
+
|
| 105 |
+
def forward(self, x):
|
| 106 |
+
outs = [x]
|
| 107 |
+
for b in self.blocks:
|
| 108 |
+
outs.append(b(outs[-1]))
|
| 109 |
+
return outs
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class _HGDecoder(nn.Module):
|
| 113 |
+
def __init__(self, block_expansion, in_features, num_blocks, max_features, gn=8):
|
| 114 |
+
super().__init__()
|
| 115 |
+
ups = []
|
| 116 |
+
for i in range(num_blocks)[::-1]:
|
| 117 |
+
cin = (1 if i == num_blocks - 1 else 2) * min(max_features, block_expansion * (2 ** (i + 1)))
|
| 118 |
+
cout = min(max_features, block_expansion * (2 ** i))
|
| 119 |
+
ups.append(_GNUpBlock(cin, cout, gn=gn))
|
| 120 |
+
self.ups = nn.ModuleList(ups)
|
| 121 |
+
self.out_filters = block_expansion + in_features
|
| 122 |
+
|
| 123 |
+
def forward(self, x):
|
| 124 |
+
out = x.pop()
|
| 125 |
+
for up in self.ups:
|
| 126 |
+
out = up(out)
|
| 127 |
+
out = torch.cat([out, x.pop()], dim=1)
|
| 128 |
+
return out
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class _Hourglass(nn.Module):
|
| 132 |
+
"""Small GroupNorm U-Net used by the dense-motion mask predictor."""
|
| 133 |
+
def __init__(self, block_expansion, in_features, num_blocks, max_features, gn=8):
|
| 134 |
+
super().__init__()
|
| 135 |
+
self.enc = _HGEncoder(block_expansion, in_features, num_blocks, max_features, gn=gn)
|
| 136 |
+
self.dec = _HGDecoder(block_expansion, in_features, num_blocks, max_features, gn=gn)
|
| 137 |
+
self.out_filters = self.dec.out_filters
|
| 138 |
+
|
| 139 |
+
def forward(self, x):
|
| 140 |
+
return self.dec(self.enc(x))
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class _MeasuredWarpRenderer(nn.Module):
|
| 144 |
+
"""Warp the fixed cond frame to the predicted pose (FOMM/MRAA similarity flow + Gao splat occlusion)."""
|
| 145 |
+
|
| 146 |
+
def __init__(self, num_kp, block_expansion=32, max_features=256, num_down_blocks=2,
|
| 147 |
+
num_bottleneck_blocks=3, flow_res=64, mask_block_expansion=32,
|
| 148 |
+
mask_num_blocks=4, mask_max_features=256,
|
| 149 |
+
occ_lo=1e-3, occ_hi=2.0, scale_min=0.04, bg_resp=0.1, gn=8):
|
| 150 |
+
super().__init__()
|
| 151 |
+
self.num_kp = num_kp; self.flow_res = flow_res
|
| 152 |
+
self.occ_lo = occ_lo; self.occ_hi = occ_hi
|
| 153 |
+
self.scale_min = scale_min; self.bg_resp = bg_resp
|
| 154 |
+
in_feat = (num_kp + 1) * (3 + 1)
|
| 155 |
+
self.hourglass = _Hourglass(mask_block_expansion, in_feat, mask_num_blocks, mask_max_features, gn=gn)
|
| 156 |
+
self.mask = nn.Conv2d(self.hourglass.out_filters, num_kp + 1, 7, padding=3)
|
| 157 |
+
self.first = _GNSameBlock(3, block_expansion, kernel_size=7, padding=3, gn=gn)
|
| 158 |
+
down, up = [], []
|
| 159 |
+
for i in range(num_down_blocks):
|
| 160 |
+
down.append(_GNDownBlock(min(max_features, block_expansion * (2 ** i)),
|
| 161 |
+
min(max_features, block_expansion * (2 ** (i + 1))), gn=gn))
|
| 162 |
+
for i in range(num_down_blocks):
|
| 163 |
+
up.append(_GNUpBlock(min(max_features, block_expansion * (2 ** (num_down_blocks - i))),
|
| 164 |
+
min(max_features, block_expansion * (2 ** (num_down_blocks - i - 1))), gn=gn))
|
| 165 |
+
self.down_blocks = nn.ModuleList(down)
|
| 166 |
+
self.up_blocks = nn.ModuleList(up)
|
| 167 |
+
self.bottleneck = nn.Sequential()
|
| 168 |
+
bc = min(max_features, block_expansion * (2 ** num_down_blocks))
|
| 169 |
+
for i in range(num_bottleneck_blocks):
|
| 170 |
+
self.bottleneck.add_module('r' + str(i), _GNResBlock(bc, gn=gn))
|
| 171 |
+
self.final = nn.Conv2d(block_expansion, 3, 7, padding=3)
|
| 172 |
+
|
| 173 |
+
# (1) per-region similarity backward flows: output(driving) grid -> source(cond) grid
|
| 174 |
+
def _sparse_backward(self, coords_t, coords_cond, res, device):
|
| 175 |
+
N = coords_t.shape[0]
|
| 176 |
+
grid = _make_coord_grid(res, res, device).view(1, 1, res, res, 2)
|
| 177 |
+
kp_t = coords_t[..., :2].reshape(N, self.num_kp, 1, 1, 2)
|
| 178 |
+
kp_c = coords_cond[..., :2].reshape(N, self.num_kp, 1, 1, 2)
|
| 179 |
+
s_t = coords_t[..., 2].clamp(min=self.scale_min).reshape(N, self.num_kp, 1, 1, 1)
|
| 180 |
+
s_c = coords_cond[..., 2].clamp(min=self.scale_min).reshape(N, self.num_kp, 1, 1, 1)
|
| 181 |
+
region = kp_c + (s_c / s_t) * (grid - kp_t)
|
| 182 |
+
bg = grid.expand(N, 1, res, res, 2)
|
| 183 |
+
return torch.cat([bg, region], dim=1)
|
| 184 |
+
|
| 185 |
+
def _heatmaps(self, coords_t, coords_cond, res, device):
|
| 186 |
+
gt = _region_gaussian(coords_t[..., :2], coords_t[..., 2].clamp(min=self.scale_min), res, device)
|
| 187 |
+
gc = _region_gaussian(coords_cond[..., :2], coords_cond[..., 2].clamp(min=self.scale_min), res, device)
|
| 188 |
+
hm = gt - gc
|
| 189 |
+
bg = torch.zeros(hm.shape[0], 1, res, res, device=device, dtype=hm.dtype)
|
| 190 |
+
return torch.cat([bg, hm], dim=1).unsqueeze(2)
|
| 191 |
+
|
| 192 |
+
# (2) dense flow = softmax-mask blend of the K+1 sparse flows (MRAA)
|
| 193 |
+
def _dense_flow(self, cond_img, coords_t, coords_cond):
|
| 194 |
+
N = coords_t.shape[0]; r = self.flow_res; device = cond_img.device
|
| 195 |
+
src = F.interpolate(cond_img, size=(r, r), mode='bilinear', align_corners=False)
|
| 196 |
+
sparse = self._sparse_backward(coords_t, coords_cond, r, device)
|
| 197 |
+
src_rep = src.unsqueeze(1).expand(N, self.num_kp + 1, 3, r, r).reshape(N * (self.num_kp + 1), 3, r, r)
|
| 198 |
+
deformed = F.grid_sample(src_rep, sparse.reshape(N * (self.num_kp + 1), r, r, 2),
|
| 199 |
+
align_corners=True, padding_mode='border')
|
| 200 |
+
deformed = deformed.view(N, self.num_kp + 1, 3, r, r)
|
| 201 |
+
hm = self._heatmaps(coords_t, coords_cond, r, device)
|
| 202 |
+
inp = torch.cat([hm, deformed], dim=2).reshape(N, (self.num_kp + 1) * 4, r, r)
|
| 203 |
+
mask = F.softmax(self.mask(self.hourglass(inp)), dim=1)
|
| 204 |
+
flow = (sparse.permute(0, 1, 4, 2, 3) * mask.unsqueeze(2)).sum(1)
|
| 205 |
+
return flow.permute(0, 2, 3, 1)
|
| 206 |
+
|
| 207 |
+
# (3) deterministic occlusion via forward splat (Gao), a measurement (no grad)
|
| 208 |
+
@torch.no_grad()
|
| 209 |
+
def _occlusion(self, coords_t, coords_cond):
|
| 210 |
+
N = coords_t.shape[0]; r = self.flow_res; device = coords_t.device
|
| 211 |
+
P = _make_coord_grid(r, r, device).view(1, 1, r, r, 2)
|
| 212 |
+
kp_t = coords_t[..., :2].reshape(N, self.num_kp, 1, 1, 2)
|
| 213 |
+
kp_c = coords_cond[..., :2].reshape(N, self.num_kp, 1, 1, 2)
|
| 214 |
+
s_t = coords_t[..., 2].clamp(min=self.scale_min).reshape(N, self.num_kp, 1, 1, 1)
|
| 215 |
+
s_c = coords_cond[..., 2].clamp(min=self.scale_min).reshape(N, self.num_kp, 1, 1, 1)
|
| 216 |
+
fwd = kp_t + (s_t / s_c) * (P - kp_c)
|
| 217 |
+
d2 = ((P - kp_c) ** 2).sum(-1)
|
| 218 |
+
w = torch.exp(-0.5 * d2 / (s_c.squeeze(-1) ** 2))
|
| 219 |
+
denom = self.bg_resp + w.sum(1, keepdim=True)
|
| 220 |
+
a_k = (w / denom).unsqueeze(-1)
|
| 221 |
+
a_bg = (self.bg_resp / denom).unsqueeze(-1)
|
| 222 |
+
Pxy = P.expand(N, 1, r, r, 2)
|
| 223 |
+
F_flow = (a_bg * Pxy + (a_k * fwd).sum(1, keepdim=True)).squeeze(1)
|
| 224 |
+
fx = (F_flow[..., 0] * 0.5 + 0.5) * (r - 1)
|
| 225 |
+
fy = (F_flow[..., 1] * 0.5 + 0.5) * (r - 1)
|
| 226 |
+
E = self._bilinear_splat(fx, fy, r, N)
|
| 227 |
+
m = ((E > self.occ_lo) & (E < self.occ_hi)).float().unsqueeze(1)
|
| 228 |
+
return m, E
|
| 229 |
+
|
| 230 |
+
@staticmethod
|
| 231 |
+
def _bilinear_splat(fx, fy, r, N):
|
| 232 |
+
device = fx.device
|
| 233 |
+
x0 = torch.floor(fx); y0 = torch.floor(fy)
|
| 234 |
+
wx = fx - x0; wy = fy - y0
|
| 235 |
+
x0 = x0.long(); y0 = y0.long(); x1 = x0 + 1; y1 = y0 + 1
|
| 236 |
+
E = torch.zeros(N, r * r, device=device)
|
| 237 |
+
|
| 238 |
+
def scat(xi, yi, wgt):
|
| 239 |
+
xi = xi.clamp(0, r - 1); yi = yi.clamp(0, r - 1)
|
| 240 |
+
E.scatter_add_(1, (yi * r + xi).reshape(N, -1), wgt.reshape(N, -1))
|
| 241 |
+
scat(x0, y0, (1 - wx) * (1 - wy)); scat(x1, y0, wx * (1 - wy))
|
| 242 |
+
scat(x0, y1, (1 - wx) * wy); scat(x1, y1, wx * wy)
|
| 243 |
+
return E.view(N, r, r)
|
| 244 |
+
|
| 245 |
+
@staticmethod
|
| 246 |
+
def _deform(inp, flow):
|
| 247 |
+
_, ho, wo, _ = flow.shape
|
| 248 |
+
_, _, h, w = inp.shape
|
| 249 |
+
if ho != h or wo != w:
|
| 250 |
+
flow = F.interpolate(flow.permute(0, 3, 1, 2), size=(h, w), mode='bilinear',
|
| 251 |
+
align_corners=False).permute(0, 2, 3, 1)
|
| 252 |
+
return F.grid_sample(inp, flow, align_corners=True, padding_mode='border')
|
| 253 |
+
|
| 254 |
+
@staticmethod
|
| 255 |
+
def _gate(warped, prev, occ):
|
| 256 |
+
if occ.shape[2:] != warped.shape[2:]:
|
| 257 |
+
occ = F.interpolate(occ, size=warped.shape[2:], mode='bilinear', align_corners=False)
|
| 258 |
+
if prev is None:
|
| 259 |
+
return warped * occ
|
| 260 |
+
return warped * occ + prev * (1 - occ)
|
| 261 |
+
|
| 262 |
+
# (4) generator: MRAA skips + occlusion-gated warp + final source-pixel blend
|
| 263 |
+
def forward(self, cond_img, coords_t, coords_cond):
|
| 264 |
+
flow = self._dense_flow(cond_img, coords_t, coords_cond)
|
| 265 |
+
occ, _ = self._occlusion(coords_t, coords_cond)
|
| 266 |
+
out = self.first(cond_img)
|
| 267 |
+
skips = [out]
|
| 268 |
+
for db in self.down_blocks:
|
| 269 |
+
out = db(out); skips.append(out)
|
| 270 |
+
out = self._gate(self._deform(out, flow), None, occ)
|
| 271 |
+
out = self.bottleneck(out)
|
| 272 |
+
for i, ub in enumerate(self.up_blocks):
|
| 273 |
+
out = self._gate(self._deform(skips[-(i + 1)], flow), out, occ)
|
| 274 |
+
out = ub(out)
|
| 275 |
+
out = self._gate(self._deform(skips[0], flow), out, occ)
|
| 276 |
+
out = torch.sigmoid(self.final(out))
|
| 277 |
+
return self._gate(self._deform(cond_img, flow), out, occ)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
class _AntiAliasInterpolation2d(nn.Module):
|
| 281 |
+
def __init__(self, channels, scale):
|
| 282 |
+
super().__init__()
|
| 283 |
+
sigma = (1 / scale - 1) / 2
|
| 284 |
+
kernel_size = 2 * round(sigma * 4) + 1
|
| 285 |
+
self.ka = kernel_size // 2
|
| 286 |
+
self.kb = self.ka - 1 if kernel_size % 2 == 0 else self.ka
|
| 287 |
+
kernel = 1
|
| 288 |
+
grids = torch.meshgrid([torch.arange(kernel_size, dtype=torch.float32)] * 2, indexing='ij')
|
| 289 |
+
for size, mgrid in zip([kernel_size, kernel_size], grids):
|
| 290 |
+
mean = (size - 1) / 2
|
| 291 |
+
kernel = kernel * torch.exp(-(mgrid - mean) ** 2 / (2 * sigma ** 2))
|
| 292 |
+
kernel = kernel / kernel.sum()
|
| 293 |
+
kernel = kernel.view(1, 1, *kernel.shape).repeat(channels, 1, 1, 1)
|
| 294 |
+
self.register_buffer('weight', kernel)
|
| 295 |
+
self.groups = channels; self.scale = scale
|
| 296 |
+
self.int_inv_scale = int(1 / scale)
|
| 297 |
+
|
| 298 |
+
def forward(self, x):
|
| 299 |
+
if self.scale == 1.0:
|
| 300 |
+
return x
|
| 301 |
+
out = F.pad(x, (self.ka, self.kb, self.ka, self.kb))
|
| 302 |
+
out = F.conv2d(out, weight=self.weight, groups=self.groups)
|
| 303 |
+
return out[:, :, ::self.int_inv_scale, ::self.int_inv_scale]
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
class _ImagePyramide(nn.Module):
|
| 307 |
+
def __init__(self, scales, num_channels=3):
|
| 308 |
+
super().__init__()
|
| 309 |
+
self.scales = list(scales)
|
| 310 |
+
self.downs = nn.ModuleDict({str(s).replace('.', '-'): _AntiAliasInterpolation2d(num_channels, s)
|
| 311 |
+
for s in scales})
|
| 312 |
+
|
| 313 |
+
def forward(self, x):
|
| 314 |
+
return {'prediction_' + str(s): self.downs[str(s).replace('.', '-')](x) for s in self.scales}
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def _load_vgg19_features():
|
| 318 |
+
"""Load ImageNet VGG-19 features from PHYWORLD_VGG19_PATH, else timm 'vgg19.tv_in1k' (== torchvision weights)."""
|
| 319 |
+
import os
|
| 320 |
+
from torchvision import models
|
| 321 |
+
net = models.vgg19(weights=None)
|
| 322 |
+
tried = []
|
| 323 |
+
path = os.environ.get('PHYWORLD_VGG19_PATH', '')
|
| 324 |
+
if path and os.path.exists(path):
|
| 325 |
+
try:
|
| 326 |
+
sd = torch.load(path, map_location='cpu', weights_only=False)
|
| 327 |
+
sd = {(k[9:] if k.startswith('features.') else k): v for k, v in sd.items()}
|
| 328 |
+
sd = {k: v for k, v in sd.items() if k in net.features.state_dict()}
|
| 329 |
+
net.features.load_state_dict(sd, strict=True)
|
| 330 |
+
return net.features
|
| 331 |
+
except Exception as e:
|
| 332 |
+
tried.append(f'path={e}')
|
| 333 |
+
try:
|
| 334 |
+
import timm
|
| 335 |
+
m = timm.create_model('vgg19.tv_in1k', pretrained=True)
|
| 336 |
+
sd = {k[9:]: v for k, v in m.state_dict().items() if k.startswith('features.')}
|
| 337 |
+
net.features.load_state_dict(sd, strict=True)
|
| 338 |
+
return net.features
|
| 339 |
+
except Exception as e:
|
| 340 |
+
tried.append(f'timm={e}')
|
| 341 |
+
raise RuntimeError('VGG19 features unavailable (' + ' | '.join(tried) + ')')
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
class _Vgg19(nn.Module):
|
| 345 |
+
def __init__(self):
|
| 346 |
+
super().__init__()
|
| 347 |
+
f = _load_vgg19_features()
|
| 348 |
+
self.slices = nn.ModuleList()
|
| 349 |
+
for lo, hi in [(0, 2), (2, 7), (7, 12), (12, 21), (21, 30)]:
|
| 350 |
+
s = nn.Sequential()
|
| 351 |
+
for x in range(lo, hi):
|
| 352 |
+
s.add_module(str(x), f[x])
|
| 353 |
+
self.slices.append(s)
|
| 354 |
+
self.register_buffer('mean', torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
|
| 355 |
+
self.register_buffer('std', torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
|
| 356 |
+
for p in self.parameters():
|
| 357 |
+
p.requires_grad_(False)
|
| 358 |
+
|
| 359 |
+
def forward(self, x):
|
| 360 |
+
x = (x - self.mean) / self.std
|
| 361 |
+
outs = []
|
| 362 |
+
for s in self.slices:
|
| 363 |
+
x = s(x); outs.append(x)
|
| 364 |
+
return outs
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
class PerceptualPyramidLoss(nn.Module):
|
| 368 |
+
"""Multi-scale VGG-19 perceptual loss (FOMM); falls back to multi-scale pixel-L1 if VGG is unavailable."""
|
| 369 |
+
def __init__(self, scales=(1, 0.5, 0.25, 0.125), slice_weights=(1., 1., 1., 1., 1.)):
|
| 370 |
+
super().__init__()
|
| 371 |
+
self.scales = list(scales); self.slice_weights = slice_weights
|
| 372 |
+
self.pyramid = _ImagePyramide(self.scales, 3)
|
| 373 |
+
try:
|
| 374 |
+
self.vgg = _Vgg19(); self.use_vgg = True; self.note = 'vgg19-perceptual'
|
| 375 |
+
except Exception as e:
|
| 376 |
+
self.vgg = None; self.use_vgg = False; self.note = 'MS-L1-fallback:' + str(e)[:100]
|
| 377 |
+
|
| 378 |
+
def forward(self, pred, target):
|
| 379 |
+
pred = (pred.clamp(-1, 1) + 1) * 0.5
|
| 380 |
+
target = (target.clamp(-1, 1) + 1) * 0.5
|
| 381 |
+
pp = self.pyramid(pred); pt = self.pyramid(target)
|
| 382 |
+
total = pred.sum() * 0.0
|
| 383 |
+
for s in self.scales:
|
| 384 |
+
a = pp['prediction_' + str(s)]; b = pt['prediction_' + str(s)]
|
| 385 |
+
if self.use_vgg:
|
| 386 |
+
xv = self.vgg(a); yv = self.vgg(b)
|
| 387 |
+
for i, wgt in enumerate(self.slice_weights):
|
| 388 |
+
total = total + wgt * (xv[i] - yv[i].detach()).abs().mean()
|
| 389 |
+
else:
|
| 390 |
+
total = total + (a - b.detach()).abs().mean()
|
| 391 |
+
return total
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
class LDR(nn.Module):
|
| 395 |
+
"""Encode each frame to a structured latent, roll it forward by kinematic integration, decode by warping the cond frame."""
|
| 396 |
+
|
| 397 |
+
def __init__(self, n_kp=16, num_pred=29, width=256, accel_scale=0.5, warp_flow_res=64, kappa_init=0.15):
|
| 398 |
+
super().__init__()
|
| 399 |
+
self.n_kp = n_kp; self.num_pred = num_pred; self.accel_scale = accel_scale
|
| 400 |
+
self.cdim = 3
|
| 401 |
+
# kappa (softplus of log_kappa): uncertainty gate for the measured second-order init
|
| 402 |
+
self.log_kappa = nn.Parameter(torch.log(torch.expm1(torch.tensor(max(float(kappa_init), 1e-3)))))
|
| 403 |
+
self.enc = _KeypointEnc(n_kp)
|
| 404 |
+
d = n_kp * self.cdim
|
| 405 |
+
self.g = nn.Sequential(nn.Linear(2 * d, width), nn.SiLU(),
|
| 406 |
+
nn.Linear(width, width), nn.SiLU(),
|
| 407 |
+
nn.Linear(width, d))
|
| 408 |
+
nn.init.zeros_(self.g[-1].weight); nn.init.zeros_(self.g[-1].bias) # zero-init: rollout starts as pure inertia
|
| 409 |
+
self.warp = _MeasuredWarpRenderer(num_kp=n_kp, flow_res=warp_flow_res)
|
| 410 |
+
|
| 411 |
+
def _residual(self, s, v):
|
| 412 |
+
return self.accel_scale * torch.tanh(self.g(torch.cat([s, v], dim=1)))
|
| 413 |
+
|
| 414 |
+
def rollout(self, c_cond, n=None):
|
| 415 |
+
n = self.num_pred if n is None else n
|
| 416 |
+
B = c_cond.shape[0]
|
| 417 |
+
assert c_cond.shape[1] >= 3, 'kinematic initialization needs 3 conditioning latents'
|
| 418 |
+
s = c_cond[:, -1].reshape(B, -1)
|
| 419 |
+
v = (c_cond[:, -1] - c_cond[:, -2]).reshape(B, -1)
|
| 420 |
+
a0_raw = (c_cond[:, -1] - 2 * c_cond[:, -2] + c_cond[:, -3]).reshape(B, -1)
|
| 421 |
+
kappa = F.softplus(self.log_kappa)
|
| 422 |
+
a0 = (a0_raw * a0_raw) / (a0_raw * a0_raw + kappa * kappa) * a0_raw
|
| 423 |
+
out = []
|
| 424 |
+
for _ in range(n):
|
| 425 |
+
v = v + a0 + self._residual(s, v)
|
| 426 |
+
s = s + v
|
| 427 |
+
out.append(s)
|
| 428 |
+
return torch.stack(out, 1).view(B, n, self.n_kp, self.cdim)
|
| 429 |
+
|
| 430 |
+
def _enc_seq(self, frames):
|
| 431 |
+
B, L = frames.shape[:2]
|
| 432 |
+
return self.enc(frames.reshape(B * L, *frames.shape[2:])).view(B, L, self.n_kp, self.cdim)
|
| 433 |
+
|
| 434 |
+
def _decode_seq(self, cond_img, coords_seq, coords_cond):
|
| 435 |
+
B, T = coords_seq.shape[:2]; H, W = cond_img.shape[-2:]
|
| 436 |
+
src01 = ((cond_img.clamp(-1, 1) + 1) * 0.5).unsqueeze(1).expand(B, T, 3, H, W).reshape(B * T, 3, H, W)
|
| 437 |
+
ct = coords_seq.reshape(B * T, self.n_kp, self.cdim)
|
| 438 |
+
cc = coords_cond.unsqueeze(1).expand(B, T, self.n_kp, self.cdim).reshape(B * T, self.n_kp, self.cdim)
|
| 439 |
+
out01 = self.warp(src01, ct, cc)
|
| 440 |
+
return (out01 * 2 - 1).view(B, T, 3, H, W)
|
| 441 |
+
|
| 442 |
+
def forward(self, frames, nc, full=False, horizon=None, cond_img=None):
|
| 443 |
+
coords = self._enc_seq(frames)
|
| 444 |
+
coords_cond = coords[:, nc - 1]
|
| 445 |
+
if not full:
|
| 446 |
+
return self._decode_seq(cond_img, self.rollout(coords[:, :nc]), coords_cond)
|
| 447 |
+
dec_ae = self._decode_seq(cond_img, coords, coords_cond)
|
| 448 |
+
roll = self.rollout(coords[:, :nc], horizon)
|
| 449 |
+
dec_roll = self._decode_seq(cond_img, roll, coords_cond)
|
| 450 |
+
return dec_roll, dec_ae, roll, coords
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def build_ldr(n_kp=16, num_pred=29, width=256, accel_scale=0.5, warp_flow_res=64, kappa_init=0.15):
|
| 454 |
+
return LDR(n_kp=n_kp, num_pred=num_pred, width=width, accel_scale=accel_scale,
|
| 455 |
+
warp_flow_res=warp_flow_res, kappa_init=kappa_init)
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
torchvision
|
| 3 |
+
numpy
|
| 4 |
+
imageio
|
| 5 |
+
imageio-ffmpeg
|
| 6 |
+
pillow
|