Spaces:
Running on Zero
Running on Zero
File size: 8,854 Bytes
36a4745 | 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 | """Camera pose -> ray-encoding utilities for pose-conditioned world model.
Adapted from `GeometryForcing/utils/geometry_utils.py` with the following
simplifications:
* Only the bits needed for ``ray_encoding`` conditioning are kept (the
variant the user picked as the best-performing one).
* ``rays`` accepts independent ``(h_res, w_res)`` so non-square latents
(e.g. 15x20) are supported without distorting the intrinsics.
All functions follow this convention:
* Raw camera pose layout: ``(B, T, 16)`` = ``[K(4), R(9 + T(3))]`` where the
first 4 columns are normalised intrinsics ``(fx, fy, px, py)`` (pixel-coords
divided by image size) and the last 12 columns are a flattened ``3x4``
world-to-camera extrinsics matrix in row-major.
* Ray encoding output: ``(B, T, 180, H_lat, W_lat)`` (6 ray dims * 2 trig fns
* 15 NeRF frequencies = 180). This matches what `DiT3DPose` consumes when
``conditioning_type=ray_encoding``.
"""
from __future__ import annotations
import math
from typing import Tuple
import torch
from einops import einsum, rearrange, repeat
def _split_pose16(raw_poses: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""``(B, T, 16)`` -> ``R (B, T, 3, 3)``, ``T (B, T, 3)``, ``K (B, T, 4)``."""
assert raw_poses.shape[-1] == 16, f"expected 16-dim pose, got {raw_poses.shape[-1]}"
K, RT = raw_poses.split([4, 12], dim=-1)
RT = rearrange(RT, "b t (i j) -> b t i j", i=3, j=4)
R = RT[..., :3, :3]
T = RT[..., :3, 3]
return R, T, K
def _normalize_by_first(R: torch.Tensor, T: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""Re-express all poses so the first frame is the world origin."""
R_ref = R[:, 0] # (B, 3, 3)
T_ref = T[:, 0] # (B, 3)
R_inv = rearrange(R_ref, "b i j -> b j i")
R_new = einsum(R, R_inv, "b t i j1, b j1 j2 -> b t i j2")
T_new = T - einsum(R_new, T_ref, "b t i j, b j -> b t i")
return R_new, T_new
def _compute_rays(
R: torch.Tensor,
T: torch.Tensor,
K: torch.Tensor,
h_res: int,
w_res: int,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Per-pixel ray origin / direction in world coords.
Args:
R: ``(B, T, 3, 3)`` world->cam rotation.
T: ``(B, T, 3)`` world->cam translation.
K: ``(B, T, 4)`` normalised intrinsics ``(fx, fy, px, py)``.
h_res, w_res: target ray grid resolution (independent so non-square
latents are handled correctly).
Returns:
origin: ``(B, T, H, W, 3)``
direction: ``(B, T, H, W, 3)`` (unnormalised; norm encodes depth scale)
"""
device, dtype = K.device, K.dtype
coord_w, coord_h = torch.meshgrid(
torch.linspace(0, w_res - 1, w_res, device=device, dtype=dtype),
torch.linspace(0, h_res - 1, h_res, device=device, dtype=dtype),
indexing="xy",
) # (H, W) each
coord_w = rearrange(coord_w, "h w -> 1 1 h w") + 0.5
coord_h = rearrange(coord_h, "h w -> 1 1 h w") + 0.5
# Normalised K -> pixel-space K (separate W / H scaling for non-square grids).
fx = (K[..., 0] * w_res).view(*K.shape[:-1], 1, 1) # (B, T, 1, 1)
fy = (K[..., 1] * h_res).view(*K.shape[:-1], 1, 1)
px = (K[..., 2] * w_res).view(*K.shape[:-1], 1, 1)
py = (K[..., 3] * h_res).view(*K.shape[:-1], 1, 1)
x = (coord_w - px) / fx
y = (coord_h - py) / fy
z = torch.ones_like(x)
direction = torch.stack([x, y, z], dim=-1) # (B, T, H, W, 3)
R_inv = rearrange(R, "b t i j -> b t j i")
direction = einsum(R_inv, direction, "b t i j, b t h w j -> b t h w i")
origin = -einsum(R_inv, T, "b t i j, b t j -> b t i")
origin = repeat(origin, "b t i -> b t h w i", h=h_res, w=w_res).clone()
return origin, direction
def _normalize_translation_scale(T: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
"""Per-clip translation-scale normalisation (CameraCtrl / lingbot style).
Monocular SfM poses (e.g. RealEstate10K) have an arbitrary, per-clip metric
scale, so raw camera translations vary wildly in magnitude across clips.
Since the ray origin feeds NeRF frequency encoding ``sin(2^k pi x)`` -- which
is very sensitive to the absolute magnitude of ``x`` -- this inconsistency
hurts learning. We rescale each clip so its largest camera displacement is
~1, making camera motion scale-invariant across clips.
Args:
T: ``(B, S, 3)`` camera translations, already expressed relative to the
first frame (so the first frame sits at the origin).
eps: guard so static / near-static clips (max norm ~ 0) are left
unchanged ("only normalize when moving").
Returns:
``(B, S, 3)`` translations divided by the per-clip max translation norm.
"""
max_norm = torch.norm(T, dim=-1).amax(dim=1, keepdim=True) # (B, 1)
scale = torch.where(max_norm > eps, max_norm, torch.ones_like(max_norm))
return T / scale.unsqueeze(-1)
def _nerf_pos_encoding(x: torch.Tensor, freq: int) -> torch.Tensor:
"""NeRF-style sin/cos positional encoding along the last dim."""
scale = (
2 ** torch.linspace(0, freq - 1, freq, device=x.device, dtype=x.dtype)
* math.pi
)
encoding = rearrange(x[..., None] * scale, "b t h w i s -> b t h w (i s)")
return torch.sin(torch.cat([encoding, encoding + 0.5 * math.pi], dim=-1))
@torch.no_grad()
@torch.autocast(device_type="cuda", enabled=False) # always fp32 for geometry
def compute_ray_encoding(
raw_poses: torch.Tensor,
h_lat: int,
w_lat: int,
freq: int = 15,
normalize_trans: bool = False,
) -> torch.Tensor:
"""End-to-end raw poses -> ray-encoding feature volume.
Args:
raw_poses: either ``(B, T, 16)`` or ``(B, T, K, 16)``. MiniWorld's RE10K
pipeline uses ``K=4`` poses inside each WAN-VAE latent chunk.
h_lat, w_lat: latent spatial size (= model input H, W after VAE).
freq: NeRF frequency count. ``freq=15`` gives ``6 * 2 * 15 = 180``
channels per pose.
normalize_trans: if True, rescale each clip's camera
translations so the largest displacement is ~1 (see
``_normalize_translation_scale``). Disabled by default to match
DFoT's RealEstate10K preprocessing; static clips are untouched.
Returns:
``(B, T, K * 6 * 2 * freq, H_lat, W_lat)`` float32. The ``K`` poses are
ray-encoded independently then concatenated along the channel axis
(so the spatial ``y_embedder`` sees ``K * 180`` channels). For the
common ``(B, T, 16)`` input ``K=1`` and the output channel count is
``180``.
"""
assert raw_poses.dim() in (3, 4), (
f"raw_poses must be (B, T, 16) or (B, T, K, 16); got {raw_poses.shape}"
)
raw_poses = raw_poses.float()
if raw_poses.dim() == 3:
b, t_lat, _ = raw_poses.shape
k_per_lat = 1
flat = raw_poses # (B, T, 16)
else:
b, t_lat, k_per_lat, _ = raw_poses.shape
# Flatten K into the time axis so we can reuse the single-pose pipeline
# (one shared normalisation anchor = first pose in the sequence).
flat = raw_poses.reshape(b, t_lat * k_per_lat, 16)
R, T, K = _split_pose16(flat)
R, T = _normalize_by_first(R, T)
if normalize_trans:
T = _normalize_translation_scale(T)
origin, direction = _compute_rays(R, T, K, h_res=h_lat, w_res=w_lat)
enc = torch.cat(
[
_nerf_pos_encoding(origin, freq),
_nerf_pos_encoding(direction, freq),
],
dim=-1,
) # (B, T*K, H, W, 6 * 2 * freq)
if k_per_lat == 1:
return rearrange(enc, "b t h w c -> b t c h w").contiguous()
return rearrange(
enc, "b (t k) h w c -> b t (k c) h w", t=t_lat, k=k_per_lat,
).contiguous()
def downsample_poses_to_latent(
raw_poses: torch.Tensor,
t_latent: int,
) -> torch.Tensor:
"""Map per-raw-frame poses to four poses per WAN-style latent frame.
The causal WAN VAE encodes ``T_raw = 4*(T_lat-1)+1`` raw frames into
``T_lat`` latents with the temporal grouping:
* latent 0 -> raw [0]
* latent j (>0) -> raw [4j-3, 4j-2, 4j-1, 4j]
Latent 0 has only raw[0], so it is duplicated four times to keep the output
shape consistent with action conditioning: ``(B, T_lat, 4, 16)``.
"""
idx_per_latent = [[0, 0, 0, 0]]
for j in range(1, t_latent):
idx_per_latent.append([4 * j - 3, 4 * j - 2, 4 * j - 1, 4 * j])
idx_flat = [i for chunk in idx_per_latent for i in chunk]
assert raw_poses.shape[1] > max(idx_flat), (
f"raw_poses has only {raw_poses.shape[1]} frames; need at least "
f"{max(idx_flat) + 1} to build {t_latent} latent poses."
)
b = raw_poses.shape[0]
return raw_poses[:, idx_flat].view(b, t_latent, 4, 16).contiguous()
|