Spaces:
Running
Running
| import os | |
| import glob | |
| from dataclasses import dataclass | |
| from typing import List, Dict, Any, Iterator, Optional, Tuple | |
| import cv2 | |
| import numpy as np | |
| import torch | |
| from PIL import Image | |
| from horizonstream.data.undistort_utils import colmap_undistort_numpy | |
| from horizonstream.utils.vendor.dust3r.utils.image import load_images_for_eval | |
| dataset_metadata: Dict[str, Dict[str, Any]] = { | |
| "davis": { | |
| "img_path": "data/davis/DAVIS/JPEGImages/480p", | |
| "mask_path": "data/davis/DAVIS/masked_images/480p", | |
| "dir_path_func": lambda img_path, seq: os.path.join(img_path, seq), | |
| "gt_traj_func": lambda img_path, anno_path, seq: None, | |
| "traj_format": None, | |
| "seq_list": None, | |
| "full_seq": True, | |
| "mask_path_seq_func": lambda mask_path, seq: os.path.join(mask_path, seq), | |
| "skip_condition": None, | |
| "process_func": None, | |
| }, | |
| "kitti": { | |
| "img_path": "data/kitti/sequences", | |
| "anno_path": "data/kitti/poses", | |
| "mask_path": None, | |
| "dir_path_func": lambda img_path, seq: os.path.join(img_path, seq, "image_2"), | |
| "gt_traj_func": lambda img_path, anno_path, seq: os.path.join( | |
| anno_path, f"{seq}.txt" | |
| ) | |
| if os.path.exists(os.path.join(anno_path, f"{seq}.txt")) | |
| else None, | |
| "traj_format": "kitti", | |
| "seq_list": ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10"], | |
| "full_seq": True, | |
| "mask_path_seq_func": lambda mask_path, seq: None, | |
| "skip_condition": None, | |
| "process_func": None, | |
| }, | |
| "bonn": { | |
| "img_path": "data/bonn/rgbd_bonn_dataset", | |
| "mask_path": None, | |
| "dir_path_func": lambda img_path, seq: os.path.join( | |
| img_path, f"rgbd_bonn_{seq}", "rgb_110" | |
| ), | |
| "gt_traj_func": lambda img_path, anno_path, seq: os.path.join( | |
| img_path, f"rgbd_bonn_{seq}", "groundtruth_110.txt" | |
| ), | |
| "traj_format": "tum", | |
| "seq_list": ["balloon2", "crowd2", "crowd3", "person_tracking2", "synchronous"], | |
| "full_seq": False, | |
| "mask_path_seq_func": lambda mask_path, seq: None, | |
| "skip_condition": None, | |
| "process_func": None, | |
| }, | |
| "nyu": { | |
| "img_path": "data/nyu-v2/val/nyu_images", | |
| "mask_path": None, | |
| "process_func": None, | |
| }, | |
| "scannet": { | |
| "img_path": "data/scannetv2", | |
| "mask_path": None, | |
| "dir_path_func": lambda img_path, seq: os.path.join(img_path, seq, "color_90"), | |
| "gt_traj_func": lambda img_path, anno_path, seq: os.path.join( | |
| img_path, seq, "pose_90.txt" | |
| ), | |
| "traj_format": "replica", | |
| "seq_list": None, | |
| "full_seq": True, | |
| "mask_path_seq_func": lambda mask_path, seq: None, | |
| "skip_condition": None, | |
| "process_func": None, | |
| }, | |
| "tum": { | |
| "img_path": "data/tum", | |
| "mask_path": None, | |
| "dir_path_func": lambda img_path, seq: os.path.join(img_path, seq, "rgb_90"), | |
| "gt_traj_func": lambda img_path, anno_path, seq: os.path.join( | |
| img_path, seq, "groundtruth_90.txt" | |
| ), | |
| "traj_format": "tum", | |
| "seq_list": None, | |
| "full_seq": True, | |
| "mask_path_seq_func": lambda mask_path, seq: None, | |
| "skip_condition": None, | |
| "process_func": None, | |
| }, | |
| "sintel": { | |
| "img_path": "data/sintel/training/final", | |
| "anno_path": "data/sintel/training/camdata_left", | |
| "mask_path": None, | |
| "dir_path_func": lambda img_path, seq: os.path.join(img_path, seq), | |
| "gt_traj_func": lambda img_path, anno_path, seq: os.path.join(anno_path, seq), | |
| "traj_format": None, | |
| "seq_list": [ | |
| "alley_2", | |
| "ambush_4", | |
| "ambush_5", | |
| "ambush_6", | |
| "cave_2", | |
| "cave_4", | |
| "market_2", | |
| "market_5", | |
| "market_6", | |
| "shaman_3", | |
| "sleeping_1", | |
| "sleeping_2", | |
| "temple_2", | |
| "temple_3", | |
| ], | |
| "full_seq": False, | |
| "mask_path_seq_func": lambda mask_path, seq: None, | |
| "skip_condition": None, | |
| "process_func": None, | |
| }, | |
| "waymo": { | |
| "img_path": "toyourdata/scatt3r_evaluation/waymo_open_dataset_v1_4_3", | |
| "anno_path": None, | |
| "mask_path": None, | |
| "dir_path_func": lambda img_path, seq: os.path.join( | |
| img_path, | |
| seq.split("_cam")[0] if "_cam" in seq else seq, | |
| "images", | |
| seq.split("_cam")[1] if "_cam" in seq else "00", | |
| ), | |
| "gt_traj_func": lambda img_path, anno_path, seq: os.path.join( | |
| img_path, | |
| seq.split("_cam")[0] if "_cam" in seq else seq, | |
| "cameras", | |
| seq.split("_cam")[1] if "_cam" in seq else "00", | |
| "extri.yml", | |
| ), | |
| "traj_format": "waymo", | |
| "seq_list": None, | |
| "full_seq": True, | |
| "mask_path_seq_func": lambda mask_path, seq: None, | |
| "skip_condition": None, | |
| "process_func": None, | |
| }, | |
| } | |
| class HorizonStreamSequenceInfo: | |
| name: str | |
| scene_root: str | |
| image_dir: str | |
| image_paths: List[str] | |
| camera: Optional[str] | |
| class HorizonStreamSequence: | |
| def __init__( | |
| self, | |
| name: str, | |
| images: torch.Tensor, | |
| image_paths: List[str], | |
| scene_root: Optional[str] = None, | |
| image_dir: Optional[str] = None, | |
| camera: Optional[str] = None, | |
| ): | |
| self.name = name | |
| self.images = images | |
| self.image_paths = image_paths | |
| self.scene_root = scene_root | |
| self.image_dir = image_dir | |
| self.camera = camera | |
| def _read_list_file(path: str) -> List[str]: | |
| with open(path, "r") as f: | |
| lines = [] | |
| for line in f.readlines(): | |
| line = line.strip() | |
| if not line: | |
| continue | |
| if line.startswith("#"): | |
| continue | |
| lines.append(line) | |
| return lines | |
| def _is_generalizable_scene_root(path: str) -> bool: | |
| return os.path.isdir(os.path.join(path, "images")) | |
| def _direct_image_files(dir_path: str) -> List[str]: | |
| filelist = sorted(glob.glob(os.path.join(dir_path, "*.png"))) | |
| if not filelist: | |
| filelist = sorted(glob.glob(os.path.join(dir_path, "*.jpg"))) | |
| if not filelist: | |
| filelist = sorted(glob.glob(os.path.join(dir_path, "*.jpeg"))) | |
| return filelist | |
| def _frame_stems(image_paths: List[str]) -> List[str]: | |
| stems = [os.path.splitext(os.path.basename(p))[0] for p in image_paths] | |
| if len(set(stems)) == len(stems): | |
| return stems | |
| parents = [os.path.basename(os.path.dirname(p)) for p in image_paths] | |
| if len(set(parents)) == len(parents): | |
| return parents | |
| return stems | |
| def _format_fps_for_name(fps: float) -> str: | |
| return f"{float(fps):g}".replace(".", "p") | |
| def _safe_video_scene_name(path: str, stride: int, target_fps: Optional[float]) -> str: | |
| stem = os.path.splitext(os.path.basename(path))[0] | |
| safe = "".join(ch if ch.isalnum() or ch in "_.-" else "_" for ch in stem).strip("._") | |
| if not safe: | |
| safe = "video" | |
| if target_fps is not None: | |
| return f"{safe}_fps{_format_fps_for_name(target_fps)}" | |
| return f"{safe}_stride{int(stride)}" | |
| class HorizonStreamDataLoader: | |
| def __init__(self, cfg: Dict[str, Any]): | |
| self.cfg = cfg | |
| self.dataset = cfg.get("dataset", None) | |
| meta = dataset_metadata.get(self.dataset, {}) | |
| self.img_path = cfg.get("img_path", meta.get("img_path")) | |
| self.mask_path = cfg.get("mask_path", meta.get("mask_path")) | |
| self.dir_path_func = meta.get("dir_path_func", lambda p, s: os.path.join(p, s)) | |
| self.mask_path_seq_func = meta.get("mask_path_seq_func", lambda p, s: None) | |
| self.full_seq = bool(cfg.get("full_seq", meta.get("full_seq", True))) | |
| self.seq_list = cfg.get("seq_list", None) | |
| self.stride = int(cfg.get("stride", 1)) | |
| self.max_frames = cfg.get("max_frames", None) | |
| self.size = int(cfg.get("size", 518)) | |
| self.crop = bool(cfg.get("crop", False)) | |
| self.patch_size = int(cfg.get("patch_size", 14)) | |
| self.format = cfg.get("format", "auto") | |
| self.data_roots_file = cfg.get("data_roots_file", None) | |
| self.split = cfg.get("split", None) | |
| self.camera = cfg.get("camera", None) | |
| self.seq_match_mode = str(cfg.get("seq_match_mode", "exact")).lower() | |
| self.video_path = cfg.get("video_path", None) | |
| self.image_paths_cfg = cfg.get("image_paths", None) | |
| self.image_scene_name = cfg.get("image_scene_name", None) | |
| self.video_stride = max(1, int(cfg.get("video_stride", 1))) | |
| self.video_sample_fps = cfg.get("video_sample_fps", None) | |
| if self.video_sample_fps in ("", "null"): | |
| self.video_sample_fps = None | |
| if self.video_sample_fps is not None: | |
| self.video_sample_fps = float(self.video_sample_fps) | |
| self.video_scene_name = cfg.get("video_scene_name", None) | |
| # camera VAL-aligned preprocess: undistort -> resize/crop with K sync. | |
| self.camera_preprocess = bool(cfg.get("camera_preprocess", True)) | |
| self.camera_preprocess_strict = bool( | |
| cfg.get("camera_preprocess_strict", True) | |
| ) | |
| self.undistort_backend = str( | |
| cfg.get("undistort_backend", "colmap") | |
| ).lower() | |
| self.undistort_dist_opt_k = bool(cfg.get("undistort_dist_opt_k", False)) | |
| self.undistort_safe_bound = int(cfg.get("undistort_safe_bound", 4)) | |
| self.undistort_center_crop = bool(cfg.get("undistort_center_crop", True)) | |
| self._undistort_warned_no_pycolmap = False | |
| def _infer_format(self) -> str: | |
| if self.image_paths_cfg not in (None, "", "null"): | |
| return "image_list" | |
| if self.video_path not in (None, "", "null"): | |
| return "video" | |
| if self.format in ["relpose", "generalizable"]: | |
| return self.format | |
| if self.img_path is None: | |
| return "relpose" | |
| if _is_generalizable_scene_root(self.img_path): | |
| return "generalizable" | |
| default_list = self.data_roots_file or "data_roots.txt" | |
| if os.path.exists(os.path.join(self.img_path, default_list)): | |
| return "generalizable" | |
| return "relpose" | |
| def _resolve_seq_list_generalizable(self) -> List[str]: | |
| if self.seq_list is not None and self.seq_match_mode not in ["contains", "in"]: | |
| return list(self.seq_list) | |
| if self.img_path is None or not os.path.isdir(self.img_path): | |
| return [] | |
| if _is_generalizable_scene_root(self.img_path): | |
| return self._filter_seq_candidates([self.img_path]) | |
| candidates = [] | |
| if isinstance(self.data_roots_file, str) and self.data_roots_file: | |
| candidates.append(self.data_roots_file) | |
| if isinstance(self.split, str) and self.split: | |
| split_name = self.split.lower() | |
| if split_name in ["val", "valid", "validate"]: | |
| split_name = "validate" | |
| candidates.append(f"{split_name}_data_roots.txt") | |
| candidates.append("data_roots.txt") | |
| candidates.append("train_data_roots.txt") | |
| candidates.append("validate_data_roots.txt") | |
| for fname in candidates: | |
| path = os.path.join(self.img_path, fname) | |
| if os.path.exists(path): | |
| return self._filter_seq_candidates(_read_list_file(path)) | |
| img_dirs = sorted( | |
| glob.glob(os.path.join(self.img_path, "**", "images"), recursive=True) | |
| ) | |
| scene_roots = [os.path.dirname(p) for p in img_dirs] | |
| rels = [] | |
| for p in scene_roots: | |
| try: | |
| rels.append(os.path.relpath(p, self.img_path)) | |
| except ValueError: | |
| rels.append(p) | |
| return self._filter_seq_candidates(sorted(set(rels))) | |
| def _resolve_seq_list_relpose(self) -> List[str]: | |
| if self.seq_list is not None and self.seq_match_mode not in ["contains", "in"]: | |
| return list(self.seq_list) | |
| meta = dataset_metadata.get(self.dataset, {}) | |
| if self.full_seq: | |
| if self.img_path is None or not os.path.isdir(self.img_path): | |
| return [] | |
| seqs = [ | |
| s | |
| for s in os.listdir(self.img_path) | |
| if os.path.isdir(os.path.join(self.img_path, s)) | |
| ] | |
| return self._filter_seq_candidates(sorted(seqs)) | |
| seqs = meta.get("seq_list", []) or [] | |
| return self._filter_seq_candidates(list(seqs)) | |
| def _filter_seq_candidates(self, candidates: List[str]) -> List[str]: | |
| if self.seq_list is None or self.seq_match_mode not in ["contains", "in"]: | |
| return candidates | |
| needles = [str(x).strip() for x in self.seq_list if str(x).strip()] | |
| if not needles: | |
| return candidates | |
| matched = [] | |
| for seq in candidates: | |
| seq_text = str(seq) | |
| seq_name = os.path.basename(os.path.normpath(seq_text)) | |
| if any(needle in seq_text or needle in seq_name for needle in needles): | |
| matched.append(seq) | |
| return matched | |
| def _resolve_seq_list(self) -> List[str]: | |
| fmt = self._infer_format() | |
| if fmt == "generalizable": | |
| result = self._resolve_seq_list_generalizable() | |
| else: | |
| result = self._resolve_seq_list_relpose() | |
| if not result: | |
| print(f"[horizonstream] warning: no sequences resolved (img_path={self.img_path}, seq_list={self.seq_list}, seq_match_mode={self.seq_match_mode})", flush=True) | |
| return result | |
| def _sequence_name_from_scene_root(self, seq_entry: str, scene_root: str) -> str: | |
| scene_root_abs = os.path.abspath(scene_root) | |
| if self.img_path is not None: | |
| try: | |
| rel = os.path.relpath(scene_root_abs, os.path.abspath(self.img_path)) | |
| except ValueError: | |
| rel = None | |
| if rel and rel not in [os.curdir, os.pardir] and not rel.startswith(os.pardir + os.path.sep): | |
| return rel | |
| if os.path.isabs(seq_entry): | |
| return os.path.basename(os.path.normpath(seq_entry)) | |
| name = os.path.normpath(seq_entry) | |
| if name in ["", os.curdir, os.pardir] or name.startswith(os.pardir + os.path.sep): | |
| return os.path.basename(os.path.normpath(scene_root)) | |
| return name | |
| def _resolve_scene_root(self, seq_entry: str) -> Tuple[str, str]: | |
| if os.path.isabs(seq_entry): | |
| scene_root = seq_entry | |
| elif os.path.sep in seq_entry: | |
| scene_root = os.path.join(self.img_path, seq_entry) | |
| else: | |
| scene_root = os.path.join(self.img_path, seq_entry) | |
| name = self._sequence_name_from_scene_root(seq_entry, scene_root) | |
| return name, scene_root | |
| def _resolve_image_dir_generalizable(self, scene_root: str) -> Optional[str]: | |
| images_root = os.path.join(scene_root, "images") | |
| if not os.path.isdir(images_root): | |
| return None | |
| if isinstance(self.camera, str) and self.camera: | |
| cam_dir = os.path.join(images_root, self.camera) | |
| if os.path.isdir(cam_dir): | |
| return cam_dir | |
| if _direct_image_files(images_root): | |
| return images_root | |
| cams = [ | |
| d | |
| for d in os.listdir(images_root) | |
| if os.path.isdir(os.path.join(images_root, d)) | |
| ] | |
| if not cams: | |
| return None | |
| cams = sorted(cams) | |
| frame_dirs = [] | |
| for name in cams: | |
| child_dir = os.path.join(images_root, name) | |
| child_images = _direct_image_files(child_dir) | |
| if child_images: | |
| frame_dirs.append((name, len(child_images))) | |
| if ( | |
| len(cams) > 10 | |
| and len(frame_dirs) == len(cams) | |
| and max(count for _, count in frame_dirs) == 1 | |
| ): | |
| return images_root | |
| return os.path.join(images_root, cams[0]) | |
| def _camera_from_image_dir(self, image_dir: str) -> Optional[str]: | |
| parent = os.path.basename(os.path.dirname(image_dir)) | |
| if parent != "images": | |
| return None | |
| return os.path.basename(image_dir) | |
| def _has_multiple_camera_dirs(self, scene_root: str) -> bool: | |
| images_root = os.path.join(scene_root, "images") | |
| if not os.path.isdir(images_root): | |
| return False | |
| camera_dirs = [] | |
| for name in os.listdir(images_root): | |
| child_dir = os.path.join(images_root, name) | |
| if os.path.isdir(child_dir) and _direct_image_files(child_dir): | |
| camera_dirs.append(name) | |
| return len(camera_dirs) > 1 | |
| def _collect_filelist(self, dir_path: str) -> List[str]: | |
| filelist = _direct_image_files(dir_path) | |
| if not filelist: | |
| nested = [] | |
| child_dirs = sorted( | |
| d for d in glob.glob(os.path.join(dir_path, "*")) if os.path.isdir(d) | |
| ) | |
| for child_dir in child_dirs: | |
| child_images = _direct_image_files(child_dir) | |
| if child_images: | |
| nested.append(child_images[0]) | |
| filelist = nested | |
| if self.stride > 1: | |
| filelist = filelist[:: self.stride] | |
| if self.max_frames is not None and self.max_frames > 0: | |
| filelist = filelist[: self.max_frames] | |
| return filelist | |
| def _load_intrinsics_distortion( | |
| self, scene_root: str, camera: Optional[str] | |
| ) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], str]: | |
| intri_candidates = [] | |
| if isinstance(camera, str) and camera: | |
| intri_candidates.append(os.path.join(scene_root, "cameras", camera, "intri.yml")) | |
| intri_candidates.append(os.path.join(scene_root, "intri.yml")) | |
| intri_path = None | |
| for p in intri_candidates: | |
| if os.path.exists(p): | |
| intri_path = p | |
| break | |
| if intri_path is None: | |
| raise FileNotFoundError( | |
| f"intri.yml not found under scene_root={scene_root} camera={camera}" | |
| ) | |
| fs = cv2.FileStorage(intri_path, cv2.FILE_STORAGE_READ) | |
| names_node = fs.getNode("names") | |
| names = [] | |
| if not names_node.empty(): | |
| for i in range(names_node.size()): | |
| names.append(names_node.at(i).string()) | |
| K_map: Dict[str, np.ndarray] = {} | |
| D_map: Dict[str, np.ndarray] = {} | |
| for name in names: | |
| K = fs.getNode(f"K_{name}").mat() | |
| if K is None: | |
| continue | |
| D = fs.getNode(f"D_{name}").mat() | |
| if D is None: | |
| D_arr = np.zeros((5, 1), dtype=np.float64) | |
| else: | |
| D_arr = np.asarray(D, dtype=np.float64).reshape(-1) | |
| if D_arr.size < 5: | |
| D_arr = np.pad(D_arr, (0, 5 - D_arr.size), mode="constant") | |
| elif D_arr.size > 5: | |
| D_arr = D_arr[:5] | |
| D_arr = D_arr.reshape(5, 1) | |
| K_map[name] = np.asarray(K, dtype=np.float64) | |
| D_map[name] = D_arr | |
| fs.release() | |
| if not K_map: | |
| raise ValueError(f"No intrinsics K_* found in {intri_path}") | |
| return K_map, D_map, intri_path | |
| def _undistort_image( | |
| self, | |
| img: np.ndarray, | |
| K: np.ndarray, | |
| D: np.ndarray, | |
| ) -> Tuple[np.ndarray, np.ndarray]: | |
| K = np.asarray(K, dtype=np.float64).copy() | |
| D = np.asarray(D, dtype=np.float64).reshape(-1) | |
| if D.size < 5: | |
| D = np.pad(D, (0, 5 - D.size), mode="constant") | |
| elif D.size > 5: | |
| D = D[:5] | |
| if np.sum(np.abs(D)) == 0.0: | |
| return img, K | |
| backend = self.undistort_backend | |
| if backend == "colmap" and os.name != "nt": | |
| try: | |
| undist, new_K = colmap_undistort_numpy( | |
| img, K, D.reshape(5, 1), blank_pixels=not self.undistort_dist_opt_k | |
| ) | |
| undist = np.rint(undist).clip(0, 255).astype(np.uint8) | |
| return undist, np.asarray(new_K, dtype=np.float64) | |
| except Exception as exc: | |
| if not self._undistort_warned_no_pycolmap: | |
| print( | |
| f"[horizonstream] warning: camera preprocess colmap undistort unavailable ({exc}); fallback to cv2", | |
| flush=True, | |
| ) | |
| self._undistort_warned_no_pycolmap = True | |
| H, W = img.shape[:2] | |
| if self.undistort_dist_opt_k: | |
| new_K, _ = cv2.getOptimalNewCameraMatrix(K, D, (W, H), 0, (W, H)) | |
| undist = cv2.undistort(img, K, D, newCameraMatrix=new_K) | |
| return undist, np.asarray(new_K, dtype=np.float64) | |
| undist = cv2.undistort(img, K, D) | |
| return undist, K | |
| def _resize_crop_with_k( | |
| self, | |
| img: np.ndarray, | |
| K: np.ndarray, | |
| target_h: int, | |
| target_w: int, | |
| ) -> Tuple[np.ndarray, np.ndarray]: | |
| H, W = img.shape[:2] | |
| K = np.asarray(K, dtype=np.float64).copy() | |
| safe_bound = max(int(self.undistort_safe_bound), 0) | |
| ratio = max( | |
| float(target_h + safe_bound) / max(H, 1), | |
| float(target_w + safe_bound) / max(W, 1), | |
| ) | |
| if ratio <= 0: | |
| ratio = 1.0 | |
| h = int(H * ratio) | |
| w = int(W * ratio) | |
| if h != H or w != W: | |
| K[0:1] *= float(w) / float(max(W, 1)) | |
| K[1:2] *= float(h) / float(max(H, 1)) | |
| img = cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA) | |
| else: | |
| h, w = H, W | |
| # Match camera strict-center crop behavior: | |
| # use principal point for crop center and pad with zeros when out-of-bound. | |
| if self.undistort_center_crop: | |
| crop_h = int(round(float(K[1, 2]))) - target_h // 2 | |
| crop_w = int(round(float(K[0, 2]))) - target_w // 2 | |
| else: | |
| crop_h = 0 | |
| crop_w = 0 | |
| end_h = crop_h + target_h | |
| end_w = crop_w + target_w | |
| offset_h = crop_h | |
| offset_w = crop_w | |
| if self.undistort_center_crop and ( | |
| crop_h < 0 or crop_w < 0 or end_h > h or end_w > w | |
| ): | |
| pad_top = max(0, -crop_h) | |
| pad_bottom = max(0, end_h - h) | |
| pad_left = max(0, -crop_w) | |
| pad_right = max(0, end_w - w) | |
| if pad_top or pad_bottom or pad_left or pad_right: | |
| img = cv2.copyMakeBorder( | |
| img, | |
| pad_top, | |
| pad_bottom, | |
| pad_left, | |
| pad_right, | |
| cv2.BORDER_CONSTANT, | |
| value=(0, 0, 0), | |
| ) | |
| crop_h += pad_top | |
| crop_w += pad_left | |
| end_h = crop_h + target_h | |
| end_w = crop_w + target_w | |
| img = img[crop_h:end_h, crop_w:end_w] | |
| K[0, 2] -= offset_w | |
| K[1, 2] -= offset_h | |
| return img, K | |
| def _load_images_preprocessed(self, info: HorizonStreamSequenceInfo) -> torch.Tensor: | |
| K_map, D_map, intri_path = self._load_intrinsics_distortion( | |
| info.scene_root, info.camera | |
| ) | |
| stems = _frame_stems(info.image_paths) | |
| missing = [s for s in stems if s not in K_map] | |
| if missing: | |
| sample = ", ".join(missing[:5]) | |
| msg = ( | |
| f"[horizonstream] preprocess: stem mismatch in {intri_path}, " | |
| f"missing={len(missing)} sample=[{sample}]" | |
| ) | |
| if self.camera_preprocess_strict: | |
| raise KeyError(msg) | |
| print(msg, flush=True) | |
| processed = [] | |
| target_h = None | |
| target_w = None | |
| for i, path in enumerate(info.image_paths): | |
| stem = stems[i] | |
| if stem not in K_map: | |
| if self.camera_preprocess_strict: | |
| raise KeyError(f"Missing K/D for frame stem={stem} path={path}") | |
| continue | |
| K = K_map[stem] | |
| D = D_map.get(stem, np.zeros((5, 1), dtype=np.float64)) | |
| img_bgr = cv2.imread(path, cv2.IMREAD_COLOR) | |
| if img_bgr is None: | |
| raise IOError(f"Failed to read image: {path}") | |
| img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) | |
| img, K = self._undistort_image(img, K, D) | |
| if target_h is None or target_w is None: | |
| H0, W0 = img.shape[:2] | |
| if self.size > 0: | |
| h = int(float(H0) / float(max(W0, 1)) * float(self.size)) | |
| w = int(self.size) | |
| else: | |
| h, w = H0, W0 | |
| target_h = max((h // self.patch_size) * self.patch_size, self.patch_size) | |
| target_w = max((w // self.patch_size) * self.patch_size, self.patch_size) | |
| img, _ = self._resize_crop_with_k(img, K, target_h, target_w) | |
| img_t = torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 | |
| img_t = (img_t - 0.5) / 0.5 | |
| processed.append(img_t[None]) | |
| if not processed: | |
| raise RuntimeError(f"preprocess produced 0 frames for {info.name}") | |
| imgs = torch.cat(processed, dim=0) | |
| images = imgs.unsqueeze(0) | |
| images = (images + 1.0) / 2.0 | |
| return images | |
| def _load_images(self, info: HorizonStreamSequenceInfo) -> torch.Tensor: | |
| if self.camera_preprocess: | |
| try: | |
| return self._load_images_preprocessed(info) | |
| except Exception as exc: | |
| if self.camera_preprocess_strict and not isinstance(exc, FileNotFoundError): | |
| raise | |
| print( | |
| f"[horizonstream] warning: preprocess failed for {info.name}, fallback to load_images_for_eval: {exc}", | |
| flush=True, | |
| ) | |
| views = load_images_for_eval( | |
| info.image_paths, | |
| size=self.size, | |
| verbose=False, | |
| crop=self.crop, | |
| patch_size=self.patch_size, | |
| ) | |
| imgs = torch.cat([view["img"] for view in views], dim=0) | |
| images = imgs.unsqueeze(0) | |
| images = (images + 1.0) / 2.0 | |
| return images | |
| def _preprocess_rgb_frame_for_eval(self, rgb: np.ndarray) -> torch.Tensor: | |
| img = Image.fromarray(np.asarray(rgb, dtype=np.uint8), mode="RGB") | |
| w1, h1 = img.size | |
| if self.size == 224: | |
| long_edge = round(self.size * max(w1 / h1, h1 / w1)) | |
| else: | |
| long_edge = self.size | |
| scale = float(long_edge) / float(max(w1, h1)) | |
| new_size = tuple(int(round(x * scale)) for x in img.size) | |
| interp = Image.Resampling.LANCZOS if max(w1, h1) > long_edge else Image.Resampling.BICUBIC | |
| img = img.resize(new_size, interp) | |
| w, h = img.size | |
| cx, cy = w // 2, h // 2 | |
| if self.size == 224: | |
| half = min(cx, cy) | |
| if self.crop: | |
| img = img.crop((cx - half, cy - half, cx + half, cy + half)) | |
| else: | |
| img = img.resize((2 * half, 2 * half), Image.Resampling.LANCZOS) | |
| else: | |
| halfw = ((2 * cx) // self.patch_size) * (self.patch_size // 2) | |
| halfh = ((2 * cy) // self.patch_size) * (self.patch_size // 2) | |
| if w == h: | |
| halfh = int(3 * halfw / 4) | |
| if self.crop: | |
| img = img.crop((cx - halfw, cy - halfh, cx + halfw, cy + halfh)) | |
| else: | |
| img = img.resize((2 * halfw, 2 * halfh), Image.Resampling.LANCZOS) | |
| arr = np.asarray(img, dtype=np.float32) / 255.0 | |
| tensor = torch.from_numpy(arr).permute(2, 0, 1) | |
| tensor = (tensor - 0.5) / 0.5 | |
| return tensor | |
| def _load_video_sequence(self) -> HorizonStreamSequence: | |
| video_path = os.path.abspath(os.path.expanduser(str(self.video_path))) | |
| if not os.path.exists(video_path): | |
| raise FileNotFoundError(f"Video file not found: {video_path}") | |
| if self.video_sample_fps is not None and self.video_sample_fps <= 0.0: | |
| raise ValueError(f"video_sample_fps must be positive, got {self.video_sample_fps}") | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| raise RuntimeError(f"Failed to open video: {video_path}") | |
| source_fps = float(cap.get(cv2.CAP_PROP_FPS) or 0.0) | |
| if source_fps <= 0.0: | |
| source_fps = None | |
| if self.video_sample_fps is not None and source_fps is None: | |
| cap.release() | |
| raise ValueError(f"Cannot sample video by FPS because source FPS is unavailable: {video_path}") | |
| scene_name = self.video_scene_name or _safe_video_scene_name( | |
| video_path, | |
| self.video_stride, | |
| self.video_sample_fps, | |
| ) | |
| effective_fps = ( | |
| min(float(self.video_sample_fps), float(source_fps)) | |
| if self.video_sample_fps is not None and source_fps is not None | |
| else (None if source_fps is None else source_fps / float(self.video_stride)) | |
| ) | |
| tensors = [] | |
| source_idx = 0 | |
| out_idx = 0 | |
| def should_keep_frame(frame_idx: int, output_idx: int) -> bool: | |
| if self.video_sample_fps is None: | |
| return frame_idx % self.video_stride == 0 | |
| if source_fps is None or self.video_sample_fps >= source_fps: | |
| return True | |
| target_source_idx = int(round(float(output_idx) * float(source_fps) / float(self.video_sample_fps))) | |
| return frame_idx >= target_source_idx | |
| try: | |
| while True: | |
| ok, frame_bgr = cap.read() | |
| if not ok: | |
| break | |
| if should_keep_frame(source_idx, out_idx): | |
| rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) | |
| tensors.append(self._preprocess_rgb_frame_for_eval(rgb)[None]) | |
| out_idx += 1 | |
| if self.max_frames is not None and self.max_frames > 0 and out_idx >= self.max_frames: | |
| break | |
| source_idx += 1 | |
| finally: | |
| cap.release() | |
| if not tensors: | |
| raise RuntimeError(f"No frames read from video: {video_path}") | |
| imgs = torch.cat(tensors, dim=0) | |
| images = ((imgs.unsqueeze(0)) + 1.0) / 2.0 | |
| print( | |
| f"[horizonstream] loaded video {scene_name}: path={video_path} " | |
| f"frames={out_idx} last_source_frame={source_idx} stride={self.video_stride} " | |
| f"target_fps={self.video_sample_fps} source_fps={source_fps} " | |
| f"effective_fps={effective_fps} shape={tuple(images.shape)}", | |
| flush=True, | |
| ) | |
| return HorizonStreamSequence( | |
| scene_name, | |
| images, | |
| [], | |
| scene_root=os.path.dirname(video_path), | |
| image_dir=None, | |
| camera="video", | |
| ) | |
| def _load_image_list_sequence(self) -> HorizonStreamSequence: | |
| image_paths = self.image_paths_cfg | |
| if isinstance(image_paths, str): | |
| image_paths = _read_list_file(image_paths) if os.path.isfile(image_paths) else [image_paths] | |
| image_paths = [os.path.abspath(os.path.expanduser(str(p))) for p in (image_paths or [])] | |
| image_paths = [p for p in image_paths if p and os.path.isfile(p)] | |
| if self.max_frames is not None and self.max_frames > 0: | |
| image_paths = image_paths[: int(self.max_frames)] | |
| if not image_paths: | |
| raise RuntimeError("No images found for data.image_paths") | |
| scene_name = self.image_scene_name or "images" | |
| info = HorizonStreamSequenceInfo( | |
| name=scene_name, | |
| scene_root=os.path.dirname(image_paths[0]), | |
| image_dir=os.path.dirname(image_paths[0]), | |
| image_paths=image_paths, | |
| camera="image_list", | |
| ) | |
| print( | |
| f"[horizonstream] loading image list {scene_name}: {len(image_paths)} frames", | |
| flush=True, | |
| ) | |
| images = self._load_images(info) | |
| print( | |
| f"[horizonstream] loaded image list {scene_name}: {tuple(images.shape)}", | |
| flush=True, | |
| ) | |
| return HorizonStreamSequence( | |
| scene_name, | |
| images, | |
| image_paths, | |
| scene_root=info.scene_root, | |
| image_dir=info.image_dir, | |
| camera=info.camera, | |
| ) | |
| def iter_sequence_infos(self) -> Iterator[HorizonStreamSequenceInfo]: | |
| fmt = self._infer_format() | |
| seqs = self._resolve_seq_list() | |
| for seq_entry in seqs: | |
| if fmt == "generalizable": | |
| seq, scene_root = self._resolve_scene_root(seq_entry) | |
| dir_path = self._resolve_image_dir_generalizable(scene_root) | |
| if dir_path is None or not os.path.isdir(dir_path): | |
| print(f"[horizonstream] skip seq={seq_entry}: image dir not found under {scene_root}", flush=True) | |
| continue | |
| camera = self._camera_from_image_dir(dir_path) | |
| if camera is not None and self._has_multiple_camera_dirs(scene_root): | |
| seq = os.path.join(seq, camera) | |
| else: | |
| seq = seq_entry | |
| scene_root = os.path.join(self.img_path, seq) | |
| dir_path = self.dir_path_func(self.img_path, seq) | |
| if not os.path.isdir(dir_path): | |
| continue | |
| camera = None | |
| filelist = self._collect_filelist(dir_path) | |
| if not filelist: | |
| print(f"[horizonstream] skip seq={seq_entry}: no images found in {dir_path}", flush=True) | |
| continue | |
| yield HorizonStreamSequenceInfo( | |
| name=seq, | |
| scene_root=scene_root, | |
| image_dir=dir_path, | |
| image_paths=filelist, | |
| camera=camera, | |
| ) | |
| def __iter__(self) -> Iterator[HorizonStreamSequence]: | |
| if self._infer_format() == "image_list": | |
| yield self._load_image_list_sequence() | |
| return | |
| if self._infer_format() == "video": | |
| yield self._load_video_sequence() | |
| return | |
| for info in self.iter_sequence_infos(): | |
| print( | |
| f"[horizonstream] loading sequence {info.name}: {len(info.image_paths)} frames", | |
| flush=True, | |
| ) | |
| images = self._load_images(info) | |
| print( | |
| f"[horizonstream] loaded sequence {info.name}: {tuple(images.shape)}", | |
| flush=True, | |
| ) | |
| yield HorizonStreamSequence( | |
| info.name, | |
| images, | |
| info.image_paths, | |
| scene_root=info.scene_root, | |
| image_dir=info.image_dir, | |
| camera=info.camera, | |
| ) | |