| import torch |
| from torch import nn |
| from thirdparty.prope.torch import PropeDotProductAttention |
| from diffsynth.models.wan_video_dit import flash_attention |
| from einops import rearrange, repeat, einsum |
| import torch.nn.functional as F |
| from typing import Tuple |
| import numpy as np |
| import os |
|
|
|
|
| def patch_dit(pipe, method, height, width, attn_compress=1, adaptation_method="parallel"): |
| keywords = [] |
| if method.startswith("recam"): |
| if method == "recammaster": |
| emb_dim = 14 |
| elif method == "recam_plucker": |
| emb_dim = 6 |
| else: |
| raise ValueError(f"Unknown method: {method}") |
|
|
| dim = pipe.dit.blocks[0].self_attn.q.weight.shape[0] |
| for block in pipe.dit.blocks: |
| block.cam_encoder = nn.Linear(emb_dim, dim) |
| block.projector = nn.Linear(dim, dim) |
| block.cam_encoder.weight.data.zero_() |
| block.cam_encoder.bias.data.zero_() |
| block.projector.weight = nn.Parameter(torch.eye(dim)) |
| block.projector.bias = nn.Parameter(torch.zeros(dim)) |
| keywords.extend(["cam_encoder", "projector", "self_attn"]) |
|
|
| if method == "plucker": |
| from diffsynth.models.wan_video_camera_controller import SimpleAdapter |
| pipe.dit.control_adapter = SimpleAdapter( |
| 24, |
| pipe.dit.dim, |
| kernel_size=[2, 2], |
| stride=[2, 2], |
| downscale_factor=pipe.vae.upsampling_factor, |
| ) |
| pipe.dit.control_adapter.conv.weight.data.zero_() |
| pipe.dit.control_adapter.conv.bias.data.zero_() |
| for block in pipe.dit.control_adapter.residual_blocks: |
| block.conv2.weight.data.zero_() |
| block.conv2.bias.data.zero_() |
| keywords = "*" |
| elif any(k in method for k in ("gta", "prope", "relray")): |
| patch_factor = pipe.vae.upsampling_factor * 2 |
| patches_x = width // patch_factor |
| patches_y = height // patch_factor |
|
|
| if "abs" in method: |
| if "absc2w" in method or "absray" in method: |
| emb_dim = 12 |
| elif "absmap" in method: |
| emb_dim = 3 |
| else: |
| raise ValueError(f"Unknown method: {method}") |
| else: |
| emb_dim = None |
|
|
| for block in pipe.dit.blocks: |
| block.cam_self_attn = UcpeSelfAttention( |
| pipe.dit.dim, |
| pipe.dit.dim // attn_compress, |
| block.num_heads // attn_compress, |
| patches_x=patches_x, |
| patches_y=patches_y, |
| image_width=width, |
| image_height=height, |
| emb_dim=emb_dim, |
| adaptation_method=adaptation_method, |
| ) |
| keywords.append("cam_self_attn") |
|
|
| pipe.dit.camera_condition = method |
| return keywords |
|
|
|
|
| def enable_grad(pipe, keywords): |
| pipe.eval() |
| pipe.requires_grad_(False) |
| if keywords == "*": |
| pipe.dit.train() |
| pipe.dit.requires_grad_(True) |
| else: |
| for name, module in pipe.dit.named_modules(): |
| if any(keyword in name for keyword in keywords): |
| print(f"Trainable: {name}") |
| module.train() |
| module.requires_grad_(True) |
|
|
| trainable_params = 0 |
| seen_params = set() |
| for name, module in pipe.dit.named_modules(): |
| for param in module.parameters(): |
| if param.requires_grad and param not in seen_params: |
| trainable_params += param.numel() |
| seen_params.add(param) |
| print(f"Total number of trainable parameters: {trainable_params}") |
|
|
|
|
| def compute_fx_from_fov_xi( |
| x_fov: torch.Tensor | float, |
| xi: torch.Tensor | float, |
| width: int, |
| device: torch.device | str = "cpu", |
| dtype: torch.dtype = torch.float32, |
| ) -> torch.Tensor: |
| """ |
| 根据水平视场角 (x_fov) 和 UCM 参数 (xi) 计算相机焦距 fx。 |
| |
| Args: |
| x_fov: float 或 [B] Tensor,水平视场角(单位:度) |
| xi: float 或 [B] Tensor,UCM 镜面参数 |
| width: 图像宽度(像素) |
| device: torch.device |
| dtype: torch.dtype |
| |
| Returns: |
| fx: [B] Tensor,焦距(像素单位) |
| """ |
| |
| def to_tensor_1d(x): |
| if torch.is_tensor(x): |
| return x.to(device=device, dtype=dtype) |
| return torch.tensor([x], dtype=dtype, device=device) |
|
|
| x_fov = to_tensor_1d(x_fov) |
| xi = to_tensor_1d(xi) |
|
|
| |
| B = max(x_fov.shape[0], xi.shape[0]) |
| x_fov = x_fov.view(-1).expand(B) |
| xi = xi.view(-1).expand(B) |
|
|
| |
| theta = torch.deg2rad(0.5 * x_fov) |
| eps = torch.finfo(dtype).eps |
| denom = torch.sin(theta).clamp_min(eps) |
| fx = (width * 0.5) * (torch.cos(theta) + xi) / denom |
| return fx |
|
|
|
|
| def compute_fov_from_fx_xi( |
| fx: torch.Tensor | float, |
| xi: torch.Tensor | float, |
| width: int, |
| device="cpu", |
| dtype=torch.float32, |
| ): |
| """ |
| 根据 UCM 模型参数 fx, xi 计算水平 FOV(度) |
| |
| Args: |
| fx: float 或 [B] Tensor, 焦距 |
| xi: float 或 [B] Tensor, UCM xi 参数 |
| width: 图像宽度 |
| Returns: |
| x_fov: [B], 单位 degree |
| """ |
| def to_tensor_1d(x): |
| if torch.is_tensor(x): |
| return x.to(device=device, dtype=dtype) |
| return torch.tensor([x], dtype=dtype, device=device) |
|
|
| fx = to_tensor_1d(fx).view(-1) |
| xi = to_tensor_1d(xi).view(-1) |
| B = max(fx.shape[0], xi.shape[0]) |
| fx = fx.expand(B) |
| xi = xi.expand(B) |
|
|
| |
| A = 2.0 * fx / width |
|
|
| |
| phi = torch.atan(1.0 / A) |
|
|
| |
| denom = torch.sqrt(A * A + 1.0) |
| ratio = (xi / denom).clamp(-1.0, 1.0) |
| theta = torch.asin(ratio) + phi |
|
|
| |
| x_fov = torch.rad2deg(2.0 * theta) |
| return x_fov |
|
|
|
|
| def ucm_unproject_grid_fov( |
| x_fov: float | torch.Tensor, |
| xi: float | torch.Tensor, |
| height: int, |
| width: int, |
| device: torch.device | str = "cpu", |
| dtype: torch.dtype = torch.float32, |
| ) -> torch.Tensor: |
| """ |
| 计算每个样本的相机方向向量 (UCM model, 用视场角定义)。 |
| 支持 float 或 [B] Tensor 的混合输入。 |
| - 若全为 float → 返回 [H, W, 3] |
| - 若任意为 [B] → 返回 [B, H, W, 3] |
| """ |
| |
| is_batched = any(torch.is_tensor(p) and p.ndim == 1 for p in [x_fov, xi]) |
|
|
| |
| fx = compute_fx_from_fov_xi(x_fov, xi, width, device, dtype) |
| fy = fx |
|
|
| |
| from equilib.equi2pers.torch import ucm_unproject_grid |
| d_cam = ucm_unproject_grid( |
| height=height, |
| width=width, |
| fx=fx, |
| fy=fy, |
| cx=width / 2, |
| cy=height / 2, |
| xi=xi if torch.is_tensor(xi) else torch.tensor([xi], dtype=dtype, device=device), |
| dtype=dtype, |
| device=device, |
| y_down=True, |
| ) |
|
|
| |
| if not is_batched: |
| d_cam = d_cam[0] |
|
|
| return d_cam |
|
|
|
|
| def project_ucm_points_fov(X, Y, Z, x_fov, xi, height, width): |
| """ |
| Project 3D points in camera frame to UCM image plane using fov-based intrinsics. |
| |
| Args: |
| X, Y, Z: torch.Tensor [..., 3D coordinates in camera frame] |
| x_fov: float or [B] —— horizontal field of view in degrees |
| xi: float or [B] —— UCM mirror parameter |
| height, width: target image dimensions |
| |
| Returns: |
| du, dv: projected pixel coordinates [..., 2] |
| """ |
| fx = compute_fx_from_fov_xi(x_fov, xi, width, X.device, X.dtype) |
| fy = fx |
| cx = width / 2 |
| cy = height / 2 |
|
|
| return project_ucm_points(X, Y, Z, fx, fy, cx, cy, xi) |
|
|
|
|
| def project_ucm_points(X, Y, Z, fx, fy, cx, cy, xi): |
| """ |
| Project 3D points in camera frame to UCM image plane. |
| |
| Args: |
| X, Y, Z: torch.Tensor [..., 3D coordinates in camera frame] |
| fx, fy, cx, cy: intrinsics (scalars or tensors) |
| xi: UCM mirror parameter |
| |
| Returns: |
| du, dv: projected pixel coordinates [..., 2] |
| """ |
| r = torch.sqrt(X * X + Y * Y + Z * Z) |
| alpha = Z + xi * r |
| du = fx * (X / alpha) + cx |
| dv = fy * (Y / alpha) + cy |
| return du, dv |
|
|
|
|
| def ray_condition_ucm( |
| x_fov, |
| xi, |
| pose, |
| height, width, |
| device, |
| ): |
| """ |
| ✅ UCM-based Plücker embedding, output format: [B, V, H, W, 6] |
| 🔁 Internally uses your ucm_unproject_grid() for consistent ray geometry. |
| |
| Only required params: |
| fov_x (degree) |
| xi |
| c2w (camera-to-world pose, same as your exported pose) |
| H, W (spatial resolution) |
| device |
| """ |
|
|
| d_cam = ucm_unproject_grid_fov( |
| x_fov, xi, height, width, device, dtype=pose.dtype |
| ) |
| d_cam = repeat(d_cam, "b ... -> b v ...", v=pose.shape[1]) |
| mask = d_cam.isnan().any(-1) |
|
|
| |
| R = pose[..., :3, :3] |
| t = pose[..., :3, 3] |
|
|
| d_world = torch.einsum("bvij,bvhwj->bvhwi", R, d_cam) |
| rays_o = t[..., None, None, :].expand_as(d_world) |
|
|
| |
| m = torch.cross(rays_o, d_world, dim=-1) |
|
|
| |
| plucker = torch.cat([m, d_world], dim=-1) |
| plucker[mask] = 0. |
| return plucker |
|
|
|
|
| def d_cam_to_angles(d_cam: torch.Tensor) -> torch.Tensor: |
| """ |
| 将方向向量 [x, y, z] 转换为 [azimuth, elevation]。 |
| 坐标系:z前,x右,y下(符合 UCM 投影输出) |
| |
| 输入: d_cam: [B, H, W, 3] |
| 输出: angles: [B, H, W, 2] — azimuth, elevation (单位: 弧度) |
| """ |
| d_unit = F.normalize(d_cam, dim=-1) |
|
|
| x = d_unit[..., 0] |
| y = d_unit[..., 1] |
| z = d_unit[..., 2] |
|
|
| |
| azimuth = torch.atan2(x, z) |
|
|
| |
| elevation = -torch.asin(y) |
|
|
| return torch.stack([azimuth, elevation], dim=-1) |
|
|
|
|
| def world_to_ray_mats( |
| d_cam: torch.Tensor, |
| c2w: torch.Tensor, |
| ) -> torch.Tensor: |
| """ |
| 构造每条 ray 的世界到 ray 局部坐标系的变换矩阵 world2ray。 |
| 坐标系定义: |
| - z: ray direction |
| - x: cam_y × ray_dir |
| - y: z × x |
| 返回: |
| raymats: [B, T, H, W, 4, 4] |
| """ |
| B, H, W, _ = d_cam.shape |
| T = c2w.shape[1] |
| device = d_cam.device |
| dtype = d_cam.dtype |
|
|
| |
| |
| d_cam = repeat(d_cam, 'b h w c -> b t h w c', t=T) |
|
|
| |
| R_cam = c2w[..., :3, :3] |
| t_cam = c2w[..., :3, 3] |
| |
| |
| d_world = einsum(R_cam, d_cam, 'b t i j, b t h w j -> b t h w i') |
|
|
| |
| cam_y = R_cam[..., :, 1] |
| cam_y = repeat(cam_y, 'b t c -> b t h w c', h=H, w=W) |
|
|
| |
| z_ray = F.normalize(d_world, dim=-1, eps=1e-6) |
| x_ray = torch.cross(cam_y, z_ray, dim=-1) |
| x_ray = F.normalize(x_ray, dim=-1, eps=1e-6) |
| y_ray = torch.cross(z_ray, x_ray, dim=-1) |
| y_ray = F.normalize(y_ray, dim=-1, eps=1e-6) |
| |
| |
| R_l2w = torch.stack([x_ray, y_ray, z_ray], dim=-1) |
|
|
| |
| R_w2l = rearrange(R_l2w, 'b t h w i j -> b t h w j i') |
|
|
| |
| t_world = repeat(t_cam, 'b t c -> b t h w c', h=H, w=W) |
|
|
| |
| t_w2l = -einsum(R_w2l, t_world, 'b t h w i j, b t h w j -> b t h w i') |
|
|
| |
| raymats = torch.zeros(B, T, H, W, 4, 4, device=device, dtype=dtype) |
| raymats[..., :3, :3] = R_w2l |
| raymats[..., :3, 3] = t_w2l |
| raymats[..., 3, 3] = 1.0 |
|
|
| |
| mask = torch.isnan(d_world).any(-1) |
| raymats[mask] = torch.eye(4, device=device, dtype=dtype) |
|
|
| return raymats |
|
|
|
|
| def rope_precompute_coeffs( |
| positions: torch.Tensor, |
| freq_base: float, |
| freq_scale: float, |
| feat_dim: int, |
| dtype: torch.dtype = torch.float32, |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| 批量计算每个样本对应的 RoPE 系数(cos, sin),用于 patch ray angle embedding。 |
| 输入: |
| positions: [B, H, W] —— 每个 patch 的 azimuth 或 elevation(单位弧度) |
| 输出: |
| cos: [B, 1, H*W, feat_dim//2] |
| sin: [B, 1, H*W, feat_dim//2] |
| """ |
| |
| mask = positions.isnan() |
| positions = positions.clone() |
| positions[mask] = 0.0 |
|
|
| B, H, W = positions.shape |
| positions_flat = positions.view(B, H * W) |
| num_freqs = feat_dim // 2 |
|
|
| freqs = freq_scale * ( |
| freq_base ** ( |
| -torch.arange(num_freqs, device=positions.device)[None, :] |
| / num_freqs |
| ) |
| ) |
|
|
| |
| angles = positions_flat[..., None] * freqs[None, :, :] |
| angles = angles.view(B, 1, H * W, num_freqs) |
|
|
| return torch.cos(angles).to(dtype), torch.sin(angles).to(dtype) |
|
|
|
|
| def compute_up_lat_map( |
| R: torch.Tensor, |
| x_fov: torch.Tensor, |
| xi: torch.Tensor, |
| height: int, |
| width: int, |
| device: torch.device = torch.device("cpu"), |
| delta: float = 0.1, |
| ): |
| """ |
| 计算 up_map 和 lat_map。 |
| |
| Args: |
| R: [B, T, 3, 3] 相机 c2w 旋转矩阵 |
| x_fov: [B] 或 [B,T] 水平视场角(度) |
| xi: [B] 或 [B,T] UCM 参数 |
| height: int,图像/patch 高度 |
| width: int,图像/patch 宽度 |
| device: torch.device |
| delta: float,小旋转角度(弧度) |
| Returns: |
| up_map: [B, T, H, W, 2] 单位向量 map |
| lat_map: [B, T, H, W, 1] 纬度 map |
| """ |
| B, T, _, _ = R.shape |
| dtype = R.dtype |
| R = R.float() |
|
|
| |
| d_cam = ucm_unproject_grid_fov( |
| x_fov=x_fov, |
| xi=xi, |
| height=height, |
| width=width, |
| device=device, |
| dtype=torch.float32, |
| ) |
| if d_cam.ndim == 3: |
| d_cam = d_cam.unsqueeze(0) |
| mask = d_cam.isnan().any(dim=-1, keepdim=True) |
|
|
| |
| d_cam_exp = repeat(d_cam, "B H W C -> B T H W C", T=T) |
| d_world = torch.einsum('btij,bthwj->bthwi', R, d_cam_exp) |
| d_world = d_world / torch.clamp_min(d_world.norm(dim=-1, keepdim=True), 1e-8) |
|
|
| |
| Xw, Yw, Zw = d_world[..., 0], d_world[..., 1], d_world[..., 2] |
| lat_map = torch.atan2(-Yw, torch.sqrt(Xw**2 + Zw**2)).unsqueeze(-1) |
|
|
| |
| v = d_world |
| up_world = torch.tensor([0, -1, 0], device=device, dtype=torch.float32) |
| k = torch.cross(v, up_world.unsqueeze(0).unsqueeze(0).unsqueeze(0).expand_as(v), dim=-1) |
| k = k / torch.clamp_min(k.norm(dim=-1, keepdim=True), 1e-8) |
|
|
| delta = torch.tensor(delta, device=device, dtype=torch.float32) |
| cos_eps = torch.cos(delta) |
| sin_eps = torch.sin(delta) |
| |
| v_rot = v * cos_eps + torch.cross(k, v, dim=-1) * sin_eps + k * (k * (v * 1).sum(dim=-1, keepdim=True)) * (1 - cos_eps) |
|
|
| dirs_cam = torch.einsum('btij,bthwj->bthwi', R.transpose(-1, -2), v_rot) |
| Xs, Ys, Zs = dirs_cam[..., 0], dirs_cam[..., 1], dirs_cam[..., 2] |
|
|
| du, dv = project_ucm_points_fov( |
| Xs, Ys, Zs, |
| x_fov=x_fov.float(), |
| xi=xi.float(), |
| height=height, |
| width=width, |
| ) |
| from equilib.torch_utils import create_grid |
| grid = create_grid( |
| height=height, |
| width=width, |
| batch=B, |
| dtype=torch.float32, |
| device=device, |
| ) |
| grid_x = grid[..., 0].unsqueeze(1) |
| grid_y = grid[..., 1].unsqueeze(1) |
|
|
| up_map = torch.stack((du - grid_x, dv - grid_y), dim=-1) |
| up_map = up_map / torch.clamp_min(up_map.norm(dim=-1, keepdim=True), 1e-8) |
|
|
| up_map = up_map.to(dtype=dtype) |
| lat_map = lat_map.to(dtype=dtype) |
|
|
| |
| mask_exp2 = mask.unsqueeze(1).expand(B, T, height, width, 1) |
| up_map = up_map.masked_fill(mask_exp2, 0.0) |
| lat_map = lat_map.masked_fill(mask_exp2, 0.0) |
|
|
| return up_map, lat_map |
|
|
|
|
| def visualize_up_lat_map(up_map: torch.Tensor, lat_map: torch.Tensor, save_path: str = None): |
| """ |
| 可视化 world-anchored 的 up_map 与 lat_map(GeoCalib-style overlay)。 |
| 仅依赖指定输入,其余设置在函数内固定。 |
| |
| Args: |
| up_map: [H, W, 2] 张量 |
| lat_map: [H, W, 1] 张量 |
| save_path: 保存文件路径 |
| """ |
| import matplotlib.pyplot as plt |
| from geocalib import viz2d |
|
|
| |
| up_vis = up_map.detach().float().cpu() |
| lat_vis = lat_map[..., 0].detach().float().cpu() |
|
|
| |
| fig, ax = plt.subplots(figsize=(6, 4), dpi=200) |
| viz2d.plot_latitudes([lat_vis], axes=[ax]) |
| viz2d.plot_vector_fields([up_vis.permute(2, 0, 1)], subsample=10, axes=[ax]) |
|
|
| ax.set_axis_off() |
|
|
| |
| if save_path is not None: |
| os.makedirs(os.path.dirname(save_path), exist_ok=True) |
| fig.canvas.draw() |
| fig.savefig(save_path, dpi=200, bbox_inches="tight") |
| plt.close(fig) |
| else: |
| return fig |
|
|
|
|
| class UcpeSelfAttention(nn.Module): |
| def __init__( |
| self, |
| dim: int, |
| attn_dim: int, |
| num_heads: int, |
| patches_x: int = 8, |
| patches_y: int = 8, |
| image_width: int = 128, |
| image_height: int = 128, |
| freq_base: float = 100.0, |
| freq_scale: float = 1.0, |
| precompute_coeffs: bool = True, |
| emb_dim: int | None = None, |
| adaptation_method: str = "parallel", |
| ): |
| super().__init__() |
| assert dim % num_heads == 0 |
| self.dim = dim |
| self.attn_dim = attn_dim |
| self.num_heads = num_heads |
| self.head_dim = attn_dim // num_heads |
| self.patches_x = patches_x |
| self.patches_y = patches_y |
| self.image_width = image_width |
| self.image_height = image_height |
| self.freq_base = freq_base |
| self.freq_scale = freq_scale |
| self.adaptation_method = adaptation_method |
|
|
| self.q_proj = nn.Linear(dim, attn_dim) |
| self.k_proj = nn.Linear(dim, attn_dim) |
| self.v_proj = nn.Linear(dim, attn_dim) |
| self.out_proj = nn.Linear(attn_dim, dim) |
| if emb_dim is not None: |
| self.cam_encoder = nn.Linear(emb_dim, dim) |
|
|
| |
| nn.init.zeros_(self.out_proj.weight) |
| nn.init.zeros_(self.out_proj.bias) |
|
|
| |
| self.prope_attn = PropeDotProductAttention( |
| head_dim=self.head_dim, |
| patches_x=patches_x, |
| patches_y=patches_y, |
| image_width=image_width, |
| image_height=image_height, |
| freq_base=freq_base, |
| freq_scale=freq_scale, |
| precompute_coeffs=precompute_coeffs, |
| ) |
|
|
| def forward(self, x: torch.Tensor, control_camera_dit_input: dict): |
| """ |
| Args: |
| x: (B, T, D) — input tokens |
| control_camera_dit_input: dict with keys: |
| - viewmats: (B, N, 4, 4) |
| - K: (B, N, 3, 3) |
| """ |
| B, T, D = x.shape |
| N = control_camera_dit_input["viewmats"].shape[1] |
| H, W = self.patches_y, self.patches_x |
| assert T == N * H * W or T == N, f"Expected token shape ({N}×{H}×{W} or {N}), got {T}" |
|
|
| if hasattr(self, "cam_encoder") and "cam_emb" in control_camera_dit_input: |
| cam_emb = control_camera_dit_input["cam_emb"] |
| y = self.cam_encoder(cam_emb) |
| if y.shape[1] != T: |
| hw = T // cam_emb.shape[1] |
| y = repeat(y, "b f d -> b (f hw) d", hw=hw) |
| x = x + y |
|
|
| |
| q = self.q_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2) |
| k = self.k_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2) |
| v = self.v_proj(x).view(B, T, self.num_heads, self.head_dim).transpose(1, 2) |
|
|
| |
| self.prope_attn._precompute_and_cache_apply_fns( |
| viewmats=control_camera_dit_input["viewmats"], |
| Ks=control_camera_dit_input.get("K", None), |
| coeffs_x=control_camera_dit_input.get("coeffs_x", None), |
| coeffs_y=control_camera_dit_input.get("coeffs_y", None), |
| ) |
|
|
| |
| q = self.prope_attn._apply_to_q(q) |
| k = self.prope_attn._apply_to_kv(k) |
| v = self.prope_attn._apply_to_kv(v) |
|
|
| |
| q = rearrange(q, "b h t d -> b t (h d)") |
| k = rearrange(k, "b h t d -> b t (h d)") |
| v = rearrange(v, "b h t d -> b t (h d)") |
|
|
| |
| out = flash_attention(q, k, v, num_heads=self.num_heads) |
|
|
| |
| out = rearrange(out, "b t (h d) -> b h t d", h=self.num_heads) |
|
|
| |
| out = self.prope_attn._apply_to_o(out) |
|
|
| |
| out = out.transpose(1, 2).reshape(B, T, -1) |
| return self.out_proj(out) |
|
|