Spaces:
Running on Zero
Running on Zero
Download model/rayzer_official_v3.py from uva-cv-lab/wildrayzer: direct link, hf CLI and curl.
- Browser
- Download file 148 kB
-
https://huggingface.co/spaces/uva-cv-lab/wildrayzer/resolve/main/model/rayzer_official_v3.py
- Command line
-
hf download hf://spaces/uva-cv-lab/wildrayzer/model/rayzer_official_v3.py
-
curl -L -o rayzer_official_v3.py https://huggingface.co/spaces/uva-cv-lab/wildrayzer/resolve/main/model/rayzer_official_v3.py
148 kB
| # Copyright (c) 2025 Haian Jin. Created for the LVSM project (ICLR 2025). | |
| import copy | |
| from datetime import datetime | |
| import os | |
| import math | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| from easydict import EasyDict as edict | |
| from skimage.metrics import structural_similarity | |
| from einops.layers.torch import Rearrange | |
| from einops import rearrange, repeat | |
| import traceback | |
| import torch.distributed as dist | |
| from transformers import AutoImageProcessor, AutoModel | |
| import cv2 | |
| from utils import camera_utils, data_utils | |
| from .transformer import ( | |
| QK_Norm_TransformerBlock, | |
| init_weights, | |
| _init_weights, | |
| _init_weights_layerwise, | |
| ) | |
| from .loss import LossComputer_official | |
| import torch.nn.functional as F | |
| from utils.pe_utils_official import get_1d_sincos_pos_emb_from_grid, get_2d_sincos_pos_embed | |
| from utils.pose_utils import rot6d2mat, quat2mat | |
| from utils.metric_utils import compute_psnr, compute_lpips, compute_ssim | |
| from utils.data_utils import create_video_from_frames | |
| from utils.training_utils import bilinear_resize | |
| from PIL import Image | |
| def _gn_groups(out_ch: int, prefer: int = 32) -> int: | |
| g = math.gcd(out_ch, prefer) | |
| return g if g > 0 else 1 | |
| class Up2x(nn.Module): | |
| def __init__(self, in_ch, out_ch, gn_groups=32): | |
| super().__init__() | |
| g = _gn_groups(out_ch, gn_groups) | |
| # 2x upsample (bilinear) then change channels in_ch -> out_ch | |
| self.conv_up = nn.Conv2d(in_ch, out_ch, kernel_size=1) | |
| # refinement at target channels | |
| self.refine = nn.Sequential( | |
| nn.GroupNorm(g, out_ch), | |
| nn.GELU(), | |
| nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1), | |
| nn.GroupNorm(g, out_ch), | |
| nn.GELU(), | |
| ) | |
| def forward(self, x): | |
| # single upsample | |
| x = F.interpolate(x, scale_factor=2, mode="bilinear", align_corners=False) | |
| # project channels to out_ch | |
| x = self.conv_up(x) | |
| # refine | |
| x = self.refine(x) | |
| return x | |
| class MotionMaskPredictor(nn.Module): | |
| """ | |
| Three-modality motion mask predictor: DINOv3 + Image Tokens + Camera Poses. | |
| Architecture: | |
| 1. Feature Fusion: Combines semantic (DINO), appearance (tokens), and geometric (pose) cues | |
| 2. Motion Reasoning Transformer: Processes fused features to understand motion patterns | |
| 3. Improved DPT Decoder: Multi-scale upsampling with skip connections for crisp masks | |
| This is the M-step of the EM training loop: learns to predict motion masks from GT images. | |
| """ | |
| def __init__(self, config, dinov3_backbone, dinov3_processor): | |
| super().__init__() | |
| self.config = config | |
| # Store DINOv3 as non-persistent reference (moves to device but not saved in state_dict) | |
| # We use object.__setattr__ to bypass nn.Module's __setattr__ which would save it | |
| object.__setattr__(self, "_dinov3_backbone_ref", dinov3_backbone) | |
| object.__setattr__(self, "_dinov3_processor_ref", dinov3_processor) | |
| self.use_grad_checkpoint = config.training.get("grad_checkpoint_every", 1) > 0 | |
| # Manually register DINOv3's device placement hook (moves to device without saving) | |
| # This ensures it follows the parent model's device | |
| self._register_dinov3_device_hook() | |
| # Dimensions | |
| d_main = config.model.transformer.d # Main transformer dim (e.g., 768) | |
| d_dino = dinov3_backbone.config.hidden_size # DINOv3 dim (e.g., 1024 for ViT-L) | |
| d_fused = d_main # Output fusion dimension | |
| # Store DINOv3 config | |
| self.patch_size_dino = getattr(dinov3_backbone.config, "patch_size", 16) | |
| self.num_register_tokens = getattr(dinov3_backbone.config, "num_register_tokens", 4) | |
| # Normalization buffers for DINOv3 (from processor) | |
| mean = torch.tensor(dinov3_processor.image_mean, dtype=torch.float32).view(1, 3, 1, 1) | |
| std = torch.tensor(dinov3_processor.image_std, dtype=torch.float32).view(1, 3, 1, 1) | |
| self.register_buffer("dino_norm_mean", mean) | |
| self.register_buffer("dino_norm_std", std) | |
| # 1. Feature Fusion Module | |
| # Project each modality to common dimension, then fuse | |
| self.dino_proj = nn.Sequential( | |
| nn.LayerNorm(d_dino, bias=False), | |
| nn.Linear(d_dino, d_fused, bias=True), | |
| ) | |
| self.token_proj = nn.Sequential( | |
| nn.LayerNorm(d_main, bias=False), | |
| nn.Linear(d_main, d_fused, bias=True), | |
| ) | |
| self.pose_proj = nn.Sequential( | |
| nn.LayerNorm(d_main, bias=False), # Plücker embeddings already in d_main | |
| nn.Linear(d_main, d_fused, bias=True), | |
| ) | |
| # Fusion via concatenation + MLP (3*d_fused -> d_fused) | |
| self.fusion_mlp = nn.Sequential( | |
| nn.LayerNorm(d_fused * 3, bias=False), | |
| nn.Linear(d_fused * 3, d_fused, bias=True), | |
| nn.GELU(), | |
| nn.Linear(d_fused, d_fused, bias=True), | |
| ) | |
| use_qk_norm = config.model.transformer.get("use_qk_norm", False) | |
| d_head = d_fused // 12 # 12 heads for 768-dim | |
| self.motion_transformer = nn.ModuleList( | |
| [QK_Norm_TransformerBlock(d_fused, d_head, use_qk_norm=use_qk_norm) for _ in range(4)] | |
| ) | |
| # 3. Improved DPT Head with Multi-Scale Features | |
| # Following original DPT: extract features at multiple scales and fuse | |
| self.dpt_readout = nn.Sequential( | |
| nn.LayerNorm(d_fused, bias=False), | |
| nn.Linear(d_fused, d_fused, bias=True), | |
| ) | |
| # Multi-scale feature extraction (3 scales) | |
| # Scale 1: 16x16 (patch level) | |
| self.scale1_refine = nn.Sequential( | |
| nn.Conv2d(d_fused, d_fused // 2, kernel_size=3, padding=1), | |
| nn.GroupNorm(num_groups=32, num_channels=d_fused // 2), | |
| nn.GELU(), | |
| nn.Conv2d(d_fused // 2, d_fused // 2, kernel_size=3, padding=1), | |
| ) | |
| self.up2 = Up2x(d_fused // 2, d_fused // 4) # 16 -> 32 | |
| self.up3 = Up2x(d_fused // 4, d_fused // 8) # 32 -> 64 | |
| self.up4 = Up2x(d_fused // 8, d_fused // 16) # 64 -> 128 | |
| self.up5 = Up2x(d_fused // 16, d_fused // 32) # 128 -> 256 | |
| self.scale2_refine = nn.Sequential( | |
| nn.Conv2d(d_fused // 4, d_fused // 4, kernel_size=3, padding=1), | |
| nn.GroupNorm(num_groups=32, num_channels=d_fused // 4), | |
| nn.GELU(), | |
| ) | |
| self.scale3_refine = nn.Sequential( | |
| nn.Conv2d(d_fused // 8, d_fused // 8, kernel_size=3, padding=1), | |
| nn.GroupNorm(num_groups=32, num_channels=d_fused // 8), | |
| nn.GELU(), | |
| ) | |
| self.scale4_refine = nn.Sequential( | |
| nn.Conv2d(d_fused // 16, d_fused // 16, kernel_size=3, padding=1), | |
| nn.GroupNorm(num_groups=16, num_channels=d_fused // 16), | |
| nn.GELU(), | |
| ) | |
| self.scale5_refine = nn.Sequential( | |
| nn.Conv2d(d_fused // 32, d_fused // 32, kernel_size=3, padding=1), | |
| nn.GroupNorm(num_groups=8, num_channels=d_fused // 32), | |
| nn.GELU(), | |
| ) | |
| # Final prediction head | |
| self.head = nn.Sequential( | |
| nn.Conv2d(d_fused // 32, d_fused // 32, kernel_size=3, padding=1), | |
| nn.GELU(), | |
| nn.Conv2d(d_fused // 32, 1, kernel_size=1, bias=True), | |
| ) | |
| # Initialize weights | |
| for m in [ | |
| self.dino_proj, | |
| self.token_proj, | |
| self.pose_proj, | |
| self.fusion_mlp, | |
| self.motion_transformer, | |
| self.scale1_refine, | |
| self.scale2_refine, | |
| self.scale3_refine, | |
| self.scale4_refine, | |
| self.scale5_refine, | |
| self.head, | |
| ]: | |
| m.apply(_init_weights) | |
| self.up2.apply(_init_weights) | |
| self.up3.apply(_init_weights) | |
| self.up4.apply(_init_weights) | |
| self.up5.apply(_init_weights) | |
| def _register_dinov3_device_hook(self): | |
| """Register a hook to move DINOv3 backbone when parent model moves to a device.""" | |
| def move_dinov3_to_device(module, inputs): | |
| if hasattr(module, "_dinov3_backbone_ref"): | |
| backbone = object.__getattribute__(module, "_dinov3_backbone_ref") | |
| if backbone is not None: | |
| device = next(module.parameters()).device | |
| backbone.to(device) | |
| # Register the hook | |
| self.register_forward_pre_hook(move_dinov3_to_device) | |
| def _extract_dino_features(self, images_224): | |
| """ | |
| Args: | |
| images_224: [B, 3, 224, 224] in [0,1], already resized to 224 | |
| Returns: | |
| [B, 16*16, D_dino] patch tokens | |
| """ | |
| img_norm = (images_224 - self.dino_norm_mean) / self.dino_norm_std | |
| outputs = self._dinov3_backbone_ref(img_norm, output_hidden_states=False) | |
| skip = 1 + self.num_register_tokens | |
| return outputs.last_hidden_state[:, skip:, :] # [B, 256, D_dino] => 16×16 | |
| def forward(self, target_images, target_image_tokens, target_plucker_emb): | |
| """ | |
| Predict motion masks from three modalities. | |
| Args: | |
| target_images: [B*V, 3, H, W] GT target images in [0, 1] | |
| target_image_tokens: [B*V, N_main, D_main] encoded image tokens from main transformer | |
| target_plucker_emb: [B*V, N_main, D_main] tokenized Plücker embeddings (camera poses) | |
| Returns: | |
| mask_logits: [B*V, 1, H_out, W_out] motion mask logits (H_out=256, W_out=256) | |
| """ | |
| BV, _, H, W = target_images.shape | |
| N_main = target_image_tokens.shape[1] | |
| Hm = Wm = int(N_main**0.5) # e.g., 16x16 | |
| assert Hm * Wm == N_main, f"Hm * Wm ({Hm}*{Wm}) != N_main ({N_main})" | |
| # Use full resolution (256x256) for DINOv3 to match main model's grid | |
| dino_in = target_images # Already 256x256, no resize needed | |
| dino_features = self._extract_dino_features(dino_in) # [BV, 256, D_dino] | |
| Nd = dino_features.shape[1] | |
| Hd = Wd = int(Nd**0.5) | |
| # 2. Align spatial dimensions (interpolate DINO to main grid size) | |
| if (Hd != Hm) or (Wd != Wm): | |
| dino_spatial = rearrange(dino_features, "bv (h w) d -> bv d h w", h=Hd, w=Wd) | |
| dino_spatial = bilinear_resize(dino_spatial, size=(Hm, Wm)) | |
| dino_features = rearrange(dino_spatial, "bv d h w -> bv (h w) d") | |
| dino_proj = self.dino_proj(dino_features) | |
| token_proj = self.token_proj(target_image_tokens) | |
| pose_proj = self.pose_proj(target_plucker_emb) | |
| fused = torch.cat([dino_proj, token_proj, pose_proj], dim=-1) | |
| fused = self.fusion_mlp(fused) | |
| # 5. Motion reasoning via transformer (with gradient checkpointing) | |
| x = fused | |
| if self.use_grad_checkpoint and self.training: | |
| for layer in self.motion_transformer: | |
| x = torch.utils.checkpoint.checkpoint(layer, x, use_reentrant=False) | |
| else: | |
| for layer in self.motion_transformer: | |
| x = layer(x) | |
| # readout -> spatial 16×16 | |
| x = self.dpt_readout(x) # [BV, 256, D_fused] | |
| x = rearrange(x, "bv (h w) d -> bv d h w", h=Hm, w=Wm) # [BV, D_fused, 16, 16] | |
| # decoder 16→32→64→128→256 | |
| x1 = self.scale1_refine(x) # 16×16, C/2 | |
| x2 = self.up2(x1) | |
| x2 = self.scale2_refine(x2) | |
| x3 = self.up3(x2) | |
| x3 = self.scale3_refine(x3) | |
| x4 = self.up4(x3) | |
| x4 = self.scale4_refine(x4) | |
| x5 = self.up5(x4) | |
| x5 = self.scale5_refine(x5) | |
| mask_logits = self.head(x5) | |
| return mask_logits | |
| class DinoV3UncertaintyPseudoLabelMaker(nn.Module): | |
| """ | |
| DINOv3-based pseudo-label generator for motion/uncertainty masks. | |
| Uses min(DINO cosine dissimilarity, SSIM dissimilarity) with percentile-based soft masking. | |
| This is the improved version from our Jupyter notebook analysis: | |
| - Combines DINOv3 features + SSIM for robustness | |
| - Generates soft masks (continuous probabilities) not binary | |
| - Uses percentile-based normalization (P75, P85, P90) | |
| - NO gradient computation (frozen DINOv3) | |
| - DDP-safe: device placement handled automatically by parent module | |
| """ | |
| def __init__( | |
| self, | |
| dinov3_backbone, | |
| dinov3_processor, | |
| percentile_threshold=75, | |
| use_ssim=True, | |
| target_size=256, | |
| use_coseg_binary=False, | |
| coseg_k=64, | |
| coseg_consistency_min_frames=4, | |
| coseg_frame_saliency_quantile=0.75, | |
| coseg_cluster_top_percent=0.05, | |
| coseg_morph_kernel=3, | |
| coseg_morph_iters=1, | |
| coseg_min_component_area_ratio=0.0025, | |
| coseg_grabcut_kernel=7, | |
| w_dino=0.5, | |
| w_ssim=0.5, | |
| ): | |
| super().__init__() | |
| self.percentile_threshold = percentile_threshold | |
| self.use_ssim = use_ssim | |
| self.target_size = target_size | |
| self.use_coseg_binary = use_coseg_binary | |
| # Online co-seg params (lightweight) | |
| self.coseg_k = coseg_k | |
| self.coseg_consistency_min_frames = coseg_consistency_min_frames | |
| self.coseg_frame_saliency_quantile = coseg_frame_saliency_quantile | |
| self.coseg_cluster_top_percent = coseg_cluster_top_percent | |
| self.coseg_morph_kernel = coseg_morph_kernel | |
| self.coseg_morph_iters = coseg_morph_iters | |
| self.coseg_min_component_area_ratio = coseg_min_component_area_ratio | |
| self.coseg_grabcut_kernel = coseg_grabcut_kernel | |
| self.w_dino = w_dino | |
| self.w_ssim = w_ssim | |
| # Store DINOv3 backbone as non-persistent reference (moves to device but not saved in state_dict) | |
| object.__setattr__(self, "backbone", dinov3_backbone) | |
| object.__setattr__(self, "processor", dinov3_processor) | |
| # Manually register DINOv3's device placement hook | |
| self._register_dinov3_device_hook() | |
| # DINOv3 config - read actual patch_size from model | |
| self.patch_size = getattr(dinov3_backbone.config, "patch_size", 16) | |
| self.num_register_tokens = getattr(dinov3_backbone.config, "num_register_tokens", 4) | |
| self.embed_dim = dinov3_backbone.config.hidden_size | |
| # Normalization parameters (as buffers for automatic device placement) | |
| mean = torch.tensor(dinov3_processor.image_mean, dtype=torch.float32).view(1, 3, 1, 1) | |
| std = torch.tensor(dinov3_processor.image_std, dtype=torch.float32).view(1, 3, 1, 1) | |
| self.register_buffer("img_norm_mean", mean) | |
| self.register_buffer("img_norm_std", std) | |
| def device(self): | |
| """Get current device from registered buffers (DDP-safe).""" | |
| return self.img_norm_mean.device | |
| def _register_dinov3_device_hook(self): | |
| """Register a hook to move DINOv3 backbone when parent model moves to a device.""" | |
| def move_dinov3_to_device(module, inputs): | |
| if hasattr(module, "backbone"): | |
| backbone = object.__getattribute__(module, "backbone") | |
| if backbone is not None: | |
| device = module.device | |
| backbone.to(device) | |
| # Register the hook | |
| self.register_forward_pre_hook(move_dinov3_to_device) | |
| def _extract_patch_features(self, img_tensor): | |
| """ | |
| Extract patch-level features from DINOv3. | |
| Args: | |
| img_tensor: [B, 3, H, W] in range [0, 1] | |
| Returns: | |
| features: [B, D, H_patches, W_patches] L2-normalized patch features | |
| """ | |
| # Get original image dimensions | |
| B, _, H, W = img_tensor.shape | |
| # Ensure backbone is on the same device as input | |
| device = img_tensor.device | |
| if hasattr(self, "backbone"): | |
| backbone = object.__getattribute__(self, "backbone") | |
| if backbone is not None and next(backbone.parameters()).device != device: | |
| backbone.to(device) | |
| # Normalize using DINOv3's preprocessing | |
| img_normalized = ( | |
| img_tensor - self.img_norm_mean.to(img_tensor.device) | |
| ) / self.img_norm_std.to(img_tensor.device) | |
| # Forward pass | |
| outputs = self.backbone(img_normalized, output_hidden_states=False) | |
| # Extract patch tokens (skip CLS + register tokens) | |
| skip_tokens = 1 + self.num_register_tokens # CLS + 4 register tokens | |
| patch_tokens = outputs.last_hidden_state[:, skip_tokens:, :] # [B, N_patches, D] | |
| B, N, D = patch_tokens.shape | |
| # Infer patch grid dimensions from N | |
| # The model might internally resize images, so we can't rely on input dimensions | |
| # First, try assuming square output | |
| sqrt_N = int(np.sqrt(N)) | |
| if sqrt_N * sqrt_N == N: | |
| # Perfect square - use it | |
| H_patches = W_patches = sqrt_N | |
| else: | |
| # Not square - need to factorize N into H_patches × W_patches | |
| # Try to match the input image aspect ratio | |
| aspect_ratio = W / H | |
| # Find factors of N that are closest to the aspect ratio | |
| best_h = best_w = None | |
| min_diff = float('inf') | |
| for h in range(1, int(np.sqrt(N)) + 1): | |
| if N % h == 0: | |
| w = N // h | |
| ratio_diff = abs(w / h - aspect_ratio) | |
| if ratio_diff < min_diff: | |
| min_diff = ratio_diff | |
| best_h, best_w = h, w | |
| if best_h is None: | |
| raise ValueError(f"Cannot factorize {N} patches (N must be composite or square)") | |
| H_patches, W_patches = best_h, best_w | |
| # Reshape to spatial grid and L2-normalize | |
| features = patch_tokens.transpose(1, 2).reshape(B, D, H_patches, W_patches) | |
| features = F.normalize(features, dim=1) # L2 normalize along channel dim | |
| return features | |
| def _refine_with_grabcut(self, gt_img_rgb: np.ndarray, init_mask: np.ndarray) -> np.ndarray: | |
| """ | |
| Refine binary mask using GrabCut (matches offline script functionality). | |
| Args: | |
| gt_img_rgb: [H, W, 3] uint8 RGB image | |
| init_mask: [H, W] uint8 {0, 1} initial mask | |
| Returns: | |
| refined_mask: [H, W] uint8 {0, 1} GrabCut-refined mask | |
| """ | |
| if self.coseg_grabcut_kernel <= 0: | |
| return init_mask | |
| # Erode to get confident FG/BG seeds | |
| kernel = np.ones((self.coseg_grabcut_kernel, self.coseg_grabcut_kernel), np.uint8) | |
| fg_seed = cv2.erode(np.uint8(init_mask), kernel) | |
| bg_seed = cv2.erode(np.uint8(1 - init_mask), kernel) | |
| # Build GrabCut mask | |
| full_mask = np.ones((gt_img_rgb.shape[0], gt_img_rgb.shape[1]), np.uint8) * cv2.GC_PR_FGD | |
| full_mask[bg_seed == 1] = cv2.GC_BGD | |
| full_mask[fg_seed == 1] = cv2.GC_FGD | |
| # Run GrabCut | |
| bgdModel = np.zeros((1, 65), np.float64) | |
| fgdModel = np.zeros((1, 65), np.float64) | |
| try: | |
| cv2.grabCut(gt_img_rgb, full_mask, None, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_MASK) | |
| grabcut_mask = np.where((full_mask == 2) | (full_mask == 0), 0, 1).astype("uint8") | |
| except Exception: | |
| # Fallback to init mask if GrabCut fails | |
| grabcut_mask = init_mask.astype("uint8") | |
| return grabcut_mask | |
| def generate_binary_coseg_masks( | |
| self, gt_img, pred_img, save_dir=None, frame_indices=None, return_intermediates=False | |
| ): | |
| """ | |
| Online co-segmentation: KMeans over GT patch features + fused saliency voting. | |
| GPU-optimized with vectorized PyTorch ops. | |
| Returns [B,1,target_size,target_size] uint8 {0,1}. | |
| Args: | |
| gt_img: [B, 3, H, W] ground truth images | |
| pred_img: [B, 3, H, W] predicted images | |
| save_dir: Optional directory to save masks as PNG files | |
| frame_indices: Optional list of frame indices for filename | |
| return_intermediates: If True, return dict with intermediate results for visualization | |
| Returns: | |
| If return_intermediates=False: torch.Tensor [B,1,H,W] final masks | |
| If return_intermediates=True: dict with keys: | |
| - 'masks': final masks [B,1,H,W] | |
| - 'clusters': K-means cluster assignments [B,H,W] | |
| - 'fg_binary': binary mask after K-means selection, before refinement [B,1,H,W] | |
| """ | |
| gt_features = self._extract_patch_features(gt_img) # [B, D, Hp, Wp] | |
| pred_features = self._extract_patch_features(pred_img) | |
| B, D, Hp, Wp = gt_features.shape | |
| device = gt_img.device | |
| gt_norm = F.normalize(gt_features, dim=1) | |
| pred_norm = F.normalize(pred_features, dim=1) | |
| cos_sim = (gt_norm * pred_norm).sum(dim=1, keepdim=True) # [B,1,Hp,Wp] | |
| cos_dissim = 1.0 - cos_sim | |
| if self.use_ssim: | |
| ssim_dissim = self._compute_ssim_dissimilarity(gt_img, pred_img) # [B,1,T,T] | |
| ssim_down = F.interpolate(ssim_dissim, size=(Hp, Wp), mode="area") | |
| def z(x): | |
| mu = x.mean(dim=(2, 3), keepdim=True) | |
| sd = x.std(dim=(2, 3), keepdim=True) + 1e-8 | |
| return (x - mu) / sd | |
| fused_patch = self.w_dino * z(cos_dissim) + self.w_ssim * z(ssim_down) | |
| else: | |
| fused_patch = cos_dissim | |
| # KMeans on GT descriptors (GPU-accelerated, deterministic) | |
| gt_desc = gt_features.permute(0, 2, 3, 1).reshape(B, Hp * Wp, D) # [B, Hp*Wp, D] | |
| X_all = gt_desc.reshape(-1, D) # [B*Hp*Wp, D] | |
| # GPU KMeans with k-means++ initialization (matches sklearn behavior) | |
| N = X_all.shape[0] | |
| K = self.coseg_k | |
| # k-means++ initialization (deterministic with fixed seed) | |
| torch.manual_seed(42) | |
| centroids = [] | |
| # Pick first centroid randomly | |
| first_idx = torch.randint(0, N, (1,), device=device) | |
| centroids.append(X_all[first_idx]) | |
| for _ in range(K - 1): | |
| # Compute distance to nearest centroid | |
| centroids_stack = torch.cat(centroids, dim=0) # [num_centroids, D] | |
| dists = torch.cdist(X_all, centroids_stack) # [N, num_centroids] | |
| min_dists = dists.min(dim=1)[0] # [N] | |
| # Sample proportional to distance squared (k-means++) | |
| probs = min_dists**2 | |
| probs = probs / (probs.sum() + 1e-10) | |
| next_idx = torch.multinomial(probs, 1) | |
| centroids.append(X_all[next_idx]) | |
| centroids = torch.cat(centroids, dim=0) # [K, D] | |
| # Lloyd's algorithm (20 iterations for convergence) | |
| for iteration in range(20): | |
| # Assignment step | |
| dists = torch.cdist(X_all, centroids) # [N, K] | |
| labels_flat = dists.argmin(dim=1) # [N] | |
| # Update step | |
| centroids_new = centroids.clone() | |
| for k in range(K): | |
| mask_k = labels_flat == k | |
| if mask_k.sum() > 0: | |
| centroids_new[k] = X_all[mask_k].mean(dim=0) | |
| # Check convergence | |
| if iteration > 0 and torch.allclose(centroids, centroids_new, atol=1e-4): | |
| break | |
| centroids = centroids_new | |
| labels_per_image = labels_flat.reshape(B, Hp * Wp) # [B, Hp*Wp] | |
| # Vectorized cluster scoring (all on GPU) | |
| fused_flat = fused_patch.squeeze(1).reshape(B, Hp * Wp) # [B, Hp*Wp] | |
| Kc = self.coseg_k | |
| # Per-cluster global average saliency: [K] | |
| cluster_avg_sals = torch.zeros(Kc, device=device) | |
| for k in range(Kc): | |
| mask_k = labels_per_image == k # [B, Hp*Wp] | |
| vals_k = fused_flat[mask_k] | |
| cluster_avg_sals[k] = vals_k.mean() if vals_k.numel() > 0 else 0.0 | |
| # Per-cluster per-frame median saliency: [K, B] | |
| cluster_per_frame_medians = torch.zeros(Kc, B, device=device) | |
| for k in range(Kc): | |
| for f in range(B): | |
| mask_kf = labels_per_image[f] == k # [Hp*Wp] | |
| vals_kf = fused_flat[f, mask_kf] | |
| cluster_per_frame_medians[k, f] = vals_kf.median() if vals_kf.numel() > 0 else 0.0 | |
| # Thresholds | |
| rank_thr = torch.quantile(cluster_avg_sals, 1.0 - self.coseg_cluster_top_percent) | |
| per_frame_thr = torch.quantile(fused_flat, self.coseg_frame_saliency_quantile) | |
| # Select foreground clusters (vectorized) | |
| high_sal_mask = cluster_avg_sals >= rank_thr # [K] | |
| consistent_mask = (cluster_per_frame_medians >= per_frame_thr).sum( | |
| dim=1 | |
| ) >= self.coseg_consistency_min_frames # [K] | |
| fg_cluster_mask = high_sal_mask & consistent_mask # [K] | |
| fg_cluster_indices = torch.nonzero(fg_cluster_mask, as_tuple=True)[0] # list of K indices | |
| # Build masks (vectorized per-frame) | |
| masks = [] | |
| fg_binary_before_refine = [] # Store binary mask before GrabCut for visualization | |
| labels_grid = labels_per_image.reshape(B, Hp, Wp) # [B, Hp, Wp] | |
| for f in range(B): | |
| # Binary mask: pixel is FG if its cluster is in fg_cluster_indices | |
| fg_mask = torch.zeros(Hp, Wp, dtype=torch.bool, device=device) | |
| for k_idx in fg_cluster_indices: | |
| fg_mask |= labels_grid[f] == k_idx | |
| fg_uint8 = fg_mask.to(torch.uint8) # [Hp, Wp] | |
| # Upsample to target_size | |
| up = F.interpolate( | |
| fg_uint8[None, None].float(), | |
| size=(self.target_size, self.target_size), | |
| mode="nearest", | |
| ) | |
| up = up.squeeze().cpu().numpy().astype(np.uint8) | |
| # Morphological ops + connected components (still on CPU with cv2) | |
| if self.coseg_morph_kernel > 0 and self.coseg_morph_iters > 0: | |
| k = cv2.getStructuringElement( | |
| cv2.MORPH_ELLIPSE, (self.coseg_morph_kernel, self.coseg_morph_kernel) | |
| ) | |
| up = cv2.morphologyEx(up, cv2.MORPH_CLOSE, k, iterations=self.coseg_morph_iters) | |
| up = cv2.morphologyEx(up, cv2.MORPH_OPEN, k, iterations=self.coseg_morph_iters) | |
| num_labels, labels_cc, stats, _ = cv2.connectedComponentsWithStats(up, connectivity=8) | |
| min_area = int( | |
| self.coseg_min_component_area_ratio * self.target_size * self.target_size | |
| ) | |
| clean = np.zeros_like(up, dtype=np.uint8) | |
| for lab in range(1, num_labels): | |
| if stats[lab, cv2.CC_STAT_AREA] >= min_area: | |
| clean[labels_cc == lab] = 1 | |
| # Store binary mask before GrabCut refinement | |
| fg_binary_before_refine.append(clean) | |
| # Apply GrabCut refinement (matches offline script) | |
| gt_img_rgb = (gt_img[f].permute(1, 2, 0).cpu().numpy() * 255.0).astype( | |
| np.uint8 | |
| ) # [H, W, 3] | |
| grabcut_mask = self._refine_with_grabcut(gt_img_rgb, clean) | |
| masks.append(grabcut_mask) | |
| masks_np = np.stack(masks, axis=0)[:, None, :, :] # [B,1,H,W] | |
| masks_tensor = torch.from_numpy(masks_np).to(device) | |
| # Optionally save masks to disk | |
| if save_dir is not None: | |
| import os | |
| os.makedirs(save_dir, exist_ok=True) | |
| for b_idx in range(B): | |
| mask_uint8 = masks_np[b_idx, 0] # [H, W] uint8 {0, 1} | |
| mask_uint8 = (mask_uint8 * 255).astype(np.uint8) # Scale to {0, 255} | |
| # Generate filename | |
| if frame_indices is not None and b_idx < len(frame_indices): | |
| frame_idx = frame_indices[b_idx] | |
| filename = f"coseg_mask_frame_{frame_idx:06d}.png" | |
| else: | |
| filename = f"coseg_mask_{b_idx:02d}.png" | |
| Image.fromarray(mask_uint8, mode="L").save(os.path.join(save_dir, filename)) | |
| # Return intermediate results if requested | |
| if return_intermediates: | |
| # Upsample cluster labels to target_size for visualization | |
| clusters_upsampled = F.interpolate( | |
| labels_grid.unsqueeze(1).float(), | |
| size=(self.target_size, self.target_size), | |
| mode="nearest", | |
| ) # [B, 1, T, T] | |
| fg_binary_np = np.stack(fg_binary_before_refine, axis=0)[:, None, :, :] # [B,1,H,W] | |
| fg_binary_tensor = torch.from_numpy(fg_binary_np).to(device).float() | |
| return { | |
| "masks": masks_tensor, | |
| "clusters": clusters_upsampled.squeeze(1), # [B, H, W] | |
| "fg_binary": fg_binary_tensor, # [B, 1, H, W] | |
| } | |
| return masks_tensor | |
| def _compute_cosine_dissimilarity(self, gt_img, pred_img): | |
| """ | |
| Compute cosine dissimilarity (1 - cosine_similarity) between GT and predicted images. | |
| Args: | |
| gt_img: [B, 3, H, W] in range [0, 1] | |
| pred_img: [B, 3, H, W] in range [0, 1] | |
| Returns: | |
| cos_dissim: [B, 1, target_size, target_size] dissimilarity map | |
| """ | |
| # Extract features | |
| gt_features = self._extract_patch_features(gt_img) # [B, D, H_p, W_p] | |
| pred_features = self._extract_patch_features(pred_img) # [B, D, H_p, W_p] | |
| # Cosine similarity (features already L2-normalized) | |
| gt_norm = F.normalize(gt_features, dim=1) | |
| pred_norm = F.normalize(pred_features, dim=1) | |
| cos_sim = (gt_norm * pred_norm).sum(dim=1, keepdim=True) # [B, 1, H_p, W_p] | |
| # Dissimilarity = 1 - similarity | |
| cos_dissim = 1.0 - cos_sim | |
| # Upsample to target size | |
| cos_dissim_resized = F.interpolate( | |
| cos_dissim, | |
| size=(self.target_size, self.target_size), | |
| mode="bilinear", | |
| align_corners=False, | |
| ) | |
| return cos_dissim_resized | |
| def _compute_ssim_dissimilarity(self, gt_img, pred_img): | |
| """ | |
| Compute SSIM dissimilarity (1 - SSIM) between GT and predicted images. | |
| Args: | |
| gt_img: [B, 3, H, W] in range [0, 1] | |
| pred_img: [B, 3, H, W] in range [0, 1] | |
| Returns: | |
| ssim_dissim: [B, 1, target_size, target_size] dissimilarity map | |
| """ | |
| B = gt_img.shape[0] | |
| # Resize to target size first | |
| gt_resized = F.interpolate( | |
| gt_img, size=(self.target_size, self.target_size), mode="bilinear", align_corners=False | |
| ) | |
| pred_resized = F.interpolate( | |
| pred_img, | |
| size=(self.target_size, self.target_size), | |
| mode="bilinear", | |
| align_corners=False, | |
| ) | |
| # Compute SSIM for each image in the batch | |
| ssim_dissim_list = [] | |
| for b in range(B): | |
| # Convert to numpy uint8 [0, 255] | |
| gt_np = (gt_resized[b].permute(1, 2, 0).cpu().numpy() * 255.0).astype(np.uint8) | |
| pred_np = (pred_resized[b].permute(1, 2, 0).cpu().numpy() * 255.0).astype(np.uint8) | |
| # Compute SSIM map | |
| ssim_map = structural_similarity( | |
| gt_np, pred_np, win_size=11, channel_axis=2, data_range=255, full=True | |
| )[ | |
| 1 | |
| ] # Get the full SSIM map | |
| # Average across channels if needed | |
| ssim_map_gray = ssim_map.mean(axis=2) if ssim_map.ndim == 3 else ssim_map | |
| # Dissimilarity = 1 - SSIM | |
| ssim_dissim = 1.0 - ssim_map_gray | |
| ssim_dissim_list.append(ssim_dissim) | |
| # Stack and convert to tensor | |
| ssim_dissim = np.stack(ssim_dissim_list, axis=0) # [B, H, W] | |
| ssim_dissim = torch.from_numpy(ssim_dissim).unsqueeze(1).to(gt_img.device) # [B, 1, H, W] | |
| return ssim_dissim | |
| def _create_soft_mask(self, dissimilarity_map): | |
| """ | |
| Create soft mask using percentile-based normalization. | |
| Args: | |
| dissimilarity_map: [B, 1, H, W] combined dissimilarity | |
| Returns: | |
| soft_mask: [B, 1, H, W] soft mask in range [0, 1] | |
| """ | |
| B = dissimilarity_map.shape[0] | |
| soft_masks = [] | |
| for b in range(B): | |
| dissim = dissimilarity_map[b, 0] # [H, W] | |
| # Percentile-based normalization | |
| p_thresh = np.percentile(dissim.cpu().numpy(), self.percentile_threshold) | |
| p_max = dissim.max().item() | |
| # Normalize: [p_thresh, p_max] -> [0, 1] | |
| soft_mask = torch.clamp((dissim - p_thresh) / (p_max - p_thresh + 1e-8), 0.0, 1.0) | |
| soft_masks.append(soft_mask) | |
| soft_masks = torch.stack(soft_masks, dim=0).unsqueeze(1) # [B, 1, H, W] | |
| return soft_masks | |
| def forward(self, gt_img, pred_img, debug_save_path=None, use_comprehensive_viz=False): | |
| """ | |
| Generate motion/uncertainty pseudo-labels (soft or binary based on use_coseg_binary flag). | |
| Args: | |
| gt_img: [B, 3, H, W] ground truth images in range [0, 1] | |
| pred_img: [B, 3, H, W] predicted images in range [0, 1] | |
| debug_save_path: Optional path to save debug visualization | |
| use_comprehensive_viz: If True, save comprehensive visualization showing full pipeline | |
| (GT, Rendering, MSE, SSIM, DINO, min, K-means, FG, Final) | |
| Returns: | |
| motion_mask: [B, 1, target_size, target_size] mask in range [0, 1] | |
| If use_coseg_binary=True: binary {0,1} via KMeans+GrabCut | |
| If use_coseg_binary=False: soft [0,1] via percentile normalization | |
| """ | |
| # Compute DINOv3 cosine dissimilarity | |
| dino_dissim = self._compute_cosine_dissimilarity(gt_img, pred_img) | |
| # Optionally combine with SSIM | |
| if self.use_ssim: | |
| ssim_dissim = self._compute_ssim_dissimilarity(gt_img, pred_img) | |
| # Conservative: min dissimilarity (both must agree it's different) | |
| combined_dissim = torch.minimum(dino_dissim, ssim_dissim) | |
| else: | |
| ssim_dissim = None | |
| combined_dissim = dino_dissim | |
| # Initialize vars for comprehensive visualization | |
| kmeans_clusters = None | |
| fg_mask_binary = None | |
| mse_dissim = None | |
| if self.use_coseg_binary: | |
| # Produce binary motion masks (1=motion, 0=static) at target_size | |
| if use_comprehensive_viz and debug_save_path is not None: | |
| # Request intermediate results for visualization | |
| result = self.generate_binary_coseg_masks(gt_img, pred_img, return_intermediates=True) | |
| motion_mask = result["masks"].to(combined_dissim.device).to(torch.float32) | |
| kmeans_clusters = result["clusters"] | |
| fg_mask_binary = result["fg_binary"] | |
| else: | |
| bin_motion = ( | |
| self.generate_binary_coseg_masks(gt_img, pred_img) | |
| .to(combined_dissim.device) | |
| .to(torch.float32) | |
| ) | |
| motion_mask = bin_motion | |
| else: | |
| # Create soft mask using percentile normalization (uncertainty-style) | |
| motion_mask = self._create_soft_mask(combined_dissim) | |
| # Debug visualization | |
| if debug_save_path is not None: | |
| if use_comprehensive_viz: | |
| # Compute MSE for comprehensive visualization | |
| mse_dissim = self._compute_mse_dissimilarity(gt_img, pred_img) | |
| # Save comprehensive visualization | |
| self._save_comprehensive_visualization( | |
| gt_img, | |
| pred_img, | |
| mse_dissim, | |
| ssim_dissim, | |
| dino_dissim, | |
| combined_dissim, | |
| kmeans_clusters, | |
| fg_mask_binary, | |
| motion_mask, | |
| debug_save_path, | |
| ) | |
| else: | |
| # Save basic debug visualization | |
| self._save_debug_visualization( | |
| gt_img, | |
| pred_img, | |
| dino_dissim, | |
| ssim_dissim, | |
| combined_dissim, | |
| motion_mask, | |
| debug_save_path, | |
| ) | |
| return motion_mask | |
| def _compute_mse_dissimilarity(self, gt_img, pred_img): | |
| """ | |
| Compute MSE dissimilarity between GT and predicted images. | |
| Args: | |
| gt_img: [B, 3, H, W] in range [0, 1] | |
| pred_img: [B, 3, H, W] in range [0, 1] | |
| Returns: | |
| mse_dissim: [B, 1, target_size, target_size] dissimilarity map | |
| """ | |
| # Resize to target size | |
| gt_resized = F.interpolate( | |
| gt_img, size=(self.target_size, self.target_size), mode="bilinear", align_corners=False | |
| ) | |
| pred_resized = F.interpolate( | |
| pred_img, | |
| size=(self.target_size, self.target_size), | |
| mode="bilinear", | |
| align_corners=False, | |
| ) | |
| # Compute MSE per pixel (average over channels) | |
| mse = ((gt_resized - pred_resized) ** 2).mean(dim=1, keepdim=True) # [B, 1, H, W] | |
| return mse | |
| def _save_comprehensive_visualization( | |
| self, | |
| gt_img, | |
| pred_img, | |
| mse_dissim, | |
| ssim_dissim, | |
| dino_dissim, | |
| combined_dissim, | |
| kmeans_clusters, | |
| fg_mask_binary, | |
| final_mask, | |
| save_path, | |
| ): | |
| """ | |
| Save comprehensive visualization showing the full pipeline: | |
| GT, Rendering, MSE, SSIM, DINO, min(SSIM,DINO), K-means, FG selection, Final mask. | |
| Args: | |
| gt_img: [B, 3, H, W] | |
| pred_img: [B, 3, H, W] | |
| mse_dissim: [B, 1, H, W] MSE dissimilarity | |
| ssim_dissim: [B, 1, H, W] SSIM dissimilarity | |
| dino_dissim: [B, 1, H, W] DINO dissimilarity | |
| combined_dissim: [B, 1, H, W] min(SSIM, DINO) | |
| kmeans_clusters: [B, H, W] cluster assignments (for visualization) | |
| fg_mask_binary: [B, 1, H, W] binary mask after K-means selection | |
| final_mask: [B, 1, H, W] final mask after all post-processing | |
| save_path: Path to save figure | |
| """ | |
| import matplotlib.pyplot as plt | |
| from pathlib import Path | |
| import matplotlib.colors as mcolors | |
| Path(save_path).parent.mkdir(parents=True, exist_ok=True) | |
| # Take first sample from batch (convert bfloat16 to float32 for numpy) | |
| gt = gt_img[0].float().cpu().permute(1, 2, 0).numpy() # [H, W, 3] | |
| pred = pred_img[0].float().cpu().permute(1, 2, 0).numpy() # [H, W, 3] | |
| mse = mse_dissim[0, 0].float().cpu().numpy() # [H, W] | |
| ssim = ssim_dissim[0, 0].float().cpu().numpy() if ssim_dissim is not None else None | |
| dino = dino_dissim[0, 0].float().cpu().numpy() # [H, W] | |
| combined = combined_dissim[0, 0].float().cpu().numpy() # [H, W] | |
| clusters = kmeans_clusters[0].float().cpu().numpy() if kmeans_clusters is not None else None | |
| fg_binary = ( | |
| fg_mask_binary[0, 0].float().cpu().numpy() if fg_mask_binary is not None else None | |
| ) | |
| final = final_mask[0, 0].float().cpu().numpy() # [H, W] | |
| # Create figure with 2 rows x 5 columns | |
| fig, axes = plt.subplots(2, 5, figsize=(25, 10)) | |
| # Row 1: Full pipeline | |
| # 1. GT Image | |
| axes[0, 0].imshow(gt) | |
| axes[0, 0].set_title("GT Image", fontsize=12, fontweight="bold") | |
| axes[0, 0].axis("off") | |
| # 2. Rendered Image | |
| axes[0, 1].imshow(pred) | |
| axes[0, 1].set_title("Rendered Image", fontsize=12, fontweight="bold") | |
| axes[0, 1].axis("off") | |
| # 3. MSE Dissimilarity | |
| im_mse = axes[0, 2].imshow(mse, cmap="hot") | |
| axes[0, 2].set_title( | |
| f"MSE Dissim\n(mean={mse.mean():.3f}, max={mse.max():.3f})", fontsize=12, fontweight="bold" | |
| ) | |
| axes[0, 2].axis("off") | |
| plt.colorbar(im_mse, ax=axes[0, 2], fraction=0.046) | |
| # 4. DINO Dissimilarity | |
| im_dino = axes[0, 3].imshow(dino, cmap="hot", vmin=0, vmax=1) | |
| axes[0, 3].set_title( | |
| f"DINO Cosine Dissim\n(mean={dino.mean():.3f}, max={dino.max():.3f})", | |
| fontsize=12, | |
| fontweight="bold", | |
| ) | |
| axes[0, 3].axis("off") | |
| plt.colorbar(im_dino, ax=axes[0, 3], fraction=0.046) | |
| # 5. SSIM Dissimilarity | |
| if ssim is not None: | |
| im_ssim = axes[0, 4].imshow(ssim, cmap="hot", vmin=0, vmax=1) | |
| axes[0, 4].set_title( | |
| f"SSIM Dissim\n(mean={ssim.mean():.3f}, max={ssim.max():.3f})", | |
| fontsize=12, | |
| fontweight="bold", | |
| ) | |
| axes[0, 4].axis("off") | |
| plt.colorbar(im_ssim, ax=axes[0, 4], fraction=0.046) | |
| else: | |
| axes[0, 4].axis("off") | |
| # Row 2: Clustering and final results | |
| # 1. min(SSIM, DINO) | |
| im_combined = axes[1, 0].imshow(combined, cmap="hot", vmin=0, vmax=1) | |
| axes[1, 0].set_title( | |
| f"min(DINO, SSIM)\n(mean={combined.mean():.3f}, max={combined.max():.3f})", | |
| fontsize=12, | |
| fontweight="bold", | |
| ) | |
| axes[1, 0].axis("off") | |
| plt.colorbar(im_combined, ax=axes[1, 0], fraction=0.046) | |
| # 2. K-means Clusters (if available) | |
| if clusters is not None: | |
| # Create a colorful visualization of clusters | |
| n_clusters = int(clusters.max()) + 1 | |
| im_clusters = axes[1, 1].imshow(clusters, cmap="tab20", interpolation="nearest") | |
| axes[1, 1].set_title( | |
| f"K-means Clusters\n(K={n_clusters})", fontsize=12, fontweight="bold" | |
| ) | |
| axes[1, 1].axis("off") | |
| else: | |
| axes[1, 1].axis("off") | |
| # 3. Binary FG mask (after K-means selection, before refinement) | |
| if fg_binary is not None: | |
| im_fg = axes[1, 2].imshow(fg_binary, cmap="hot", vmin=0, vmax=1) | |
| axes[1, 2].set_title( | |
| f"FG Selection (binary)\n(coverage={fg_binary.mean():.3f})", | |
| fontsize=12, | |
| fontweight="bold", | |
| ) | |
| axes[1, 2].axis("off") | |
| plt.colorbar(im_fg, ax=axes[1, 2], fraction=0.046) | |
| else: | |
| axes[1, 2].axis("off") | |
| # 4. Final mask (after all post-processing) | |
| im_final = axes[1, 3].imshow(final, cmap="hot", vmin=0, vmax=1) | |
| axes[1, 3].set_title( | |
| f"Final Mask\n(coverage={final.mean():.3f})", fontsize=12, fontweight="bold" | |
| ) | |
| axes[1, 3].axis("off") | |
| plt.colorbar(im_final, ax=axes[1, 3], fraction=0.046) | |
| # 5. Final mask overlay on GT | |
| axes[1, 4].imshow(gt) | |
| axes[1, 4].imshow(final, cmap="jet", alpha=0.5, vmin=0, vmax=1) | |
| axes[1, 4].set_title("Final Mask on GT", fontsize=12, fontweight="bold") | |
| axes[1, 4].axis("off") | |
| # Title | |
| mode = "Binary Co-Seg" if self.use_coseg_binary else "Soft Percentile" | |
| param_str = f"mode={mode}, w_dino={self.w_dino:.2f}, w_ssim={self.w_ssim:.2f}" | |
| if self.use_coseg_binary: | |
| param_str += f", K={self.coseg_k}" | |
| else: | |
| param_str += f", P={self.percentile_threshold}" | |
| plt.suptitle(f"DINOv3 Motion Mask Pipeline - {param_str}", fontsize=14, fontweight="bold") | |
| plt.tight_layout() | |
| plt.savefig(save_path, dpi=150, bbox_inches="tight") | |
| plt.close() | |
| def _save_debug_visualization( | |
| self, gt_img, pred_img, dino_dissim, ssim_dissim, combined_dissim, motion_mask, save_path | |
| ): | |
| """Save debug visualization showing all intermediate steps.""" | |
| import matplotlib.pyplot as plt | |
| from pathlib import Path | |
| Path(save_path).parent.mkdir(parents=True, exist_ok=True) | |
| # Take first sample from batch (convert bfloat16 to float32 for numpy) | |
| gt = gt_img[0].float().cpu().permute(1, 2, 0).numpy() # [H, W, 3] | |
| pred = pred_img[0].float().cpu().permute(1, 2, 0).numpy() # [H, W, 3] | |
| dino = dino_dissim[0, 0].float().cpu().numpy() # [H, W] | |
| ssim = ( | |
| ssim_dissim[0, 0].float().cpu().numpy() if ssim_dissim is not None else None | |
| ) # [H, W] | |
| combined = combined_dissim[0, 0].float().cpu().numpy() # [H, W] | |
| mask = motion_mask[0, 0].float().cpu().numpy() # [H, W] | |
| # Create figure | |
| n_cols = 6 if ssim is not None else 5 | |
| fig, axes = plt.subplots(2, n_cols, figsize=(n_cols * 4, 8)) | |
| # Row 1: Images and dissimilarity maps | |
| axes[0, 0].imshow(gt) | |
| axes[0, 0].set_title("GT Image", fontsize=14, fontweight="bold") | |
| axes[0, 0].axis("off") | |
| axes[0, 1].imshow(pred) | |
| axes[0, 1].set_title("Rendered Image", fontsize=14, fontweight="bold") | |
| axes[0, 1].axis("off") | |
| im2 = axes[0, 2].imshow(dino, cmap="hot", vmin=0, vmax=1) | |
| axes[0, 2].set_title( | |
| f"DINO Dissim\n(mean={dino.mean():.3f}, max={dino.max():.3f})", | |
| fontsize=14, | |
| fontweight="bold", | |
| ) | |
| axes[0, 2].axis("off") | |
| plt.colorbar(im2, ax=axes[0, 2], fraction=0.046) | |
| if ssim is not None: | |
| im3 = axes[0, 3].imshow(ssim, cmap="hot", vmin=0, vmax=1) | |
| axes[0, 3].set_title( | |
| f"SSIM Dissim\n(mean={ssim.mean():.3f}, max={ssim.max():.3f})", | |
| fontsize=14, | |
| fontweight="bold", | |
| ) | |
| axes[0, 3].axis("off") | |
| plt.colorbar(im3, ax=axes[0, 3], fraction=0.046) | |
| im4 = axes[0, 4].imshow(combined, cmap="hot", vmin=0, vmax=1) | |
| axes[0, 4].set_title( | |
| f"Combined (min)\n(mean={combined.mean():.3f}, max={combined.max():.3f})", | |
| fontsize=14, | |
| fontweight="bold", | |
| ) | |
| axes[0, 4].axis("off") | |
| plt.colorbar(im4, ax=axes[0, 4], fraction=0.046) | |
| im5 = axes[0, 5].imshow(mask, cmap="hot", vmin=0, vmax=1) | |
| axes[0, 5].set_title( | |
| f"Final Mask (P{self.percentile_threshold})\n(mean={mask.mean():.3f}, max={mask.max():.3f})", | |
| fontsize=14, | |
| fontweight="bold", | |
| ) | |
| axes[0, 5].axis("off") | |
| plt.colorbar(im5, ax=axes[0, 5], fraction=0.046) | |
| else: | |
| im4 = axes[0, 3].imshow(combined, cmap="hot", vmin=0, vmax=1) | |
| axes[0, 3].set_title( | |
| f"Combined\n(mean={combined.mean():.3f}, max={combined.max():.3f})", | |
| fontsize=14, | |
| fontweight="bold", | |
| ) | |
| axes[0, 3].axis("off") | |
| plt.colorbar(im4, ax=axes[0, 3], fraction=0.046) | |
| im5 = axes[0, 4].imshow(mask, cmap="hot", vmin=0, vmax=1) | |
| axes[0, 4].set_title( | |
| f"Final Mask (P{self.percentile_threshold})\n(mean={mask.mean():.3f}, max={mask.max():.3f})", | |
| fontsize=14, | |
| fontweight="bold", | |
| ) | |
| axes[0, 4].axis("off") | |
| plt.colorbar(im5, ax=axes[0, 4], fraction=0.046) | |
| # Row 2: Overlays on GT image | |
| axes[1, 0].imshow(gt) | |
| axes[1, 0].set_title("GT (reference)", fontsize=14) | |
| axes[1, 0].axis("off") | |
| axes[1, 1].imshow(pred) | |
| axes[1, 1].set_title("Rendered (reference)", fontsize=14) | |
| axes[1, 1].axis("off") | |
| # DINO overlay | |
| axes[1, 2].imshow(gt) | |
| axes[1, 2].imshow(dino, cmap="hot", alpha=0.5, vmin=0, vmax=1) | |
| axes[1, 2].set_title("GT + DINO overlay", fontsize=14) | |
| axes[1, 2].axis("off") | |
| if ssim is not None: | |
| # SSIM overlay | |
| axes[1, 3].imshow(gt) | |
| axes[1, 3].imshow(ssim, cmap="hot", alpha=0.5, vmin=0, vmax=1) | |
| axes[1, 3].set_title("GT + SSIM overlay", fontsize=14) | |
| axes[1, 3].axis("off") | |
| # Combined overlay | |
| axes[1, 4].imshow(gt) | |
| axes[1, 4].imshow(combined, cmap="hot", alpha=0.5, vmin=0, vmax=1) | |
| axes[1, 4].set_title("GT + Combined overlay", fontsize=14) | |
| axes[1, 4].axis("off") | |
| # Final mask overlay | |
| axes[1, 5].imshow(gt) | |
| axes[1, 5].imshow(mask, cmap="hot", alpha=0.5, vmin=0, vmax=1) | |
| axes[1, 5].set_title("GT + Final Mask overlay", fontsize=14) | |
| axes[1, 5].axis("off") | |
| else: | |
| # Combined overlay | |
| axes[1, 3].imshow(gt) | |
| axes[1, 3].imshow(combined, cmap="hot", alpha=0.5, vmin=0, vmax=1) | |
| axes[1, 3].set_title("GT + Combined overlay", fontsize=14) | |
| axes[1, 3].axis("off") | |
| # Final mask overlay | |
| axes[1, 4].imshow(gt) | |
| axes[1, 4].imshow(mask, cmap="hot", alpha=0.5, vmin=0, vmax=1) | |
| axes[1, 4].set_title("GT + Final Mask overlay", fontsize=14) | |
| axes[1, 4].axis("off") | |
| plt.suptitle( | |
| f"DINOv3 Pseudo-Label Debug (w_dino={self.w_dino:.2f}, w_ssim={self.w_ssim:.2f})", | |
| fontsize=16, | |
| fontweight="bold", | |
| ) | |
| plt.tight_layout() | |
| plt.savefig(save_path, dpi=150, bbox_inches="tight") | |
| plt.close() | |
| class Images2Latent4D(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| self.config = config | |
| self.process_data = data_utils.ProcessData(config) | |
| # self.transform_input = TransformInput(config) | |
| # image tokenizer | |
| if not config.model.image_tokenizer.get("use_off_the_shelf", False): | |
| self.image_tokenizer = nn.Sequential( | |
| Rearrange( | |
| "b v c (hh ph) (ww pw) -> (b v) (hh ww) (ph pw c)", | |
| ph=self.config.model.image_tokenizer.patch_size, | |
| pw=self.config.model.image_tokenizer.patch_size, | |
| ), | |
| nn.Linear( | |
| config.model.image_tokenizer.in_channels | |
| * (config.model.image_tokenizer.patch_size**2), | |
| config.model.transformer.d, | |
| bias=False, | |
| ), | |
| ) | |
| self.image_tokenizer.apply(_init_weights) | |
| else: | |
| # self.image_tokenizer = CustomImageTokenizer(config) | |
| pass | |
| # image positional embedding embedder | |
| self.use_pe_embedding_layer = config.model.get("input_with_pe", True) | |
| self.pe_embedder = ( | |
| nn.Sequential( | |
| nn.Linear( | |
| config.model.transformer.d, | |
| config.model.transformer.d, | |
| ), | |
| nn.SiLU(), | |
| nn.Linear( | |
| config.model.transformer.d, | |
| config.model.transformer.d, | |
| ), | |
| ) | |
| if self.use_pe_embedding_layer | |
| else nn.Identity() | |
| ) | |
| self.pe_embedder.apply(_init_weights) | |
| # latent scene representation | |
| self.scene_code = nn.Parameter( | |
| torch.randn( | |
| config.model.scene_latent.length, | |
| config.model.transformer.d, | |
| ) | |
| ) | |
| nn.init.trunc_normal_(self.scene_code, std=0.02) | |
| # pose tokens | |
| self.cam_code = nn.Parameter( | |
| torch.randn( | |
| self.config.model.pose_latent.get("length", 1), | |
| config.model.transformer.d, | |
| ) | |
| ) | |
| nn.init.trunc_normal_(self.cam_code, std=0.02) | |
| # pose pe temporal embedder | |
| self.temporal_pe_embedder = ( | |
| nn.Sequential( | |
| nn.Linear( | |
| config.model.transformer.d, | |
| config.model.transformer.d, | |
| ), | |
| nn.SiLU(), | |
| nn.Linear( | |
| config.model.transformer.d, | |
| config.model.transformer.d, | |
| ), | |
| ) | |
| if self.use_pe_embedding_layer | |
| else nn.Identity() | |
| ) | |
| self.temporal_pe_embedder.apply(_init_weights) | |
| # qk norm settings | |
| use_qk_norm = config.model.transformer.get("use_qk_norm", False) | |
| # transformer encoder and init | |
| self.transformer_encoder = [ | |
| QK_Norm_TransformerBlock( | |
| config.model.transformer.d, | |
| config.model.transformer.d_head, | |
| use_qk_norm=use_qk_norm, | |
| ) | |
| for _ in range(config.model.transformer.encoder_n_layer) | |
| ] | |
| if config.model.transformer.get("special_init", False): | |
| if config.model.transformer.get("depth_init", False): | |
| for idx in range(len(self.transformer_encoder)): | |
| weight_init_std = 0.02 / (2 * (idx + 1)) ** 0.5 | |
| self.transformer_encoder[idx].apply( | |
| lambda module: _init_weights_layerwise(module, weight_init_std) | |
| ) | |
| else: | |
| for idx in range(len(self.transformer_encoder)): | |
| weight_init_std = 0.02 / (2 * config.model.transformer.encoder_n_layer) ** 0.5 | |
| self.transformer_encoder[idx].apply( | |
| lambda module: _init_weights_layerwise(module, weight_init_std) | |
| ) | |
| self.transformer_encoder = nn.ModuleList(self.transformer_encoder) | |
| else: | |
| self.transformer_encoder = nn.ModuleList(self.transformer_encoder) | |
| self.transformer_encoder.apply(_init_weights) | |
| # transformer encoder2 and init | |
| self.transformer_encoder_geom = [ | |
| QK_Norm_TransformerBlock( | |
| config.model.transformer.d, | |
| config.model.transformer.d_head, | |
| use_qk_norm=use_qk_norm, | |
| ) | |
| for _ in range(config.model.transformer.encoder_geom_n_layer) | |
| ] | |
| if config.model.transformer.get("special_init", False): | |
| if config.model.transformer.get("depth_init", False): | |
| for idx in range(len(self.transformer_encoder_geom)): | |
| weight_init_std = 0.02 / (2 * (idx + 1)) ** 0.5 | |
| self.transformer_encoder_geom[idx].apply( | |
| lambda module: _init_weights_layerwise(module, weight_init_std) | |
| ) | |
| else: | |
| for idx in range(len(self.transformer_encoder_geom)): | |
| weight_init_std = ( | |
| 0.02 / (2 * config.model.transformer.encoder_geom_n_layer) ** 0.5 | |
| ) | |
| self.transformer_encoder_geom[idx].apply( | |
| lambda module: _init_weights_layerwise(module, weight_init_std) | |
| ) | |
| self.transformer_encoder_geom = nn.ModuleList(self.transformer_encoder_geom) | |
| else: | |
| self.transformer_encoder_geom = nn.ModuleList(self.transformer_encoder_geom) | |
| self.transformer_encoder_geom.apply(_init_weights) | |
| # ln before decoder | |
| self.decoder_ln = nn.LayerNorm(config.model.transformer.d, bias=False) | |
| # transformer decoder and init | |
| self.transformer_decoder = [ | |
| QK_Norm_TransformerBlock( | |
| config.model.transformer.d, | |
| config.model.transformer.d_head, | |
| use_qk_norm=use_qk_norm, | |
| ) | |
| for _ in range(config.model.transformer.decoder_n_layer) | |
| ] | |
| if config.model.transformer.get("special_init", False): | |
| if config.model.transformer.depth_init: | |
| for idx in range(len(self.transformer_decoder)): | |
| weight_init_std = 0.02 / (2 * (idx + 1)) ** 0.5 | |
| self.transformer_decoder[idx].apply( | |
| lambda module: _init_weights_layerwise(module, weight_init_std) | |
| ) | |
| else: | |
| for idx in range(len(self.transformer_decoder)): | |
| weight_init_std = 0.02 / (2 * config.model.transformer.decoder_n_layer) ** 0.5 | |
| self.transformer_decoder[idx].apply( | |
| lambda module: _init_weights_layerwise(module, weight_init_std) | |
| ) | |
| self.transformer_decoder = nn.ModuleList(self.transformer_decoder) | |
| else: | |
| self.transformer_decoder = nn.ModuleList(self.transformer_decoder) | |
| self.transformer_decoder.apply(_init_weights) | |
| # pose predictor | |
| self.pose_predictor = PoseEstimator(config) | |
| # target pose tokenizer | |
| self.target_latent_h = ( | |
| config.model.target_image.height // config.model.target_image.patch_size | |
| ) | |
| self.target_latent_w = ( | |
| config.model.target_image.width // config.model.target_image.patch_size | |
| ) | |
| self.target_pose_tokenizer = nn.Sequential( | |
| Rearrange( | |
| "b v c (hh ph) (ww pw) -> (b v) (hh ww) (ph pw c)", | |
| ph=self.config.model.target_pose_tokenizer.patch_size, | |
| pw=self.config.model.target_pose_tokenizer.patch_size, | |
| ), | |
| nn.Linear( | |
| config.model.target_pose_tokenizer.in_channels | |
| * (config.model.target_pose_tokenizer.patch_size**2), | |
| config.model.transformer.d, | |
| bias=False, | |
| ), | |
| ) | |
| self.target_pose_tokenizer.apply(_init_weights) | |
| self.target_pose_tokenizer2 = nn.Sequential( | |
| Rearrange( | |
| "b v c (hh ph) (ww pw) -> (b v) (hh ww) (ph pw c)", | |
| ph=self.config.model.target_pose_tokenizer.patch_size, | |
| pw=self.config.model.target_pose_tokenizer.patch_size, | |
| ), | |
| nn.Linear( | |
| config.model.target_pose_tokenizer.in_channels | |
| * (config.model.target_pose_tokenizer.patch_size**2), | |
| config.model.transformer.d, | |
| bias=False, | |
| ), | |
| ) | |
| self.target_pose_tokenizer2.apply(_init_weights) | |
| # fuse mlp | |
| self.mlp_fuse = nn.Sequential( | |
| nn.LayerNorm(config.model.transformer.d * 2, bias=False), | |
| nn.Linear( | |
| config.model.transformer.d * 2, | |
| config.model.transformer.d, | |
| bias=True, | |
| ), | |
| nn.SiLU(), | |
| nn.Linear( | |
| config.model.transformer.d, | |
| config.model.transformer.d, | |
| bias=True, | |
| ), | |
| ) | |
| self.mlp_fuse.apply(_init_weights) | |
| # output regresser | |
| self.image_token_decoder = nn.Sequential( | |
| nn.LayerNorm(config.model.transformer.d, bias=False), | |
| nn.Linear( | |
| config.model.transformer.d, | |
| (config.model.target_image.patch_size**2) * 3, | |
| bias=False, | |
| ), | |
| nn.Sigmoid(), | |
| ) | |
| self.image_token_decoder.apply(_init_weights) | |
| # Motion mask system (two components): | |
| # 1. DINOv3 pseudo-label maker: generates GT masks from (rendered, GT) pairs | |
| # 2. MotionMaskPredictor: learns to predict masks from encoded tokens | |
| self.use_motion_mask = config.model.get("use_motion_mask", False) | |
| self.use_dinov3_pseudolabel = config.model.get("use_dinov3_pseudolabel", False) | |
| # Component 1: DINOv3 pseudo-label generator (frozen, for supervision) | |
| if self.use_dinov3_pseudolabel: | |
| dinov3_cfg = config.model.get("dinov3_pseudolabel", {}) | |
| model_name = dinov3_cfg.get("model_name", "facebook/dinov3-vit7b16-pretrain-lvd1689m") | |
| # Load shared DINOv3 backbone | |
| dinov3_backbone = AutoModel.from_pretrained(model_name, torch_dtype=torch.bfloat16) | |
| dinov3_processor = AutoImageProcessor.from_pretrained(model_name) | |
| dinov3_backbone.eval() | |
| # Move to GPU (will be handled by .to(device) in __init__, but ensure dtype is set first) | |
| # Note: Don't call .cuda() here, let the parent model's .to(device) handle it | |
| # Freeze all DINOv3 parameters | |
| for p in dinov3_backbone.parameters(): | |
| p.requires_grad = False | |
| self.dinov3_pseudolabel_maker = DinoV3UncertaintyPseudoLabelMaker( | |
| dinov3_backbone=dinov3_backbone, | |
| dinov3_processor=dinov3_processor, | |
| percentile_threshold=dinov3_cfg.get("percentile_threshold", 75), | |
| use_ssim=dinov3_cfg.get("use_ssim", True), | |
| target_size=dinov3_cfg.get("target_size", 256), | |
| use_coseg_binary=dinov3_cfg.get("use_coseg_binary", False), | |
| coseg_k=dinov3_cfg.get("coseg_k", 64), | |
| coseg_consistency_min_frames=dinov3_cfg.get("coseg_consistency_min_frames", 4), | |
| coseg_frame_saliency_quantile=dinov3_cfg.get("coseg_frame_saliency_quantile", 0.75), | |
| coseg_cluster_top_percent=dinov3_cfg.get("coseg_cluster_top_percent", 0.05), | |
| coseg_morph_kernel=dinov3_cfg.get("coseg_morph_kernel", 3), | |
| coseg_morph_iters=dinov3_cfg.get("coseg_morph_iters", 1), | |
| coseg_min_component_area_ratio=dinov3_cfg.get( | |
| "coseg_min_component_area_ratio", 0.0025 | |
| ), | |
| coseg_grabcut_kernel=dinov3_cfg.get("coseg_grabcut_kernel", 7), | |
| w_dino=dinov3_cfg.get("w_dino", 0.5), | |
| w_ssim=dinov3_cfg.get("w_ssim", 0.5), | |
| ) | |
| percentile = dinov3_cfg.get("percentile_threshold", 75) | |
| use_ssim = dinov3_cfg.get("use_ssim", True) | |
| print(f"✓ DINOv3 pseudo-label maker initialized: P{percentile}, SSIM={use_ssim}") | |
| else: | |
| self.dinov3_pseudolabel_maker = None | |
| dinov3_backbone = None | |
| dinov3_processor = None | |
| # Component 2: Learned motion mask predictor (trainable) | |
| if self.use_motion_mask: | |
| if self.use_dinov3_pseudolabel and self.dinov3_pseudolabel_maker is not None: | |
| # Share the DINOv3 backbone from pseudo-label maker | |
| self.motion_mask_predictor = MotionMaskPredictor( | |
| config, | |
| self.dinov3_pseudolabel_maker.backbone, | |
| self.dinov3_pseudolabel_maker.processor, | |
| ) | |
| print("✓ Motion mask predictor initialized (sharing DINOv3 backbone)") | |
| else: | |
| # Load separate DINOv3 if pseudo-label maker not enabled | |
| model_name = config.model.get("dinov3_pseudolabel", {}).get( | |
| "model_name", "facebook/dinov3-vit7b16-pretrain-lvd1689m" | |
| ) | |
| dinov3_backbone = AutoModel.from_pretrained(model_name, torch_dtype=torch.bfloat16) | |
| dinov3_processor = AutoImageProcessor.from_pretrained(model_name) | |
| dinov3_backbone.eval() | |
| for p in dinov3_backbone.parameters(): | |
| p.requires_grad = False | |
| self.motion_mask_predictor = MotionMaskPredictor( | |
| config, | |
| dinov3_backbone, | |
| dinov3_processor, | |
| ) | |
| print("✓ Motion mask predictor initialized (separate DINOv3 backbone)") | |
| else: | |
| self.motion_mask_predictor = None | |
| # Opportunistic caching for DINOv3 pseudo-labels (saves compute on frozen renderings) | |
| self.enable_mask_cache = config.model.dinov3_pseudolabel.get("enable_mask_cache", False) | |
| if self.enable_mask_cache: | |
| self.mask_cache_dir = config.model.dinov3_pseudolabel.get( | |
| "mask_cache_dir", "./motion_mask_cache" | |
| ) | |
| print(f"✓ Opportunistic mask caching enabled: {self.mask_cache_dir}") | |
| else: | |
| self.mask_cache_dir = None | |
| # loss | |
| self.loss_computer = LossComputer_official(config) | |
| # config backup | |
| self.config_bk = copy.deepcopy(config) | |
| self.render_interpolate = config.training.get("render_interpolate", False) | |
| copy_paste_cfg = ( | |
| getattr(config.training, "copy_paste", {}) if hasattr(config, "training") else {} | |
| ) | |
| inference_cfg = getattr(config, "inference", None) | |
| if hasattr(inference_cfg, "get"): | |
| is_inference_mode = inference_cfg.get("if_inference", False) | |
| else: | |
| is_inference_mode = bool(inference_cfg) | |
| self.copy_paste_training_enabled = not is_inference_mode and copy_paste_cfg.get( | |
| "enabled", False | |
| ) | |
| if config.model.transformer.get("fix_decoder", False): | |
| self.freeze_weights() | |
| if self.copy_paste_training_enabled: | |
| self._unfreeze_renderer_weights() | |
| # training settings | |
| if config.inference or config.get("evaluation", False): | |
| if config.training.get("random_inputs", False): | |
| self.random_index = True | |
| else: | |
| self.random_index = False | |
| else: | |
| self.random_index = config.training.get("random_split", False) | |
| print("Use random index:", self.random_index) | |
| self.motion_mask_threshold = config.model.get("motion_mask_threshold", 0.1) | |
| if config.model.get("use_mae_masking", False): | |
| self.noise_token = nn.Parameter(torch.randn(1, 1, self.config.model.transformer.d)) | |
| nn.init.trunc_normal_(self.noise_token, std=0.02) | |
| # Stage 3: Freeze motion mask predictor if motion_mask_only_training is False | |
| # (This means we're training the rendering model, not the motion mask) | |
| motion_mask_only = config.model.get("motion_mask_only_training", False) | |
| if self.use_motion_mask and not motion_mask_only: | |
| if self.copy_paste_training_enabled: | |
| print("✓ Stage 3 + copy-paste detected: keeping motion mask predictor trainable") | |
| else: | |
| print( | |
| "✓ Stage 3 detected: Freezing motion mask predictor (training rendering only)" | |
| ) | |
| self._freeze_motion_mask_predictor() | |
| def freeze_weights(self): | |
| for param in self.target_pose_tokenizer.parameters(): | |
| param.requires_grad = False | |
| for param in self.image_token_decoder.parameters(): | |
| param.requires_grad = False | |
| for param in self.transformer_decoder.parameters(): | |
| param.requires_grad = False | |
| def _unfreeze_renderer_weights(self): | |
| for module in [self.transformer_decoder, self.image_token_decoder]: | |
| for param in module.parameters(): | |
| param.requires_grad = True | |
| print("✓ Copy-paste enabled: renderer weights unfrozen") | |
| def _freeze_motion_mask_predictor(self): | |
| """Freeze motion mask predictor for Stage 3 training (rendering only).""" | |
| if self.motion_mask_predictor is not None: | |
| for param in self.motion_mask_predictor.parameters(): | |
| param.requires_grad = False | |
| print( | |
| f" Frozen {sum(p.numel() for p in self.motion_mask_predictor.parameters())} motion mask parameters" | |
| ) | |
| def train(self, mode=True): | |
| # override the train method to keep the fronzon modules in eval mode | |
| super().train(mode) | |
| self.loss_computer.eval() | |
| def get_overview(self): | |
| count_train_params = lambda model: sum( | |
| p.numel() for p in model.parameters() if p.requires_grad | |
| ) | |
| overview = edict( | |
| image_tokenizer=count_train_params(self.image_tokenizer), | |
| scene_latent=self.scene_code.data.numel(), | |
| transformer=count_train_params(self.transformer_decoder) | |
| + count_train_params(self.transformer_encoder), | |
| image_token_decoder=count_train_params(self.image_token_decoder), | |
| ) | |
| return overview | |
| def _try_load_mask_from_cache(self, uid): | |
| """Try to load cached mask for a given scene UID.""" | |
| if not self.enable_mask_cache or self.mask_cache_dir is None: | |
| return None | |
| cache_file = os.path.join(self.mask_cache_dir, f"mask_{uid:08d}.pt") | |
| if os.path.exists(cache_file): | |
| try: | |
| mask = torch.load(cache_file, map_location="cpu") | |
| return mask | |
| except Exception as e: | |
| print(f"Warning: Failed to load cache {cache_file}: {e}") | |
| return None | |
| return None | |
| def _save_mask_to_cache(self, uid, mask): | |
| """Save computed mask to cache for future use.""" | |
| if not self.enable_mask_cache or self.mask_cache_dir is None: | |
| return | |
| os.makedirs(self.mask_cache_dir, exist_ok=True) | |
| cache_file = os.path.join(self.mask_cache_dir, f"mask_{uid:08d}.pt") | |
| try: | |
| torch.save(mask.cpu(), cache_file) | |
| except Exception as e: | |
| print(f"Warning: Failed to save cache {cache_file}: {e}") | |
| def _apply_token_dropping( | |
| self, | |
| img_tokens_fused, # [b, v_input*n, d] - AFTER fusion! | |
| predicted_masks, # [b*v_input, 1, 256, 256] - motion mask logits OR binary probs | |
| b, | |
| v_input, | |
| n, | |
| d, | |
| dataset_sources=None, | |
| using_gt_masks=False, | |
| copy_paste_mask_input=None, | |
| ): | |
| """ | |
| Apply token dropping to FUSED tokens using predicted motion masks. | |
| Zeros out dynamic tokens instead of replacing with learnable noise. | |
| For dynamic scenes (DRE10K): Use predicted motion masks | |
| For static scenes (RE10K): Apply random dropout (20% of tokens) | |
| Args: | |
| img_tokens_fused: [b, v_input*n, d] fused tokens (img + plucker) | |
| predicted_masks: [b*v_input, 1, 256, 256] motion masks (logits OR binary probs) | |
| b, v_input, n, d: batch size, num input views, num tokens, token dim | |
| dataset_sources: List[str] of length b, indicating dataset source per batch element | |
| using_gt_masks: bool, if True predicted_masks are already binary probs {0,1} | |
| Returns: | |
| img_tokens_dropped: [b, v_input*n, d] tokens with dynamic positions zeroed | |
| dynamic_mask: [b*v_input, 1, H_patch, W_patch] binary mask for visualization | |
| """ | |
| if copy_paste_mask_input is None and (predicted_masks is None or not self.use_motion_mask): | |
| # No dropping | |
| return img_tokens_fused, None | |
| # Downsample masks from 256×256 to token grid (16×16) | |
| H_patch = W_patch = int(n**0.5) | |
| # Determine which masks to use for token dropping | |
| if copy_paste_mask_input is not None: | |
| mask_tokens_probs = copy_paste_mask_input.to(img_tokens_fused.device).float() | |
| # mask_tokens = F.adaptive_avg_pool2d(mask_tokens_probs, (H_patch, W_patch)) | |
| # dynamic_mask = (mask_tokens > 0.5).float() | |
| use_copy_paste_masks = True | |
| else: | |
| use_copy_paste_masks = False | |
| if using_gt_masks: | |
| mask_tokens_probs = predicted_masks # Already binary probabilities {0, 1} | |
| else: | |
| mask_tokens_probs = torch.sigmoid(predicted_masks) # Convert logits to probs | |
| mask_tokens = F.adaptive_avg_pool2d(mask_tokens_probs, (H_patch, W_patch)) | |
| dynamic_mask = (mask_tokens > self.motion_mask_threshold).float() | |
| # Apply random dropout based on dataset source | |
| if dataset_sources is not None and self.training and not use_copy_paste_masks: | |
| for batch_idx in range(b): | |
| dataset_source = dataset_sources[batch_idx] | |
| if dataset_source == "re10k" and self.config.model.get( | |
| "re10k_random_dropout", False | |
| ): | |
| # RE10K (static): Replace predicted masks with semantic box masks | |
| dropout_ratio = self.config.model.get("re10k_dropout_ratio", 0.2) # Default 20% | |
| box_masks = torch.zeros( | |
| v_input, 1, H_patch, W_patch, device=dynamic_mask.device | |
| ) | |
| for v_idx in range(v_input): | |
| # Number of boxes per view (1-3) | |
| num_boxes = torch.randint(1, 4, (1,)).item() | |
| target_area_per_box = (dropout_ratio * H_patch * W_patch) / num_boxes | |
| for _ in range(num_boxes): | |
| # Random box size (aspect ratio between 0.5 and 2.0) | |
| aspect_ratio = 0.5 + 1.5 * torch.rand(1).item() # [0.5, 2.0] | |
| box_h = int((target_area_per_box * aspect_ratio) ** 0.5) | |
| box_w = int(target_area_per_box / box_h) | |
| # Clamp to valid range | |
| box_h = min(max(box_h, 2), H_patch) | |
| box_w = min(max(box_w, 2), W_patch) | |
| # Random position | |
| y = torch.randint(0, H_patch - box_h + 1, (1,)).item() | |
| x = torch.randint(0, W_patch - box_w + 1, (1,)).item() | |
| # Fill box region | |
| box_masks[v_idx, 0, y : y + box_h, x : x + box_w] = 1.0 | |
| # Replace predicted masks with box masks for RE10K | |
| start_idx = batch_idx * v_input | |
| end_idx = start_idx + v_input | |
| dynamic_mask[start_idx:end_idx] = box_masks | |
| # DRE10K: Add random dropout ON TOP of motion mask (not elif!) | |
| if dataset_source == "dre10k" and self.config.model.get( | |
| "dre10k_random_dropout", False | |
| ): | |
| # DRE10K (dynamic): Add additional random dropout on top of motion mask | |
| dropout_ratio = self.config.model.get( | |
| "dre10k_dropout_ratio", 0.05 | |
| ) # Default 5% | |
| for v_idx in range(v_input): | |
| # Generate small random box masks (1-2 boxes) | |
| num_boxes = torch.randint(1, 3, (1,)).item() | |
| target_area_per_box = (dropout_ratio * H_patch * W_patch) / num_boxes | |
| for _ in range(num_boxes): | |
| # Random box size (aspect ratio between 0.5 and 2.0) | |
| aspect_ratio = 0.5 + 1.5 * torch.rand(1).item() # [0.5, 2.0] | |
| box_h = int((target_area_per_box * aspect_ratio) ** 0.5) | |
| box_w = int(target_area_per_box / box_h) | |
| # Clamp to valid range | |
| box_h = min(max(box_h, 2), H_patch) | |
| box_w = min(max(box_w, 2), W_patch) | |
| # Random position | |
| y = torch.randint(0, H_patch - box_h + 1, (1,)).item() | |
| x = torch.randint(0, W_patch - box_w + 1, (1,)).item() | |
| # Fill box region (add to existing motion mask using OR operation) | |
| view_global_idx = batch_idx * v_input + v_idx | |
| dynamic_mask[view_global_idx, 0, y : y + box_h, x : x + box_w] = 1.0 | |
| # Reshape to match token sequence | |
| dynamic_mask_flat = rearrange( | |
| dynamic_mask, "(b v) 1 h w -> b (v h w) 1", b=b, v=v_input | |
| ) # [b, v_input*n, 1] | |
| # Token dropping: zero out dynamic tokens (no learnable noise!) | |
| img_tokens_dropped = img_tokens_fused * (1.0 - dynamic_mask_flat) | |
| return img_tokens_dropped, dynamic_mask | |
| def _apply_mae_masking( | |
| self, | |
| img_tokens_input, | |
| predicted_masks, | |
| b, | |
| v_input, | |
| n, | |
| d, | |
| dataset_sources=None, | |
| using_gt_masks=False, | |
| copy_paste_mask_input=None, | |
| ): | |
| """ | |
| Apply MAE-style token masking to input tokens using predicted motion masks. | |
| """ | |
| if copy_paste_mask_input is None and (predicted_masks is None or not self.use_motion_mask): | |
| return img_tokens_input | |
| # Downsample masks from 256×256 to token grid (16×16) | |
| H_patch = W_patch = int(n**0.5) | |
| if copy_paste_mask_input is not None: | |
| mask_tokens_probs = copy_paste_mask_input.to(img_tokens_input.device).float() | |
| # mask_tokens = F.adaptive_avg_pool2d(mask_tokens_probs, (H_patch, W_patch)) | |
| # dynamic_mask = (mask_tokens > 0.5).float() | |
| else: | |
| # Binarize: sigmoid + threshold | |
| if using_gt_masks: | |
| mask_tokens_probs = predicted_masks # Already binary probabilities {0, 1} | |
| else: | |
| mask_tokens_probs = torch.sigmoid(predicted_masks) | |
| mask_tokens = F.adaptive_avg_pool2d(mask_tokens_probs, (H_patch, W_patch)) | |
| dynamic_mask = (mask_tokens > self.motion_mask_threshold).float() # [b*v_input, 1, 16, 16] | |
| # Apply random dropout based on dataset source | |
| if dataset_sources is not None and self.training and copy_paste_mask_input is None: | |
| for batch_idx in range(b): | |
| dataset_source = dataset_sources[batch_idx] | |
| if dataset_source == "re10k" and self.config.model.get( | |
| "re10k_random_dropout", False | |
| ): | |
| # RE10K (static): Replace predicted masks with semantic box masks | |
| dropout_ratio = self.config.model.get("re10k_dropout_ratio", 0.2) # Default 20% | |
| box_masks = torch.zeros( | |
| v_input, 1, H_patch, W_patch, device=dynamic_mask.device | |
| ) | |
| for v_idx in range(v_input): | |
| # Number of boxes per view (1-3) | |
| num_boxes = torch.randint(1, 4, (1,)).item() | |
| target_area_per_box = (dropout_ratio * H_patch * W_patch) / num_boxes | |
| for _ in range(num_boxes): | |
| # Random box size (aspect ratio between 0.5 and 2.0) | |
| aspect_ratio = 0.5 + 1.5 * torch.rand(1).item() # [0.5, 2.0] | |
| box_h = int((target_area_per_box * aspect_ratio) ** 0.5) | |
| box_w = int(target_area_per_box / box_h) | |
| # Clamp to valid range | |
| box_h = min(max(box_h, 2), H_patch) | |
| box_w = min(max(box_w, 2), W_patch) | |
| # Random position | |
| y = torch.randint(0, H_patch - box_h + 1, (1,)).item() | |
| x = torch.randint(0, W_patch - box_w + 1, (1,)).item() | |
| # Fill box region | |
| box_masks[v_idx, 0, y : y + box_h, x : x + box_w] = 1.0 | |
| # Replace predicted masks with box masks for RE10K | |
| start_idx = batch_idx * v_input | |
| end_idx = start_idx + v_input | |
| dynamic_mask[start_idx:end_idx] = box_masks | |
| # DRE10K: Add random dropout ON TOP of motion mask (not elif!) | |
| if dataset_source == "dre10k" and self.config.model.get( | |
| "dre10k_random_dropout", False | |
| ): | |
| # DRE10K (dynamic): Add additional random dropout on top of motion mask | |
| dropout_ratio = self.config.model.get( | |
| "dre10k_dropout_ratio", 0.05 | |
| ) # Default 5% | |
| for v_idx in range(v_input): | |
| # Generate small random box masks (1-2 boxes) | |
| num_boxes = torch.randint(1, 3, (1,)).item() | |
| target_area_per_box = (dropout_ratio * H_patch * W_patch) / num_boxes | |
| for _ in range(num_boxes): | |
| # Random box size (aspect ratio between 0.5 and 2.0) | |
| aspect_ratio = 0.5 + 1.5 * torch.rand(1).item() # [0.5, 2.0] | |
| box_h = int((target_area_per_box * aspect_ratio) ** 0.5) | |
| box_w = int(target_area_per_box / box_h) | |
| # Clamp to valid range | |
| box_h = min(max(box_h, 2), H_patch) | |
| box_w = min(max(box_w, 2), W_patch) | |
| # Random position | |
| y = torch.randint(0, H_patch - box_h + 1, (1,)).item() | |
| x = torch.randint(0, W_patch - box_w + 1, (1,)).item() | |
| # Fill box region (add to existing motion mask using OR operation) | |
| view_global_idx = batch_idx * v_input + v_idx | |
| dynamic_mask[view_global_idx, 0, y : y + box_h, x : x + box_w] = 1.0 | |
| # Reshape to match token sequence | |
| dynamic_mask_flat = rearrange( | |
| dynamic_mask, "(b v) 1 h w -> b (v h w) 1", b=b, v=v_input | |
| ) # [b, v_input*n, 1] | |
| # MAE-style replacement: keep static tokens, replace dynamic with learnable noise | |
| noise_tokens = self.noise_token.expand(b, v_input * n, d) | |
| img_tokens_masked = ( | |
| img_tokens_input * (1.0 - dynamic_mask_flat) # Keep static | |
| + noise_tokens * dynamic_mask_flat # Replace dynamic with noise | |
| ) | |
| return img_tokens_masked, dynamic_mask | |
| def reconstruct_images(self, data_batch): | |
| """ | |
| Minimal API used by train.py for visualization. | |
| Delegates to forward with create_visual=True to produce renderings and visuals. | |
| Returns an edict with at least: input, target, render, and optional render_video. | |
| """ | |
| return self.forward(data_batch, create_visual=True, render_video=False) | |
| def forward(self, data, create_visual=False, render_video=False, iter=0): | |
| """ | |
| Split all images into two sets, use one set to get scene representation, | |
| use the other to render & train | |
| """ | |
| input, target = self.process_data( | |
| data, has_target_image=True, target_has_input=True, compute_rays=False | |
| ) | |
| image = input.image * 2.0 - 1.0 # [b, v, c, h, w], range (0,1) to (-1,1) | |
| b, v_input, c, h, w = image.shape | |
| image_all = data["image"] * 2.0 - 1.0 # [b, v_all, c, h, w], range (0,1) to (-1,1) | |
| v_all = image_all.shape[1] | |
| v_target = ( | |
| target.image.shape[1] if hasattr(target, "image") and target.image is not None else 0 | |
| ) | |
| device = image.device | |
| batch_idx = torch.arange(b).unsqueeze(1).to(device) | |
| """se3 pose prediction for all views""" | |
| # tokenize images, add spatial-temporal p.e. | |
| img_tokens = self.image_tokenizer(image_all) # [b * v, n, d] | |
| _, n, d = img_tokens.shape | |
| if self.use_pe_embedding_layer: | |
| img_tokens = self.add_sptial_temporal_pe(img_tokens, b, v_all, h, w) | |
| img_tokens = rearrange(img_tokens, "(b v) n d -> b (v n) d", b=b, v=v_all) # [b, v * n, d] | |
| # get camera tokens, add temporal p.e. | |
| cam_tokens = self.get_camera_tokens(b, v_all) # [b, v_all * n_cam, d] | |
| n_cam = cam_tokens.shape[1] // v_all | |
| assert n_cam == 1 | |
| cam_tokens = rearrange(cam_tokens, "b (v n) d -> b v n d", v=v_all) # [b, v_all, n_cam, d] | |
| cam_tokens = rearrange(cam_tokens, "b v n d -> b (v n) d") # [b, v_all * n_cam, d] | |
| # pose estimation for all views | |
| all_tokens = torch.cat([cam_tokens, img_tokens], dim=1) | |
| all_tokens = self.run_encoder(all_tokens) | |
| cam_tokens, _ = all_tokens.split([v_all * n_cam, v_all * n], dim=1) | |
| # get se3 poses and intrinsics | |
| cam_tokens = rearrange(cam_tokens, "b (v n) d -> (b v) n d", b=b, v=v_all, n=n_cam)[ | |
| :, 0 | |
| ] # [b * v_all, d] | |
| cam_info = self.pose_predictor( | |
| cam_tokens, v_all | |
| ) # [b * v_all, num_pose_element+3+4], rot, 3d trans, 4d fxfycxcy | |
| c2w, fxfycxcy = get_cam_se3(cam_info) # [b * v_all,4,4], [b * v_all,4] | |
| normalized = True | |
| # get plucker ray and embeddings | |
| plucker_rays = cam_info_to_plucker( | |
| c2w, fxfycxcy, self.config.model.target_image, normalized=normalized | |
| ) | |
| plucker_rays = rearrange(plucker_rays, "(b v) c h w -> b v c h w", b=b, v=v_all) | |
| # Split plucker rays into input and target | |
| plucker_input = plucker_rays[:, :v_input] # [b, v_input, 6, h, w] | |
| plucker_target = ( | |
| plucker_rays[:, v_input : v_input + v_target] if v_target > 0 else None | |
| ) # [b, v_target, 6, h, w] | |
| plucker_emb_input_tokens = self.target_pose_tokenizer(plucker_input) # [b * v_input, n, d] | |
| plucker_emb_input_render_tokens = self.target_pose_tokenizer2( | |
| plucker_input | |
| ) # [b * v_input, n, d] | |
| plucker_emb_target_tokens = ( | |
| self.target_pose_tokenizer2(plucker_target) if plucker_target is not None else None | |
| ) # [b * v_target, n, d] | |
| # ===== COPY-PASTE AUGMENTATION MASKS (GROUND TRUTH TRANSIENTS) ===== | |
| copy_paste_mask = data.get("copy_paste_mask", None) | |
| copy_paste_mask_input = None | |
| copy_paste_mask_target = None | |
| copy_paste_mask_input_flat = None | |
| copy_paste_mask_target_flat = None | |
| if copy_paste_mask is not None: | |
| if not isinstance(copy_paste_mask, torch.Tensor): | |
| copy_paste_mask = torch.tensor(copy_paste_mask) | |
| copy_paste_mask = copy_paste_mask.to(device=device, dtype=torch.float32) | |
| if copy_paste_mask.dim() == 4: | |
| # Assume shape (views, 1, H, W) → add batch dim | |
| copy_paste_mask = copy_paste_mask.unsqueeze(0) | |
| # Ensure shape [b, v_all, 1, H, W] | |
| shape_is_valid = ( | |
| copy_paste_mask.dim() == 5 | |
| and copy_paste_mask.shape[0] == b | |
| and copy_paste_mask.shape[1] == v_all | |
| ) | |
| if not shape_is_valid: | |
| print( | |
| "[Model] Warning: copy_paste_mask shape mismatch. " | |
| f"Got {copy_paste_mask.shape}, expected ({b}, {v_all}, 1, H, W). " | |
| "Ignoring copy-paste masks for this batch." | |
| ) | |
| copy_paste_mask = None | |
| else: | |
| if v_input > 0: | |
| copy_paste_mask_input = copy_paste_mask[:, :v_input] | |
| copy_paste_mask_input_flat = rearrange( | |
| copy_paste_mask_input, "b v c h w -> (b v) c h w" | |
| ) | |
| if v_target > 0: | |
| copy_paste_mask_target = copy_paste_mask[:, v_input : v_input + v_target] | |
| copy_paste_mask_target_flat = rearrange( | |
| copy_paste_mask_target, "b v c h w -> (b v) c h w" | |
| ) | |
| # ===== PREDICT MOTION MASKS BEFORE RESHAPING ===== | |
| # Use per-view Plücker embeddings [b*v, n, d] for motion mask predictor | |
| predicted_masks_input = None | |
| predicted_masks_target = None | |
| dinov3_masks = None | |
| if self.use_motion_mask and self.motion_mask_predictor is not None: | |
| input_image_tokens = rearrange(img_tokens, "b (v n) d -> (b v) n d", v=v_all)[ | |
| : b * v_input | |
| ] # [b*v_input, n, d] | |
| input_image_for_motion = input.image.reshape(b * v_input, c, h, w) | |
| # Extract target image tokens from all tokens | |
| target_image_tokens = rearrange(img_tokens, "b (v n) d -> (b v) n d", v=v_all)[ | |
| b * v_input : | |
| ] # [b*v_target, n, d] | |
| # Prepare target images for motion mask prediction (in range [0, 1]) | |
| target_images_for_motion = target.image.reshape( | |
| b * v_target, c, h, w | |
| ) # [b*v_target, 3, h, w] | |
| # Predict motion masks BEFORE reshaping plucker_emb_input | |
| predicted_masks_input = self.motion_mask_predictor( | |
| input_image_for_motion, input_image_tokens, plucker_emb_input_tokens | |
| ) # [b*v_input, 1, 256, 256] | |
| if v_target > 0: | |
| predicted_masks_target = self.motion_mask_predictor( | |
| target_images_for_motion, target_image_tokens, plucker_emb_target_tokens | |
| ) # [b*v_target, 1, 256, 256] | |
| # Override with GT motion masks if provided (inference only) | |
| # Store flag to skip sigmoid later (GT masks are already binary probabilities) | |
| using_gt_masks_input = False | |
| using_gt_masks_target = False | |
| # Safely check inference config (might be bool or dict) | |
| inference_cfg = self.config.inference | |
| if hasattr(inference_cfg, "get"): | |
| use_gt_motion_masks = inference_cfg.get("use_gt_motion_masks", False) | |
| else: | |
| use_gt_motion_masks = False | |
| if not self.training and use_gt_motion_masks: | |
| if hasattr(input, "gt_motion_masks") and input.gt_motion_masks is not None: | |
| gt_masks = input.gt_motion_masks.to(predicted_masks_input.device).float() | |
| predicted_masks_input = gt_masks.view(-1, 1, 256, 256) | |
| using_gt_masks_input = True | |
| print( | |
| "[Model] Using GT motion masks for INPUT views " | |
| f"(shape: {predicted_masks_input.shape})" | |
| ) | |
| if ( | |
| v_target > 0 | |
| and hasattr(target, "gt_motion_masks") | |
| and target.gt_motion_masks is not None | |
| ): | |
| gt_masks = target.gt_motion_masks.to(predicted_masks_target.device).float() | |
| predicted_masks_target = gt_masks.view(-1, 1, 256, 256) | |
| using_gt_masks_target = True | |
| print( | |
| "[Model] Using GT motion masks for TARGET views " | |
| f"(shape: {predicted_masks_target.shape})" | |
| ) | |
| # ===== NOW RESHAPE FOR RENDERING PIPELINE ===== | |
| plucker_emb_input_flat = rearrange( | |
| plucker_emb_input_tokens, "(b v) n d -> b (v n) d", b=b, v=v_input | |
| ) # [b, v_input * n, d] | |
| # breakpoint() | |
| # ## ===== TEMPORARY VISUALIZATION CODE ===== | |
| # import matplotlib.pyplot as plt | |
| # import numpy as np | |
| # from pathlib import Path | |
| # vis_dir = Path("./experiments/temp_vis") | |
| # vis_dir.mkdir(parents=True, exist_ok=True) | |
| # # Get first sample from batch | |
| # sample_idx = 0 | |
| # # Get input images [b, v_input, 3, 256, 256] | |
| # input_imgs = data["image"][sample_idx, :v_input].cpu() # [v_input, 3, 256, 256] | |
| # # Get target images [b, v_target, 3, 256, 256] | |
| # target_imgs = data["image"][sample_idx, v_input:].cpu() # [v_target, 3, 256, 256] | |
| # # Get masks for this sample (detach to avoid gradient issues) | |
| # input_masks_sample = predicted_masks_input[sample_idx * v_input:(sample_idx + 1) * v_input].detach().cpu() # [v_input, 1, 256, 256] | |
| # target_masks_sample = predicted_masks_target[sample_idx * v_target:(sample_idx + 1) * v_target].detach().cpu() # [v_target, 1, 256, 256] | |
| # # Convert to numpy and denormalize images | |
| # def to_numpy_img(img): | |
| # # img: [3, 256, 256] in range [0, 1] | |
| # img = img.permute(1, 2, 0).numpy() # [256, 256, 3] | |
| # img = np.clip(img, 0, 1) | |
| # return img | |
| # def mask_to_numpy(mask): | |
| # # mask: [1, 256, 256] logits | |
| # mask_prob = torch.sigmoid(mask).squeeze(0).float().numpy() # [256, 256] | |
| # return mask_prob | |
| # # Create figure: 2 rows (input, target), columns = max(v_input, v_target) | |
| # n_cols = max(v_input, v_target) | |
| # fig, axes = plt.subplots(2, n_cols, figsize=(3 * n_cols, 6)) | |
| # if n_cols == 1: | |
| # axes = axes.reshape(2, 1) | |
| # # Row 0: Input views with masks | |
| # for i in range(n_cols): | |
| # ax = axes[0, i] | |
| # if i < v_input: | |
| # img = to_numpy_img(input_imgs[i]) | |
| # mask = mask_to_numpy(input_masks_sample[i]) | |
| # ax.imshow(img) | |
| # ax.imshow(mask, cmap='jet', alpha=0.5, vmin=0, vmax=1) | |
| # ax.set_title(f'Input {i}\n(mask overlay)', fontsize=10) | |
| # else: | |
| # ax.axis('off') | |
| # ax.set_xticks([]) | |
| # ax.set_yticks([]) | |
| # # Row 1: Target views with masks | |
| # for i in range(n_cols): | |
| # ax = axes[1, i] | |
| # if i < v_target: | |
| # img = to_numpy_img(target_imgs[i]) | |
| # mask = mask_to_numpy(target_masks_sample[i]) | |
| # ax.imshow(img) | |
| # ax.imshow(mask, cmap='jet', alpha=0.5, vmin=0, vmax=1) | |
| # ax.set_title(f'Target {i}\n(mask overlay)', fontsize=10) | |
| # else: | |
| # ax.axis('off') | |
| # ax.set_xticks([]) | |
| # ax.set_yticks([]) | |
| # plt.suptitle(f'Predicted Motion Masks (Iter {iter}, Sample {sample_idx})', fontsize=14, fontweight='bold') | |
| # plt.tight_layout() | |
| # save_path = vis_dir / f'masks_iter_{iter:08d}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.png' | |
| # plt.savefig(save_path, dpi=150, bbox_inches='tight') | |
| # plt.close(fig) | |
| # print(f"[DEBUG] Saved mask visualization to {save_path}") | |
| # # ===== END TEMPORARY VISUALIZATION CODE ===== | |
| # breakpoint() | |
| # Generate DINOv3 pseudo-labels (only after rendering, in loss computation) | |
| # This will be done in the loss computer to ensure rendered images are available | |
| """predict scene representation using (posed) input views""" | |
| # get posed image representation | |
| img_tokens_input = rearrange(img_tokens, "b (v n) d -> b v n d", v=v_all)[ | |
| :, :v_input | |
| ] # [b, v_input, n, d] | |
| img_tokens_input = rearrange(img_tokens_input, "b v n d -> b (v n) d") | |
| img_tokens_input = torch.cat( | |
| [img_tokens_input, plucker_emb_input_flat], dim=-1 | |
| ) # [b, v_input * n, 2d] | |
| img_tokens_input = self.mlp_fuse(img_tokens_input) # [b, v_input * n, d] | |
| # ===== TOKEN MASKING OR DROPPING (APPLY TO INPUT TOKENS) ===== | |
| use_token_dropping = self.config.model.get("use_token_dropping", True) | |
| if use_token_dropping: | |
| img_tokens_input_masked_fused, input_patch_mask = self._apply_token_dropping( | |
| img_tokens_input, | |
| predicted_masks_input, | |
| b, | |
| v_input, | |
| n, | |
| d, | |
| dataset_sources=data.get("dataset_source", [None] * b), | |
| using_gt_masks=using_gt_masks_input, | |
| copy_paste_mask_input=copy_paste_mask_input_flat, | |
| ) | |
| else: | |
| img_tokens_input_masked_fused, input_patch_mask = self._apply_mae_masking( | |
| img_tokens_input, | |
| predicted_masks_input, | |
| b, | |
| v_input, | |
| n, | |
| d, | |
| dataset_sources=data.get("dataset_source", [None] * b), | |
| using_gt_masks=using_gt_masks_input, | |
| copy_paste_mask_input=copy_paste_mask_input_flat, | |
| ) | |
| ## TEMPORARY VISUALIZATION CODE ### | |
| ## ===== VISUALIZATION ===== | |
| # breakpoint() | |
| # from pathlib import Path | |
| # sample_idx = 0 | |
| # vis_dir = Path("./temp_vis") | |
| # vis_dir.mkdir(parents=True, exist_ok=True) | |
| # # Input images and their patch-masks (these masks include any RE10K random-dropout logic) | |
| # input_imgs = data["image"][sample_idx, :v_input].cpu() # [v_input, 3, 256, 256] | |
| # input_masks = input_patch_mask[sample_idx * v_input:(sample_idx + 1) * v_input].detach().cpu() # [(v_input),1,Hpatch,Wpatch] | |
| # # Optional: target masks (if you also predicted them) | |
| # # target_imgs = data["image"][sample_idx, v_input:].cpu() # [v_target, 3, 256, 256] | |
| # # target_masks = target_patch_mask[sample_idx*v_target:(sample_idx+1)*v_target].detach().cpu() | |
| # # Save a grid for inputs | |
| # n_cols = v_input | |
| # fig, axes = plt.subplots(1, n_cols, figsize=(3*n_cols, 3)) | |
| # if n_cols == 1: | |
| # axes = [axes] | |
| # for i in range(v_input): | |
| # overlay = self.red_patch_overlay(input_imgs[i], input_masks[i]) # PIL | |
| # axes[i].imshow(overlay) | |
| # axes[i].set_title(f"Input {i}") | |
| # axes[i].axis("off") | |
| # fig.tight_layout() | |
| # ts = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| # save_path = vis_dir / f"inputs_overlay_iter_{iter:08d}_{ts}.png" | |
| # plt.savefig(save_path, dpi=150, bbox_inches="tight") | |
| # plt.close(fig) | |
| # print(f"[viz] saved {save_path}") | |
| # breakpoint() | |
| ### END TEMPORARY VISUALIZATION CODE ### | |
| # replicate scene tokens | |
| scene_tokens = self.scene_code.expand(b, -1, -1) # [b, n_scene, d] | |
| n_scene = scene_tokens.shape[1] | |
| all_tokens = torch.cat( | |
| [scene_tokens, img_tokens_input_masked_fused], dim=1 | |
| ) # [b, n_scene + v_input * n, d] | |
| # encoder layers, update scene representation | |
| all_tokens = self.run_encoder_geom(all_tokens) | |
| scene_tokens, _ = all_tokens.split( | |
| [n_scene, v_input * n], dim=1 | |
| ) # [b, n_scene, d], [b, v_input * n, d] | |
| """render with scene representation from input views and pose of target views""" | |
| # render target images | |
| render_input_images = None | |
| # Safely check inference config (might be bool or dict) | |
| inference_cfg = self.config.inference | |
| render_input_preds = False | |
| if hasattr(inference_cfg, "get"): | |
| render_input_preds = inference_cfg.get("render_input_predictions", False) | |
| if v_input > 0 and not self.training and render_input_preds: | |
| render_input_results = self.render_images(scene_tokens, plucker_emb_input_render_tokens) | |
| render_input_images = render_input_results.rendered_images | |
| if plucker_emb_target_tokens is not None and v_target > 0: | |
| render_results = self.render_images(scene_tokens, plucker_emb_target_tokens) # dict | |
| # Generate DINOv3 pseudo-labels after rendering (for motion mask supervision) | |
| if ( | |
| self.use_dinov3_pseudolabel | |
| and self.dinov3_pseudolabel_maker is not None | |
| and v_target > 0 | |
| ): | |
| # Get dataset sources - PyTorch's default collate converts "dataset_source" (per sample) | |
| # into "dataset_source" (list) NOT "dataset_sources" (plural) | |
| dataset_sources = data.get("dataset_source", [None] * b) | |
| if not isinstance(dataset_sources, list): | |
| # Fallback: if it's a single string, replicate it | |
| dataset_sources = [dataset_sources] * b | |
| dinov3_masks_list = [] | |
| cache_hits = 0 | |
| cache_misses = 0 | |
| for batch_idx_i in range(b): | |
| is_re10k = dataset_sources[batch_idx_i] == "re10k" | |
| uid = int(target.index[batch_idx_i, 0, -1].item()) | |
| sample_copy_mask = None | |
| if copy_paste_mask_target is not None: | |
| sample_copy_mask = copy_paste_mask_target[batch_idx_i] | |
| if sample_copy_mask.numel() == 0: | |
| sample_copy_mask = None | |
| if sample_copy_mask is not None: | |
| mask_per_sample = sample_copy_mask | |
| elif is_re10k: | |
| # Static RE10K - skip DINOv3 computation, just use all-zeros | |
| # This teaches the model: "static scenes → zero motion mask" | |
| mask_per_sample = torch.zeros(v_target, 1, 256, 256, device=device) | |
| else: | |
| # Dynamic DRE10K - compute or load from cache | |
| # Try loading from cache if enabled | |
| # SAFE during motion mask training: rendering is frozen, so masks are deterministic! | |
| cached_mask = None | |
| if self.enable_mask_cache: | |
| cached_mask = self._try_load_mask_from_cache(uid) | |
| if cached_mask is not None: | |
| mask_per_sample = cached_mask.to(device) | |
| cache_hits += 1 | |
| # Compute if cache miss | |
| if cached_mask is None: | |
| cache_misses += 1 | |
| # Dynamic DRE10K - generate actual pseudo-labels with DINOv3 | |
| # Prepare GT and rendered images for this batch element | |
| gt_sample = target.image[batch_idx_i] # [v_target, 3, h, w] | |
| pred_sample = render_results.rendered_images[ | |
| batch_idx_i | |
| ] # [v_target, 3, h, w] | |
| # Generate pseudo-labels using DINOv3 with debug visualization | |
| debug_path = None | |
| if create_visual and batch_idx_i == 0: | |
| # Save debug visualization for first batch only | |
| if not dist.is_initialized() or dist.get_rank() == 0: | |
| debug_path = ( | |
| f"./debug_dinov3/step_{iter:06d}_batch{batch_idx_i}.png" | |
| ) | |
| mask_per_sample = self.dinov3_pseudolabel_maker( | |
| gt_sample, pred_sample, debug_save_path=debug_path | |
| ) # [v_target, 1, 256, 256] | |
| # Save to cache for future use | |
| if self.enable_mask_cache: | |
| self._save_mask_to_cache(uid, mask_per_sample) | |
| dinov3_masks_list.append(mask_per_sample) | |
| # Log cache stats every iteration (only for DRE10K samples that were actually computed) | |
| if self.enable_mask_cache and cache_misses > 0: | |
| total_dre10k = cache_hits + cache_misses | |
| hit_rate = cache_hits / total_dre10k * 100 | |
| print( | |
| f"[Iter {iter}] DINOv3 mask cache (DRE10K only): {cache_hits}/{total_dre10k} hits ({hit_rate:.1f}% hit rate)" | |
| ) | |
| dinov3_masks = torch.stack(dinov3_masks_list, dim=0) # [b, v_target, 1, 256, 256] | |
| dinov3_masks = rearrange( | |
| dinov3_masks, "b v c h w -> (b v) c h w" | |
| ) # [b*v_target, 1, 256, 256] | |
| if dinov3_masks is not None: | |
| # NOTE: Copy-paste masks are already merged per-sample in dinov3_masks_list | |
| # at line 2051-2052. No need to replace here. | |
| # compute loss with motion mask distillation | |
| # Pass dataset_sources to enable per-dataset masking logic (RE10K vs DRE10K) | |
| dataset_sources = data.get("dataset_source", None) | |
| target_images_for_loss = ( | |
| target.image_clean if hasattr(target, "image_clean") else target.image | |
| ) | |
| loss_metrics = self.loss_computer( | |
| render_results.rendered_images, | |
| target_images_for_loss, | |
| input, | |
| predicted_target_masks=predicted_masks_target, | |
| dinov3_target_masks=dinov3_masks, | |
| predicted_input_masks=predicted_masks_input, | |
| copy_paste_input_masks=copy_paste_mask_input_flat, | |
| create_visual=create_visual, | |
| dataset_sources=dataset_sources, | |
| ) | |
| else: | |
| # No pseudo-labels (e.g. inference with use_dinov3_pseudolabel=false). | |
| # Keep the real render_results from above; only produce a dummy loss. | |
| device = scene_tokens.device | |
| loss_metrics = edict( | |
| loss=torch.tensor(0.0, device=device, requires_grad=True), | |
| l2_loss=torch.tensor(0.0, device=device), | |
| psnr=torch.tensor(0.0, device=device), | |
| lpips_loss=torch.tensor(0.0, device=device), | |
| perceptual_loss=torch.tensor(0.0, device=device), | |
| ssim_loss=torch.tensor(0.0, device=device), | |
| visual=None, | |
| ) | |
| if v_target == 0 or plucker_emb_target_tokens is None: | |
| render_results = edict(rendered_images=None) | |
| vis_only_results = None | |
| if ( | |
| create_visual | |
| and v_target > 0 | |
| and not (self.training and self.copy_paste_training_enabled) | |
| ): | |
| with torch.no_grad(): | |
| c2w_all_reshaped = rearrange(c2w, "(b v) c d -> b v c d", v=v_all) | |
| fxfycxcy_all_reshaped = rearrange(fxfycxcy, "(b v) c -> b v c", v=v_all) | |
| c2w_target = c2w_all_reshaped[ | |
| :, v_input : v_input + v_target | |
| ] # [b, v_target, 4, 4] | |
| fxfycxcy_target = fxfycxcy_all_reshaped[ | |
| :, v_input : v_input + v_target | |
| ] # [b, v_target, 4] | |
| c2w_target = rearrange(c2w_target, "b v c d -> (b v) c d") | |
| fxfycxcy_target = rearrange(fxfycxcy_target, "b v c -> (b v) c") | |
| vis_only_results = self.render_images_video( | |
| scene_tokens, c2w_target, fxfycxcy_target, normalized=normalized | |
| ) | |
| # Reshape masks for visualization (from flattened to per-batch) | |
| if dinov3_masks is not None: | |
| dinov3_masks_viz = rearrange(dinov3_masks, "(b v) c h w -> b v c h w", b=b, v=v_target) | |
| else: | |
| dinov3_masks_viz = None | |
| if predicted_masks_target is not None: | |
| predicted_masks_viz = rearrange( | |
| predicted_masks_target, "(b v) c h w -> b v c h w", b=b, v=v_target | |
| ) | |
| else: | |
| predicted_masks_viz = None | |
| # return results | |
| if hasattr(target, "image_clean"): | |
| target.image_aug = target.image | |
| target.image = target.image_clean | |
| result = edict( | |
| input=input, | |
| target=target, | |
| loss_metrics=loss_metrics, | |
| render=render_results.rendered_images, | |
| c2w=rearrange(c2w, "(b v) c d -> b v c d", b=b, v=v_all), | |
| fxfycxcy=fxfycxcy, | |
| ) | |
| if render_input_images is not None: | |
| result.render_input = render_input_images | |
| # Add motion mask visualization keys if available | |
| if dinov3_masks_viz is not None: | |
| result.dinov3_masks = dinov3_masks_viz.detach() | |
| if predicted_masks_viz is not None: | |
| result.predicted_target_masks = predicted_masks_viz.detach() | |
| if input_patch_mask is not None: | |
| result.input_patch_mask = input_patch_mask.detach() # [b*v_input, 1, H_patch, W_patch] | |
| if predicted_masks_input is not None: | |
| result.predicted_input_masks = ( | |
| predicted_masks_input.detach() | |
| ) # [b*v_input, 1, H_patch, W_patch] | |
| if predicted_masks_target is not None: | |
| result.predicted_target_masks = ( | |
| predicted_masks_target.detach() | |
| ) # [b*v_target, 1, H_patch, W_patch] | |
| if vis_only_results is not None: | |
| result.video_rendering = vis_only_results.rendered_images_video.detach() | |
| return result | |
| def render_images(self, scene_tokens, target_tokens): | |
| """ | |
| Render target views based on the scene representation, target view pose tokens and target view tokens | |
| Args: | |
| scene_tokens: [b, n_scene, d] | |
| target_tokens: plucker embedding tokens of input images [b*v, n_target, d] | |
| Return: | |
| rendered_images: rendered target views in [b,v,c,h,w] | |
| """ | |
| b, _, d = scene_tokens.shape | |
| bv = target_tokens.shape[0] | |
| v = bv // b | |
| # repeat scene tokens | |
| scene_tokens = scene_tokens.unsqueeze(1).repeat(1, v, 1, 1) | |
| scene_tokens = rearrange(scene_tokens, "b v n d -> (b v) n d") # [b*v, n_scene, d] | |
| # get all tokens | |
| n_target, n_scene = target_tokens.shape[1], scene_tokens.shape[1] | |
| all_tokens = torch.cat( | |
| [target_tokens, scene_tokens], dim=1 | |
| ) # [b*v, n_target+n_pose+n_scene, d] | |
| # render | |
| rendered_images_all = self.render(all_tokens, n_target, n_scene, v) # [b,v,c,h,w] | |
| render_results = edict(rendered_images=rendered_images_all) | |
| return render_results | |
| def render(self, all_tokens, n_target, n_scene, v): | |
| """ | |
| Run decoder layers and output mlp to render target views | |
| Args: | |
| all_tokens: [b*v, n_target+n_pose+n_scene, d] | |
| Return: | |
| rendered views: [b, v, c, h, w] | |
| """ | |
| all_tokens = self.decoder_ln(all_tokens) | |
| all_tokens = self.run_decoder(all_tokens) | |
| # split tokens | |
| target_tokens, _ = all_tokens.split([n_target, n_scene], dim=1) # [b*v, n_target, d] | |
| # regress image | |
| rendered_images_all = self.image_token_decoder(target_tokens) # [b*v, n_target, p*p*3] | |
| patch_size = self.config.model.target_image.patch_size | |
| rendered_images_all = rearrange( | |
| rendered_images_all, | |
| "(b v) (h w) (p1 p2 c) -> b v c (h p1) (w p2)", | |
| v=v, | |
| h=self.target_latent_h, | |
| w=self.target_latent_w, | |
| p1=patch_size, | |
| p2=patch_size, | |
| c=3, | |
| ) | |
| return rendered_images_all | |
| def get_camera_tokens(self, b, v): | |
| n, d = self.cam_code.shape[-2:] | |
| cam_tokens = rearrange(self.cam_code, "n d -> 1 1 n d") # [1, 1, n_cam, d] | |
| cam_tokens = repeat(cam_tokens, "1 1 n d -> 1 v n d", v=v) # [1, v, n_cam, d] | |
| cam_tokens = rearrange(cam_tokens, "1 v n d -> 1 (v n) d") # [1, v*n_cam, d] | |
| # get temporal pe | |
| img_indices = torch.arange(v).repeat_interleave(n) # [v*n_cam] | |
| img_indices = img_indices.to(cam_tokens.device) | |
| temporal_pe = get_1d_sincos_pos_emb_from_grid( | |
| embed_dim=d, pos=img_indices, device=cam_tokens.device | |
| ).to( | |
| cam_tokens.dtype | |
| ) # [v*n_cam, d] | |
| temporal_pe = temporal_pe.reshape(1, v * n, d) # [1, v*n_cam, d] | |
| temporal_pe = self.temporal_pe_embedder(temporal_pe) # [1, v*n_cam, d] | |
| return (cam_tokens + temporal_pe).repeat(b, 1, 1) # [b, v*n_cam, d] | |
| def add_sptial_temporal_pe(self, img_tokens, b, v, h_origin, w_origin): | |
| """ | |
| Adding spatial-temporal pe to input image tokens | |
| Args: | |
| img_tokens: shape [b*v, n, d] | |
| Return: | |
| image tokens with positional embedding | |
| """ | |
| patch_size = self.config.model.image_tokenizer.patch_size | |
| num_h_tokens = h_origin // patch_size | |
| num_w_tokens = w_origin // patch_size | |
| assert (num_h_tokens * num_w_tokens) == img_tokens.shape[1] | |
| bv, n, d = img_tokens.shape | |
| # get temporal pe | |
| img_indices = torch.arange(v).repeat_interleave(n) # [v*n] | |
| img_indices = img_indices.unsqueeze(0).repeat(b, 1).reshape(-1) # [b*v*n] | |
| img_indices = img_indices.to(img_tokens.device) | |
| temporal_pe = get_1d_sincos_pos_emb_from_grid( | |
| embed_dim=d // 2, pos=img_indices, device=img_tokens.device | |
| ).to( | |
| img_tokens.dtype | |
| ) # [b*v*n, d2] | |
| temporal_pe = temporal_pe.reshape(b, v, n, d // 2) # [b,v,n,d2] | |
| # get spatial pe | |
| spatial_pe = get_2d_sincos_pos_embed( | |
| embed_dim=d // 2, | |
| grid_size=(num_h_tokens, num_w_tokens), | |
| device=img_tokens.device, | |
| ).to( | |
| img_tokens.dtype | |
| ) # [n, d3] | |
| spatial_pe = spatial_pe.reshape(1, 1, n, d // 2).repeat(b, v, 1, 1) # [b,v,n,d3] | |
| # embed pe | |
| pe = self.pe_embedder( | |
| torch.cat([spatial_pe, temporal_pe], dim=-1).reshape(bv, n, d) | |
| ) # [b*v,n,d] | |
| return img_tokens + pe | |
| def run_layers_encoder(self, start, end): | |
| def custom_forward(concat_nerf_img_tokens): | |
| for i in range(start, min(end, len(self.transformer_encoder))): | |
| concat_nerf_img_tokens = self.transformer_encoder[i](concat_nerf_img_tokens) | |
| return concat_nerf_img_tokens | |
| return custom_forward | |
| def run_layers_encoder_geom(self, start, end): | |
| def custom_forward(concat_nerf_img_tokens): | |
| for i in range(start, min(end, len(self.transformer_encoder_geom))): | |
| concat_nerf_img_tokens = self.transformer_encoder_geom[i](concat_nerf_img_tokens) | |
| return concat_nerf_img_tokens | |
| return custom_forward | |
| def run_layers_decoder(self, start, end): | |
| def custom_forward(concat_nerf_img_tokens): | |
| for i in range(start, min(end, len(self.transformer_decoder))): | |
| concat_nerf_img_tokens = self.transformer_decoder[i](concat_nerf_img_tokens) | |
| return concat_nerf_img_tokens | |
| return custom_forward | |
| def run_encoder(self, all_tokens_encoder): | |
| checkpoint_every = self.config.training.grad_checkpoint_every | |
| for i in range(0, len(self.transformer_encoder), checkpoint_every): | |
| all_tokens_encoder = torch.utils.checkpoint.checkpoint( | |
| self.run_layers_encoder(i, i + 1), | |
| all_tokens_encoder, | |
| use_reentrant=False, | |
| ) | |
| if checkpoint_every > 1: | |
| all_tokens_encoder = self.run_layers_encoder(i + 1, i + checkpoint_every)( | |
| all_tokens_encoder | |
| ) | |
| return all_tokens_encoder | |
| def run_encoder_geom(self, all_tokens_encoder): | |
| checkpoint_every = self.config.training.grad_checkpoint_every | |
| for i in range(0, len(self.transformer_encoder_geom), checkpoint_every): | |
| all_tokens_encoder = torch.utils.checkpoint.checkpoint( | |
| self.run_layers_encoder_geom(i, i + 1), | |
| all_tokens_encoder, | |
| use_reentrant=False, | |
| ) | |
| if checkpoint_every > 1: | |
| all_tokens_encoder = self.run_layers_encoder_geom(i + 1, i + checkpoint_every)( | |
| all_tokens_encoder | |
| ) | |
| return all_tokens_encoder | |
| def run_decoder(self, all_tokens_encoder): | |
| checkpoint_every = self.config.training.grad_checkpoint_every | |
| for i in range(0, len(self.transformer_decoder), checkpoint_every): | |
| all_tokens_encoder = torch.utils.checkpoint.checkpoint( | |
| self.run_layers_decoder(i, i + 1), | |
| all_tokens_encoder, | |
| use_reentrant=False, | |
| ) | |
| if checkpoint_every > 1: | |
| all_tokens_encoder = self.run_layers_decoder(i + 1, i + checkpoint_every)( | |
| all_tokens_encoder | |
| ) | |
| return all_tokens_encoder | |
| def red_patch_overlay(self, img_3chw, patch_mask_1hw, alpha=0.5): | |
| """ | |
| img_3chw: torch.Tensor [3, H, W], values in [0,1] | |
| patch_mask_1hw: torch.Tensor [1, Hpatch, Wpatch] with {0,1} per token | |
| returns: PIL.Image (H, W, 3) with red patches overlayed | |
| """ | |
| H, W = img_3chw.shape[-2:] | |
| # nearest upsample so each token becomes a solid square | |
| mask_hw = F.interpolate(patch_mask_1hw.unsqueeze(0), size=(H, W), mode="nearest").squeeze( | |
| 0 | |
| ) # [1,H,W] | |
| img = img_3chw.permute(1, 2, 0).cpu().numpy() # [H,W,3], 0..1 | |
| mask = mask_hw.squeeze(0).cpu().numpy()[..., None] # [H,W,1], {0,1} | |
| red = np.zeros_like(img) | |
| red[..., 0] = 1.0 # pure red | |
| vis = img * (1.0 - alpha * mask) + red * (alpha * mask) # simple alpha blend | |
| vis = (np.clip(vis, 0, 1) * 255).astype(np.uint8) | |
| return Image.fromarray(vis) | |
| def render_images_video(self, scene_tokens_all, c2w_all, fxfycxcy_all, normalized=False): | |
| """ | |
| scene_tokens_all: [b, n_scene, d] | |
| c2w_all: [b*v, 4, 4] | |
| fxfycxcy_all: [b*v, 4] | |
| """ | |
| with torch.no_grad(): | |
| scene_tokens_all = scene_tokens_all.detach() | |
| c2w_all = c2w_all.detach() | |
| fxfycxcy_all = fxfycxcy_all.detach() | |
| b, _, d = scene_tokens_all.shape | |
| bv = c2w_all.shape[0] | |
| v = bv // b | |
| c2w_all = rearrange(c2w_all, "(b v) x y -> b v x y", v=v) | |
| fxfycxcy_all = rearrange(fxfycxcy_all, "(b v) x -> b v x", v=v) | |
| device = scene_tokens_all.device | |
| all_renderings = [] | |
| num_frames = 30 | |
| traj_type = "interpolate" | |
| order_poses = False | |
| for i in range(b): | |
| scene_tokens = scene_tokens_all[i] # [n_scene, d] | |
| c2ws = c2w_all[i] # [v, 4, 4] | |
| fxfycxcy = fxfycxcy_all[i] # [v, 4] | |
| if traj_type == "interpolate": | |
| # build Ks from fxfycxcy | |
| Ks = torch.zeros((c2ws.shape[0], 3, 3), device=device) | |
| Ks[:, 0, 0] = fxfycxcy[:, 0] | |
| Ks[:, 1, 1] = fxfycxcy[:, 1] | |
| Ks[:, 0, 2] = fxfycxcy[:, 2] | |
| Ks[:, 1, 2] = fxfycxcy[:, 3] | |
| c2ws, Ks = camera_utils.get_interpolated_poses_many( | |
| c2ws[:, :3, :4], Ks, num_frames, order_poses=order_poses | |
| ) | |
| frame_c2ws = torch.cat( | |
| [ | |
| c2ws.to(device), | |
| torch.tensor([[[0, 0, 0, 1]]], device=device).repeat( | |
| c2ws.shape[0], 1, 1 | |
| ), | |
| ], | |
| dim=1, | |
| ) # [v',4,4] | |
| frame_fxfycxcy = torch.zeros((c2ws.shape[0], 4), device=device) | |
| frame_fxfycxcy[:, 0] = Ks[:, 0, 0] # [v',4] | |
| frame_fxfycxcy[:, 1] = Ks[:, 1, 1] | |
| frame_fxfycxcy[:, 2] = Ks[:, 0, 2] | |
| frame_fxfycxcy[:, 3] = Ks[:, 1, 2] | |
| elif traj_type == "same": | |
| frame_c2ws = c2ws.clone() | |
| frame_fxfycxcy = fxfycxcy.clone() | |
| else: | |
| raise NotImplementedError | |
| plucker_rays = cam_info_to_plucker( | |
| frame_c2ws, | |
| frame_fxfycxcy, | |
| self.config.model.target_image, | |
| normalized=normalized, | |
| ) # [v',6,h,w] | |
| plucker_embeddings = self.target_pose_tokenizer2( | |
| plucker_rays.unsqueeze(0) | |
| ) # [v',n_target,d] | |
| v_render = plucker_embeddings.shape[0] | |
| scene_tokens = scene_tokens.unsqueeze(0).repeat(v_render, 1, 1) # [v',n_scene,d] | |
| all_tokens = torch.cat( | |
| [plucker_embeddings, scene_tokens], dim=1 | |
| ) # [v', n_target+n_scene, d] | |
| # render | |
| n_target, n_scene = plucker_embeddings.shape[1], scene_tokens.shape[1] | |
| rendered_images_all = self.render(all_tokens, n_target, n_scene, v_render).squeeze( | |
| 0 | |
| ) # [v',c,h,w] | |
| all_renderings.append(rendered_images_all) | |
| all_renderings = torch.stack(all_renderings) # [b,v',c,h,w] | |
| render_results = edict(rendered_images_video=all_renderings) | |
| return render_results | |
| def visualize_dinov3_pseudolabels(self, data_batch, save_dir, render_only=False): | |
| """ | |
| Visualize complete DINOv3 pseudo-mask generation pipeline. | |
| Shows the full pipeline: GT, Rendered, SSIM dissim, DINO dissim, | |
| soft mask (percentile-based), and binary co-seg mask (KMeans+GrabCut). | |
| Args: | |
| data_batch: Input batch from dataloader | |
| save_dir: Directory to save visualizations | |
| render_only: If True, skip pseudo-label generation (just show GT vs rendered) | |
| Saves: | |
| - 4-column grid: GT | Rendered | Soft Mask on GT | Binary Mask on GT | |
| - Per-view metrics (PSNR, LPIPS, SSIM) | |
| - Debug visualization with SSIM/DINO/Combined dissimilarity | |
| """ | |
| import matplotlib.pyplot as plt | |
| from matplotlib import cm | |
| os.makedirs(save_dir, exist_ok=True) | |
| # Run forward pass to get renderings | |
| result = self.forward(data_batch, create_visual=False, render_video=False) | |
| input, target = result.input, result.target | |
| rendered_images = result.render # [b, v_target, 3, h, w] | |
| b = input.image.shape[0] | |
| v_target = ( | |
| target.image.shape[1] if hasattr(target, "image") and target.image is not None else 0 | |
| ) | |
| h, w = target.image.shape[-2:] | |
| device = target.image.device | |
| # Generate complete pseudo-labels (soft + binary) | |
| soft_masks = None | |
| binary_masks = None | |
| debug_info = None | |
| if not render_only and self.use_dinov3_pseudolabel and self.dinov3_pseudolabel_maker: | |
| dataset_sources = data_batch.get("dataset_sources", [None] * b) | |
| soft_masks_list = [] | |
| binary_masks_list = [] | |
| debug_info_list = [] | |
| for batch_idx_i in range(b): | |
| is_re10k = dataset_sources[batch_idx_i] == "re10k" | |
| if is_re10k: | |
| # Static RE10K - use all-zeros | |
| soft_mask = torch.zeros(v_target, 1, h, w, device=device) | |
| binary_mask = torch.zeros(v_target, 1, h, w, device=device) | |
| debug = None | |
| else: | |
| # Dynamic RE10K - compute full pseudo-mask pipeline | |
| gt_sample = target.image[batch_idx_i] # [v_target, 3, h, w] | |
| pred_sample = rendered_images[batch_idx_i] # [v_target, 3, h, w] | |
| # Generate soft mask (DINO + SSIM percentile-based) | |
| self.dinov3_pseudolabel_maker.use_coseg_binary = False | |
| soft_mask = self.dinov3_pseudolabel_maker(gt_sample, pred_sample) | |
| # Generate binary co-seg mask (KMeans + GrabCut) | |
| self.dinov3_pseudolabel_maker.use_coseg_binary = True | |
| binary_mask = self.dinov3_pseudolabel_maker(gt_sample, pred_sample) | |
| # Extract debug info (SSIM, DINO dissimilarity) | |
| dino_dissim = self.dinov3_pseudolabel_maker._compute_cosine_dissimilarity( | |
| gt_sample, pred_sample | |
| ) | |
| ssim_dissim = self.dinov3_pseudolabel_maker._compute_ssim_dissimilarity( | |
| gt_sample, pred_sample | |
| ) | |
| combined_dissim = torch.minimum(dino_dissim, ssim_dissim) | |
| debug = { | |
| "dino_dissim": dino_dissim.cpu(), | |
| "ssim_dissim": ssim_dissim.cpu(), | |
| "combined_dissim": combined_dissim.cpu(), | |
| } | |
| soft_masks_list.append(soft_mask) | |
| binary_masks_list.append(binary_mask) | |
| debug_info_list.append(debug) | |
| soft_masks = torch.stack(soft_masks_list, dim=0) # [b, v_target, 1, h, w] | |
| binary_masks = torch.stack(binary_masks_list, dim=0) # [b, v_target, 1, h, w] | |
| debug_info = debug_info_list | |
| # Compute metrics | |
| psnr_per_view = [] | |
| lpips_per_view = [] | |
| ssim_per_view = [] | |
| for batch_idx_i in range(b): | |
| psnr = compute_psnr(target.image[batch_idx_i], rendered_images[batch_idx_i]) | |
| lpips = compute_lpips(target.image[batch_idx_i], rendered_images[batch_idx_i]) | |
| ssim = compute_ssim( | |
| target.image[batch_idx_i].to(torch.float32), | |
| rendered_images[batch_idx_i].to(torch.float32), | |
| ) | |
| psnr_per_view.append(psnr) | |
| lpips_per_view.append(lpips) | |
| ssim_per_view.append(ssim) | |
| # Save visualizations | |
| for batch_idx_i in range(b): | |
| scene_idx = int(target.index[batch_idx_i, 0, -1].item()) | |
| dataset_source = data_batch.get("dataset_sources", ["unknown"] * b)[batch_idx_i] | |
| # Get scene name and original filenames for saving masks | |
| scene_name = data_batch.get("scene_name", [f"scene_{scene_idx:06d}"] * b)[batch_idx_i] | |
| original_filenames = data_batch.get("original_filenames", [None] * b)[batch_idx_i] | |
| scene_dir = os.path.join(save_dir, f"scene_{scene_idx:06d}_{dataset_source}") | |
| os.makedirs(scene_dir, exist_ok=True) | |
| # Create masks directory for .npy files | |
| masks_dir = os.path.join(scene_dir, "masks_npy") | |
| os.makedirs(masks_dir, exist_ok=True) | |
| gt_imgs = target.image[batch_idx_i].cpu() # [v_target, 3, h, w] | |
| rendered_imgs = rendered_images[batch_idx_i].cpu() # [v_target, 3, h, w] | |
| v_target_curr = gt_imgs.shape[0] | |
| # Save binary masks as .npy files with original image names | |
| if binary_masks is not None and original_filenames is not None: | |
| binary_masks_curr = binary_masks[batch_idx_i].cpu().numpy() # [v_target, 1, h, w] | |
| soft_masks_curr_np = soft_masks[batch_idx_i].cpu().numpy() # [v_target, 1, h, w] | |
| # Get number of input views to correctly index target filenames | |
| v_input = input.image.shape[1] | |
| for v_idx in range(v_target_curr): | |
| # Get original filename for this target view | |
| # original_filenames contains all views (input + target) | |
| # Target views start after input views | |
| original_path = original_filenames[v_input + v_idx] | |
| # Extract filename without extension (e.g., "00042" from "images/scene/00042.png") | |
| from pathlib import Path | |
| image_name = Path(original_path).stem # e.g., "00042" | |
| # Save binary mask as .npy | |
| binary_mask_np = binary_masks_curr[v_idx, 0] # [h, w] | |
| binary_mask_path = os.path.join(masks_dir, f"{image_name}_binary.npy") | |
| np.save(binary_mask_path, binary_mask_np.astype(np.float32)) | |
| # Also save soft mask for comparison | |
| soft_mask_np = soft_masks_curr_np[v_idx, 0] # [h, w] | |
| soft_mask_path = os.path.join(masks_dir, f"{image_name}_soft.npy") | |
| np.save(soft_mask_path, soft_mask_np.astype(np.float32)) | |
| # Save a metadata file for this scene | |
| metadata = { | |
| "scene_name": scene_name, | |
| "scene_idx": scene_idx, | |
| "dataset_source": dataset_source, | |
| "num_views": v_target_curr, | |
| "mask_files": [ | |
| f"{Path(original_filenames[v_input + v_idx]).stem}_binary.npy" | |
| for v_idx in range(v_target_curr) | |
| ], | |
| "original_filenames": [ | |
| original_filenames[v_input + v_idx] for v_idx in range(v_target_curr) | |
| ], | |
| } | |
| metadata_path = os.path.join(masks_dir, "metadata.json") | |
| import json | |
| with open(metadata_path, "w") as f: | |
| json.dump(metadata, f, indent=2) | |
| print(f" ✓ Saved {v_target_curr} mask pairs (.npy) for scene {scene_name}") | |
| # Main visualization: GT | Rendered | Soft Mask on GT | Binary Mask on GT | |
| if soft_masks is not None and binary_masks is not None: | |
| fig, axes = plt.subplots(v_target_curr, 4, figsize=(20, 5 * v_target_curr)) | |
| if v_target_curr == 1: | |
| axes = axes.reshape(1, -1) | |
| soft_masks_curr = soft_masks[batch_idx_i].cpu() # [v_target, 1, h, w] | |
| binary_masks_curr = binary_masks[batch_idx_i].cpu() # [v_target, 1, h, w] | |
| cmap = cm.get_cmap("hot") | |
| for v_idx in range(v_target_curr): | |
| gt_np = gt_imgs[v_idx].permute(1, 2, 0).float().numpy() | |
| rendered_np = rendered_imgs[v_idx].permute(1, 2, 0).float().numpy() | |
| soft_mask_np = soft_masks_curr[v_idx, 0].float().numpy() | |
| binary_mask_np = binary_masks_curr[v_idx, 0].float().numpy() | |
| psnr_val = psnr_per_view[batch_idx_i][v_idx].item() | |
| lpips_val = lpips_per_view[batch_idx_i][v_idx].item() | |
| ssim_val = ssim_per_view[batch_idx_i][v_idx].item() | |
| # Column 1: GT | |
| axes[v_idx, 0].imshow(gt_np) | |
| title = f"GT View {v_idx}" | |
| if v_idx == 0: | |
| title += f"\n({dataset_source.upper()})" | |
| axes[v_idx, 0].set_title(title, fontsize=12, fontweight="bold") | |
| axes[v_idx, 0].axis("off") | |
| # Column 2: Rendered | |
| axes[v_idx, 1].imshow(rendered_np) | |
| axes[v_idx, 1].set_title( | |
| f"Rendered\nPSNR:{psnr_val:.2f} SSIM:{ssim_val:.3f}", | |
| fontsize=12, | |
| fontweight="bold", | |
| ) | |
| axes[v_idx, 1].axis("off") | |
| # Column 3: Soft Mask on GT | |
| axes[v_idx, 2].imshow(gt_np) | |
| mask_colored = cmap(soft_mask_np)[:, :, :3] | |
| axes[v_idx, 2].imshow(mask_colored, alpha=0.6) | |
| axes[v_idx, 2].set_title( | |
| f"Soft Mask (Percentile)\n[0,1] continuous", fontsize=12, fontweight="bold" | |
| ) | |
| axes[v_idx, 2].axis("off") | |
| # Column 4: Binary Mask on GT | |
| axes[v_idx, 3].imshow(gt_np) | |
| binary_colored = cmap(binary_mask_np)[:, :, :3] | |
| axes[v_idx, 3].imshow(binary_colored, alpha=0.6) | |
| axes[v_idx, 3].set_title( | |
| f"Binary Mask (Co-Seg)\n{{0,1}} KMeans+GrabCut", | |
| fontsize=12, | |
| fontweight="bold", | |
| ) | |
| axes[v_idx, 3].axis("off") | |
| plt.tight_layout() | |
| plt.savefig( | |
| os.path.join(scene_dir, "pseudolabels.png"), dpi=150, bbox_inches="tight" | |
| ) | |
| plt.close() | |
| # Debug visualization: SSIM | DINO | Combined dissimilarity | |
| if debug_info[batch_idx_i] is not None: | |
| debug = debug_info[batch_idx_i] | |
| fig, axes = plt.subplots(v_target_curr, 3, figsize=(15, 5 * v_target_curr)) | |
| if v_target_curr == 1: | |
| axes = axes.reshape(1, -1) | |
| for v_idx in range(v_target_curr): | |
| dino = debug["dino_dissim"][v_idx, 0].float().numpy() | |
| ssim_d = debug["ssim_dissim"][v_idx, 0].float().numpy() | |
| combined = debug["combined_dissim"][v_idx, 0].float().numpy() | |
| # SSIM dissimilarity | |
| im = axes[v_idx, 0].imshow(ssim_d, cmap="hot", vmin=0, vmax=1) | |
| axes[v_idx, 0].set_title( | |
| f"SSIM Dissimilarity\nView {v_idx}", fontsize=12, fontweight="bold" | |
| ) | |
| axes[v_idx, 0].axis("off") | |
| plt.colorbar(im, ax=axes[v_idx, 0], fraction=0.046) | |
| # DINO dissimilarity | |
| im = axes[v_idx, 1].imshow(dino, cmap="hot", vmin=0, vmax=1) | |
| axes[v_idx, 1].set_title( | |
| f"DINO Dissimilarity\nView {v_idx}", fontsize=12, fontweight="bold" | |
| ) | |
| axes[v_idx, 1].axis("off") | |
| plt.colorbar(im, ax=axes[v_idx, 1], fraction=0.046) | |
| # Combined (min) | |
| im = axes[v_idx, 2].imshow(combined, cmap="hot", vmin=0, vmax=1) | |
| axes[v_idx, 2].set_title( | |
| f"Combined (min)\nView {v_idx}", fontsize=12, fontweight="bold" | |
| ) | |
| axes[v_idx, 2].axis("off") | |
| plt.colorbar(im, ax=axes[v_idx, 2], fraction=0.046) | |
| plt.tight_layout() | |
| plt.savefig( | |
| os.path.join(scene_dir, "debug_dissimilarity.png"), | |
| dpi=150, | |
| bbox_inches="tight", | |
| ) | |
| plt.close() | |
| else: | |
| # Fallback: GT vs Rendered only | |
| fig, axes = plt.subplots(v_target_curr, 2, figsize=(10, 5 * v_target_curr)) | |
| if v_target_curr == 1: | |
| axes = axes.reshape(1, -1) | |
| for v_idx in range(v_target_curr): | |
| gt_np = gt_imgs[v_idx].permute(1, 2, 0).float().numpy() | |
| rendered_np = rendered_imgs[v_idx].permute(1, 2, 0).float().numpy() | |
| psnr_val = psnr_per_view[batch_idx_i][v_idx].item() | |
| ssim_val = ssim_per_view[batch_idx_i][v_idx].item() | |
| axes[v_idx, 0].imshow(gt_np) | |
| axes[v_idx, 0].set_title(f"GT View {v_idx}", fontsize=12) | |
| axes[v_idx, 0].axis("off") | |
| axes[v_idx, 1].imshow(rendered_np) | |
| axes[v_idx, 1].set_title( | |
| f"Rendered\nPSNR:{psnr_val:.2f} SSIM:{ssim_val:.3f}", fontsize=12 | |
| ) | |
| axes[v_idx, 1].axis("off") | |
| plt.tight_layout() | |
| plt.savefig(os.path.join(scene_dir, "rendering.png"), dpi=150, bbox_inches="tight") | |
| plt.close() | |
| # Save metrics | |
| with open(os.path.join(scene_dir, "metrics.txt"), "w") as f: | |
| f.write(f"Scene: {scene_idx:06d}\n") | |
| f.write(f"Dataset: {dataset_source}\n") | |
| f.write(f"Num views: {v_target_curr}\n\n") | |
| f.write("Per-view metrics:\n") | |
| for v_idx in range(v_target_curr): | |
| f.write(f" View {v_idx}: ") | |
| f.write(f"PSNR={psnr_per_view[batch_idx_i][v_idx].item():.2f}, ") | |
| f.write(f"LPIPS={lpips_per_view[batch_idx_i][v_idx].item():.4f}, ") | |
| f.write(f"SSIM={ssim_per_view[batch_idx_i][v_idx].item():.4f}\n") | |
| f.write(f"\nAverage:\n") | |
| f.write(f" PSNR: {psnr_per_view[batch_idx_i].mean().item():.2f}\n") | |
| f.write(f" LPIPS: {lpips_per_view[batch_idx_i].mean().item():.4f}\n") | |
| f.write(f" SSIM: {ssim_per_view[batch_idx_i].mean().item():.4f}\n") | |
| print( | |
| f"✓ Saved scene {scene_idx:06d} ({dataset_source}): " | |
| f"PSNR={psnr_per_view[batch_idx_i].mean().item():.2f}" | |
| ) | |
| print(f"\n✓ Visualization complete! Saved to: {save_dir}") | |
| def save_visuals(self, out_dir, result, batch, save_all=False): | |
| os.makedirs(out_dir, exist_ok=True) | |
| input, target = result.input, result.target | |
| # save comparison | |
| if result.loss_metrics.visual is not None: | |
| uids = [target.index[b, 0, -1].item() for b in range(target.index.size(0))] | |
| uid_firstlast = f"{uids[0]:08}_{uids[-1]:08}" | |
| Image.fromarray(result.loss_metrics.visual).save( | |
| os.path.join(out_dir, f"supervision_{uid_firstlast}.jpg") | |
| ) | |
| with open(os.path.join(out_dir, f"uids.txt"), "w") as f: | |
| uids = "_".join([f"{uid:08}" for uid in uids]) | |
| f.write(uids) | |
| # save input, rendered interpolated imaged, video | |
| for b in range(input.image.size(0)): | |
| uid = input.index[b, 0, -1].item() | |
| # vis input | |
| vis_image = rearrange(input.image[b], "v c h w -> h (v w) c") | |
| vis_image = (vis_image.cpu().numpy() * 255.0).clip(0.0, 255.0).astype(np.uint8) | |
| Image.fromarray(vis_image).save(os.path.join(out_dir, f"input_{uid}.png")) | |
| if ("render_video" in result.keys()) and torch.is_tensor(result.render_video): | |
| render_video = result.render_video[b].detach().cpu() # [v, c, h, w] | |
| render_video = np.ascontiguousarray(np.array(render_video.to(torch.float32))) | |
| render_video = rearrange(render_video, "v c h w -> v h w c") | |
| create_video_from_frames( | |
| render_video, f"{out_dir}/render_video_{uid}.mp4", framerate=30 | |
| ) | |
| if not save_all: | |
| break | |
| def save_evaluations(self, out_dir, result, batch, dataset): | |
| os.makedirs(out_dir, exist_ok=True) | |
| input, target = result.input, result.target | |
| for b in range(input.image.size(0)): | |
| uid = input.index[b, 0, -1].item() | |
| curr_out_dir = os.path.join(out_dir, "%06d" % (uid)) | |
| os.makedirs(curr_out_dir, exist_ok=True) | |
| curr_out_color_dir = curr_out_dir | |
| os.makedirs(curr_out_color_dir, exist_ok=True) | |
| target_idx = target.index[b, :, 0].cpu().numpy() | |
| _, v, _, h, w = input.image.size() | |
| # vis gt and predicted target views | |
| im = torch.cat((result.target.image[b], result.render[b]), dim=2).detach().cpu() | |
| im = rearrange(im, "v c h w -> h (v w) c") | |
| im = (im.cpu().numpy() * 255.0).clip(0.0, 255.0).astype(np.uint8) | |
| Image.fromarray(im).save(os.path.join(curr_out_color_dir, f"gt_vs_pred.png")) | |
| # vis input views | |
| input_im = result.input.image[b] # V C H W | |
| input_im = rearrange(input_im, "v c h w -> h (v w) c") | |
| input_im = (input_im.cpu().numpy() * 255.0).clip(0.0, 255.0).astype(np.uint8) | |
| Image.fromarray(input_im).save(os.path.join(curr_out_color_dir, f"input.png")) | |
| # # vis interpolated views | |
| # if torch.is_tensor(result.render_interpolate): | |
| # interpolate_im = result.render_interpolate[b].detach().cpu().to(torch.float32) | |
| # interpolate_im = rearrange(interpolate_im, 'v c h w -> h (v w) c') | |
| # interpolate_im = (interpolate_im.cpu().numpy() * 255.0).clip(0.0, 255.0).astype(np.uint8) | |
| # Image.fromarray(interpolate_im).save( | |
| # os.path.join(curr_out_color_dir, f"pred_interpolate.png")) | |
| # get metrics | |
| psnr = compute_psnr(target.image[b], result.render[b]) # .mean() | |
| lpips = compute_lpips(target.image[b], result.render[b]) # .mean() | |
| ssim = compute_ssim( | |
| target.image[b].to(torch.float32), result.render[b].to(torch.float32) | |
| ) # .mean() | |
| with open(os.path.join(curr_out_dir, "per_view_metrics.txt"), "w") as f: | |
| for i in range(psnr.shape[0]): | |
| f.write(f"view: {target_idx[i]}, psnr: {psnr[i]},") | |
| f.write(f"lpips: {lpips[i]},") | |
| f.write(f"ssim: {ssim[i]}\n") | |
| with open(os.path.join(curr_out_dir, "metrics.txt"), "w") as f: | |
| metrics = f"psnr: {psnr.mean()}\nlpips: {lpips.mean()}\nssim: {ssim.mean()}\n" | |
| f.write(metrics) | |
| # save rendered video | |
| if torch.is_tensor(result.get("render_video", None)): | |
| all_frames = result.render_video[b].detach().cpu() # [v h w c] | |
| all_frames = np.ascontiguousarray(np.array(all_frames.to(torch.float32))) | |
| all_frames = rearrange(all_frames, "v c h w -> v h w c") | |
| create_video_from_frames(all_frames, f"{curr_out_dir}/input_traj.mp4", framerate=30) | |
| # save rendered video | |
| if torch.is_tensor(result.get("render_video", None)): | |
| all_frames = result.render_video[b].detach().cpu() # [v h w c] | |
| all_frames = np.ascontiguousarray(np.array(all_frames.to(torch.float32))) | |
| all_frames = rearrange(all_frames, "v c h w -> v h w c") | |
| create_video_from_frames(all_frames, f"{curr_out_dir}/input_traj.mp4", framerate=30) | |
| # visualize cameras (placeholder - implement if needed) | |
| # c2w = result.c2w[b].detach().cpu() | |
| # input_idx_cur = input_idx[b].detach().cpu() | |
| # c2w_input = c2w[input_idx_cur] | |
| # TODO: Implement camera trajectory visualization if needed | |
| # pass | |
| def get_cam_se3(cam_info): | |
| """ | |
| cam_info: [b,num_pose_element+3+4], rot, 3d trans, 4d fxfycxcy | |
| """ | |
| b, n = cam_info.shape | |
| if n == 13: | |
| rot_6d = cam_info[:, :6] | |
| R = rot6d2mat(rot_6d) # [b,3,3] | |
| t = cam_info[:, 6:9].unsqueeze(-1) # [b,3,1] | |
| fxfycxcy = cam_info[:, 9:] # normalized by resolution / shift from average, [b,4] | |
| elif n == 11: | |
| rot_quat = cam_info[:, :4] | |
| R = quat2mat(rot_quat) | |
| t = cam_info[:, 4:7].unsqueeze(-1) # [b,3,1] | |
| fxfycxcy = cam_info[:, 7:] # normalized by resolution / shift from average, [b,4] | |
| else: | |
| raise NotImplementedError | |
| Rt = torch.cat([R, t], dim=2) # [b,3,4] | |
| c2w = torch.cat( | |
| [ | |
| Rt, | |
| torch.tensor([0, 0, 0, 1], dtype=R.dtype, device=R.device) | |
| .view(1, 1, 4) | |
| .repeat(b, 1, 1), | |
| ], | |
| dim=1, | |
| ) # [b,4,4] | |
| return c2w, fxfycxcy | |
| def cam_info_to_plucker(c2w, fxfycxcy, target_imgs_info, normalized=True): | |
| """ | |
| c2w: [b,4,4] | |
| fxfycxcy: [b,4] | |
| """ | |
| b = c2w.shape[0] | |
| device = c2w.device | |
| h, w = target_imgs_info.height, target_imgs_info.width | |
| fxfycxcy = fxfycxcy.clone() | |
| if normalized: | |
| fxfycxcy[:, 0] *= w | |
| fxfycxcy[:, 1] *= h | |
| fxfycxcy[:, 2] *= w | |
| fxfycxcy[:, 3] *= h | |
| y, x = torch.meshgrid(torch.arange(h), torch.arange(w), indexing="ij") | |
| y, x = y.to(c2w), x.to(c2w) | |
| x = x[None, :, :].expand(b, -1, -1).reshape(b, -1) | |
| y = y[None, :, :].expand(b, -1, -1).reshape(b, -1) | |
| x = (x + 0.5 - fxfycxcy[:, 2:3]) / fxfycxcy[:, 0:1] | |
| y = (y + 0.5 - fxfycxcy[:, 3:4]) / fxfycxcy[:, 1:2] | |
| z = torch.ones_like(x) | |
| ray_d = torch.stack([x, y, z], dim=2) # [b, h*w, 3] | |
| ray_d = torch.bmm(ray_d, c2w[:, :3, :3].transpose(1, 2)) # [b, h*w, 3] | |
| ray_d = ray_d / torch.norm(ray_d, dim=2, keepdim=True) # [b, h*w, 3] | |
| ray_o = c2w[:, :3, 3][:, None, :].expand_as(ray_d) # [b, h*w, 3] | |
| ray_o = ray_o.reshape(b, h, w, 3).permute(0, 3, 1, 2) # [b,3,h,w] | |
| ray_d = ray_d.reshape(b, h, w, 3).permute(0, 3, 1, 2) | |
| plucker = torch.cat( | |
| [ | |
| torch.cross(ray_o, ray_d, dim=1), | |
| ray_d, | |
| ], | |
| dim=1, | |
| ) | |
| return plucker # [b,c=6,h,w] | |
| class PoseEstimator(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| self.config = config | |
| self.pose_rep = self.config.model.pose_latent.get("representation", "6d") | |
| print("Pose representation:", self.pose_rep) | |
| if self.pose_rep == "6d": | |
| self.num_pose_element = 6 | |
| elif self.pose_rep == "quat": | |
| self.num_pose_element = 4 | |
| else: | |
| raise NotImplementedError | |
| self.rel_head = nn.Sequential( | |
| nn.Linear( | |
| config.model.transformer.d * 2, | |
| config.model.transformer.d, | |
| bias=True, | |
| ), | |
| nn.SiLU(), | |
| nn.Linear( | |
| config.model.transformer.d, | |
| self.num_pose_element + 3, | |
| bias=True, | |
| ), | |
| ) | |
| self.rel_head.apply(_init_weights) | |
| self.canonical_k_head = nn.Sequential( | |
| nn.Linear( | |
| config.model.transformer.d, | |
| config.model.transformer.d, | |
| bias=True, | |
| ), | |
| nn.SiLU(), | |
| nn.Linear( | |
| config.model.transformer.d, | |
| 1, | |
| bias=True, | |
| ), | |
| ) | |
| self.canonical_k_head.apply(_init_weights) | |
| self.f_bias = 1.25 | |
| def forward(self, x, v): | |
| """ | |
| x: [b*v, d] | |
| """ | |
| canonical = self.config.model.pose_latent.get("canonical", "first") | |
| x = rearrange(x, "(b v) d -> b v d", v=v) | |
| b = x.shape[0] | |
| if canonical == "first": | |
| x_canonical = x[:, 0:1] # [b,1,d] | |
| x_rel = x[:, 1:] # [b,v-1,d] | |
| elif canonical == "middle": | |
| cano_idx = v // 2 | |
| rel_indices = torch.cat([torch.arange(cano_idx), torch.arange(cano_idx + 1, v)]) | |
| x_canonical = x[:, cano_idx : cano_idx + 1] # [b,1,d] | |
| x_rel = x[:, rel_indices] # [b,v-1,d] | |
| else: | |
| raise NotImplementedError | |
| fxfy_canonical = self.canonical_k_head(x_canonical[:, 0]) + self.f_bias # [b,1] | |
| fxfy_canonical = fxfy_canonical.unsqueeze(1).repeat(1, 1, 2) # [b,1,2] | |
| if self.pose_rep == "6d": | |
| rt_canonical = ( | |
| torch.tensor([1, 0, 0, 0, 1, 0, 0, 0, 0]) | |
| .reshape(1, 1, 9) | |
| .to(fxfy_canonical) | |
| .repeat(b, 1, 1) | |
| ) # [b,1,9] | |
| elif self.pose_rep == "quat": | |
| rt_canonical = ( | |
| torch.tensor([1, 0, 0, 0, 0, 0, 0]) | |
| .reshape(1, 1, 7) | |
| .to(fxfy_canonical) | |
| .repeat(b, 1, 1) | |
| ) # [b,1,7] | |
| info_canonical = torch.cat([rt_canonical, fxfy_canonical], dim=-1) # [b,1,11] | |
| feat_rel = torch.cat([x_canonical.repeat(1, v - 1, 1), x_rel], dim=-1) # [b,v-1,2*d] | |
| info_rel = self.rel_head(feat_rel) # [b,v-1,num_pose_element+3] | |
| info_all = info_canonical.repeat(1, v, 1) # [b,v,num_pose_element+3+2] | |
| if canonical == "first": | |
| info_all[:, 1:, : self.num_pose_element + 3] += info_rel | |
| elif canonical == "middle": | |
| info_all[:, rel_indices, : self.num_pose_element + 3] += info_rel | |
| else: | |
| raise NotImplementedError | |
| cxcy_all = torch.tensor([0.5, 0.5]).reshape(1, 1, 2).repeat(b, v, 1).to(info_all) | |
| info_all = torch.cat([info_all, cxcy_all], dim=-1) | |
| return rearrange(info_all, "b v d -> (b v) d") | |