A2A-Video / helper_functions.py
Muhammad Uzair Khattak
Deploy A2A-Video demo
4bc559f
Raw
History Blame Contribute Delete
48.1 kB
from PIL import Image, ImageDraw, ImageFont
from itertools import accumulate
import numpy as np
import torch
import copy
import imageio
from einops import rearrange
import cv2
from sklearn.decomposition import PCA
from flow_viz import flow_to_rgb
from fourm.utils.plotting_utils import _get_optimal_text_placement
import albumentations as A
import os
import torchvision.transforms.functional as TF
import einops
import torch.nn.functional as F
def expand_to_aspect_ratio(input_shape, target_aspect_ratio=None):
"""Increase the size of the bounding box to match the target shape."""
if target_aspect_ratio is None:
return input_shape
try:
w, h = input_shape
except (ValueError, TypeError):
return input_shape
w_t, h_t = target_aspect_ratio
if h / w < h_t / w_t:
h_new = max(w * h_t / w_t, h)
w_new = w
else:
h_new = h
w_new = max(h * w_t / h_t, w)
if h_new < h or w_new < w:
breakpoint()
return np.array([w_new, h_new])
def fill_masked_positions_with_random(tensor, mask, min_val=0, max_val=32000):
"""
Fill positions in a tensor with random values where the mask is True.
Args:
tensor (torch.Tensor): The input tensor to modify
mask (torch.Tensor): Boolean mask of same shape as tensor
min_val (int): Minimum random value (inclusive)
max_val (int): Maximum random value (exclusive)
Returns:
torch.Tensor: The modified tensor
"""
# Make a copy of the original tensor to avoid modifying the input
result = tensor.clone()
# Generate random values for the masked positions
# We convert mask to int64 to make random_values have the right shape
num_masked = mask.sum().item()
if num_masked > 0:
random_values = torch.randint(min_val, max_val, (num_masked,),
dtype=tensor.dtype, device=tensor.device)
# Apply random values where mask is True
result[mask] = random_values
return result
def check_video_presence(base_dir, base_video_name):
# Walk through all subfolders recursively
for root, _, files in os.walk(base_dir):
if base_video_name in files:
print(f"Found in: {os.path.join(root, base_video_name)}") # optional debug
return True
return False
def save_video_pairs_as_image(sorted_video_tensors_gt, sorted_video_tensors_pred, output_path='video_comparison.png',
gap_size=10, separator_after_frame=None, separator_width=3):
"""
Save video tensor pairs as a single image with frames arranged horizontally.
Args:
sorted_video_tensors_gt: List of ground truth video tensors, each with shape (17, H, W, C)
sorted_video_tensors_pred: List of predicted video tensors, each with shape (17, H, W, C)
output_path: Path to save the output image
gap_size: Size of the gap (in pixels) between video pairs
separator_after_frame: If set, insert a vertical green line after this frame index to mark
the boundary between conditioning frames and predicted frames.
separator_width: Width in pixels of the green separator line (default: 3)
"""
pair_rows = []
def _make_row(video, T, h, c):
frames = [np.array(video[i]) for i in range(T)]
if separator_after_frame is not None and 0 < separator_after_frame < T:
sep = np.zeros((h, separator_width, c), dtype=np.uint8)
sep[:, :, 1] = 255 # green
return np.concatenate(frames[:separator_after_frame] + [sep] + frames[separator_after_frame:], axis=1)
return np.concatenate(frames, axis=1)
# Process each video pair
for idx, (gt_video, pred_video) in enumerate(zip(sorted_video_tensors_gt, sorted_video_tensors_pred)):
# gt_video and pred_video have shape (17, H, W, C)
T = gt_video.shape[0]
h = np.array(gt_video[0]).shape[0]
c = np.array(gt_video[0]).shape[2]
gt_row = _make_row(gt_video, T, h, c)
pred_row = _make_row(pred_video, T, h, c)
# Stack GT and Pred vertically for this pair
pair = np.concatenate([gt_row, pred_row], axis=0) # Concatenate along height
pair_rows.append(pair)
# Add gap after each pair except the last one
if idx < len(sorted_video_tensors_gt) - 1:
# Create a white gap with same width as the pair
gap = np.ones((gap_size, pair.shape[1], pair.shape[2]), dtype=pair.dtype) * 255
pair_rows.append(gap)
# Concatenate all pairs vertically
final_image = np.concatenate(pair_rows, axis=0)
# Ensure values are in valid range [0, 255] for uint8
if final_image.dtype == np.float32 or final_image.dtype == np.float64:
if final_image.max() <= 1.0:
final_image = (final_image * 255).astype(np.uint8)
else:
final_image = np.clip(final_image, 0, 255).astype(np.uint8)
# Convert to PIL Image and save
print(final_image.shape)
final_image = np.clip(final_image, 0, 255).astype(np.uint8) # <-- Add this line
img = Image.fromarray(final_image)
img.save(output_path)
print(f"Saved visualization to {output_path}")
print(f"Final image shape: {final_image.shape}")
return final_image
def concat_videos_horizontally(*videos, gap=10):
"""
Concatenates multiple video tensors horizontally with a gap.
Args:
*videos: Sequence of video tensors of shape (T, H, W, C) or None.
Videos must be provided left-to-right. Once a None is encountered,
all subsequent videos must also be None.
gap (int): Width of the gap in pixels (default: 10).
Returns:
Tensor of shape (T, H, W_new, C) with videos concatenated horizontally.
"""
# Filter videos until the first None, then stop
valid_videos = []
for v in videos:
if v is None:
break
valid_videos.append(v)
# Ensure no "gaps" like [video0, video1, None, video3]
if any(v is not None for v in videos[len(valid_videos):]):
raise ValueError("Videos must be filled left-to-right without gaps (no None in between).")
# Check shape consistency
T, H, W, C = valid_videos[0].shape
for v in valid_videos:
if v.shape[0] != T or v.shape[1] != H or v.shape[3] != C:
raise ValueError("All videos must have the same (T, H, C). Width can differ.")
# Build concatenation with gaps
device = valid_videos[0].device
dtype = valid_videos[0].dtype
gap_tensor = torch.zeros((T, H, gap, C), dtype=dtype, device=device)
combined = [valid_videos[0]]
for v in valid_videos[1:]:
combined.append(gap_tensor)
combined.append(v)
return torch.cat(combined, dim=2) # concatenate along width
def concat_videos_horizontally_np(*videos, gap=10):
"""
Concatenates multiple video arrays horizontally with a gap.
Args:
*videos: Sequence of numpy arrays of shape (T, H, W, C) or None.
Videos must be provided left-to-right. Once a None is encountered,
all subsequent entries must also be None.
gap (int): Width of the gap in pixels.
Returns:
numpy array of shape (T, H, W_new, C)
"""
# Filter videos until the first None, then stop
valid_videos = []
for v in videos:
if v is None:
break
valid_videos.append(v)
# Ensure no gaps after a None
if any(v is not None for v in videos[len(valid_videos):]):
raise ValueError("Videos must be given left-to-right without gaps (no None in between).")
# Shape consistency check
T, H, W, C = valid_videos[0].shape
for v in valid_videos:
if v.shape[0] != T or v.shape[1] != H or v.shape[3] != C:
raise ValueError("All videos must have the same (T, H, C). Width can differ.")
# Gap array
gap_array = np.zeros((T, H, gap, C), dtype=valid_videos[0].dtype)
# Construct final list
combined = [valid_videos[0]]
for v in valid_videos[1:]:
combined.append(gap_array)
combined.append(v)
return np.concatenate(combined, axis=2) # concatenate along width
def scale_bbox_dict_with_crop(data_dict, orig_size, new_size, crop_coords=None, bbox_shape=[192, 256]):
"""
Scale bbox dictionary, optionally applying crop like in humanposes_crop_and_resize.
Args:
data_dict: dict with "bbox_xyxy" etc.
orig_size: (W, H) of original
new_size: (W, H) of target
crop_coords: (top, left, h, w) if cropping applied, else None
bbox_shape: target aspect ratio for bbox expansion, default [192, 256]
"""
if data_dict is None:
return None
scaled_dict = copy.deepcopy(data_dict)
W_orig, H_orig = orig_size
W_new, H_new = new_size
# Normalize
bboxes = scaled_dict["bbox_xyxy"].clone()
bboxes[:, [0, 2]] /= W_orig
bboxes[:, [1, 3]] /= H_orig
# Apply crop in normalized coords
if crop_coords is not None:
top, left, h, w = crop_coords
xmin, ymin, xmax, ymax = left / W_orig, top / H_orig, (left + w) / W_orig, (top + h) / H_orig
bboxes[:, [0, 2]] = (bboxes[:, [0, 2]] - xmin) / (xmax - xmin)
bboxes[:, [1, 3]] = (bboxes[:, [1, 3]] - ymin) / (ymax - ymin)
# Clip
# bboxes = torch.clamp(bboxes, 0, 1)
# Map back to target pixel space
bboxes[:, [0, 2]] *= W_new
bboxes[:, [1, 3]] *= H_new
scaled_dict["bbox_xyxy"] = bboxes
# Recompute center
cx = (bboxes[:, 0] + bboxes[:, 2]) / 2
cy = (bboxes[:, 1] + bboxes[:, 3]) / 2
scaled_dict["box_center"] = torch.stack([cx, cy], dim=1)
# Compute box_size following the original logic
box_sizes = []
for i in range(bboxes.shape[0]):
box_w = (bboxes[i, 2] - bboxes[i, 0]).item()
box_h = (bboxes[i, 3] - bboxes[i, 1]).item()
# Follow original logic: scale = bbox_dims / 200, then bbox_size = expand(scale*200).max()
scale = np.array([box_w, box_h]) / 200.0
expanded_dims = expand_to_aspect_ratio(scale * 200.0, target_aspect_ratio=bbox_shape)
bbox_size = expanded_dims.max()
box_sizes.append(bbox_size)
scaled_dict["box_size"] = torch.tensor(box_sizes, dtype=torch.float32)
# Update img_size
B = bboxes.shape[0]
scaled_dict["img_size"] = torch.tensor([W_new, H_new], dtype=torch.float32).repeat(B, 1)
return scaled_dict
def save_video(frames, output_video_path, fps=4):
with imageio.get_writer(output_video_path, fps=fps, format=".mp4",
quality=10, macro_block_size=None) as writer:
for frame in frames:
writer.append_data(frame)
def to_cuda_recursive(d):
"""Recursively move all tensors in a nested dict to CUDA."""
for k, v in d.items():
if isinstance(v, torch.Tensor):
d[k] = v.cuda().float()
elif isinstance(v, dict):
d[k] = to_cuda_recursive(v)
return d
def filter_dict_by_crop(data_dict, crop_coords, orig_width, orig_height):
"""
Filter dictionary entries based on crop coordinates.
Args:
data_dict: Dictionary with batched tensors
crop_coords: Tuple of (top, left, height, width) for crop region
orig_width: Original image width
orig_height: Original image height
Returns:
Filtered dictionary with only valid entries
"""
top, left, h, w = crop_coords
bbox_xyxy = data_dict['bbox_xyxy']
# Convert to numpy if it's a tensor
if isinstance(bbox_xyxy, torch.Tensor):
bbox_xyxy_np = bbox_xyxy.cpu().numpy()
else:
bbox_xyxy_np = bbox_xyxy.copy()
valid_indices = []
# Check each bbox
for i in range(len(bbox_xyxy_np)):
bbox_curr = bbox_xyxy_np[i].copy()
# Normalize bbox coordinates
bbox_curr[0::2] = bbox_curr[0::2] / orig_width # x coordinates
bbox_curr[1::2] = bbox_curr[1::2] / orig_height # y coordinates
# Define crop region in normalized coordinates
xmin, ymin, xmax, ymax = left, top, left + w, top + h
bbox_curr = A.bbox_crop(bbox_curr, x_min=xmin, y_min=ymin, x_max=xmax, y_max=ymax,
rows=orig_height, cols=orig_width)
bbox_curr = np.array(bbox_curr)
# Check if bbox is out of range
if (np.all(bbox_curr[1::2] < 0) or np.all(bbox_curr[0::2] < 0) or
np.all(bbox_curr[1::2] > 1.0) or np.all(bbox_curr[0::2] > 1.0)):
print("Skipping out-of-range bbox")
continue
valid_indices.append(i)
# Filter all dictionary entries using valid indices
if not valid_indices:
print("No valid bboxes found after filtering")
return None
filtered_dict = {}
for key, value in data_dict.items():
if key == 'pred_smpl_params':
filtered_dict[key] = {}
for new_key, new_value in value.items():
filtered_dict[key][new_key] = new_value[valid_indices]
elif isinstance(value, (torch.Tensor, np.ndarray)):
filtered_dict[key] = value[valid_indices]
elif isinstance(value, list):
filtered_dict[key] = [value[i] for i in valid_indices]
else:
filtered_dict[key] = value
return filtered_dict
def convert_dict_to_tensors(all_out, device='cuda'):
"""
Convert a dictionary with list values back to tensors.
Args:
all_out (dict): Dictionary with list values to convert
device (str): Device to put tensors on ('cuda', 'cpu', etc.)
Returns:
dict: Dictionary with tensor values
"""
converted_dict = {}
for k, v in all_out.items():
if k == 'pred_smpl_params':
# Handle nested dictionary for pred_smpl_params
converted_dict[k] = {}
for k2, v2 in v.items():
converted_dict[k][k2] = torch.tensor(v2, device=device)
else:
# Convert regular list to tensor
converted_dict[k] = torch.tensor(v, device=device)
return converted_dict
def vertical_concat_videos(video_arrays):
"""
Vertically concatenate a list of video arrays.
Args:
video_arrays (list): List of numpy arrays with shape [17, H, W, C]
Returns:
np.ndarray: A single array with videos stacked vertically
Shape will be [17, sum(H), W, C]
"""
# Check if list is empty
if not video_arrays:
return None
# Ensure all videos have the same number of frames, width and channels
frames, heights, widths, channels = zip(*[v.shape for v in video_arrays])
# Verify all videos have the same number of frames, width and channels
if len(set(frames)) > 1 or len(set(widths)) > 1 or len(set(channels)) > 1:
raise ValueError("All videos must have the same number of frames, width, and channels")
# Stack each frame vertically
result = []
for f in range(frames[0]):
frame_list = [video[f] for video in video_arrays]
stacked_frame = np.concatenate(frame_list, axis=0) # Concat along height dimension
result.append(stacked_frame)
# Stack all frames back together
return np.stack(result)
def create_frame_ids(x):
# Define special token range
special_min, special_max = 30004, 30020
# Initialize frame ID tensor
frame_ids = torch.zeros_like(x)
# We'll process each batch element
for b in range(x.size(0)):
tokens = x[b]
special_mask = (tokens >= special_min) & (tokens <= special_max)
special_indices = torch.nonzero(special_mask, as_tuple=False).squeeze(-1)
current_id = 0
for i, idx in enumerate(special_indices):
start = idx
end = special_indices[i + 1] if i + 1 < len(special_indices) else len(tokens)
frame_ids[b, start:end] = current_id
current_id += 1
return frame_ids
def image_mask_first_frame_conditional(tensor: torch.Tensor, GT_tokens: int, input_budget: int, target_budget: int):
"""Applies input and target masking to an image tensor sequentially
Args:
tensor: Image tensor
GT_tokens: Number of tokens in the tensor
input_budget: Token budget for the input
target_budget: Token budget for the target
Returns:
Dictionary containing the masked image tensor, the input mask, the target mask, and the decoder attention mask
"""
# Input mask: First `input_budget` tokens are not masked (0), rest are masked (1)
input_mask = torch.ones(GT_tokens, dtype=torch.bool)
input_mask[:input_budget] = 0 # First `input_budget` positions are not masked
# Target mask: The next `target_budget` tokens are not masked (0), rest are masked (1)
target_mask = torch.ones(GT_tokens, dtype=torch.bool)
if target_budget is not None:
target_mask[input_budget:input_budget + target_budget] = 0 # Next `target_budget` positions are not masked
else:
target_mask = ~input_mask # If target_budget is None, complement input_mask
# Compute decoder attention mask
decoder_attention_mask = torch.zeros(GT_tokens, dtype=torch.int)
first_mask_token = torch.argmin(target_mask + torch.arange(target_mask.shape[0], device=target_mask.device) * 1e-6)
decoder_attention_mask[first_mask_token] = (~target_mask).sum() # Equivalent to target budget
return {
"tensor": torch.tensor(tensor).long().cuda(),
"input_mask": input_mask.unsqueeze(0).cuda(),
"target_mask": target_mask.unsqueeze(0).cuda(),
"decoder_attention_mask": decoder_attention_mask.unsqueeze(0).cuda(),
}
def transform_tensor_with_markers(tensor, start_sentinel=5, end_sentinel=21,
start_marker=30004, num_markers=17):
"""
Transform tensor by replacing sentinel tokens with special markers and
extending to always have num_markers frame markers.
Args:
tensor: Input tensor containing sentinel tokens
start_sentinel: First sentinel token value (default: 5)
end_sentinel: Last sentinel token value (default: 21)
start_marker: First special marker value (default: 30004)
num_markers: Total number of frame markers to ensure (default: 17)
"""
device = tensor.device
result = []
# Find all sentinel tokens in the tensor
sentinel_positions = {}
flat_tensor = tensor.flatten()
for i, token in enumerate(flat_tensor):
token_val = token.item()
if start_sentinel <= token_val <= end_sentinel:
if token_val not in sentinel_positions:
sentinel_positions[token_val] = []
sentinel_positions[token_val].append(i)
# Get sorted sentinel tokens that exist in the tensor
existing_sentinels = sorted(sentinel_positions.keys())
# Process existing sentinels
last_pos = 0
for sentinel_idx, sentinel_val in enumerate(existing_sentinels):
# Get the position of this sentinel
sentinel_pos = sentinel_positions[sentinel_val][0]
# Add the marker for this sentinel
marker = start_marker + sentinel_idx
result.append(marker)
# Find tokens between this sentinel and the next (or end)
if sentinel_idx < len(existing_sentinels) - 1:
next_sentinel_pos = sentinel_positions[existing_sentinels[sentinel_idx + 1]][0]
else:
next_sentinel_pos = len(flat_tensor)
# Add tokens between sentinels (excluding the sentinel token itself)
for j in range(sentinel_pos + 1, next_sentinel_pos):
if flat_tensor[j].item() not in range(start_sentinel, end_sentinel + 1):
result.append(flat_tensor[j].item())
# Complete with remaining markers if we have fewer than num_markers
num_existing = len(existing_sentinels)
if num_existing < num_markers:
for i in range(num_existing, num_markers):
marker = start_marker + i
# Reset sentinel numbering to start from start_sentinel
sentinel = start_sentinel + (i - num_existing)
result.append(marker)
result.append(sentinel)
return torch.tensor(result, device=device).unsqueeze(0)
def pop_conditioning_domain(cond_domains, target_domains, partial_conditioning_tokens, complete_partial_conditioned_modalities, *other_lists):
for cond_domain, partial_tokens in zip(cond_domains, partial_conditioning_tokens):
if cond_domain not in target_domains:
raise ValueError(f"{cond_domain} not found in target_domains")
idx = target_domains.index(cond_domain)
popped_values = []
# Remove cond_domain from target_domains only if partial tokens are none, means full, and user wants to complete it
if (partial_tokens is None) or not complete_partial_conditioned_modalities:
target_domains.pop(idx)
# Remove corresponding elements from each of the other lists
popped_values = [lst.pop(idx) for lst in other_lists]
return target_domains, *other_lists, popped_values
def create_text_frame(text, height, width=50, font_size=45, bg_color=(0, 0, 0), text_color=(255, 255, 255)):
"""
Create a static frame with text.
Args:
text (str): Text to display
height (int): Height of the frame (should match video height)
width (int): Width of the text frame
font_size (int): Font size for the text
bg_color (tuple): Background color (R, G, B)
text_color (tuple): Text color (R, G, B)
Returns:
np.ndarray: Text frame with shape [height, width, 3]
"""
# Create PIL image
img = Image.new('RGB', (width, height), bg_color)
draw = ImageDraw.Draw(img)
try:
# Try to use a better font if available
font = ImageFont.truetype("arial.ttf", font_size)
except:
# Fall back to default font
font = ImageFont.load_default()
# Get text bounding box for centering
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Calculate position to center the text
x = (width - text_width) // 2
y = (height - text_height) // 2
# Draw text
draw.text((x, y), text, fill=text_color, font=font)
# Convert PIL image to numpy array
return np.array(img)
def resize_and_duplicate(arr, target_size=(128, 128)):
"""
arr: numpy array of shape (T, H, W, C)
"""
T, H, W, C = arr.shape
# ---- 1. Resize ----
resized = np.zeros((T, target_size[0], target_size[1], C), dtype=arr.dtype)
for i in range(T):
resized[i] = cv2.resize(arr[i], target_size, interpolation=cv2.INTER_NEAREST)
# ---- 2. Duplicate if T == 8 ----
if T == 8:
idx = [0, 0, 0] + sum([[i, i] for i in range(1, T)], [])
resized = resized[idx]
return resized
def resize_and_duplicate_batched(arr, target_size=(128, 128)):
"""
arr: numpy array of shape (B, T, H, W, C) or (T, H, W, C)
Returns: numpy array of same batch structure with resized frames
"""
# Handle both batched and unbatched inputs
if arr.ndim == 4:
arr = arr[np.newaxis, ...] # Add batch dim
unbatch_output = True
else:
unbatch_output = False
B, T, H, W, C = arr.shape
# ---- 1. Resize using PyTorch (vectorized) ----
# Convert to torch: (B, T, H, W, C) -> (B*T, C, H, W)
arr_torch = torch.from_numpy(arr).reshape(B * T, H, W, C).permute(0, 3, 1, 2)
# Resize all frames at once
resized_torch = F.interpolate(
arr_torch.float(),
size=target_size,
mode='nearest'
)
# Back to numpy: (B*T, C, H, W) -> (B, T, target_H, target_W, C)
resized = resized_torch.permute(0, 2, 3, 1).reshape(
B, T, target_size[0], target_size[1], C
).numpy().astype(arr.dtype)
# ---- 2. Duplicate if T == 8 (vectorized with indexing) ----
if T == 8:
idx = [0, 0, 0] + sum([[i, i] for i in range(1, T)], [])
resized = resized[:, idx] # Works for entire batch
# Remove batch dim if input was unbatched
if unbatch_output:
resized = resized[0]
return resized
class FeatureToPCAConverter:
"""Convert high-dimensional features to RGB using PCA"""
def __init__(self):
self.pca = None
self.fitted = False
def convert_to_rgb(self, features):
"""
Convert features [B, C, T, H, W] to RGB [B, T, H, W, 3] using PCA
"""
with torch.no_grad():
B, C, T, H, W = features.shape
device = features.device
# Move to CPU for PCA
feats_reshaped = features.permute(0, 2, 3, 4, 1).reshape(-1, C).cpu().numpy()
# Fit PCA on first call
if not self.fitted:
self.pca = PCA(n_components=3)
self.pca.fit(feats_reshaped)
self.fitted = True
print(f"PCA fitted - Variance explained: {self.pca.explained_variance_ratio_.sum():.3f}")
# PCA transform
pca_features = self.pca.transform(feats_reshaped)
# Back to tensor
rgb = torch.from_numpy(pca_features).float().reshape(B, T, H, W, 3)
rgb = rgb.to(device)
# Normalize per batch to [0, 1]
flat = rgb.view(B, -1, 3)
rgb_min = flat.min(dim=1)[0].view(B, 1, 1, 1, 3)
rgb_max = flat.max(dim=1)[0].view(B, 1, 1, 1, 3)
rgb = (rgb - rgb_min) / (rgb_max - rgb_min + 1e-8)
return rgb
def convert_dict_to_tensors(all_out, device='cpu'):
"""
Convert a dictionary with list values back to tensors.
Args:
all_out (dict): Dictionary with list values to convert
device (str): Device to put tensors on ('cuda', 'cpu', etc.)
Returns:
dict: Dictionary with tensor values
"""
converted_dict = {}
for k, v in all_out.items():
if k == 'pred_smpl_params':
# Handle nested dictionary for pred_smpl_params
converted_dict[k] = {}
for k2, v2 in v.items():
converted_dict[k][k2] = torch.tensor(v2, device=device)
else:
# Convert regular list to tensor
converted_dict[k] = torch.tensor(v, device=device)
return converted_dict
def load_and_decode_tokens_batched(tokens, tokenizer_model, device='cuda', original_len=17, crop_settings=None,
modality="DINOv2-B14"):
"""
Load saved tokens and decode them to reconstruction
Args:
tokens: Token indices [num_crops, num_tokens] or [num_tokens]
tokenizer_model: Loaded VidTok model
device: Device to use
original_len: Original temporal length for decoding
Returns:
Reconstructed video [B, C, T, H, W]
"""
# Convert to tensor if numpy
if isinstance(tokens, np.ndarray):
tokens = torch.from_numpy(tokens)
tokens = tokens.to(device)
if modality == "vjepa":
# if original_len == 8:
t = 4 # TODO: hard coded for now
padding_applied = 0
else:
t = 5
padding_applied = 3
h = w = int((tokens.shape[1] / t) ** 0.5)
tokens = rearrange(tokens, "b (t h w) ->b t h w", t=t, h=h, w=w)
with torch.no_grad(), torch.autocast(device_type='cuda', dtype=torch.float16):
# Decode from indices
reconstructed = tokenizer_model.decode(tokens, decode_from_indices=True, original_len=original_len,
padding_applied=padding_applied)
return reconstructed.cpu()
def load_and_decode_tokens(tokens, tokenizer_model, device='cuda', original_len=17, crop_settings=None,
modality="DINOv2-B14"):
"""
Load saved tokens and decode them to reconstruction
Args:
tokens: Token indices [num_crops, num_tokens] or [num_tokens]
tokenizer_model: Loaded VidTok model
device: Device to use
original_len: Original temporal length for decoding
Returns:
Reconstructed video [B, C, T, H, W]
"""
# Convert to tensor if numpy
if isinstance(tokens, np.ndarray):
tokens = torch.from_numpy(tokens)
tokens = tokens.to(device)
if modality == "vjepa":
# if original_len == 8:
t = 4 # TODO: hard coded for now
padding_applied = 0
else:
t = 5
padding_applied = 3
h = w = int((tokens.shape[0] / t) ** 0.5)
tokens = rearrange(tokens, "(t h w) -> t h w", t=t, h=h, w=w)
tokens = tokens.unsqueeze(0)
with torch.no_grad(), torch.autocast(device_type='cuda', dtype=torch.float16):
# Decode from indices
reconstructed = tokenizer_model.decode(tokens, decode_from_indices=True, original_len=original_len,
padding_applied=padding_applied)
return reconstructed.cpu()
def get_class_name_by_index(file_path, index):
"""
Get class name by index from a text file.
Args:
file_path (str): Path to the text file containing class names
index (int): Index of the class name to retrieve (0-based)
Returns:
str: Class name at the given index, or None if index is out of range
"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
# Strip whitespace from each line
class_names = [line.strip() for line in lines if line.strip()]
# Check if index is valid
if 0 <= index < len(class_names):
return class_names[index]
else:
print(f"Index {index} is out of range. Valid range: 0-{len(class_names) - 1}")
return None
except FileNotFoundError:
print(f"File not found: {file_path}")
return None
except Exception as e:
print(f"Error reading file: {e}")
return None
def load_class_names(file_path):
"""
Load all class names into a list for multiple lookups.
More efficient if you need to do many lookups.
Args:
file_path (str): Path to the text file containing class names
Returns:
list: List of class names, or empty list if error
"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
return [line.strip() for line in file if line.strip()]
except Exception as e:
print(f"Error loading class names: {e}")
return []
def vertical_concat_videos_with_labels(video_arrays, labels=None, text_width=100, font_size=60, line_thickness=5):
"""
Vertically concatenate a list of video arrays with text labels on the left,
adding a green line separator between videos.
Args:
video_arrays (list): List of numpy arrays with shape [T, H, W, C]
labels (list): List of text labels for each video. If None, uses default labels.
text_width (int): Width of the text label area
font_size (int): Font size for labels
line_thickness (int): Thickness of the green separator line
Returns:
np.ndarray: A single array with labeled videos stacked vertically
"""
if not video_arrays:
return None
if labels is None:
labels = ["Only class"] + [f"{tokens} tokens" for tokens in [256, 512]]
if len(labels) < len(video_arrays):
for i in range(len(labels), len(video_arrays)):
labels.append(f"Video {i + 1}")
# Get shape info
frames, heights, widths, channels = zip(*[v.shape for v in video_arrays])
if len(set(frames)) > 1 or len(set(widths)) > 1 or len(set(channels)) > 1:
raise ValueError("All videos must have the same number of frames, width, and channels")
labeled_videos = []
for i, (video, label) in enumerate(zip(video_arrays, labels)):
text_frame = create_text_frame(
text=label,
height=video.shape[1],
width=text_width,
font_size=font_size
)
labeled_video_frames = []
for frame_idx in range(video.shape[0]):
labeled_frame = np.concatenate([text_frame, video[frame_idx]], axis=1)
labeled_video_frames.append(labeled_frame)
labeled_video = np.stack(labeled_video_frames)
labeled_videos.append(labeled_video)
# Now vertically concatenate with green line separators
result = []
green_color = np.array([0, 255, 0], dtype=np.uint8) # Green (BGR or RGB depending on your video)
for f in range(frames[0]):
frame_list = []
for i, video in enumerate(labeled_videos):
frame_list.append(video[f])
if i < len(labeled_videos) - 1: # Add separator except after the last one
sep = np.full(
(line_thickness, video[f].shape[1], video[f].shape[2]),
green_color,
dtype=np.uint8
)
frame_list.append(sep)
stacked_frame = np.concatenate(frame_list, axis=0)
result.append(stacked_frame)
return np.stack(result)
def tensor_to_uint8(tensor):
tensor = torch.clamp(tensor, -1.0, 1.0)
tensor = (tensor + 1.0) / 2.0 # -1,1 -> 0,1; c,h,w
tensor = (tensor.cpu().numpy() * 255).astype(np.uint8)
return tensor
def merge_tokens_with_frames_no_eos(tokens, frames, counts):
"""Merge tokens with frame identifiers efficiently."""
if not tokens or not frames or not counts:
return ""
output = []
indices = [0] + list(accumulate(counts))
for i in range(len(counts)):
frame_tokens = ' '.join(tokens[indices[i]:indices[i + 1]])
merged = f"{frames[i]} {frame_tokens}" if frame_tokens else frames[i]
output.append(merged)
return ' '.join(output)
def merge_detection_tokens_with_sentinel_tokens_no_eos(tokens, sentinel_tokens, counts):
"""Merge tokens with only sentinel tokens"""
if not tokens or not sentinel_tokens or not counts:
return ""
output = []
indices = [0] + list(accumulate(counts))
for i in range(len(counts)):
frame_tokens = ' '.join(tokens[indices[i]:indices[i + 1]])
merged = f"{sentinel_tokens[i]} {frame_tokens}" if frame_tokens else sentinel_tokens[i]
output.append(merged)
return ' '.join(output)
def merge_detection_tokens_with_sentinel_tokens(tokens, sentinel_tokens, counts):
"""Merge tokens with only sentinel tokens"""
# if not tokens or not sentinel_tokens or not counts:
# return ""
output = []
indices = [0] + list(accumulate(counts))
for i in range(len(counts)):
frame_tokens = ' '.join(tokens[indices[i]:indices[i + 1]])
merged = f"{sentinel_tokens[i]} {frame_tokens} [EOS]" if frame_tokens else f"{sentinel_tokens[i]} [EOS]"
output.append(merged)
return ' '.join(output)
def reorder_lists_by_conditioning_domain(conditioning_domain, target_domains, *lists):
"""
Reorder all lists to put the conditioning domain's corresponding values first.
Args:
conditioning_domain: The domain to use as conditioning (e.g., 'det')
target_domains: List of domain names
*lists: Variable number of lists to reorder
Returns:
Tuple of reordered lists in the same order as input
"""
if conditioning_domain not in target_domains:
raise ValueError(f"Conditioning domain '{conditioning_domain}' not found in target_domains")
# Find the index of the conditioning domain
cond_index = target_domains.index(conditioning_domain)
# Reorder target_domains first
new_target_domains = [target_domains[cond_index]] + target_domains[:cond_index] + target_domains[cond_index + 1:]
# Reorder all other lists
reordered_lists = []
for lst in lists:
if lst is None or len(lst) != len(target_domains):
# Handle None or mismatched length lists
reordered_lists.append(lst)
else:
new_list = [lst[cond_index]] + lst[:cond_index] + lst[cond_index + 1:]
reordered_lists.append(new_list)
return new_target_domains, *reordered_lists
def reorder_lists_by_conditioning_domains(conditioning_domains, target_domains, *lists):
"""
Reorder all lists to put the conditioning domains' corresponding values first.
Args:
conditioning_domains: List of domains to use as conditioning (e.g., ['det', 'seg'])
target_domains: List of all domain names
*lists: Variable number of lists to reorder
Returns:
Tuple of reordered lists in the same order as input
"""
# Ensure conditioning_domains is a list
if isinstance(conditioning_domains, str):
conditioning_domains = [conditioning_domains]
# Check that all conditioning domains exist in target_domains
missing = [d for d in conditioning_domains if d not in target_domains]
if missing:
raise ValueError(f"Conditioning domains {missing} not found in target_domains")
# Get indices for conditioning domains and non-conditioning domains
cond_indices = [target_domains.index(d) for d in conditioning_domains]
remaining_indices = [i for i in range(len(target_domains)) if i not in cond_indices]
# Reorder target_domains
new_target_domains = [target_domains[i] for i in cond_indices + remaining_indices]
# Reorder all other lists
reordered_lists = []
for lst in lists:
if lst is None or len(lst) != len(target_domains):
reordered_lists.append(lst)
else:
new_list = [lst[i] for i in cond_indices + remaining_indices]
reordered_lists.append(new_list)
return new_target_domains, *reordered_lists
def merge_tokens_with_frames(tokens, frames, counts):
"""Merge tokens with frame identifiers efficiently."""
# if not tokens or not frames or not counts:
# return ""
output = []
indices = [0] + list(accumulate(counts))
for i in range(len(counts)):
frame_tokens = ' '.join(tokens[indices[i]:indices[i + 1]])
merged = f"{frames[i]} {frame_tokens} [EOS]" if frame_tokens else f"{frames[i]} [EOS]"
output.append(merged)
return ' '.join(output)
def image_mask(tensor: torch.Tensor, GT_tokens: int, input_budget: int, target_budget: int):
"""Applies input and target masking to an image tensor
Args:
tensor: Image tensor
GT_tokens: Number of tokens in the tensor
input_budget: Token budget for the input
target_budget: Token budget for the target
Returns:
Dictionary containing the masked image tensor, the input mask, the target mask, and the decoder attention mask
"""
# Use fixed seed for deterministic ordering across different calls
torch.manual_seed(42)
noise = torch.rand(GT_tokens)
ids_shuffle = torch.argsort(noise, dim=0)
input_mask = torch.ones(GT_tokens, dtype=torch.bool)
input_mask[:input_budget] = 0
input_mask = torch.gather(input_mask, dim=0, index=ids_shuffle)
if target_budget is None:
target_mask = ~input_mask
else:
target_mask = torch.ones(GT_tokens, dtype=torch.bool)
target_mask[input_budget:input_budget + target_budget] = 0
target_mask = torch.gather(target_mask, dim=0, index=ids_shuffle)
decoder_attention_mask = torch.zeros(GT_tokens, dtype=torch.int)
first_mask_token = torch.argmin(target_mask + torch.arange(target_mask.shape[0], device=target_mask.device) * 1e-6)
decoder_attention_mask[first_mask_token] = (~target_mask).sum() # Equiv. to target budget
return {
"tensor": torch.tensor(tensor).long().cuda(),
"input_mask": input_mask.unsqueeze(0).cuda(),
"target_mask": target_mask.unsqueeze(0).cuda(),
"decoder_attention_mask": decoder_attention_mask.unsqueeze(0).cuda(),
}
def denormalize(img, mean=None, std=None):
"""
Denormalizes an image.
Args:
img (torch.Tensor): Image to denormalize.
mean (tuple): Mean to use for denormalization.
std (tuple): Standard deviation to use for denormalization.
"""
return TF.normalize(
img.clone(), mean=[-m / s for m, s in zip(mean, std)], std=[1 / s for s in std]
)
def denormalize_video(video, mean=None, std=None):
"""
Denormalizes videos.
Args:
video (torch.Tensor): Video to denormalize.
mean (tuple): Mean to use for denormalization.
std (tuple): Standard deviation to use for denormalization.
"""
if len(video.shape) == 4:
# single video, use denormalize
return denormalize(video, mean=mean, std=std)
B = video.shape[0]
# pack frames into the batch dimension
img = einops.rearrange(video, "b c t h w -> (b t) c h w")
# denormalize each frame
img_norm = denormalize(img, mean=mean, std=std)
# unpack videos
norm_video = einops.rearrange(img_norm, "(b t) c h w -> b c t h w", b=B)
return norm_video
def image_mask_first_frame_conditional(tensor: torch.Tensor, GT_tokens: int, input_budget: int, target_budget: int, device='cuda'):
"""Applies input and target masking to an image tensor sequentially
Args:
tensor: Image tensor
GT_tokens: Number of tokens in the tensor
input_budget: Token budget for the input
target_budget: Token budget for the target
Returns:
Dictionary containing the masked image tensor, the input mask, the target mask, and the decoder attention mask
"""
# Input mask: First `input_budget` tokens are not masked (0), rest are masked (1)
input_mask = torch.ones(GT_tokens, dtype=torch.bool)
input_mask[:input_budget] = 0 # First `input_budget` positions are not masked
# Target mask: The next `target_budget` tokens are not masked (0), rest are masked (1)
target_mask = torch.ones(GT_tokens, dtype=torch.bool)
if target_budget is not None:
target_mask[input_budget:input_budget + target_budget] = 0 # Next `target_budget` positions are not masked
else:
target_mask = ~input_mask # If target_budget is None, complement input_mask
# Compute decoder attention mask
decoder_attention_mask = torch.zeros(GT_tokens, dtype=torch.int)
first_mask_token = torch.argmin(target_mask + torch.arange(target_mask.shape[0], device=target_mask.device) * 1e-6)
decoder_attention_mask[first_mask_token] = (~target_mask).sum() # Equivalent to target budget
# FIXED: Only include the conditioning tokens (first input_budget tokens) in the tensor
# The rest should be zeros (will be predicted during generation)
full_tensor = torch.zeros(GT_tokens, dtype=torch.long, device=device)
full_tensor[:input_budget] = torch.tensor(tensor[:input_budget]).long().to(device)
return {
"tensor": full_tensor.unsqueeze(0),
"input_mask": input_mask.unsqueeze(0).to(device),
"target_mask": target_mask.unsqueeze(0).to(device),
"decoder_attention_mask": decoder_attention_mask.unsqueeze(0).to(device),
}
# Function to save side-by-side video using imageio
def save_video_with_imageio(frames, output_path, fps):
with imageio.get_writer(output_path, fps=fps) as writer:
for frame in frames:
writer.append_data(frame)
print(f"Side-by-side video saved at: {output_path}")
def process_tsn_frame(frame_rgb: np.ndarray, bound: int) -> tuple[np.ndarray, np.ndarray]:
flow_xy = tsn_rgb_to_flow(frame_rgb, bound=bound)
colored = flow_to_rgb(flow_xy)
return colored, flow_xy
def tsn_rgb_to_flow(flow_img: np.ndarray, bound: int = 20) -> np.ndarray:
"""
Convert TSN-encoded optical-flow RGB frame into (H,W,2) raw flow (float32).
Uses only the first two channels as x and y; third channel (if present) is ignored.
"""
flow_x_norm = flow_img[..., 0].astype(np.float32)
flow_y_norm = flow_img[..., 1].astype(np.float32)
flow_x = np.expand_dims(((flow_x_norm * (2 * bound) / 255.0) - bound), -1)
flow_y = np.expand_dims(((flow_y_norm * (2 * bound) / 255.0) - bound), -1)
return np.concatenate([flow_x, flow_y], axis=-1)
def only_extract_indices(crop_settings):
fps = 30
required_frames = []
indices_per_crop = []
for single_crop in crop_settings:
start_t, end_t, i, j, h, w, h_flip = single_crop
frame_indices = np.linspace(
start_t * fps, end_t * fps, 17, dtype=np.int32
)
frame_indices[-1] -= 1
indices_per_crop.append(frame_indices)
required_frames.extend(frame_indices)
indices = sorted(set(required_frames))
return indices, indices_per_crop # default 4 FPS
def convert_raw_optical_flow(frames, bound: int) -> None:
"""
paths keys: original, reconstructed -> Path
"""
all_frames_original = []
for frame_idx in (range(len(frames))):
orig_frame = frames[frame_idx]
# Process TSN frames to get flow visualization
orig_colored, orig_raw_flow = process_tsn_frame(orig_frame, bound=bound)
all_frames_original.append(orig_colored)
return np.stack(all_frames_original, axis=0)
def denorm_bbox(bbox, width, height):
"""Convert normalized bbox [x1, y1, x2, y2] → pixel coords"""
x1 = int(bbox[0] * width)
y1 = int(bbox[1] * height)
x2 = int(bbox[2] * width)
y2 = int(bbox[3] * height)
return [x1, y1, x2, y2]
def order_bboxes_by_dist_to_orig(detections, thresh=0.6, max_det=6):
# keep only above threshold
# detections = [det for det in detections if det["confidence"] >= thresh]
# sort by distance of top-left corner (x1, y1) to origin
# detections = sorted(
# detections,
# key=lambda d: d["bbox"][0]**2 + d["bbox"][1]**2 # bbox coords are normalized
# )
# keep top-k
return detections[:max_det]
def draw_boxes_on_blank(frame_size, detections, color=(0,0,255), bg_color=(255,255,255)):
"""Draw detections + labels on blank image"""
h, w = frame_size
img = np.full((h, w, 3), bg_color, dtype=np.uint8)
for det in detections:
bbox = det["bbox"]
x1, y1, x2, y2 = denorm_bbox(bbox, w, h)
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
# --- Add label text ---
label_text = f"{det['label']}"
# Determine optimal text placement and size
text_position, font_scale, background_rect = _get_optimal_text_placement(
img, x1, y1, x2, y2, label_text
)
# Draw text background rectangle
if background_rect:
cv2.rectangle(img, background_rect[0], background_rect[1], color, -1)
# Draw the text
cv2.putText(
img,
text=label_text,
org=text_position,
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=font_scale,
color=bg_color,
lineType=cv2.LINE_AA,
)
return img
def crop_and_resize_detections(detections, crop, frame_size, target_size):
"""
Crop detections based on crop [top, left, h, w].
Normalize wrt cropped frame, then we will directly draw on resized frame.
"""
top, left, ch, cw = crop
H, W = frame_size
filtered = []
for det in detections:
x1, y1, x2, y2 = denorm_bbox(det["bbox"], W, H)
# Intersection with crop
nx1 = max(x1, left)
ny1 = max(y1, top)
nx2 = min(x2, left + cw)
ny2 = min(y2, top + ch)
if nx1 < nx2 and ny1 < ny2: # valid intersection
# Shift to cropped coords and normalize wrt cropped region
adj_x1 = (nx1 - left) / cw
adj_y1 = (ny1 - top) / ch
adj_x2 = (nx2 - left) / cw
adj_y2 = (ny2 - top) / ch
new_det = {
"label": det["label"],
"confidence": det["confidence"],
"bbox": [adj_x1, adj_y1, adj_x2, adj_y2],
}
filtered.append(new_det)
return filtered