Spaces:
Running on Zero
Running on Zero
Download src/utils/geometry_utils.py from prs-eth/PaGeR: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/prs-eth/PaGeR/resolve/main/src/utils/geometry_utils.py
- Command line
-
hf download hf://spaces/prs-eth/PaGeR/src/utils/geometry_utils.py
-
curl -L -o geometry_utils.py https://huggingface.co/spaces/prs-eth/PaGeR/resolve/main/src/utils/geometry_utils.py
11.1 kB
| import math | |
| import torch | |
| import numpy as np | |
| from pytorch360convert import e2c, c2e, e2p | |
| # Standard 'stack' face order: ['Front', 'Right', 'Back', 'Left', 'Up', 'Down']. | |
| # (h_deg, v_deg) for e2p that reproduce the same faces as e2c at fov=90. | |
| _CUBE_FACE_ANGLES = ( | |
| (0.0, 0.0), # Front | |
| (90.0, 0.0), # Right | |
| (180.0, 0.0), # Back | |
| (-90.0, 0.0), # Left | |
| (0.0, 90.0), # Up | |
| (0.0, -90.0), # Down | |
| ) | |
| def _fov_crop_size(face_w: int, fov_deg: float) -> int: | |
| """Pixel size of the central 90-deg crop inside a face_w-pixel face at fov_deg.""" | |
| if fov_deg <= 90.0: | |
| return face_w | |
| return int(round(face_w / math.tan(math.radians(fov_deg / 2.0)))) | |
| def _e2p_stack(erp_tensor, face_w, fov_deg, mode): | |
| """Build a (6, C, face_w, face_w) cubemap stack from a single ERP using e2p.""" | |
| faces = [ | |
| e2p(erp_tensor, fov_deg=fov_deg, h_deg=h, v_deg=v, | |
| out_hw=(face_w, face_w), mode=mode) | |
| for h, v in _CUBE_FACE_ANGLES | |
| ] | |
| return torch.stack(faces, dim=0) | |
| def _crop_overlap(cube_stack, face_w, fov_deg): | |
| """Center-crop the central 90-deg region from faces rendered at fov_deg. | |
| Discards the FOV>90 overlap so faces tile the sphere without gaps | |
| and can be fed to c2e (which assumes 90-deg faces). | |
| """ | |
| crop = _fov_crop_size(face_w, fov_deg) | |
| if crop == face_w: | |
| return cube_stack | |
| start = (face_w - crop) // 2 | |
| end = start + crop | |
| return cube_stack[..., start:end, start:end] | |
| def erp_to_cubemap(erp_tensor, face_w=504, fov=90.0, cube_format="stack", mode="bilinear", **kwargs): | |
| """Convert ERP to cubemap stack. | |
| fov=90 uses the fast e2c path. fov>90 uses e2p per face so neighbouring | |
| faces overlap β useful for cubemap-format losses that fight seams. | |
| """ | |
| if fov == 90.0: | |
| if erp_tensor.ndim == 3: | |
| return e2c(erp_tensor, face_w=face_w, cube_format=cube_format, mode=mode, **kwargs) | |
| if erp_tensor.ndim == 4: | |
| return torch.stack([ | |
| e2c(erp_tensor[b], face_w=face_w, cube_format=cube_format, mode=mode, **kwargs) | |
| for b in range(erp_tensor.shape[0]) | |
| ], dim=0) | |
| raise ValueError( | |
| f"Unsupported erp_tensor shape {tuple(erp_tensor.shape)}. Expected (C, H, W) or (B, C, H, W)." | |
| ) | |
| # fov > 90: use e2p per face (only 'stack' format supported) | |
| if cube_format != "stack": | |
| raise NotImplementedError( | |
| f"erp_to_cubemap with fov != 90 only supports cube_format='stack', got {cube_format!r}" | |
| ) | |
| if erp_tensor.ndim == 3: | |
| return _e2p_stack(erp_tensor, face_w, fov, mode) | |
| if erp_tensor.ndim == 4: | |
| return torch.stack([ | |
| _e2p_stack(erp_tensor[b], face_w, fov, mode) | |
| for b in range(erp_tensor.shape[0]) | |
| ], dim=0) | |
| raise ValueError( | |
| f"Unsupported erp_tensor shape {tuple(erp_tensor.shape)}. Expected (C, H, W) or (B, C, H, W)." | |
| ) | |
| def cubemap_to_erp(cube_tensor, erp_h=1024, erp_w=2048, fov=90.0, cube_format="stack", mode="bilinear", **kwargs): | |
| """Convert cubemap stack to ERP. | |
| For fov>90 the overlap region of each face is center-cropped to 90 deg | |
| before stitching via c2e, so each ERP pixel comes from exactly one face. | |
| """ | |
| if fov != 90.0: | |
| if cube_format != "stack": | |
| raise NotImplementedError( | |
| f"cubemap_to_erp with fov != 90 only supports cube_format='stack', got {cube_format!r}" | |
| ) | |
| face_w = cube_tensor.shape[-1] | |
| cube_tensor = _crop_overlap(cube_tensor, face_w, fov) | |
| if cube_tensor.ndim == 4: | |
| if cube_tensor.shape[0] != 6: | |
| raise ValueError( | |
| f"Expected cube_tensor shape (6, C, h, w) for 4D input, got {tuple(cube_tensor.shape)}" | |
| ) | |
| return c2e(cube_tensor, h=erp_h, w=erp_w, cube_format=cube_format, mode=mode, **kwargs) | |
| if cube_tensor.ndim == 5: | |
| if cube_tensor.shape[1] != 6: | |
| raise ValueError( | |
| f"Expected cube_tensor shape (B, 6, C, h, w) for 5D input, got {tuple(cube_tensor.shape)}" | |
| ) | |
| erp_batch = [] | |
| for b in range(cube_tensor.shape[0]): | |
| erp_batch.append( | |
| c2e(cube_tensor[b], h=erp_h, w=erp_w, cube_format=cube_format, mode=mode, **kwargs) | |
| ) | |
| return torch.stack(erp_batch, dim=0) | |
| raise ValueError( | |
| f"Unsupported cube_tensor shape {tuple(cube_tensor.shape)}. Expected (6, C, h, w) or (B, 6, C, h, w)." | |
| ) | |
| def unit_normals(n, eps = 1e-6): | |
| assert n.dim() >= 3 and n.size(-3) == 3, "normals must have channel=3 at dim -3" | |
| denom = torch.clamp(torch.linalg.norm(n, dim=-3, keepdim=True), min=eps) | |
| return n / denom | |
| def z_depth_to_euclidean(z_depth, intrinsics): | |
| # S may be any subset of cubemap faces β see euclidean_to_z_depth for the | |
| # same relaxation rationale (identical K per face). | |
| squeeze_batch = False | |
| if z_depth.ndim == 4: | |
| if z_depth.shape[1] != 1: | |
| raise ValueError( | |
| f"Expected z_depth shape (S, 1, H, W) for 4D input, got {tuple(z_depth.shape)}" | |
| ) | |
| z_depth = z_depth.unsqueeze(0) | |
| squeeze_batch = True | |
| elif z_depth.ndim == 5: | |
| if z_depth.shape[2] != 1: | |
| raise ValueError( | |
| f"Expected z_depth shape (B, S, 1, H, W) for 5D input, got {tuple(z_depth.shape)}" | |
| ) | |
| else: | |
| raise ValueError( | |
| f"Unsupported z_depth shape {tuple(z_depth.shape)}. Expected (6, 1, H, W) or (B, 6, 1, H, W)." | |
| ) | |
| if intrinsics.ndim == 3: | |
| intrinsics = intrinsics[0] | |
| fx = intrinsics[0, 0] | |
| fy = intrinsics[1, 1] | |
| cx = intrinsics[0, 2] | |
| cy = intrinsics[1, 2] | |
| _, _, _, H, W = z_depth.shape | |
| u = torch.arange(W, device=z_depth.device, dtype=z_depth.dtype) | |
| v = torch.arange(H, device=z_depth.device, dtype=z_depth.dtype) | |
| vv, uu = torch.meshgrid(v, u, indexing="ij") | |
| x_norm = (uu - cx) / fx | |
| y_norm = (vv - cy) / fy | |
| ray_length_multiplier = torch.sqrt(x_norm * x_norm + y_norm * y_norm + 1.0) | |
| ray_length_multiplier = ray_length_multiplier.unsqueeze(0).unsqueeze(0).unsqueeze(0) | |
| out = z_depth * ray_length_multiplier | |
| return out.squeeze(0) if squeeze_batch else out | |
| def compute_edge_mask(depth, abs_thresh = 0.1, rel_thresh = 0.1): | |
| assert depth.ndim == 2 | |
| depth = depth.astype(np.float32, copy=False) | |
| valid = depth > 0 | |
| eps = 1e-6 | |
| edge = np.zeros_like(valid, dtype=bool) | |
| d1 = depth[:, :-1] | |
| d2 = depth[:, 1:] | |
| v_pair = valid[:, :-1] & valid[:, 1:] | |
| diff = np.abs(d1 - d2) | |
| rel = diff / (np.minimum(d1, d2) + eps) | |
| edge_pair = v_pair & (diff > abs_thresh) & (rel > rel_thresh) | |
| edge[:, :-1] |= edge_pair | |
| edge[:, 1:] |= edge_pair | |
| d1 = depth[:-1, :] | |
| d2 = depth[1:, :] | |
| v_pair = valid[:-1, :] & valid[1:, :] | |
| diff = np.abs(d1 - d2) | |
| rel = diff / (np.minimum(d1, d2) + eps) | |
| edge_pair = v_pair & (diff > abs_thresh) & (rel > rel_thresh) | |
| edge[:-1, :] |= edge_pair | |
| edge[1:, :] |= edge_pair | |
| keep = valid & (~edge) | |
| return keep | |
| _ERP_DIRS_CACHE: dict = {} | |
| def _get_erp_dirs(H: int, W: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor: | |
| """Cached (3, H, W) unit direction vectors for ERP β 3D unprojection. | |
| Convention: x = cos(phi)*cos(theta), y = sin(phi), z = cos(phi)*sin(theta) | |
| """ | |
| key = (H, W, device, dtype) | |
| if key not in _ERP_DIRS_CACHE: | |
| u = (torch.arange(W, device=device, dtype=dtype) + 0.5) / W # (W,) | |
| v = (torch.arange(H, device=device, dtype=dtype) + 0.5) / H # (H,) | |
| theta = u * (2.0 * torch.pi) - torch.pi # (W,) azimuth | |
| phi = (0.5 - v) * torch.pi # (H,) elevation | |
| cos_phi = torch.cos(phi) # (H,) | |
| sin_phi = torch.sin(phi) # (H,) | |
| cos_the = torch.cos(theta) # (W,) | |
| sin_the = torch.sin(theta) # (W,) | |
| dir_x = cos_phi[:, None] * cos_the[None, :] # (H, W) | |
| dir_y = sin_phi[:, None].expand(H, W) # (H, W) | |
| dir_z = cos_phi[:, None] * sin_the[None, :] # (H, W) | |
| _ERP_DIRS_CACHE[key] = torch.stack([dir_x, dir_y, dir_z], dim=0) # (3, H, W) | |
| return _ERP_DIRS_CACHE[key] | |
| def erp_to_pointcloud(depth: torch.Tensor) -> torch.Tensor: | |
| """Differentiable ERP depth β XYZ point map. | |
| The direction grid is cached per (H, W, device, dtype) so calls inside a | |
| training loop pay only the cost of a single broadcast multiply. | |
| Args: | |
| depth: float tensor (H, W) | (B, H, W) | (B, 1, H, W) | |
| Returns: | |
| xyz: float tensor, same dtype/device | |
| (H, W) β (3, H, W) | |
| (B, H, W) β (B, 3, H, W) | |
| (B, 1, H, W) β (B, 3, H, W) | |
| """ | |
| ndim = depth.dim() | |
| if ndim == 2: | |
| H, W = depth.shape | |
| dirs = _get_erp_dirs(H, W, depth.device, depth.dtype) # (3, H, W) | |
| return depth.unsqueeze(0) * dirs # (3, H, W) | |
| if ndim == 3: | |
| depth = depth.unsqueeze(1) # (B, 1, H, W) | |
| # depth is (B, 1, H, W) from here | |
| B, _, H, W = depth.shape | |
| dirs = _get_erp_dirs(H, W, depth.device, depth.dtype) # (3, H, W) | |
| return depth * dirs.unsqueeze(0) # (B, 3, H, W) | |
| def get_cubemap_intrinsics_extrinsics(image_size=512, fov=90.0): | |
| """Compute cubemap camera intrinsics and extrinsics for 6 faces. | |
| Args: | |
| image_size: face resolution in pixels (square faces assumed). | |
| fov: per-face field of view in degrees (>=90). At fov=90 the | |
| focal length equals image_size/2 (standard cubemap). | |
| Returns: | |
| (extrinsics, intrinsics) β both float32 tensors on CPU. | |
| intrinsics: (6, 3, 3) | |
| extrinsics: (6, 4, 4) | |
| """ | |
| S = float(image_size) | |
| f = S / (2.0 * math.tan(math.radians(fov / 2.0))) | |
| cx, cy = S / 2.0, S / 2.0 | |
| K = torch.tensor([ | |
| [f, 0, cx], | |
| [0, f, cy], | |
| [0, 0, 1] | |
| ], dtype=torch.float32) | |
| intrinsics = K.unsqueeze(0).expand(6, -1, -1).clone() | |
| face_configs = [ | |
| (0, 0), # Front (Look +Z) | |
| (-90, 0), # Right (Look +X) | |
| (180, 0), # Back (Look -Z) | |
| (90, 0), # Left (Look -X) | |
| (0, -90), # Top (Look +Y) | |
| (0, 90) # Bottom (Look -Y) | |
| ] | |
| face_configs = torch.tensor(face_configs, dtype=torch.float32) | |
| y = torch.deg2rad(face_configs[:, 0]) | |
| p = torch.deg2rad(face_configs[:, 1]) | |
| cy_r = torch.cos(y) | |
| sy = torch.sin(y) | |
| cp = torch.cos(p) | |
| sp = torch.sin(p) | |
| Ry = torch.zeros((6, 3, 3), dtype=torch.float32) | |
| Ry[:, 0, 0] = cy_r | |
| Ry[:, 0, 2] = sy | |
| Ry[:, 1, 1] = 1.0 | |
| Ry[:, 2, 0] = -sy | |
| Ry[:, 2, 2] = cy_r | |
| Rx = torch.zeros((6, 3, 3), dtype=torch.float32) | |
| Rx[:, 0, 0] = 1.0 | |
| Rx[:, 1, 1] = cp | |
| Rx[:, 1, 2] = -sp | |
| Rx[:, 2, 1] = sp | |
| Rx[:, 2, 2] = cp | |
| R = torch.bmm(Rx, Ry) | |
| extrinsics = torch.zeros((6, 4, 4), dtype=torch.float32) | |
| extrinsics[:, :3, :3] = R | |
| extrinsics[:, 3, 3] = 1.0 | |
| return extrinsics, intrinsics | |