| """ |
| FoundationStereo β Clean Python API Wrapper |
| ============================================ |
| |
| A clean, function-based interface to NVlabs/FoundationStereo (CVPR 2025 Best Paper Nomination). |
| Zero-shot stereo matching with foundation model quality. |
| |
| Source: https://github.com/NVlabs/FoundationStereo |
| |
| Setup: |
| ------ |
| 1. Clone the repo: |
| git clone https://github.com/NVlabs/FoundationStereo.git |
| cd FoundationStereo |
| |
| 2. Install dependencies: |
| conda env create -f environment.yml |
| conda activate foundation_stereo |
| pip install flash-attn # optional, needs GPU compute >= 8.0 |
| |
| 3. Download pretrained weights (pick one): |
| # Option A: From Google Drive (see README) |
| # Option B: From HuggingFace mirror |
| # huggingface-cli download vitaebin/foundation-stereo-model --local-dir pretrained_models |
| |
| 4. Place this file in the repo root or add the repo root to sys.path. |
| |
| Usage: |
| ------ |
| from foundation_stereo import FoundationStereoInference |
| |
| # Initialize once |
| stereo = FoundationStereoInference( |
| repo_dir="/path/to/FoundationStereo", |
| ckpt_path="pretrained_models/23-51-11/model_best_bp2.pth", |
| ) |
| |
| # Run on a stereo pair |
| disp = stereo.predict("left.png", "right.png") |
| |
| # Or with numpy arrays directly |
| disp = stereo.predict_arrays(left_rgb, right_rgb) |
| |
| # Get depth map |
| depth = stereo.disparity_to_depth(disp, focal_length=754.668, baseline=0.063) |
| |
| # Visualize |
| colored = stereo.visualize_disparity(disp) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import sys |
| from dataclasses import dataclass |
| from pathlib import Path |
| from typing import Optional, Tuple |
|
|
| import cv2 |
| import numpy as np |
|
|
|
|
| |
| |
| |
|
|
|
|
| @dataclass |
| class FoundationStereoConfig: |
| """All configurable parameters for FoundationStereo inference. |
| |
| Attributes: |
| repo_dir: Path to the cloned FoundationStereo repository root. |
| ckpt_path: Path to the .pth checkpoint file (relative to repo_dir or absolute). |
| E.g. "pretrained_models/23-51-11/model_best_bp2.pth" |
| vit_size: Vision Transformer backbone size. |
| - "vitl" = ViT-Large (best accuracy, slower, for 23-51-11 model) |
| - "vits" = ViT-Small (faster, lighter, for 11-33-40 model) |
| - "vitb" = ViT-Base (medium) |
| valid_iters: Number of GRU refinement iterations at inference. |
| Higher = more accurate but slower. Recommended: 32 (best), 16 (fast). |
| scale: Input image downscale factor (0 < scale <= 1.0). |
| Use < 1.0 to reduce memory for very large images. |
| mixed_precision: Use AMP (automatic mixed precision) for inference. |
| Faster and uses less VRAM, negligible quality impact. |
| low_memory: Trade speed for lower VRAM usage. Enable for GPUs with <16GB. |
| use_hierarchical: Use hierarchical (coarse-to-fine) inference. |
| Recommended for images > 1K resolution. |
| hierarchical_ratio: Resolution ratio for the first (coarse) pass in hierarchical mode. |
| Typically 0.5 (half resolution first pass). |
| max_disp: Maximum disparity search range (pixels). Default 416 from config. |
| Increase for close-range scenes with large disparities. |
| device: CUDA device string. E.g. "cuda:0", "cuda:1". |
| seed: Random seed for reproducibility. |
| """ |
|
|
| repo_dir: str = "./FoundationStereo" |
| ckpt_path: str = "pretrained_models/23-51-11/model_best_bp2.pth" |
| vit_size: str = "vitl" |
| valid_iters: int = 32 |
| scale: float = 1.0 |
| mixed_precision: bool = True |
| low_memory: bool = False |
| use_hierarchical: bool = False |
| hierarchical_ratio: float = 0.5 |
| max_disp: int = 416 |
| device: str = "cuda:0" |
| seed: int = 0 |
|
|
|
|
| |
| |
| |
|
|
|
|
| class FoundationStereoInference: |
| """Clean Python interface for FoundationStereo inference. |
| |
| Handles model loading, preprocessing, inference, and postprocessing. |
| Call predict() or predict_arrays() for stereo disparity estimation. |
| |
| Example: |
| stereo = FoundationStereoInference( |
| repo_dir="/path/to/FoundationStereo", |
| ckpt_path="pretrained_models/23-51-11/model_best_bp2.pth", |
| ) |
| disp = stereo.predict("left.png", "right.png") |
| """ |
|
|
| def __init__( |
| self, |
| repo_dir: str = "./FoundationStereo", |
| ckpt_path: str = "pretrained_models/23-51-11/model_best_bp2.pth", |
| vit_size: str = "vitl", |
| valid_iters: int = 32, |
| scale: float = 1.0, |
| mixed_precision: bool = True, |
| low_memory: bool = False, |
| use_hierarchical: bool = False, |
| hierarchical_ratio: float = 0.5, |
| max_disp: int = 416, |
| device: str = "cuda:0", |
| seed: int = 0, |
| ): |
| """Initialize FoundationStereo. Alternatively use from_config().""" |
| self.config = FoundationStereoConfig( |
| repo_dir=repo_dir, |
| ckpt_path=ckpt_path, |
| vit_size=vit_size, |
| valid_iters=valid_iters, |
| scale=scale, |
| mixed_precision=mixed_precision, |
| low_memory=low_memory, |
| use_hierarchical=use_hierarchical, |
| hierarchical_ratio=hierarchical_ratio, |
| max_disp=max_disp, |
| device=device, |
| seed=seed, |
| ) |
| self._model = None |
| self._cfg = None |
| self._setup_imports() |
| self._load_model() |
|
|
| @classmethod |
| def from_config(cls, config: FoundationStereoConfig) -> "FoundationStereoInference": |
| """Create instance from a config dataclass.""" |
| return cls(**config.__dict__) |
|
|
| |
| |
| |
|
|
| def predict( |
| self, |
| left_path: str, |
| right_path: str, |
| scale: Optional[float] = None, |
| valid_iters: Optional[int] = None, |
| use_hierarchical: Optional[bool] = None, |
| ) -> np.ndarray: |
| """Run stereo matching on a pair of image files. |
| |
| Args: |
| left_path: Path to the left (reference) image. Any format imageio supports. |
| right_path: Path to the right image. |
| scale: Override config scale for this call (0 < scale <= 1.0). |
| valid_iters: Override GRU iterations for this call. |
| use_hierarchical: Override hierarchical mode for this call. |
| |
| Returns: |
| Disparity map as float32 numpy array, shape (H, W). |
| Values are in pixels (at the possibly-scaled resolution). |
| Higher values = closer to camera. |
| """ |
| import imageio |
|
|
| left_img = imageio.imread(left_path) |
| right_img = imageio.imread(right_path) |
|
|
| return self.predict_arrays( |
| left_img, right_img, |
| scale=scale, |
| valid_iters=valid_iters, |
| use_hierarchical=use_hierarchical, |
| ) |
|
|
| def predict_arrays( |
| self, |
| left_img: np.ndarray, |
| right_img: np.ndarray, |
| scale: Optional[float] = None, |
| valid_iters: Optional[int] = None, |
| use_hierarchical: Optional[bool] = None, |
| ) -> np.ndarray: |
| """Run stereo matching on numpy arrays. |
| |
| Args: |
| left_img: Left image as numpy array, shape (H, W, 3), RGB, uint8. |
| right_img: Right image as numpy array, shape (H, W, 3), RGB, uint8. |
| scale: Override config scale (0 < scale <= 1.0). |
| valid_iters: Override GRU iterations. |
| use_hierarchical: Override hierarchical mode. |
| |
| Returns: |
| Disparity map as float32 numpy array, shape (H, W). |
| """ |
| import torch |
|
|
| |
| _scale = scale if scale is not None else self.config.scale |
| _iters = valid_iters if valid_iters is not None else self.config.valid_iters |
| _hierarchical = ( |
| use_hierarchical if use_hierarchical is not None else self.config.use_hierarchical |
| ) |
|
|
| |
| assert left_img.ndim == 3 and left_img.shape[2] == 3, ( |
| f"Expected (H, W, 3) RGB image, got shape {left_img.shape}" |
| ) |
| assert left_img.shape == right_img.shape, ( |
| f"Left/right shape mismatch: {left_img.shape} vs {right_img.shape}" |
| ) |
|
|
| |
| if _scale != 1.0: |
| assert 0 < _scale <= 1.0, f"Scale must be in (0, 1], got {_scale}" |
| left_img = cv2.resize( |
| left_img, None, fx=_scale, fy=_scale, interpolation=cv2.INTER_LINEAR |
| ) |
| right_img = cv2.resize( |
| right_img, None, fx=_scale, fy=_scale, interpolation=cv2.INTER_LINEAR |
| ) |
|
|
| H, W = left_img.shape[:2] |
|
|
| |
| |
| img0_t = ( |
| torch.as_tensor(left_img).to(self.config.device).float().permute(2, 0, 1).unsqueeze(0) |
| ) |
| img1_t = ( |
| torch.as_tensor(right_img).to(self.config.device).float().permute(2, 0, 1).unsqueeze(0) |
| ) |
|
|
| |
| padder = self._InputPadder(img0_t.shape, divis_by=32) |
| img0_t, img1_t = padder.pad(img0_t, img1_t) |
|
|
| |
| with torch.cuda.amp.autocast(enabled=self.config.mixed_precision): |
| if not _hierarchical: |
| disp = self._model.forward( |
| img0_t, |
| img1_t, |
| iters=_iters, |
| test_mode=True, |
| low_memory=self.config.low_memory, |
| ) |
| else: |
| disp = self._model.run_hierachical( |
| img0_t, |
| img1_t, |
| iters=_iters, |
| test_mode=True, |
| low_memory=self.config.low_memory, |
| small_ratio=self.config.hierarchical_ratio, |
| ) |
|
|
| |
| disp = padder.unpad(disp.float()) |
| disp_np = disp.squeeze().cpu().numpy() |
|
|
| assert disp_np.shape == (H, W), ( |
| f"Output shape mismatch: {disp_np.shape} vs expected ({H}, {W})" |
| ) |
| return disp_np |
|
|
| def predict_batch( |
| self, |
| pairs: list[Tuple[str, str]], |
| scale: Optional[float] = None, |
| valid_iters: Optional[int] = None, |
| use_hierarchical: Optional[bool] = None, |
| ) -> list[np.ndarray]: |
| """Run stereo matching on multiple pairs sequentially. |
| |
| Args: |
| pairs: List of (left_path, right_path) tuples. |
| scale: Override config scale. |
| valid_iters: Override GRU iterations. |
| use_hierarchical: Override hierarchical mode. |
| |
| Returns: |
| List of disparity maps, each shape (H, W) float32. |
| """ |
| results = [] |
| for left_path, right_path in pairs: |
| disp = self.predict( |
| left_path, |
| right_path, |
| scale=scale, |
| valid_iters=valid_iters, |
| use_hierarchical=use_hierarchical, |
| ) |
| results.append(disp) |
| return results |
|
|
| |
| |
| |
|
|
| @staticmethod |
| def disparity_to_depth( |
| disparity: np.ndarray, |
| focal_length: float, |
| baseline: float, |
| min_depth: float = 0.01, |
| max_depth: float = 100.0, |
| ) -> np.ndarray: |
| """Convert disparity map to metric depth map. |
| |
| Formula: depth = focal_length * baseline / disparity |
| |
| Args: |
| disparity: Disparity in pixels, shape (H, W), float32. |
| focal_length: Camera focal length in pixels (fx from intrinsic matrix K[0,0]). |
| If images were scaled, use: fx_original * scale. |
| baseline: Stereo baseline in meters (distance between cameras). |
| min_depth: Minimum valid depth (meters). Pixels below this are clipped. |
| max_depth: Maximum valid depth (meters). Pixels above this are clipped. |
| |
| Returns: |
| Depth map in meters, shape (H, W), float32. |
| Invalid pixels (disparity <= 0) are set to 0. |
| """ |
| depth = np.zeros_like(disparity) |
| valid = disparity > 0 |
| depth[valid] = (focal_length * baseline) / disparity[valid] |
| depth = np.clip(depth, min_depth, max_depth) |
| depth[~valid] = 0.0 |
| return depth |
|
|
| @staticmethod |
| def disparity_to_depth_with_intrinsics( |
| disparity: np.ndarray, |
| K: np.ndarray, |
| baseline: float, |
| scale: float = 1.0, |
| min_depth: float = 0.01, |
| max_depth: float = 100.0, |
| ) -> np.ndarray: |
| """Convert disparity to depth using full intrinsic matrix. |
| |
| Args: |
| disparity: Disparity in pixels, shape (H, W), float32. |
| K: 3x3 camera intrinsic matrix (for original image resolution). |
| baseline: Stereo baseline in meters. |
| scale: Image scale factor that was applied (adjusts focal length). |
| min_depth: Minimum valid depth (meters). |
| max_depth: Maximum valid depth (meters). |
| |
| Returns: |
| Depth map in meters, shape (H, W). |
| """ |
| fx = K[0, 0] * scale |
| return FoundationStereoInference.disparity_to_depth( |
| disparity, fx, baseline, min_depth, max_depth |
| ) |
|
|
| @staticmethod |
| def depth_to_pointcloud( |
| depth: np.ndarray, |
| K: np.ndarray, |
| rgb: Optional[np.ndarray] = None, |
| scale: float = 1.0, |
| max_depth: float = 50.0, |
| ) -> Tuple[np.ndarray, Optional[np.ndarray]]: |
| """Back-project depth map to 3D point cloud. |
| |
| Args: |
| depth: Depth map in meters, shape (H, W). |
| K: 3x3 camera intrinsic matrix (for original resolution). |
| rgb: Optional RGB image for coloring points, shape (H, W, 3), uint8. |
| scale: Scale factor applied to images (adjusts K accordingly). |
| max_depth: Maximum depth to include in point cloud. |
| |
| Returns: |
| Tuple of: |
| - points: (N, 3) float32 array of 3D points [x, y, z] in meters. |
| - colors: (N, 3) float32 array of RGB colors in [0, 1], or None. |
| """ |
| H, W = depth.shape |
| K_scaled = K.copy().astype(np.float64) |
| K_scaled[:2] *= scale |
|
|
| fx, fy = K_scaled[0, 0], K_scaled[1, 1] |
| cx, cy = K_scaled[0, 2], K_scaled[1, 2] |
|
|
| |
| u, v = np.meshgrid(np.arange(W), np.arange(H)) |
|
|
| |
| z = depth |
| x = (u - cx) * z / fx |
| y = (v - cy) * z / fy |
|
|
| |
| xyz = np.stack([x, y, z], axis=-1) |
|
|
| |
| valid = (depth > 0) & (depth < max_depth) |
| points = xyz[valid].astype(np.float32) |
|
|
| colors = None |
| if rgb is not None: |
| colors = rgb[valid].astype(np.float32) / 255.0 |
|
|
| return points, colors |
|
|
| @staticmethod |
| def visualize_disparity( |
| disparity: np.ndarray, |
| colormap: int = cv2.COLORMAP_INFERNO, |
| max_disp: Optional[float] = None, |
| ) -> np.ndarray: |
| """Create a colored visualization of a disparity map. |
| |
| Args: |
| disparity: Disparity map, shape (H, W), float32. |
| colormap: OpenCV colormap constant. Options: |
| cv2.COLORMAP_INFERNO (default, good contrast) |
| cv2.COLORMAP_TURBO (rainbow) |
| cv2.COLORMAP_MAGMA (dark-to-bright) |
| cv2.COLORMAP_JET (classic) |
| max_disp: Maximum disparity for normalization. None = auto (99th percentile). |
| |
| Returns: |
| Colored image, shape (H, W, 3), uint8, BGR format. |
| Save with cv2.imwrite() or convert: cv2.cvtColor(..., cv2.COLOR_BGR2RGB). |
| """ |
| disp_vis = disparity.copy() |
| disp_vis[disp_vis <= 0] = 0 |
|
|
| if max_disp is None: |
| valid_pixels = disp_vis[disp_vis > 0] |
| max_disp = np.percentile(valid_pixels, 99) if len(valid_pixels) > 0 else 1.0 |
|
|
| disp_normalized = np.clip(disp_vis / max_disp, 0, 1) |
| disp_uint8 = (disp_normalized * 255).astype(np.uint8) |
| colored = cv2.applyColorMap(disp_uint8, colormap) |
|
|
| return colored |
|
|
| @staticmethod |
| def load_intrinsics(intrinsics_path: str) -> Tuple[np.ndarray, float]: |
| """Load camera intrinsics from a K.txt file (FoundationStereo format). |
| |
| File format: |
| Line 1: 9 space-separated floats = 3x3 K matrix (row-major) |
| Line 2: single float = baseline in meters |
| |
| Example K.txt: |
| 754.668 0.0 489.379 0.0 754.668 265.161 0.0 0.0 1.0 |
| 0.063 |
| |
| Args: |
| intrinsics_path: Path to the intrinsics file. |
| |
| Returns: |
| Tuple of (K, baseline): |
| - K: 3x3 intrinsic matrix, float32. |
| - baseline: Stereo baseline in meters (float). |
| """ |
| with open(intrinsics_path, "r") as f: |
| lines = f.readlines() |
|
|
| K = np.array( |
| list(map(float, lines[0].strip().split())) |
| ).reshape(3, 3).astype(np.float32) |
| baseline = float(lines[1].strip()) |
| return K, baseline |
|
|
| @staticmethod |
| def save_disparity( |
| disparity: np.ndarray, |
| output_path: str, |
| format: str = "pfm", |
| ) -> None: |
| """Save disparity map to file. |
| |
| Args: |
| disparity: Disparity map, shape (H, W), float32. |
| output_path: Output file path. |
| format: Output format: |
| - "pfm": Portable FloatMap (lossless, standard for stereo benchmarks) |
| - "npy": NumPy binary (fast, lossless) |
| - "png": 16-bit PNG (x256 for sub-pixel precision) |
| - "exr": OpenEXR float (requires imageio[openexr]) |
| """ |
| if format == "npy": |
| np.save(output_path, disparity) |
|
|
| elif format == "pfm": |
| _write_pfm(output_path, disparity) |
|
|
| elif format == "png": |
| disp_16bit = (disparity * 256.0).astype(np.uint16) |
| cv2.imwrite(output_path, disp_16bit) |
|
|
| elif format == "exr": |
| import imageio |
| imageio.imwrite(output_path, disparity) |
|
|
| else: |
| raise ValueError( |
| f"Unknown format: '{format}'. Use 'pfm', 'npy', 'png', or 'exr'." |
| ) |
|
|
| |
| |
| |
|
|
| def _setup_imports(self): |
| """Add FoundationStereo repo to sys.path so internal imports work.""" |
| repo_dir = str(Path(self.config.repo_dir).resolve()) |
| if repo_dir not in sys.path: |
| sys.path.insert(0, repo_dir) |
|
|
| |
| core_dir = os.path.join(repo_dir, "core") |
| if not os.path.isdir(core_dir): |
| raise FileNotFoundError( |
| f"Cannot find 'core/' directory in repo_dir='{repo_dir}'. " |
| f"Make sure repo_dir points to the cloned FoundationStereo repository root." |
| ) |
|
|
| def _load_model(self): |
| """Load the FoundationStereo model from checkpoint.""" |
| import torch |
| from omegaconf import OmegaConf |
| from core.foundation_stereo import FoundationStereo |
| from core.utils.utils import InputPadder |
|
|
| |
| self._InputPadder = InputPadder |
|
|
| |
| ckpt_path = self.config.ckpt_path |
| if not os.path.isabs(ckpt_path): |
| ckpt_path = os.path.join(self.config.repo_dir, ckpt_path) |
| ckpt_path = str(Path(ckpt_path).resolve()) |
|
|
| if not os.path.isfile(ckpt_path): |
| raise FileNotFoundError( |
| f"Checkpoint not found at '{ckpt_path}'. " |
| f"Download weights from Google Drive or HuggingFace mirror. " |
| f"See: https://github.com/NVlabs/FoundationStereo#download-models" |
| ) |
|
|
| |
| cfg_path = os.path.join(os.path.dirname(ckpt_path), "cfg.yaml") |
| if not os.path.isfile(cfg_path): |
| raise FileNotFoundError( |
| f"Config file not found at '{cfg_path}'. " |
| f"The cfg.yaml must be in the same directory as the .pth checkpoint." |
| ) |
|
|
| cfg = OmegaConf.load(cfg_path) |
|
|
| |
| if "vit_size" not in cfg: |
| cfg["vit_size"] = self.config.vit_size |
|
|
| |
| cfg["valid_iters"] = self.config.valid_iters |
| cfg["mixed_precision"] = self.config.mixed_precision |
| cfg["low_memory"] = int(self.config.low_memory) |
| cfg["max_disp"] = self.config.max_disp |
|
|
| self._cfg = OmegaConf.create(cfg) |
|
|
| |
| _set_seed(self.config.seed) |
|
|
| |
| model = FoundationStereo(self._cfg) |
| ckpt = torch.load(ckpt_path, map_location="cpu") |
| model.load_state_dict(ckpt["model"], strict=True) |
| model.to(self.config.device) |
| model.eval() |
|
|
| |
| torch.set_grad_enabled(False) |
|
|
| self._model = model |
| print(f"[FoundationStereo] Model loaded successfully from: {ckpt_path}") |
| print( |
| f"[FoundationStereo] ViT size: {self.config.vit_size} | " |
| f"Iters: {self.config.valid_iters} | " |
| f"Device: {self.config.device}" |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def load_stereo_model( |
| repo_dir: str = "./FoundationStereo", |
| ckpt_path: str = "pretrained_models/23-51-11/model_best_bp2.pth", |
| vit_size: str = "vitl", |
| valid_iters: int = 32, |
| device: str = "cuda:0", |
| **kwargs, |
| ) -> FoundationStereoInference: |
| """Convenience function to load FoundationStereo. |
| |
| Args: |
| repo_dir: Path to cloned repo. |
| ckpt_path: Relative or absolute path to .pth file. |
| vit_size: "vitl" (best), "vits" (fast), or "vitb" (medium). |
| valid_iters: GRU iterations (32=best, 16=fast). |
| device: CUDA device. |
| **kwargs: Any other FoundationStereoConfig fields. |
| |
| Returns: |
| FoundationStereoInference instance ready for prediction. |
| """ |
| return FoundationStereoInference( |
| repo_dir=repo_dir, |
| ckpt_path=ckpt_path, |
| vit_size=vit_size, |
| valid_iters=valid_iters, |
| device=device, |
| **kwargs, |
| ) |
|
|
|
|
| def estimate_disparity( |
| left_path: str, |
| right_path: str, |
| repo_dir: str = "./FoundationStereo", |
| ckpt_path: str = "pretrained_models/23-51-11/model_best_bp2.pth", |
| vit_size: str = "vitl", |
| valid_iters: int = 32, |
| device: str = "cuda:0", |
| **kwargs, |
| ) -> np.ndarray: |
| """One-shot convenience: load model and predict. |
| |
| WARNING: Loads the model every call. For multiple predictions, |
| use load_stereo_model() once and call .predict() repeatedly. |
| """ |
| model = load_stereo_model( |
| repo_dir=repo_dir, |
| ckpt_path=ckpt_path, |
| vit_size=vit_size, |
| valid_iters=valid_iters, |
| device=device, |
| **kwargs, |
| ) |
| return model.predict(left_path, right_path) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _set_seed(seed: int): |
| """Set random seeds for reproducibility.""" |
| import torch |
| import random |
|
|
| random.seed(seed) |
| np.random.seed(seed) |
| torch.manual_seed(seed) |
| if torch.cuda.is_available(): |
| torch.cuda.manual_seed_all(seed) |
|
|
|
|
| def _write_pfm(path: str, image: np.ndarray, scale: float = 1.0): |
| """Write a PFM (Portable FloatMap) file.""" |
| if image.ndim == 2: |
| color = False |
| elif image.ndim == 3 and image.shape[2] == 3: |
| color = True |
| else: |
| raise ValueError(f"Unsupported image shape for PFM: {image.shape}") |
|
|
| with open(path, "wb") as f: |
| header = "PF\n" if color else "Pf\n" |
| f.write(header.encode()) |
| f.write(f"{image.shape[1]} {image.shape[0]}\n".encode()) |
| |
| endian = image.dtype.byteorder |
| if endian == "<" or (endian == "=" and sys.byteorder == "little"): |
| scale = -scale |
| f.write(f"{scale}\n".encode()) |
| |
| image = np.flipud(image).astype(np.float32) |
| f.write(image.tobytes()) |
|
|
|
|
| def _save_ply(path: str, points: np.ndarray, colors: Optional[np.ndarray] = None): |
| """Save point cloud as PLY file (simple ASCII format). |
| |
| Viewable in MeshLab, CloudCompare, Open3D, Blender, etc. |
| """ |
| n = points.shape[0] |
| has_color = colors is not None |
|
|
| with open(path, "w") as f: |
| f.write("ply\n") |
| f.write("format ascii 1.0\n") |
| f.write(f"element vertex {n}\n") |
| f.write("property float x\n") |
| f.write("property float y\n") |
| f.write("property float z\n") |
| if has_color: |
| f.write("property uchar red\n") |
| f.write("property uchar green\n") |
| f.write("property uchar blue\n") |
| f.write("end_header\n") |
|
|
| for i in range(n): |
| line = f"{points[i, 0]:.6f} {points[i, 1]:.6f} {points[i, 2]:.6f}" |
| if has_color: |
| r, g, b = (colors[i] * 255).astype(np.uint8) |
| line += f" {r} {g} {b}" |
| f.write(line + "\n") |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| |
| |
| |
|
|
| REPO_DIR = "./FoundationStereo" |
| CKPT_PATH = "pretrained_models/23-51-11/model_best_bp2.pth" |
| VIT_SIZE = "vitl" |
| VALID_ITERS = 32 |
| SCALE = 1.0 |
| USE_HIERARCHICAL = False |
| DEVICE = "cuda:0" |
| MIXED_PRECISION = True |
|
|
| LEFT_IMAGE = "assets/left.png" |
| RIGHT_IMAGE = "assets/right.png" |
| INTRINSICS_FILE = "assets/K.txt" |
|
|
| OUTPUT_DIR = "./output" |
|
|
| |
| |
| |
|
|
| |
| stereo = FoundationStereoInference( |
| repo_dir=REPO_DIR, |
| ckpt_path=CKPT_PATH, |
| vit_size=VIT_SIZE, |
| valid_iters=VALID_ITERS, |
| scale=SCALE, |
| mixed_precision=MIXED_PRECISION, |
| use_hierarchical=USE_HIERARCHICAL, |
| device=DEVICE, |
| ) |
|
|
| |
| disparity = stereo.predict( |
| os.path.join(REPO_DIR, LEFT_IMAGE), |
| os.path.join(REPO_DIR, RIGHT_IMAGE), |
| ) |
| print(f"Disparity shape: {disparity.shape}") |
| print(f"Disparity range: [{disparity.min():.2f}, {disparity.max():.2f}] pixels") |
|
|
| |
| os.makedirs(OUTPUT_DIR, exist_ok=True) |
| colored_disp = stereo.visualize_disparity(disparity) |
| cv2.imwrite(os.path.join(OUTPUT_DIR, "disparity_colored.png"), colored_disp) |
| print(f"Saved colored disparity to {OUTPUT_DIR}/disparity_colored.png") |
|
|
| |
| intrinsics_path = os.path.join(REPO_DIR, INTRINSICS_FILE) |
| if os.path.isfile(intrinsics_path): |
| K, baseline = stereo.load_intrinsics(intrinsics_path) |
| depth = stereo.disparity_to_depth_with_intrinsics( |
| disparity, K, baseline, scale=SCALE |
| ) |
| print(f"Depth range: [{depth[depth > 0].min():.3f}, {depth[depth > 0].max():.3f}] meters") |
|
|
| |
| import imageio |
|
|
| left_rgb = imageio.imread(os.path.join(REPO_DIR, LEFT_IMAGE)) |
| if SCALE != 1.0: |
| left_rgb = cv2.resize(left_rgb, None, fx=SCALE, fy=SCALE) |
|
|
| points, colors = stereo.depth_to_pointcloud( |
| depth, K, rgb=left_rgb, scale=SCALE, max_depth=50.0 |
| ) |
| print(f"Point cloud: {points.shape[0]} points") |
|
|
| |
| _save_ply(os.path.join(OUTPUT_DIR, "pointcloud.ply"), points, colors) |
| print(f"Saved point cloud to {OUTPUT_DIR}/pointcloud.ply") |
|
|
| |
| stereo.save_disparity( |
| disparity, os.path.join(OUTPUT_DIR, "disparity.npy"), format="npy" |
| ) |
| print(f"Saved raw disparity to {OUTPUT_DIR}/disparity.npy") |
|
|