| """ |
| FOV (Field of View) Overlap-based Memory Retrieval Module |
| Implements geometric retrieval based on camera pose overlap for Context-as-Memory |
| |
| Aligned with the Context-as-Memory paper [2506.03141]. |
| """ |
|
|
| import os |
| import json |
| import random |
| import sys |
| from concurrent.futures import ThreadPoolExecutor, as_completed |
| import numpy as np |
| from typing import List, Dict, Tuple, Optional |
| from pathlib import Path |
|
|
| import torch |
| from PIL import Image |
|
|
| def degrees_to_radians(degrees: float) -> float: |
| """Convert degrees to radians.""" |
| return degrees * np.pi / 180 |
|
|
|
|
| def compute_rotation_list_z_only(x: float, y: float, z: float, yaw_degrees: float) -> List[float]: |
| """ |
| Paper-aligned: rotation only around Z-axis (yaw). 4 parameters. |
| R = R_z(yaw). No roll/pitch. |
| |
| Returns: |
| [t_x, t_y, t_z, R_11, R_12, ..., R_33] (12 elements) |
| """ |
| yaw_rad = degrees_to_radians(yaw_degrees) |
| c, s = np.cos(yaw_rad), np.sin(yaw_rad) |
| R_z = np.array([ |
| [c, -s, 0], |
| [s, c, 0], |
| [0, 0, 1], |
| ], dtype=np.float64) |
| return [float(x), float(y), float(z)] + R_z.flatten().tolist() |
|
|
|
|
| def compute_rotation_list_yaw_pitch( |
| x: float, y: float, z: float, yaw_degrees: float, pitch_degrees: float |
| ) -> List[float]: |
| """ |
| Five-parameter rotation: R = R_z(pitch) @ R_y(yaw). Same convention as external pipelines. |
| - R_y: rotation around Y (yaw in degrees). |
| - R_z: rotation around Z (pitch in degrees). |
| Returns [t_x, t_y, t_z, R_11, ..., R_33] (12 elements), encoder-compatible. |
| """ |
| yaw_rad = degrees_to_radians(yaw_degrees) |
| pitch_rad = degrees_to_radians(pitch_degrees) |
| cy, sy = np.cos(yaw_rad), np.sin(yaw_rad) |
| cp, sp = np.cos(pitch_rad), np.sin(pitch_rad) |
| R_y = np.array([ |
| [cy, 0, sy], |
| [0, 1, 0], |
| [-sy, 0, cy], |
| ], dtype=np.float64) |
| R_z = np.array([ |
| [cp, -sp, 0], |
| [sp, cp, 0], |
| [0, 0, 1], |
| ], dtype=np.float64) |
| R = R_z @ R_y |
| return [float(x), float(y), float(z)] + R.flatten().tolist() |
|
|
|
|
| def compute_rotation_list(params: List[float]) -> List[float]: |
| """ |
| Compute rotation matrix from camera parameters and flatten to list. |
| - 4 params [x, y, z, yaw]: Z-only, R = R_z(yaw) (paper default). |
| - 5 params [x, y, z, yaw, pitch]: R = R_z(pitch) @ R_y(yaw). |
| Returns [t_x, t_y, t_z, R_11, ..., R_33] (12 elements). |
| """ |
| if len(params) >= 5: |
| x, y, z = float(params[0]), float(params[1]), float(params[2]) |
| yaw = float(params[3]) |
| pitch = float(params[4]) |
| return compute_rotation_list_yaw_pitch(x, y, z, yaw, pitch) |
| if len(params) >= 4: |
| x, y, z = float(params[0]), float(params[1]), float(params[2]) |
| yaw = float(params[3]) |
| return compute_rotation_list_z_only(x, y, z, yaw) |
| x = y = z = yaw = 0.0 |
| return compute_rotation_list_z_only(x, y, z, yaw) |
|
|
|
|
| def yaw_deg_from_rt(rt: List[float]) -> float: |
| """Extract yaw (degrees) from 12-dim RT. Z-only: yaw = atan2(R_21, R_11).""" |
| if rt is None or len(rt) < 12: |
| return 0.0 |
| R = np.array(rt[3:12]).reshape(3, 3) |
| return float(np.degrees(np.arctan2(R[1, 0], R[0, 0]))) |
|
|
|
|
| def flip_yaw_rt(rt: List[float]) -> List[float]: |
| """Return RT with yaw negated (CW<->CCW). rt is 12 elements [t_x,t_y,t_z, R_11..R_33].""" |
| if rt is None or len(rt) < 12: |
| return list(rt) if rt else [] |
| tx, ty, tz = rt[0], rt[1], rt[2] |
| yaw_deg = yaw_deg_from_rt(rt) |
| return compute_rotation_list_z_only(tx, ty, tz, -yaw_deg) |
|
|
|
|
| def flip_yaw_rt_list(rt_list: List[List[float]]) -> List[List[float]]: |
| """Flip yaw for each RT in list (data aug for direction sensitivity).""" |
| return [flip_yaw_rt(rt) for rt in rt_list] |
|
|
|
|
| def convert_rt_to_relative(rt_list_all: List[List[float]], ref_rt: List[float]) -> List[List[float]]: |
| """ |
| Convert RT (rotation-translation) poses to relative coordinates. |
| |
| This aligns with the Context-as-Memory paper's use of relative camera poses |
| for better geometric consistency in context frame retrieval. |
| |
| Args: |
| rt_list_all: List of RT poses, each is [t_x, t_y, t_z, R_11, R_12, ..., R_33] (12 elements) |
| ref_rt: Reference RT pose [t_x, t_y, t_z, R_11, R_12, ..., R_33] (12 elements) |
| |
| Returns: |
| new_rt_list: List of relative RT poses in the same format |
| """ |
| def parse_rt(rt: List[float]) -> tuple: |
| """Parse RT list into rotation matrix R and translation vector t.""" |
| t = np.array(rt[:3]).reshape((3, 1)) |
| R = np.array(rt[3:]).reshape((3, 3)) |
| return R, t |
|
|
| R_ref, T_ref = parse_rt(ref_rt) |
| R_ref_inv = R_ref.T |
| T_ref_inv = -R_ref_inv @ T_ref |
|
|
| new_rt_list = [] |
|
|
| for rt in rt_list_all: |
| R_i, T_i = parse_rt(rt) |
|
|
| |
| R_new = R_ref_inv @ R_i |
| T_new = R_ref_inv @ T_i + T_ref_inv |
|
|
| |
| rt_new = T_new.flatten().tolist() + R_new.flatten().tolist() |
| new_rt_list.append(rt_new) |
|
|
| return new_rt_list |
|
|
|
|
| def pose_to_rt(pose: Dict, constrain_to_xy: bool = True) -> Optional[List[float]]: |
| """ |
| Convert camera pose dict to RT format. Paper-aligned by default. |
| |
| - 2D plane (constrain_to_xy=True): use position x, y only; z=0. |
| - Rotation: Z-axis only; use rotation[2] as yaw (degrees). Roll/pitch ignored. |
| |
| Args: |
| pose: Dict with 'position' [x, y, z] and 'rotation' [roll, pitch, yaw] in degrees |
| constrain_to_xy: If True (default), set z=0 for strict XY-plane displacement (paper). |
| |
| Returns: |
| RT list: [t_x, t_y, t_z, R_11, R_12, ..., R_33] (12 elements) or None if invalid |
| """ |
| if pose is None: |
| return None |
|
|
| pos = pose.get('position', [0, 0, 0]) |
| rot = pose.get('rotation', [0, 0, 0]) |
|
|
| if len(pos) < 2: |
| return None |
|
|
| x = float(pos[0]) |
| y = float(pos[1]) |
| z = 0.0 if constrain_to_xy else (float(pos[2]) if len(pos) >= 3 else 0.0) |
| |
| yaw = float(rot[2]) if len(rot) > 2 else 0.0 |
|
|
| return compute_rotation_list([x, y, z, yaw]) |
|
|
|
|
| def rt_to_pose(rt: List[float]) -> Optional[Dict]: |
| """ |
| Convert RT format back to pose dict. |
| |
| Args: |
| rt: RT list [t_x, t_y, t_z, R_11, R_12, ..., R_33] (12 elements) |
| |
| Returns: |
| pose dict with 'position' and 'rotation' or None if invalid |
| """ |
| if rt is None or len(rt) < 12: |
| return None |
|
|
| t = np.array(rt[:3]) |
| R = np.array(rt[3:]).reshape((3, 3)) |
|
|
| |
| |
| sy = np.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0]) |
| singular = sy < 1e-6 |
|
|
| if not singular: |
| roll = np.arctan2(R[2, 1], R[2, 2]) |
| pitch = np.arctan2(-R[2, 0], sy) |
| yaw = np.arctan2(R[1, 0], R[0, 0]) |
| else: |
| roll = np.arctan2(-R[1, 2], R[1, 1]) |
| pitch = np.arctan2(-R[2, 0], sy) |
| yaw = 0 |
|
|
| return { |
| 'position': t.tolist(), |
| 'rotation': [np.degrees(roll), np.degrees(pitch), np.degrees(yaw)] |
| } |
|
|
|
|
|
|
| def _parse_poses_dict(data: dict) -> dict: |
| """Extract poses dict from JSON data (CineCameraActor or flat dict).""" |
| if 'CineCameraActor' in data: |
| return data['CineCameraActor'] |
| return data if isinstance(data, dict) else {} |
|
|
|
|
| def load_poses_dict(json_file: str) -> dict: |
| """ |
| Load full camera poses dict from JSON file (one read for all frames). |
| |
| Args: |
| json_file: Path to camera pose JSON file |
| |
| Returns: |
| Dict mapping frame_idx (str) -> pose dict, or {} if failed |
| """ |
| if not os.path.exists(json_file): |
| return {} |
| try: |
| with open(json_file, 'r') as f: |
| data = json.load(f) |
| return _parse_poses_dict(data) |
| except Exception as e: |
| print(f"Error loading poses from {json_file}: {e}") |
| return {} |
|
|
|
|
| def load_camera_pose(json_file: str, frame_idx: int) -> Optional[Dict]: |
| """ |
| Load camera pose for a specific frame from JSON file. |
| |
| Args: |
| json_file: Path to camera pose JSON file |
| frame_idx: Frame index |
| |
| Returns: |
| Dict with 'position' and 'rotation' keys, or None if not found |
| """ |
| poses = load_poses_dict(json_file) |
| frame_key = str(frame_idx) |
| return poses.get(frame_key) |
|
|
|
|
| def load_camera_poses_batch(json_file: str, frame_indices: List[int]) -> List[Optional[Dict]]: |
| """ |
| Load camera poses for multiple frames in one JSON read. |
| |
| Args: |
| json_file: Path to camera pose JSON file |
| frame_indices: List of frame indices |
| |
| Returns: |
| List of pose dicts (or None) in same order as frame_indices |
| """ |
| poses = load_poses_dict(json_file) |
| return [poses.get(str(fi)) for fi in frame_indices] |
|
|
|
|
| def load_overlap_frames(overlap_labels_dir: str, video_name: str, frame_idx: int) -> List[int]: |
| """ |
| Load overlapping frame indices for a given frame from overlap_labels. |
| |
| Args: |
| overlap_labels_dir: Base directory for overlap labels |
| video_name: Name of the video |
| frame_idx: Current frame index |
| |
| Returns: |
| List of overlapping frame indices |
| """ |
| overlap_file = os.path.join(overlap_labels_dir, video_name, f"{frame_idx}.json") |
| if not os.path.exists(overlap_file): |
| return [] |
|
|
| try: |
| |
| import signal |
| import torch.distributed as dist |
|
|
| def timeout_handler(signum, frame): |
| raise TimeoutError(f"Timeout loading overlap file: {overlap_file}") |
|
|
| |
| if dist.is_available() and dist.is_initialized() and dist.get_world_size() > 1: |
| signal.signal(signal.SIGALRM, timeout_handler) |
| signal.alarm(10) |
|
|
| try: |
| with open(overlap_file, 'r') as f: |
| data = json.load(f) |
| overlapping_frames = data.get('overlapping_frames', []) |
| |
| result = [int(f) for f in overlapping_frames if f.isdigit() or isinstance(f, int)] |
| if dist.is_available() and dist.is_initialized() and dist.get_world_size() > 1: |
| signal.alarm(0) |
| return result |
| finally: |
| if dist.is_available() and dist.is_initialized() and dist.get_world_size() > 1: |
| signal.alarm(0) |
|
|
| except TimeoutError as e: |
| print(f"Warning: Timeout loading overlap file {overlap_file} (distributed training): {e}") |
| return [] |
| except Exception as e: |
| print(f"Error loading overlap labels from {overlap_file}: {e}") |
| return [] |
|
|
|
|
| def compute_fov_overlap_3d( |
| pose1: Dict, |
| pose2: Dict, |
| fov_degrees: float = 52.67, |
| max_distance: float = 500.0 |
| ) -> float: |
| """ |
| Compute FOV overlap score between two camera poses using 3D geometry. |
| |
| This implementation uses full 6-DoF camera poses (position + rotation) |
| to compute more accurate FOV overlap, as described in Context-as-Memory. |
| |
| Args: |
| pose1: First camera pose with 'position' [x, y, z] and 'rotation' [roll, pitch, yaw] in degrees |
| pose2: Second camera pose with 'position' [x, y, z] and 'rotation' [roll, pitch, yaw] in degrees |
| fov_degrees: Field of view in degrees (default: 52.67 from paper) |
| max_distance: Maximum distance to consider (meters) |
| |
| Returns: |
| Overlap score between 0 and 1 |
| """ |
| if pose1 is None or pose2 is None: |
| return 0.0 |
|
|
| pos1 = np.array(pose1.get('position', [0, 0, 0]), dtype=np.float32) |
| pos2 = np.array(pose2.get('position', [0, 0, 0]), dtype=np.float32) |
|
|
| |
| distance = np.linalg.norm(pos2 - pos1) |
| |
| |
| distance_exceeds_max = distance > max_distance |
|
|
| if distance < 1e-6: |
| |
| return 1.0 |
|
|
| |
| rot1 = pose1.get('rotation', [0, 0, 0]) |
| rot2 = pose2.get('rotation', [0, 0, 0]) |
|
|
| |
| rot1 = np.array(rot1, dtype=np.float32) |
| rot2 = np.array(rot2, dtype=np.float32) |
|
|
| |
| |
| |
| |
| |
| |
|
|
| def euler_to_rotation_matrix(euler_angles): |
| """Convert Euler angles [roll, pitch, yaw] in degrees to rotation matrix (ZYX order)""" |
| roll, pitch, yaw = np.radians(euler_angles) |
|
|
| |
| Rx = np.array([ |
| [1, 0, 0], |
| [0, np.cos(roll), -np.sin(roll)], |
| [0, np.sin(roll), np.cos(roll)] |
| ]) |
|
|
| |
| Ry = np.array([ |
| [np.cos(pitch), 0, np.sin(pitch)], |
| [0, 1, 0], |
| [-np.sin(pitch), 0, np.cos(pitch)] |
| ]) |
|
|
| |
| Rz = np.array([ |
| [np.cos(yaw), -np.sin(yaw), 0], |
| [np.sin(yaw), np.cos(yaw), 0], |
| [0, 0, 1] |
| ]) |
|
|
| |
| R = Rz @ Ry @ Rx |
| return R |
|
|
| |
| if len(rot1) >= 3: |
| |
| R1 = euler_to_rotation_matrix([rot1[0], rot1[1], rot1[2]]) |
| elif len(rot1) == 9: |
| |
| R1 = rot1.reshape(3, 3) |
| else: |
| |
| R1 = euler_to_rotation_matrix([0, 0, rot1[2] if len(rot1) > 2 else 0]) |
|
|
| if len(rot2) >= 3: |
| R2 = euler_to_rotation_matrix([rot2[0], rot2[1], rot2[2]]) |
| elif len(rot2) == 9: |
| R2 = rot2.reshape(3, 3) |
| else: |
| R2 = euler_to_rotation_matrix([0, 0, rot2[2] if len(rot2) > 2 else 0]) |
|
|
| |
| |
| |
| forward1 = R1[:, 2] |
| forward2 = R2[:, 2] |
|
|
| |
| vec_1_to_2 = pos2 - pos1 |
| vec_1_to_2_norm = np.linalg.norm(vec_1_to_2) |
| vec_1_to_2_unit = vec_1_to_2 / (vec_1_to_2_norm + 1e-6) |
|
|
| |
| fov_rad = np.radians(fov_degrees) |
| fov_half_cos = np.cos(fov_rad / 2) |
|
|
| |
| |
| |
| dot1 = np.dot(forward1, vec_1_to_2_unit) |
| can_1_see_2 = dot1 > fov_half_cos |
|
|
| |
| dot2 = np.dot(forward2, -vec_1_to_2_unit) |
| can_2_see_1 = dot2 > fov_half_cos |
|
|
| |
| |
| normalized_distance = min(1.0, distance / max_distance) if max_distance > 0 else 1.0 |
|
|
| if can_1_see_2 and can_2_see_1: |
| |
| |
| distance_factor = 1.0 - normalized_distance * 0.5 |
| overlap = 0.8 + 0.2 * distance_factor |
| elif can_1_see_2 or can_2_see_1: |
| |
| distance_factor = 1.0 - normalized_distance * 0.6 |
| overlap = 0.4 + 0.3 * distance_factor |
| else: |
| |
| |
| forward_similarity = np.dot(forward1, forward2) |
| if forward_similarity > 0.7: |
| |
| distance_factor = 1.0 - normalized_distance * 0.7 |
| overlap = 0.2 + 0.3 * distance_factor |
| elif forward_similarity > 0.0: |
| |
| distance_factor = 1.0 - normalized_distance * 0.8 |
| overlap = 0.05 + 0.15 * distance_factor * forward_similarity |
| else: |
| |
| |
| overlap = max(0.0, 0.01 - normalized_distance * 0.01) |
|
|
| |
| if distance_exceeds_max: |
| |
| distance_penalty = min(0.5, (distance - max_distance) / max_distance * 0.3) |
| overlap = overlap * (1.0 - distance_penalty) |
|
|
| return np.clip(overlap, 0.0, 1.0) |
|
|
|
|
| |
| def compute_fov_overlap_simple( |
| pose1: Dict, |
| pose2: Dict, |
| fov_degrees: float = 52.67, |
| max_distance: float = 50.0 |
| ) -> float: |
| """ |
| Simplified FOV overlap computation (fallback). |
| Use compute_fov_overlap_3d for more accurate results. |
| """ |
| return compute_fov_overlap_3d(pose1, pose2, fov_degrees, max_distance) |
|
|
|
|
| class FOVMemoryRetriever: |
| """ |
| FOV-based Memory Retriever for Context-as-Memory. |
| |
| Retrieves relevant historical frames based on FOV overlap with current frame. |
| """ |
|
|
| def __init__( |
| self, |
| dataset_base_path: str, |
| fov_degrees: float = 52.67, |
| max_distance: float = 50.0, |
| use_precomputed_overlaps: bool = True |
| ): |
| """ |
| Initialize FOV Memory Retriever. |
| |
| Args: |
| dataset_base_path: Base path to Context-as-Memory dataset |
| fov_degrees: Field of view in degrees (default from paper: 52.67) |
| max_distance: Maximum distance to consider for overlap (meters) |
| use_precomputed_overlaps: Whether to use precomputed overlap_labels if available |
| """ |
| self.dataset_base_path = dataset_base_path |
| self.fov_degrees = fov_degrees |
| self.max_distance = max_distance |
| self.use_precomputed_overlaps = use_precomputed_overlaps |
|
|
| self.jsons_dir = os.path.join(dataset_base_path, 'jsons') |
| self.overlap_labels_dir = os.path.join(dataset_base_path, 'overlap_labels') |
|
|
| |
| self._pose_cache: Dict[str, Dict] = {} |
|
|
| def retrieve_frames( |
| self, |
| video_name: str, |
| current_frame_idx: int, |
| candidate_frame_indices: List[int], |
| top_k: int = 5, |
| include_last_frame: bool = True, |
| use_relative_poses: bool = False |
| ) -> List[int]: |
| """ |
| Retrieve top-k most relevant frames based on FOV overlap. |
| |
| According to Context-as-Memory, for temporal coherence, we should: |
| 1. Always include the last frame (current_frame_idx - 1) as short-term memory |
| 2. Retrieve top-(k-1) frames from history as long-term memory |
| |
| Args: |
| video_name: Name of the video |
| current_frame_idx: Index of current frame to generate |
| candidate_frame_indices: List of candidate frame indices to consider |
| top_k: Number of frames to retrieve |
| include_last_frame: Whether to force include the last frame (default: True, per Context-as-Memory) |
| use_relative_poses: Whether to use RT relative conversion (experiment 1_4_2, aligned with paper) |
| |
| Returns: |
| List of top-k frame indices sorted by relevance (last frame first if included) |
| """ |
| if not candidate_frame_indices: |
| return [] |
|
|
| retrieved_frames = [] |
|
|
| |
| if include_last_frame and current_frame_idx > 0: |
| last_frame_idx = current_frame_idx - 1 |
| if last_frame_idx in candidate_frame_indices: |
| retrieved_frames.append(last_frame_idx) |
| |
| candidate_frame_indices = [idx for idx in candidate_frame_indices if idx != last_frame_idx] |
|
|
| |
| remaining_k = top_k - len(retrieved_frames) |
| if remaining_k <= 0: |
| return retrieved_frames[:top_k] |
|
|
| |
| |
| if self.use_precomputed_overlaps: |
| overlap_frames = load_overlap_frames( |
| self.overlap_labels_dir, |
| video_name, |
| current_frame_idx |
| ) |
|
|
| |
| overlap_frames = [f for f in overlap_frames |
| if f in candidate_frame_indices and f not in retrieved_frames] |
|
|
| if overlap_frames: |
| |
| retrieved_frames.extend(overlap_frames[:remaining_k]) |
| return retrieved_frames[:top_k] |
|
|
| |
| current_pose = self._load_pose(video_name, current_frame_idx) |
| if current_pose is None: |
| |
| retrieved_frames.extend(candidate_frame_indices[:remaining_k]) |
| return retrieved_frames[:top_k] |
|
|
| |
| if use_relative_poses: |
| |
| ref_rt = pose_to_rt(current_pose) |
| if ref_rt is None: |
| |
| use_relative_poses = False |
|
|
| |
| overlap_scores = [] |
| for candidate_idx in candidate_frame_indices: |
| if candidate_idx in retrieved_frames: |
| continue |
|
|
| candidate_pose = self._load_pose(video_name, candidate_idx) |
| if candidate_pose is None: |
| continue |
|
|
| |
| if use_relative_poses and ref_rt is not None: |
| |
| candidate_rt = pose_to_rt(candidate_pose) |
| if candidate_rt is not None: |
| |
| relative_rt_list = convert_rt_to_relative([candidate_rt], ref_rt) |
| if relative_rt_list: |
| |
| relative_pose = rt_to_pose(relative_rt_list[0]) |
| if relative_pose is not None: |
| |
| |
| ref_relative_pose = {'position': [0, 0, 0], 'rotation': [0, 0, 0]} |
| score = compute_fov_overlap_3d( |
| ref_relative_pose, |
| relative_pose, |
| self.fov_degrees, |
| self.max_distance |
| ) |
| overlap_scores.append((candidate_idx, score)) |
| continue |
|
|
| |
| score = compute_fov_overlap_3d( |
| current_pose, |
| candidate_pose, |
| self.fov_degrees, |
| self.max_distance |
| ) |
| overlap_scores.append((candidate_idx, score)) |
|
|
| |
| overlap_scores.sort(key=lambda x: x[1], reverse=True) |
| retrieved_frames.extend([idx for idx, _ in overlap_scores[:remaining_k]]) |
|
|
| return retrieved_frames[:top_k] |
|
|
| def _load_pose(self, video_name: str, frame_idx: int) -> Optional[Dict]: |
| """Load and cache camera pose.""" |
| cache_key = f"{video_name}_{frame_idx}" |
| if cache_key in self._pose_cache: |
| return self._pose_cache[cache_key] |
|
|
| json_file = os.path.join(self.jsons_dir, f"{video_name}.json") |
| pose = load_camera_pose(json_file, frame_idx) |
|
|
| if pose is not None: |
| self._pose_cache[cache_key] = pose |
|
|
| return pose |
|
|
| def clear_cache(self): |
| """Clear pose cache.""" |
| self._pose_cache.clear() |
|
|
|
|
| def create_fov_retriever(dataset_base_path: str) -> Optional[FOVMemoryRetriever]: |
| """ |
| Create FOV retriever if dataset has camera pose information. |
| |
| Args: |
| dataset_base_path: Base path to dataset |
| |
| Returns: |
| FOVMemoryRetriever instance or None if dataset doesn't support it |
| """ |
| jsons_dir = os.path.join(dataset_base_path, 'jsons') |
| if not os.path.exists(jsons_dir): |
| return None |
|
|
| return FOVMemoryRetriever(dataset_base_path) |
|
|
|
|
| |
| def _load_frame_png(frame_file: str) -> Optional[Image.Image]: |
| """Load a single frame from PNG file.""" |
| if os.path.exists(frame_file): |
| try: |
| return Image.open(frame_file).convert('RGB') |
| except Exception: |
| pass |
| return None |
|
|
|
|
|
|
| def retrieve_simple_context_frames( |
| data: Dict, |
| dataset_base_path: str, |
| top_k: int = 4, |
| drop_overlap_probability: float = 0.1, |
| use_rt_relative: bool = False, |
| ) -> Tuple[List[Image.Image], List, List[int], int, str, str]: |
| """ |
| Retrieve context frames according to the Context-as-Memory paper [2506.03141]. |
| |
| Data Structure (Precomputed Retrieval Results): |
| - Each JSON file: overlap_labels/{video_name}/{frame_index}.json |
| - JSON structure: { |
| "frame_index": "0", # First Frame (short-term memory) |
| "overlapping_frames": ["2796", "2797", ..., "3839", ...] # Long-term memory (sample 4) |
| } |
| - frame_index and the next 80 frames constitute GT (target frames to generate) |
| - Iterating through all JSON files = one epoch |
| |
| Design principles: |
| 1. First Frame (frame_index from JSON) as Immediate Condition: |
| - The frame_index in JSON is the First Frame (short-term memory) |
| - Provides immediate visual and temporal starting point (Image-to-Video mode) |
| - Always included as context |
| - GT: frame_index and the next 80 frames (81 frames total) |
| |
| 2. Overlap Frames (from overlapping_frames in JSON) as Long-term Memory: |
| - Retrieved from precomputed overlap_labels JSON files |
| - Precomputed lists may be very long (e.g., [2796, 3839, 4183, ..., 6339]) |
| - Random uniform sampling: sample top_k (4) frames from overlapping_frames list |
| - Provides long-term consistency information |
| - Memory frames are unordered snapshots (no temporal sequence) |
| |
| 3. Context Composition: |
| - Order: [First Frame, Overlap Frame 1, Overlap Frame 2, Overlap Frame 3, Overlap Frame 4] |
| - Total: 1 First Frame + top_k Overlap Frames = top_k + 1 frames (e.g., 5 frames) |
| - Context frames are concatenated with target frames in temporal dimension |
| |
| 4. 10% Probability Drop Strategy: |
| - With 10% probability, drop all Overlap Frames, only use First Frame |
| - Simulates video generation starting stage (no historical memory) |
| - Forces model to generate reasonable videos without long-term memory assistance |
| |
| 5. Epoch Definition: |
| - One epoch = iterate through all JSON files in overlap_labels/{video_name}/ |
| - Each JSON file = one training sample |
| |
| 6. Positional Encoding Note: |
| - Memory frames should NOT use original absolute time positions |
| - They should be treated as unordered image collection or use memory ID encoding only |
| - First Frame should use explicit "Ref Frame" encoding |
| |
| Args: |
| data: Training data dict containing video frames and metadata |
| dataset_base_path: Base path to Context-as-Memory dataset |
| top_k: Number of overlap frames to retrieve (default: 4). First Frame will be added automatically. |
| drop_overlap_probability: Probability to drop overlap frames (default: 0.1 = 10%) |
| |
| Returns: |
| Tuple of: |
| (context_frames, context_actions, context_indices, current_frame_idx, video_name, source) |
| """ |
| video_frames = data.get("video", []) |
| |
| start_frame = data.get("start_frame", 0) |
| end_frame = data.get("end_frame", None) |
|
|
| |
| |
| if "frame_idx" in data: |
| current_frame_idx = data.get("frame_idx") |
| else: |
| |
| if end_frame is not None: |
| current_frame_idx = (start_frame + end_frame) // 2 |
| else: |
| |
| if len(video_frames) > 0: |
| current_frame_idx = len(video_frames) // 2 |
| else: |
| current_frame_idx = 0 |
|
|
| |
| first_frame_idx = start_frame |
|
|
| video_name = data.get("video_name", "") |
| context_frames: List[Image.Image] = [] |
| context_actions: List = [] |
| context_indices: List[int] = [] |
| source = "none" |
|
|
| |
| if not isinstance(video_frames, list): |
| video_frames = [] |
|
|
| if not video_name: |
| |
| if "video_path" in data: |
| video_name = os.path.basename(data["video_path"]).replace(".mp4", "").replace(".avi", "") |
| elif "file_path" in data: |
| video_name = os.path.basename(data["file_path"]).replace(".mp4", "").replace(".avi", "") |
|
|
| |
| |
| |
| |
| frames_dir = os.path.join(dataset_base_path, 'frames', video_name) |
| first_frame_loaded = False |
|
|
| |
| json_file = os.path.join(dataset_base_path, "jsons", f"{video_name}.json") |
| poses_dict = load_poses_dict(json_file) |
| first_frame_pose_rt = None |
| first_frame_pose = poses_dict.get(str(first_frame_idx)) |
| if first_frame_pose is not None: |
| first_frame_pose_rt = pose_to_rt(first_frame_pose) |
| if first_frame_pose_rt is not None: |
| |
| |
| |
| if use_rt_relative: |
| |
| context_actions.append([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]) |
| else: |
| context_actions.append(first_frame_pose_rt) |
| else: |
| context_actions.append([0.0] * 12) |
| else: |
| context_actions.append([0.0] * 12) |
|
|
| if os.path.exists(frames_dir): |
| first_frame_file = os.path.join(frames_dir, f"{first_frame_idx:04d}.png") |
| if os.path.exists(first_frame_file): |
| try: |
| first_frame = Image.open(first_frame_file).convert('RGB') |
| context_frames.append(first_frame) |
| context_indices.append(first_frame_idx) |
| first_frame_loaded = True |
| except Exception as e: |
| pass |
|
|
| |
| if not first_frame_loaded and len(video_frames) > 0: |
| if isinstance(video_frames[0], Image.Image): |
| context_frames.append(video_frames[0]) |
| context_indices.append(start_frame) |
| first_frame_loaded = True |
| elif isinstance(video_frames[0], str): |
| try: |
| first_frame = Image.open(video_frames[0]).convert('RGB') |
| context_frames.append(first_frame) |
| context_indices.append(start_frame) |
| first_frame_loaded = True |
| except Exception as e: |
| pass |
|
|
| |
| |
| |
| |
| |
| |
| |
|
|
| drop_overlap = random.random() < drop_overlap_probability |
|
|
| if not drop_overlap: |
| |
| overlap_labels_dir = os.path.join(dataset_base_path, 'overlap_labels') |
| if os.path.exists(overlap_labels_dir): |
| |
| |
| overlapping_indices = load_overlap_frames( |
| overlap_labels_dir, |
| video_name, |
| current_frame_idx |
| ) |
| |
| |
| |
| overlapping_indices = [idx for idx in overlapping_indices |
| if idx != first_frame_idx] |
|
|
| |
| |
| |
| |
| |
| if overlapping_indices: |
| |
| |
| if use_rt_relative: |
| ref_pose = poses_dict.get(str(current_frame_idx)) |
|
|
| if ref_pose is not None: |
| |
| ref_rt = pose_to_rt(ref_pose) |
|
|
| if ref_rt is not None: |
| |
| candidate_scores = [] |
| for candidate_idx in overlapping_indices: |
| candidate_pose = poses_dict.get(str(candidate_idx)) |
| if candidate_pose is None: |
| continue |
|
|
| |
| candidate_rt = pose_to_rt(candidate_pose) |
| if candidate_rt is not None: |
| relative_rt_list = convert_rt_to_relative([candidate_rt], ref_rt) |
| if relative_rt_list: |
| relative_pose = rt_to_pose(relative_rt_list[0]) |
| if relative_pose is not None: |
| |
| ref_relative_pose = {'position': [0, 0, 0], 'rotation': [0, 0, 0]} |
| score = compute_fov_overlap_3d( |
| ref_relative_pose, |
| relative_pose, |
| fov_degrees=52.67, |
| max_distance=500.0 |
| ) |
| candidate_scores.append((candidate_idx, score)) |
|
|
| |
| if candidate_scores: |
| candidate_scores.sort(key=lambda x: x[1], reverse=True) |
| sampled_overlap_indices = [idx for idx, _ in candidate_scores[:top_k]] |
| else: |
| |
| num_overlap_frames = max(1, top_k) |
| sampled_overlap_indices = random.sample( |
| overlapping_indices, |
| min(len(overlapping_indices), num_overlap_frames) |
| ) |
| else: |
| |
| num_overlap_frames = max(1, top_k) |
| sampled_overlap_indices = random.sample( |
| overlapping_indices, |
| min(len(overlapping_indices), num_overlap_frames) |
| ) |
| else: |
| |
| num_overlap_frames = max(1, top_k) |
| sampled_overlap_indices = random.sample( |
| overlapping_indices, |
| min(len(overlapping_indices), num_overlap_frames) |
| ) |
| else: |
| |
| |
| |
| |
| num_overlap_frames = max(1, top_k) |
|
|
| |
| |
| |
| |
| sampled_overlap_indices = random.sample( |
| overlapping_indices, |
| min(len(overlapping_indices), num_overlap_frames) |
| ) |
|
|
| |
| |
| |
| ref_pose_for_rt = first_frame_pose_rt if use_rt_relative else None |
|
|
| if os.path.exists(frames_dir): |
| to_load = [(idx, os.path.join(frames_dir, f"{idx:04d}.png")) for idx in sampled_overlap_indices[:top_k]] |
| frames_loaded = {} |
| with ThreadPoolExecutor(max_workers=max(1, min(5, len(to_load)))) as ex: |
| futures = {ex.submit(_load_frame_png, fp): idx for idx, fp in to_load} |
| for fut in as_completed(futures): |
| frame_idx = futures[fut] |
| frame = fut.result() |
| if frame is not None: |
| frames_loaded[frame_idx] = frame |
| for frame_idx in sampled_overlap_indices[:top_k]: |
| if frame_idx not in frames_loaded: |
| continue |
| frame = frames_loaded[frame_idx] |
| context_frames.append(frame) |
| context_indices.append(frame_idx) |
| pose = poses_dict.get(str(frame_idx)) |
| if pose is not None: |
| rt_pose = pose_to_rt(pose) |
| if rt_pose is not None: |
| if use_rt_relative and ref_pose_for_rt is not None: |
| relative_rt_list = convert_rt_to_relative([rt_pose], ref_pose_for_rt) |
| context_actions.append(relative_rt_list[0] if relative_rt_list else rt_pose) |
| else: |
| context_actions.append(rt_pose) |
| else: |
| context_actions.append([0.0] * 12) |
| else: |
| context_actions.append([0.0] * 12) |
|
|
| source = "overlap_labels_random" |
| else: |
| |
| source = "first_frame_only" |
| else: |
| |
| source = "first_frame_only" |
| else: |
| |
| |
| source = "first_frame_only_dropped" |
| |
| |
| if source not in ["first_frame_only_dropped"] and len(context_frames) < top_k + 1: |
| |
| max_prev_frame = max(1, current_frame_idx - 1) |
| if max_prev_frame > 1: |
| |
| candidate_indices = [idx for idx in range(max_prev_frame) |
| if idx != first_frame_idx and idx < current_frame_idx] |
| if candidate_indices: |
| num_needed = top_k + 1 - len(context_frames) |
| num_random_frames = min(len(candidate_indices), num_needed) |
| if num_random_frames > 0: |
| sampled_indices = random.sample(candidate_indices, num_random_frames) |
|
|
| |
| if os.path.exists(frames_dir): |
| for frame_idx in sampled_indices: |
| frame_file = os.path.join(frames_dir, f"{frame_idx:04d}.png") |
| if os.path.exists(frame_file): |
| try: |
| frame = Image.open(frame_file).convert('RGB') |
| context_frames.append(frame) |
| context_indices.append(frame_idx) |
| pose = poses_dict.get(str(frame_idx)) |
| if pose is not None: |
| rt_pose = pose_to_rt(pose) |
| if rt_pose is not None: |
| |
| if use_rt_relative and first_frame_pose_rt is not None: |
| relative_rt_list = convert_rt_to_relative([rt_pose], first_frame_pose_rt) |
| context_actions.append(relative_rt_list[0] if relative_rt_list else rt_pose) |
| else: |
| context_actions.append(rt_pose) |
| else: |
| context_actions.append([0.0] * 12) |
| else: |
| context_actions.append([0.0] * 12) |
| except Exception as e: |
| pass |
|
|
| if source == "first_frame_only": |
| source = "random_fallback" |
|
|
| |
| |
| target_total_frames = top_k + 1 |
| if len(context_frames) < target_total_frames and len(video_frames) > 0: |
| num_needed = target_total_frames - len(context_frames) |
| |
| segment_start_idx = 1 if len(context_frames) > 0 else 0 |
| for i in range(segment_start_idx, min(segment_start_idx + num_needed, len(video_frames))): |
| frame_idx_seg = start_frame + i |
| if isinstance(video_frames[i], Image.Image): |
| context_frames.append(video_frames[i]) |
| context_indices.append(frame_idx_seg) |
| elif isinstance(video_frames[i], str): |
| |
| try: |
| frame = Image.open(video_frames[i]).convert('RGB') |
| context_frames.append(frame) |
| context_indices.append(frame_idx_seg) |
| except: |
| pass |
|
|
| pose = poses_dict.get(str(frame_idx_seg)) |
| if pose is not None: |
| rt_pose = pose_to_rt(pose) |
| if rt_pose is not None: |
| |
| if use_rt_relative and first_frame_pose_rt is not None: |
| relative_rt_list = convert_rt_to_relative([rt_pose], first_frame_pose_rt) |
| context_actions.append(relative_rt_list[0] if relative_rt_list else rt_pose) |
| else: |
| context_actions.append(rt_pose) |
| else: |
| context_actions.append([0.0] * 12) |
| else: |
| context_actions.append([0.0] * 12) |
|
|
| |
| if len(context_frames) >= num_needed and source in ["first_frame_only", "none"]: |
| source = "segment_fallback" |
|
|
| |
| |
| if len(context_frames) < target_total_frames: |
| if context_frames: |
| last_frame = context_frames[-1] |
| last_idx = context_indices[-1] if context_indices else first_frame_idx |
| last_action = context_actions[-1] if context_actions else [0.0] * 12 |
| while len(context_frames) < target_total_frames: |
| context_frames.append(last_frame) |
| context_indices.append(last_idx) |
| context_actions.append(last_action) |
|
|
| |
| context_frames = context_frames[:target_total_frames] |
| if context_indices: |
| context_indices = context_indices[:target_total_frames] |
| if context_actions: |
| context_actions = context_actions[:target_total_frames] |
|
|
| |
| |
| while len(context_actions) < len(context_frames): |
| context_actions.append([0.0] * 12) |
|
|
| return context_frames, context_actions, context_indices, current_frame_idx, video_name, source |
|
|
|
|
| def retrieve_fov_context_frames( |
| data: Dict, |
| dataset_base_path: str, |
| fov_retriever=None, |
| top_k: int = 4, |
| use_precomputed_overlaps: bool = True, |
| use_rt_relative: bool = False, |
| strict_overlap_labels: bool = False, |
| allow_realtime_fallback: bool = True, |
| allow_segment_fallback: bool = True, |
| drop_overlap_probability: float = 0.1, |
| ): |
| """ |
| Backward-compatible wrapper. |
| We use FOV overlap scoring to select top-k overlap frames (as in Context-as-Memory). |
| |
| According to Context-as-Memory [2506.03141]: |
| - First Frame (current segment's first frame) is always included as immediate condition |
| - Overlap Frames are retrieved as long-term memory |
| - With 10% probability, drop overlap frames to simulate starting stage |
| |
| Experiment 1_4_2: Uses RT relative conversion for better geometric consistency. |
| """ |
| context_frames, context_actions, context_indices, cur_idx, video_name, source = retrieve_simple_context_frames( |
| data=data, |
| dataset_base_path=dataset_base_path, |
| top_k=top_k, |
| use_rt_relative=use_rt_relative, |
| drop_overlap_probability=drop_overlap_probability, |
| ) |
| |
| target_total_frames = top_k + 1 |
| if strict_overlap_labels and len(context_frames) < target_total_frames: |
| return [], [], [], cur_idx, video_name, "overlap_labels_insufficient" |
| return context_frames, context_actions, context_indices, cur_idx, video_name, source |
|
|
|
|
| def save_sampling_jsonl( |
| output_path: str, |
| video_name: str, |
| frame_index: int, |
| context_indices: List[int], |
| prompt: Optional[str] = None, |
| start_frame: Optional[int] = None, |
| end_frame: Optional[int] = None, |
| source: Optional[str] = None, |
| append: bool = True, |
| ) -> None: |
| """ |
| Save context sampling result to JSONL file for eval consistency. |
| |
| Format: |
| { |
| "video_name": "AncientTempleEnv_0", |
| "frame_index": 0, # First Frame (short-term memory) |
| "context_indices": [0, 2796, 3839, 4183, 6339], # First Frame + 4 Overlap Frames |
| "prompt": "...", # Optional |
| "start_frame": 0, # Optional: GT segment start |
| "end_frame": 80, # Optional: GT segment end |
| "source": "overlap_labels_random" # Optional: sampling source |
| } |
| |
| Args: |
| output_path: Path to JSONL file |
| video_name: Video name |
| frame_index: First Frame index (from JSON file) |
| context_indices: List of context frame indices [first_frame, overlap1, overlap2, ...] |
| prompt: Optional prompt text |
| start_frame: Optional GT segment start frame |
| end_frame: Optional GT segment end frame |
| source: Optional sampling source |
| append: Whether to append to existing file (default: True) |
| """ |
| os.makedirs(os.path.dirname(output_path), exist_ok=True) |
|
|
| item = { |
| "video_name": video_name, |
| "frame_index": frame_index, |
| "context_indices": context_indices, |
| } |
|
|
| if prompt is not None: |
| item["prompt"] = prompt |
| if start_frame is not None: |
| item["start_frame"] = start_frame |
| if end_frame is not None: |
| item["end_frame"] = end_frame |
| if source is not None: |
| item["source"] = source |
|
|
| mode = "a" if append else "w" |
| with open(output_path, mode, encoding="utf-8") as f: |
| f.write(json.dumps(item, ensure_ascii=False) + "\n") |
|
|
|
|
| def load_sampling_jsonl(jsonl_path: str) -> List[Dict]: |
| """ |
| Load context sampling results from JSONL file. |
| |
| Args: |
| jsonl_path: Path to JSONL file |
| |
| Returns: |
| List of sampling items, each containing: |
| { |
| "video_name": str, |
| "frame_index": int, |
| "context_indices": List[int], |
| "prompt": Optional[str], |
| "start_frame": Optional[int], |
| "end_frame": Optional[int], |
| "source": Optional[str] |
| } |
| """ |
| if not os.path.exists(jsonl_path): |
| return [] |
|
|
| items = [] |
| with open(jsonl_path, "r", encoding="utf-8") as f: |
| for line in f: |
| line = line.strip() |
| if not line: |
| continue |
| try: |
| item = json.loads(line) |
| items.append(item) |
| except json.JSONDecodeError as e: |
| print(f"Warning: Failed to parse JSONL line: {e}") |
| continue |
|
|
| return items |
|
|
|
|
| def setup_fov_retriever_for_training( |
| dataset_base_path: str, |
| enable_fov_retrieval: bool = True, |
| ) -> Optional[object]: |
| """ |
| Setup FOV retriever for training (simplified version). |
| Context frames are selected by FOV overlap scoring from precomputed `overlap_labels`. |
| """ |
| return None |
|
|
|
|
| _WARN_ONCE_KEYS = set() |
|
|
|
|
| def _warn_once(key: str, msg: str) -> None: |
| if key in _WARN_ONCE_KEYS: |
| return |
| _WARN_ONCE_KEYS.add(key) |
| print(f"[context_retrieval] WARN: {msg}", file=sys.stderr, flush=True) |
|
|
|
|
| def _load_latent(latent_dir: str, video_name: str, frame_idx: int): |
| """Load a single-frame latent. Expects latent_dir/video_name/{frame_idx:04d}.pt or .pt with key 'latent' or raw tensor.""" |
| base = os.path.join(latent_dir, video_name) |
| for fmt in (f"{frame_idx:04d}.pt", f"{frame_idx}.pt"): |
| path = os.path.join(base, fmt) |
| if os.path.isfile(path): |
| try: |
| try: |
| x = torch.load(path, map_location="cpu", weights_only=True) |
| except TypeError: |
| x = torch.load(path, map_location="cpu") |
| if isinstance(x, dict) and "latent" in x: |
| z = x["latent"] |
| else: |
| z = x |
| if hasattr(z, "shape"): |
| |
| return z.flatten() |
| return None |
| except Exception: |
| pass |
| return None |
|
|
|
|
| def latent_sim_rank( |
| video_name: str, |
| first_frame_idx: int, |
| overlapping_indices: List[int], |
| dataset_base_path: str, |
| top_k: int, |
| latent_dir: Optional[str] = None, |
| use_cosine: bool = True, |
| ) -> List[int]: |
| """ |
| Rank overlapping frame indices by latent similarity to the first (reference) frame. |
| If latent_dir is None or latents are missing, falls back to random sample. |
| Expects per-frame latents under latent_dir/video_name/{frame_idx:04d}.pt (or {frame_idx}.pt). |
| """ |
| if not overlapping_indices or top_k <= 0: |
| return [] |
| if latent_dir is None or not os.path.isdir(latent_dir): |
| return random.sample(overlapping_indices, min(top_k, len(overlapping_indices))) |
|
|
| ref = _load_latent(latent_dir, video_name, first_frame_idx) |
| if ref is None: |
| return random.sample(overlapping_indices, min(top_k, len(overlapping_indices))) |
|
|
| ref = ref.float().unsqueeze(0) |
| scores = [] |
| for idx in overlapping_indices: |
| cand = _load_latent(latent_dir, video_name, idx) |
| if cand is None: |
| continue |
| cand = cand.float().unsqueeze(0) |
| if use_cosine: |
| sim = torch.nn.functional.cosine_similarity(ref, cand, dim=1).item() |
| else: |
| sim = -((ref - cand) ** 2).sum().item() |
| scores.append((idx, sim)) |
| if not scores: |
| return random.sample(overlapping_indices, min(top_k, len(overlapping_indices))) |
| scores.sort(key=lambda x: x[1], reverse=True) |
| return [idx for idx, _ in scores[:top_k]] |
|
|
|
|
| def retrieve_context_frames_advanced( |
| data: Dict, |
| dataset_base_path: str, |
| top_k: int = 4, |
| drop_overlap_probability: float = 0.1, |
| use_rt_relative: bool = False, |
| retrieval_method: str = "fov", |
| latent_retrieval_dir: Optional[str] = None, |
| strict_overlap_labels: bool = False, |
| ) -> Tuple[List, List, List[int], int, str, str]: |
| """ |
| Retrieve context frames with pluggable retrieval method. |
| Interface matches retrieve_fov_context_frames return: |
| (context_frames, context_actions, context_indices, cur_idx, video_name, source). |
| |
| retrieval_method: |
| - "fov": use existing FOV/overlap_labels logic (random or RT-scored from overlap). |
| - "latent_sim": rank overlap candidates by latent similarity to first frame (requires latent_retrieval_dir). |
| When latent_sim is used but latent_retrieval_dir is missing or latents absent, falls back to FOV behavior. |
| """ |
| if retrieval_method == "latent_sim" and not latent_retrieval_dir: |
| _warn_once( |
| "latent_sim_missing_dir", |
| "retrieval_method=latent_sim but latent_retrieval_dir is not set; fallback to FOV retrieval.", |
| ) |
| if retrieval_method == "fov" or (retrieval_method == "latent_sim" and not latent_retrieval_dir): |
| return retrieve_simple_context_frames( |
| data=data, |
| dataset_base_path=dataset_base_path, |
| top_k=top_k, |
| drop_overlap_probability=drop_overlap_probability, |
| use_rt_relative=use_rt_relative, |
| ) |
| if retrieval_method == "latent_sim" and latent_retrieval_dir and not os.path.isdir(latent_retrieval_dir): |
| _warn_once( |
| "latent_sim_bad_dir", |
| f"latent_retrieval_dir not found: {latent_retrieval_dir}; latent_sim will degrade to random-overlap selection.", |
| ) |
|
|
| |
| |
| |
| |
|
|
| video_frames = data.get("video", []) |
| start_frame = data.get("start_frame", 0) |
| end_frame = data.get("end_frame", None) |
| if "frame_idx" in data: |
| current_frame_idx = data.get("frame_idx") |
| else: |
| current_frame_idx = (start_frame + end_frame) // 2 if end_frame is not None else (len(video_frames) // 2 if video_frames else 0) |
| first_frame_idx = start_frame |
| video_name = data.get("video_name", "") |
| if not video_name and "video_path" in data: |
| video_name = os.path.basename(data["video_path"]).replace(".mp4", "").replace(".avi", "") |
| elif not video_name and "file_path" in data: |
| video_name = os.path.basename(data["file_path"]).replace(".mp4", "").replace(".avi", "") |
|
|
| frames_dir = os.path.join(dataset_base_path, "frames", video_name) |
| json_file = os.path.join(dataset_base_path, "jsons", f"{video_name}.json") |
| poses_dict = load_poses_dict(json_file) if os.path.isfile(json_file) else {} |
| first_frame_pose = poses_dict.get(str(first_frame_idx)) |
| first_frame_pose_rt = pose_to_rt(first_frame_pose) if first_frame_pose is not None and pose_to_rt else None |
|
|
| context_frames: List[Image.Image] = [] |
| context_actions: List = [] |
| context_indices: List[int] = [] |
| source = "none" |
|
|
| def _append_pose(frame_idx: int): |
| pose = poses_dict.get(str(frame_idx)) |
| if pose is not None and pose_to_rt and use_rt_relative and first_frame_pose_rt is not None: |
| rt = pose_to_rt(pose) |
| if rt is not None and convert_rt_to_relative: |
| rel = convert_rt_to_relative([rt], first_frame_pose_rt) |
| context_actions.append(rel[0] if rel else [0.0] * 12) |
| else: |
| context_actions.append([0.0] * 12) |
| elif pose is not None and pose_to_rt: |
| rt = pose_to_rt(pose) |
| context_actions.append(rt if rt is not None else [0.0] * 12) |
| else: |
| context_actions.append([0.0] * 12) |
|
|
| if first_frame_pose_rt is not None and use_rt_relative: |
| context_actions.append([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]) |
| else: |
| context_actions.append(first_frame_pose_rt if first_frame_pose_rt is not None else [0.0] * 12) |
|
|
| if os.path.isdir(frames_dir): |
| first_path = os.path.join(frames_dir, f"{first_frame_idx:04d}.png") |
| if os.path.isfile(first_path): |
| try: |
| context_frames.append(Image.open(first_path).convert("RGB")) |
| context_indices.append(first_frame_idx) |
| except Exception: |
| pass |
| if not context_frames and video_frames: |
| if isinstance(video_frames[0], Image.Image): |
| context_frames.append(video_frames[0]) |
| context_indices.append(start_frame) |
| elif isinstance(video_frames[0], str) and os.path.isfile(video_frames[0]): |
| try: |
| context_frames.append(Image.open(video_frames[0]).convert("RGB")) |
| context_indices.append(start_frame) |
| except Exception: |
| pass |
|
|
| drop_overlap = random.random() < drop_overlap_probability |
| if drop_overlap: |
| target_total = top_k + 1 |
| if strict_overlap_labels and len(context_frames) < target_total: |
| return [], [], [], current_frame_idx, video_name, "overlap_labels_insufficient" |
| return context_frames, context_actions, context_indices, current_frame_idx, video_name, "first_frame_only_dropped" |
|
|
| overlap_labels_dir = os.path.join(dataset_base_path, "overlap_labels") |
| overlapping_indices = [] |
| if os.path.isdir(overlap_labels_dir): |
| overlapping_indices = load_overlap_frames(overlap_labels_dir, video_name, current_frame_idx) |
| overlapping_indices = [i for i in overlapping_indices if i != first_frame_idx] |
|
|
| if not overlapping_indices: |
| source = "first_frame_only" |
| if strict_overlap_labels and len(context_frames) < top_k + 1: |
| return [], [], [], current_frame_idx, video_name, "overlap_labels_insufficient" |
| return context_frames, context_actions, context_indices, current_frame_idx, video_name, source |
|
|
| sampled_overlap_indices = latent_sim_rank( |
| video_name, first_frame_idx, overlapping_indices, dataset_base_path, top_k, |
| latent_dir=latent_retrieval_dir, use_cosine=True, |
| ) |
|
|
| def _load_frame(path: str): |
| if os.path.isfile(path): |
| try: |
| return Image.open(path).convert("RGB") |
| except Exception: |
| pass |
| return None |
|
|
| to_load = [(idx, os.path.join(frames_dir, f"{idx:04d}.png")) for idx in sampled_overlap_indices[:top_k]] |
| with ThreadPoolExecutor(max_workers=max(1, min(5, len(to_load)))) as ex: |
| futures = {ex.submit(_load_frame, path): idx for idx, path in to_load} |
| for fut in futures: |
| idx = futures[fut] |
| frame = fut.result() |
| if frame is not None: |
| context_frames.append(frame) |
| context_indices.append(idx) |
| _append_pose(idx) |
|
|
| source = "latent_sim" |
| if strict_overlap_labels and len(context_frames) < top_k + 1: |
| return [], [], [], current_frame_idx, video_name, "overlap_labels_insufficient" |
| return context_frames, context_actions, context_indices, current_frame_idx, video_name, source |
|
|