Files

721 lines
30 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
This module implements the 3D cache system for GEN3C video generation with camera control.
The cache maintains a point cloud representation of the scene, enabling:
- Unprojecting depth maps to 3D world points
- Forward warping rendered views to new camera poses
- Managing multiple frame buffers for temporal consistency
"""
from __future__ import annotations
import torch
import torch.nn.functional as F
from einops import rearrange
def inverse_with_conversion(mtx: torch.Tensor) -> torch.Tensor:
"""Compute matrix inverse with float32 conversion for numerical stability."""
return torch.linalg.inv(mtx.to(torch.float32)).to(mtx.dtype)
def create_grid(b: int, h: int, w: int, device: str = "cpu", dtype: torch.dtype = torch.float32) -> torch.Tensor:
"""
Create a dense grid of (x, y) coordinates of shape (b, 2, h, w).
Args:
b: Batch size
h: Height
w: Width
device: Device for tensor creation
dtype: Data type for tensor
Returns:
Grid tensor of shape (b, 2, h, w)
"""
x = torch.arange(0, w, device=device, dtype=dtype).view(1, 1, 1, w).expand(b, 1, h, w)
y = torch.arange(0, h, device=device, dtype=dtype).view(1, 1, h, 1).expand(b, 1, h, w)
return torch.cat([x, y], dim=1)
def unproject_points(
depth: torch.Tensor,
w2c: torch.Tensor,
intrinsic: torch.Tensor,
is_depth: bool = True,
mask: torch.Tensor | None = None,
) -> torch.Tensor:
"""
Unproject depth map to 3D world points.
Args:
depth: (b, 1, h, w) depth map
w2c: (b, 4, 4) world-to-camera transformation matrix
intrinsic: (b, 3, 3) camera intrinsic matrix
is_depth: If True, depth is z-depth; if False, depth is distance to camera
mask: Optional (b, h, w) or (b, 1, h, w) mask for valid pixels
Returns:
world_points: (b, h, w, 3) 3D world coordinates
"""
b, _, h, w = depth.shape
device = depth.device
dtype = depth.dtype
if mask is None:
mask = depth > 0
if mask.dim() == depth.dim() and mask.shape[1] == 1:
mask = mask[:, 0]
idx = torch.nonzero(mask)
if idx.numel() == 0:
return torch.zeros((b, h, w, 3), device=device, dtype=dtype)
b_idx, y_idx, x_idx = idx[:, 0], idx[:, 1], idx[:, 2]
intrinsic_inv = inverse_with_conversion(intrinsic) # (b, 3, 3)
x_valid = x_idx.to(dtype)
y_valid = y_idx.to(dtype)
ones = torch.ones_like(x_valid)
pos = torch.stack([x_valid, y_valid, ones], dim=1).unsqueeze(-1) # (N, 3, 1)
intrinsic_inv_valid = intrinsic_inv[b_idx] # (N, 3, 3)
unnormalized_pos = torch.matmul(intrinsic_inv_valid, pos) # (N, 3, 1)
depth_valid = depth[b_idx, 0, y_idx, x_idx].view(-1, 1, 1)
if is_depth:
world_points_cam = depth_valid * unnormalized_pos
else:
norm_val = torch.norm(unnormalized_pos, dim=1, keepdim=True)
direction = unnormalized_pos / (norm_val + 1e-8)
world_points_cam = depth_valid * direction
ones_h = torch.ones((world_points_cam.shape[0], 1, 1), device=device, dtype=dtype)
world_points_homo = torch.cat([world_points_cam, ones_h], dim=1) # (N, 4, 1)
trans = inverse_with_conversion(w2c) # (b, 4, 4)
trans_valid = trans[b_idx] # (N, 4, 4)
world_points_transformed = torch.matmul(trans_valid, world_points_homo) # (N, 4, 1)
sparse_points = world_points_transformed[:, :3, 0] # (N, 3)
out_points = torch.zeros((b, h, w, 3), device=device, dtype=dtype)
out_points[b_idx, y_idx, x_idx, :] = sparse_points
return out_points
def project_points(
world_points: torch.Tensor,
w2c: torch.Tensor,
intrinsic: torch.Tensor,
) -> torch.Tensor:
"""
Project 3D world points to 2D pixel coordinates.
Args:
world_points: (b, h, w, 3) 3D world coordinates
w2c: (b, 4, 4) world-to-camera transformation matrix
intrinsic: (b, 3, 3) camera intrinsic matrix
Returns:
projected_points: (b, h, w, 3, 1) projected 2D coordinates (x, y, z)
"""
world_points = world_points.unsqueeze(-1) # (b, h, w, 3, 1)
b, h, w, _, _ = world_points.shape
ones_4d = torch.ones((b, h, w, 1, 1), device=world_points.device, dtype=world_points.dtype)
world_points_homo = torch.cat([world_points, ones_4d], dim=3) # (b, h, w, 4, 1)
trans_4d = w2c[:, None, None] # (b, 1, 1, 4, 4)
camera_points_homo = torch.matmul(trans_4d, world_points_homo) # (b, h, w, 4, 1)
camera_points = camera_points_homo[:, :, :, :3] # (b, h, w, 3, 1)
intrinsic_4d = intrinsic[:, None, None] # (b, 1, 1, 3, 3)
projected_points = torch.matmul(intrinsic_4d, camera_points) # (b, h, w, 3, 1)
return projected_points
def bilinear_splatting(
frame1: torch.Tensor,
mask1: torch.Tensor | None,
depth1: torch.Tensor,
flow12: torch.Tensor,
flow12_mask: torch.Tensor | None = None,
is_image: bool = False,
depth_weight_scale: float = 50.0,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Bilinear splatting for forward warping.
Args:
frame1: (b, c, h, w) source frame
mask1: (b, 1, h, w) valid pixel mask (1 for known, 0 for unknown)
depth1: (b, 1, h, w) depth map
flow12: (b, 2, h, w) optical flow from frame1 to frame2
flow12_mask: (b, 1, h, w) flow validity mask
is_image: If True, output will be clipped to (-1, 1) range
depth_weight_scale: Scale factor for depth weighting
Returns:
warped_frame2: (b, c, h, w) warped frame
mask2: (b, 1, h, w) validity mask for warped frame
"""
b, c, h, w = frame1.shape
device = frame1.device
dtype = frame1.dtype
if mask1 is None:
mask1 = torch.ones(size=(b, 1, h, w), device=device, dtype=dtype)
if flow12_mask is None:
flow12_mask = torch.ones(size=(b, 1, h, w), device=device, dtype=dtype)
grid = create_grid(b, h, w, device=device, dtype=dtype)
trans_pos = flow12 + grid
trans_pos_offset = trans_pos + 1
trans_pos_floor = torch.floor(trans_pos_offset).long()
trans_pos_ceil = torch.ceil(trans_pos_offset).long()
trans_pos_offset = torch.stack(
[torch.clamp(trans_pos_offset[:, 0], min=0, max=w + 1),
torch.clamp(trans_pos_offset[:, 1], min=0, max=h + 1)],
dim=1)
trans_pos_floor = torch.stack(
[torch.clamp(trans_pos_floor[:, 0], min=0, max=w + 1),
torch.clamp(trans_pos_floor[:, 1], min=0, max=h + 1)],
dim=1)
trans_pos_ceil = torch.stack(
[torch.clamp(trans_pos_ceil[:, 0], min=0, max=w + 1),
torch.clamp(trans_pos_ceil[:, 1], min=0, max=h + 1)],
dim=1)
# Bilinear weights
prox_weight_nw = (1 - (trans_pos_offset[:, 1:2] - trans_pos_floor[:, 1:2])) * \
(1 - (trans_pos_offset[:, 0:1] - trans_pos_floor[:, 0:1]))
prox_weight_sw = (1 - (trans_pos_ceil[:, 1:2] - trans_pos_offset[:, 1:2])) * \
(1 - (trans_pos_offset[:, 0:1] - trans_pos_floor[:, 0:1]))
prox_weight_ne = (1 - (trans_pos_offset[:, 1:2] - trans_pos_floor[:, 1:2])) * \
(1 - (trans_pos_ceil[:, 0:1] - trans_pos_offset[:, 0:1]))
prox_weight_se = (1 - (trans_pos_ceil[:, 1:2] - trans_pos_offset[:, 1:2])) * \
(1 - (trans_pos_ceil[:, 0:1] - trans_pos_offset[:, 0:1]))
# Depth weighting for occlusion handling
clamped_depth1 = torch.clamp(depth1, min=0)
log_depth1 = torch.log1p(clamped_depth1)
exponent = log_depth1 / (log_depth1.max() + 1e-7) * depth_weight_scale
max_exponent = 80.0 if dtype in [torch.float32, torch.bfloat16] else 10.0
clamped_exponent = torch.clamp(exponent, max=max_exponent)
depth_weights = torch.exp(clamped_exponent) + 1e-7
weight_nw = torch.moveaxis(prox_weight_nw * mask1 * flow12_mask / depth_weights, [0, 1, 2, 3], [0, 3, 1, 2])
weight_sw = torch.moveaxis(prox_weight_sw * mask1 * flow12_mask / depth_weights, [0, 1, 2, 3], [0, 3, 1, 2])
weight_ne = torch.moveaxis(prox_weight_ne * mask1 * flow12_mask / depth_weights, [0, 1, 2, 3], [0, 3, 1, 2])
weight_se = torch.moveaxis(prox_weight_se * mask1 * flow12_mask / depth_weights, [0, 1, 2, 3], [0, 3, 1, 2])
warped_frame = torch.zeros(size=(b, h + 2, w + 2, c), dtype=dtype, device=device)
warped_weights = torch.zeros(size=(b, h + 2, w + 2, 1), dtype=dtype, device=device)
frame1_cl = torch.moveaxis(frame1, [0, 1, 2, 3], [0, 3, 1, 2])
batch_indices = torch.arange(b, device=device, dtype=torch.long)[:, None, None]
warped_frame.index_put_((batch_indices, trans_pos_floor[:, 1], trans_pos_floor[:, 0]),
frame1_cl * weight_nw,
accumulate=True)
warped_frame.index_put_((batch_indices, trans_pos_ceil[:, 1], trans_pos_floor[:, 0]),
frame1_cl * weight_sw,
accumulate=True)
warped_frame.index_put_((batch_indices, trans_pos_floor[:, 1], trans_pos_ceil[:, 0]),
frame1_cl * weight_ne,
accumulate=True)
warped_frame.index_put_((batch_indices, trans_pos_ceil[:, 1], trans_pos_ceil[:, 0]),
frame1_cl * weight_se,
accumulate=True)
warped_weights.index_put_((batch_indices, trans_pos_floor[:, 1], trans_pos_floor[:, 0]), weight_nw, accumulate=True)
warped_weights.index_put_((batch_indices, trans_pos_ceil[:, 1], trans_pos_floor[:, 0]), weight_sw, accumulate=True)
warped_weights.index_put_((batch_indices, trans_pos_floor[:, 1], trans_pos_ceil[:, 0]), weight_ne, accumulate=True)
warped_weights.index_put_((batch_indices, trans_pos_ceil[:, 1], trans_pos_ceil[:, 0]), weight_se, accumulate=True)
warped_frame_cf = torch.moveaxis(warped_frame, [0, 1, 2, 3], [0, 2, 3, 1])
warped_weights_cf = torch.moveaxis(warped_weights, [0, 1, 2, 3], [0, 2, 3, 1])
cropped_warped_frame = warped_frame_cf[:, :, 1:-1, 1:-1]
cropped_weights = warped_weights_cf[:, :, 1:-1, 1:-1]
cropped_weights = torch.nan_to_num(cropped_weights, nan=1000.0)
mask = cropped_weights > 0
zero_value = -1 if is_image else 0
zero_tensor = torch.tensor(zero_value, dtype=frame1.dtype, device=frame1.device)
warped_frame2 = torch.where(mask, cropped_warped_frame / cropped_weights, zero_tensor)
mask2 = mask.to(frame1)
if is_image:
warped_frame2 = torch.clamp(warped_frame2, min=-1, max=1)
return warped_frame2, mask2
def forward_warp(
frame1: torch.Tensor,
mask1: torch.Tensor | None,
depth1: torch.Tensor | None,
transformation1: torch.Tensor | None,
transformation2: torch.Tensor,
intrinsic1: torch.Tensor | None,
intrinsic2: torch.Tensor | None,
is_image: bool = True,
is_depth: bool = True,
render_depth: bool = False,
world_points1: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor]:
"""
Forward warp frame1 to a new view defined by transformation2.
Args:
frame1: (b, c, h, w) source frame in range [-1, 1] for images
mask1: (b, 1, h, w) valid pixel mask
depth1: (b, 1, h, w) depth map (required if world_points1 is None)
transformation1: (b, 4, 4) source camera w2c (required if depth1 is provided)
transformation2: (b, 4, 4) target camera w2c
intrinsic1: (b, 3, 3) source camera intrinsics
intrinsic2: (b, 3, 3) target camera intrinsics
is_image: If True, output will be clipped to (-1, 1)
is_depth: If True, depth1 is z-depth; if False, it's distance
render_depth: If True, also return the warped depth map
world_points1: (b, h, w, 3) pre-computed world points (alternative to depth1)
Returns:
warped_frame2: (b, c, h, w) warped frame
mask2: (b, 1, h, w) validity mask
warped_depth2: (b, h, w) warped depth (if render_depth=True)
flow12: (b, 2, h, w) optical flow
"""
device = frame1.device
b, c, h, w = frame1.shape
dtype = frame1.dtype
if mask1 is None:
mask1 = torch.ones(size=(b, 1, h, w), device=device, dtype=dtype)
if intrinsic2 is None:
assert intrinsic1 is not None
intrinsic2 = intrinsic1.clone()
if world_points1 is not None:
# Use pre-computed world points
assert world_points1.shape == (b, h, w, 3)
trans_points1 = project_points(world_points1, transformation2, intrinsic2)
else:
# Compute from depth
assert depth1 is not None and transformation1 is not None
assert depth1.shape == (b, 1, h, w)
depth1 = torch.nan_to_num(depth1, nan=1e4)
depth1 = torch.clamp(depth1, min=0, max=1e4)
# Unproject to world, then project to target view
world_points1 = unproject_points(depth1, transformation1, intrinsic1, is_depth=is_depth)
trans_points1 = project_points(world_points1, transformation2, intrinsic2)
# Filter points behind camera
mask1 = mask1 * (trans_points1[:, :, :, 2, 0].unsqueeze(1) > 0)
trans_coordinates = trans_points1[:, :, :, :2, 0] / (trans_points1[:, :, :, 2:3, 0] + 1e-7)
trans_coordinates = trans_coordinates.permute(0, 3, 1, 2) # b, 2, h, w
trans_depth1 = trans_points1[:, :, :, 2, 0].unsqueeze(1)
grid = create_grid(b, h, w, device=device, dtype=dtype)
flow12 = trans_coordinates - grid
warped_frame2, mask2 = bilinear_splatting(frame1, mask1, trans_depth1, flow12, None, is_image=is_image)
warped_depth2 = None
if render_depth:
warped_depth2 = bilinear_splatting(trans_depth1, mask1, trans_depth1, flow12, None, is_image=False)[0][:, 0]
return warped_frame2, mask2, warped_depth2, flow12
def reliable_depth_mask_range_batch(
depth: torch.Tensor,
window_size: int = 5,
ratio_thresh: float = 0.05,
eps: float = 1e-6,
) -> torch.Tensor:
"""
Compute a mask for reliable depth values based on local variation.
Args:
depth: (b, h, w) or (b, 1, h, w) depth map
window_size: Size of the local window (must be odd)
ratio_thresh: Threshold for depth variation ratio
eps: Small epsilon for numerical stability
Returns:
reliable_mask: Boolean mask where True indicates reliable depth
"""
assert window_size % 2 == 1, "Window size must be odd."
if depth.dim() == 3:
depth_unsq = depth.unsqueeze(1)
elif depth.dim() == 4:
depth_unsq = depth
else:
raise ValueError("depth tensor must be of shape (b, h, w) or (b, 1, h, w)")
local_max = F.max_pool2d(depth_unsq, kernel_size=window_size, stride=1, padding=window_size // 2)
local_min = -F.max_pool2d(-depth_unsq, kernel_size=window_size, stride=1, padding=window_size // 2)
local_mean = F.avg_pool2d(depth_unsq, kernel_size=window_size, stride=1, padding=window_size // 2)
ratio = (local_max - local_min) / (local_mean + eps)
reliable_mask = (ratio < ratio_thresh) & (depth_unsq > 0)
return reliable_mask
class Cache3DBase:
"""
Base class for 3D cache management.
The cache maintains:
- input_image: RGB images stored in the cache
- input_points: 3D world coordinates for each pixel
- input_mask: Validity mask for each pixel
"""
def __init__(
self,
input_image: torch.Tensor,
input_depth: torch.Tensor,
input_w2c: torch.Tensor,
input_intrinsics: torch.Tensor,
input_mask: torch.Tensor | None = None,
input_format: list[str] | None = None,
input_points: torch.Tensor | None = None,
weight_dtype: torch.dtype = torch.float32,
is_depth: bool = True,
device: str = "cuda",
filter_points_threshold: float = 1.0,
):
"""
Initialize the 3D cache.
Args:
input_image: Input image tensor with varying dimensions
input_depth: Depth map tensor
input_w2c: World-to-camera transformation matrix
input_intrinsics: Camera intrinsic matrix
input_mask: Optional validity mask
input_format: Dimension labels for input_image (e.g., ['B', 'C', 'H', 'W'])
input_points: Pre-computed 3D world points (alternative to depth)
weight_dtype: Data type for computations
is_depth: If True, input_depth is z-depth; if False, it's distance
device: Computation device
filter_points_threshold: Threshold for filtering unreliable depth
"""
self.weight_dtype = weight_dtype
self.is_depth = is_depth
self.device = device
self.filter_points_threshold = filter_points_threshold
if input_format is None:
assert input_image.dim() == 4
input_format = ["B", "C", "H", "W"]
# Map dimension names to indices
format_to_indices = {dim: idx for idx, dim in enumerate(input_format)}
input_shape = input_image.shape
if input_mask is not None:
input_image = torch.cat([input_image, input_mask], dim=format_to_indices.get("C"))
# Extract dimensions
B = input_shape[format_to_indices.get("B", 0)] if "B" in format_to_indices else 1
F = input_shape[format_to_indices.get("F", 0)] if "F" in format_to_indices else 1
N = input_shape[format_to_indices.get("N", 0)] if "N" in format_to_indices else 1
V = input_shape[format_to_indices.get("V", 0)] if "V" in format_to_indices else 1
H = input_shape[format_to_indices.get("H", 0)] if "H" in format_to_indices else None
W = input_shape[format_to_indices.get("W", 0)] if "W" in format_to_indices else None
# Reorder dimensions to B x F x N x V x C x H x W
desired_dims = ["B", "F", "N", "V", "C", "H", "W"]
permute_order: list[int | None] = []
for dim in desired_dims:
idx = format_to_indices.get(dim)
permute_order.append(idx)
permute_indices = [idx for idx in permute_order if idx is not None]
input_image = input_image.permute(*permute_indices)
for i, idx in enumerate(permute_order):
if idx is None:
input_image = input_image.unsqueeze(i)
# Now input_image has shape B x F x N x V x C x H x W
if input_mask is not None:
self.input_image, self.input_mask = input_image[:, :, :, :, :3], input_image[:, :, :, :, 3:]
self.input_mask = self.input_mask.to("cpu")
else:
self.input_mask = None
self.input_image = input_image
self.input_image = self.input_image.to(weight_dtype).to("cpu")
# Compute 3D world points
if input_points is not None:
self.input_points = input_points.reshape(B, F, N, V, H, W, 3).to("cpu")
self.input_depth = None
else:
input_depth = torch.nan_to_num(input_depth, nan=100)
input_depth = torch.clamp(input_depth, min=0, max=100)
if weight_dtype == torch.float16:
input_depth = torch.clamp(input_depth, max=70)
self.input_points = (unproject_points(
input_depth.reshape(-1, 1, H, W),
input_w2c.reshape(-1, 4, 4),
input_intrinsics.reshape(-1, 3, 3),
is_depth=self.is_depth,
).to(weight_dtype).reshape(B, F, N, V, H, W, 3).to("cpu"))
self.input_depth = input_depth
# Filter unreliable depth
if self.filter_points_threshold < 1.0 and input_depth is not None:
input_depth = input_depth.reshape(-1, 1, H, W)
depth_mask = reliable_depth_mask_range_batch(input_depth,
ratio_thresh=self.filter_points_threshold).reshape(
B, F, N, V, 1, H, W)
if self.input_mask is None:
self.input_mask = depth_mask.to("cpu")
else:
self.input_mask = self.input_mask * depth_mask.to(self.input_mask.device)
def update_cache(self, **kwargs):
"""Update the cache with new frames. To be implemented by subclasses."""
raise NotImplementedError
def input_frame_count(self) -> int:
"""Return the number of frames in the cache."""
return self.input_image.shape[1]
def render_cache(
self,
target_w2cs: torch.Tensor,
target_intrinsics: torch.Tensor,
render_depth: bool = False,
start_frame_idx: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Render the cached 3D points from new camera viewpoints.
Args:
target_w2cs: (b, F_target, 4, 4) target camera transformations
target_intrinsics: (b, F_target, 3, 3) target camera intrinsics
render_depth: If True, return depth instead of RGB
start_frame_idx: Starting frame index in the cache
Returns:
pixels: (b, F_target, N, c, h, w) rendered images or depth
masks: (b, F_target, N, 1, h, w) validity masks
"""
bs, F_target, _, _ = target_w2cs.shape
B, F, N, V, C, H, W = self.input_image.shape
assert bs == B
target_w2cs = target_w2cs.reshape(B, F_target, 1, 4, 4).expand(B, F_target, N, 4, 4).reshape(-1, 4, 4)
target_intrinsics = target_intrinsics.reshape(B, F_target, 1, 3, 3).expand(B, F_target, N, 3,
3).reshape(-1, 3, 3)
# Prepare inputs
first_images = rearrange(
self.input_image[:, start_frame_idx:start_frame_idx + F_target].expand(B, F_target, N, V, C, H, W),
"B F N V C H W -> (B F N) V C H W")
first_points = rearrange(
self.input_points[:, start_frame_idx:start_frame_idx + F_target].expand(B, F_target, N, V, H, W, 3),
"B F N V H W C -> (B F N) V H W C")
first_masks = rearrange(
self.input_mask[:, start_frame_idx:start_frame_idx + F_target].expand(B, F_target, N, V, 1, H, W),
"B F N V C H W -> (B F N) V C H W") if self.input_mask is not None else None
# Process in chunks for memory efficiency
if first_images.shape[1] == 1:
warp_chunk_size = 2
rendered_warp_images = []
rendered_warp_masks = []
rendered_warp_depth = []
first_images = first_images.squeeze(1)
first_points = first_points.squeeze(1)
first_masks = first_masks.squeeze(1) if first_masks is not None else None
for i in range(0, first_images.shape[0], warp_chunk_size):
with torch.no_grad():
imgs_chunk = first_images[i:i + warp_chunk_size].to(self.device, non_blocking=True)
pts_chunk = first_points[i:i + warp_chunk_size].to(self.device, non_blocking=True)
masks_chunk = (first_masks[i:i + warp_chunk_size].to(self.device, non_blocking=True)
if first_masks is not None else None)
(
rendered_warp_images_chunk,
rendered_warp_masks_chunk,
rendered_warp_depth_chunk,
_,
) = forward_warp(
imgs_chunk,
mask1=masks_chunk,
depth1=None,
transformation1=None,
transformation2=target_w2cs[i:i + warp_chunk_size],
intrinsic1=target_intrinsics[i:i + warp_chunk_size],
intrinsic2=target_intrinsics[i:i + warp_chunk_size],
render_depth=render_depth,
world_points1=pts_chunk,
)
rendered_warp_images.append(rendered_warp_images_chunk.to("cpu"))
rendered_warp_masks.append(rendered_warp_masks_chunk.to("cpu"))
if render_depth:
rendered_warp_depth.append(rendered_warp_depth_chunk.to("cpu"))
del imgs_chunk, pts_chunk, masks_chunk
torch.cuda.empty_cache()
rendered_warp_images = torch.cat(rendered_warp_images, dim=0)
rendered_warp_masks = torch.cat(rendered_warp_masks, dim=0)
if render_depth:
rendered_warp_depth = torch.cat(rendered_warp_depth, dim=0)
else:
raise NotImplementedError("Multi-view rendering not yet supported")
pixels = rearrange(rendered_warp_images, "(b f n) c h w -> b f n c h w", b=bs, f=F_target, n=N)
masks = rearrange(rendered_warp_masks, "(b f n) c h w -> b f n c h w", b=bs, f=F_target, n=N)
if render_depth:
pixels = rearrange(rendered_warp_depth, "(b f n) h w -> b f n h w", b=bs, f=F_target, n=N)
return pixels.to(self.device), masks.to(self.device)
class Cache3DBuffer(Cache3DBase):
"""
3D cache with frame buffer support.
This class manages multiple frame buffers for temporal consistency
and supports noise augmentation for training stability.
"""
def __init__(
self,
frame_buffer_max: int = 2,
noise_aug_strength: float = 0.0,
generator: torch.Generator | None = None,
**kwargs,
):
"""
Initialize the buffered 3D cache.
Args:
frame_buffer_max: Maximum number of frames to buffer
noise_aug_strength: Strength of noise augmentation per buffer
generator: Random generator for reproducibility
**kwargs: Arguments passed to Cache3DBase
"""
super().__init__(**kwargs)
self.frame_buffer_max = frame_buffer_max
self.noise_aug_strength = noise_aug_strength
self.generator = generator
def update_cache(
self,
new_image: torch.Tensor,
new_depth: torch.Tensor,
new_w2c: torch.Tensor,
new_mask: torch.Tensor | None = None,
new_intrinsics: torch.Tensor | None = None,
):
"""
Update the cache with a new frame.
Args:
new_image: (B, C, H, W) new RGB image
new_depth: (B, 1, H, W) new depth map
new_w2c: (B, 4, 4) new world-to-camera transformation
new_mask: Optional (B, 1, H, W) validity mask
new_intrinsics: (B, 3, 3) camera intrinsics (optional)
"""
new_image = new_image.to(self.weight_dtype).to(self.device)
new_depth = new_depth.to(self.weight_dtype).to(self.device)
new_w2c = new_w2c.to(self.weight_dtype).to(self.device)
if new_intrinsics is not None:
new_intrinsics = new_intrinsics.to(self.weight_dtype).to(self.device)
new_depth = torch.nan_to_num(new_depth, nan=1e4)
new_depth = torch.clamp(new_depth, min=0, max=1e4)
B, F, N, V, C, H, W = self.input_image.shape
# Compute new 3D points
new_points = unproject_points(new_depth, new_w2c, new_intrinsics, is_depth=self.is_depth).cpu()
new_image = new_image.cpu()
if self.filter_points_threshold < 1.0:
new_depth = new_depth.reshape(-1, 1, H, W)
depth_mask = reliable_depth_mask_range_batch(new_depth,
ratio_thresh=self.filter_points_threshold).reshape(B, 1, H, W)
new_mask = depth_mask.to("cpu") if new_mask is None else new_mask * depth_mask.to(new_mask.device)
if new_mask is not None:
new_mask = new_mask.cpu()
# Update buffer (newest frame first)
if self.frame_buffer_max > 1:
if self.input_image.shape[2] < self.frame_buffer_max:
self.input_image = torch.cat([new_image[:, None, None, None], self.input_image], 2)
self.input_points = torch.cat([new_points[:, None, None, None], self.input_points], 2)
if self.input_mask is not None:
self.input_mask = torch.cat([new_mask[:, None, None, None], self.input_mask], 2)
else:
self.input_image[:, :, 0] = new_image[:, None, None]
self.input_points[:, :, 0] = new_points[:, None, None]
if self.input_mask is not None:
self.input_mask[:, :, 0] = new_mask[:, None, None]
else:
self.input_image = new_image[:, None, None, None]
self.input_points = new_points[:, None, None, None]
def render_cache(
self,
target_w2cs: torch.Tensor,
target_intrinsics: torch.Tensor,
render_depth: bool = False,
start_frame_idx: int = 0,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Render the cache with optional noise augmentation.
Args:
target_w2cs: (b, F_target, 4, 4) target camera transformations
target_intrinsics: (b, F_target, 3, 3) target camera intrinsics
render_depth: If True, return depth instead of RGB
start_frame_idx: Starting frame index (must be 0 for this class)
Returns:
pixels: (b, F_target, N, c, h, w) rendered images
masks: (b, F_target, N, 1, h, w) validity masks
"""
assert start_frame_idx == 0, "start_frame_idx must be 0 for Cache3DBuffer"
output_device = target_w2cs.device
target_w2cs = target_w2cs.to(self.weight_dtype).to(self.device)
target_intrinsics = target_intrinsics.to(self.weight_dtype).to(self.device)
pixels, masks = super().render_cache(target_w2cs, target_intrinsics, render_depth)
pixels = pixels.to(output_device)
masks = masks.to(output_device)
# Apply noise augmentation (stronger for older buffers)
if not render_depth and self.noise_aug_strength > 0:
noise = torch.randn(pixels.shape, generator=self.generator, device=pixels.device, dtype=pixels.dtype)
per_buffer_noise = (torch.arange(start=pixels.shape[2] - 1, end=-1, step=-1, device=pixels.device) *
self.noise_aug_strength)
pixels = pixels + noise * per_buffer_noise.reshape(1, 1, -1, 1, 1, 1)
return pixels, masks