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()