| import math |
|
|
| import numpy as np |
| import torch |
| from scipy.spatial.transform import Rotation as R |
|
|
|
|
| def dot(x, y): |
| if isinstance(x, np.ndarray): |
| return np.sum(x * y, -1, keepdims=True) |
| else: |
| return torch.sum(x * y, -1, keepdim=True) |
|
|
|
|
| def length(x, eps=1e-20): |
| if isinstance(x, np.ndarray): |
| return np.sqrt(np.maximum(np.sum(x * x, axis=-1, keepdims=True), eps)) |
| else: |
| return torch.sqrt(torch.clamp(dot(x, x), min=eps)) |
|
|
|
|
| def safe_normalize(x, eps=1e-20): |
| return x / length(x, eps) |
|
|
|
|
| def look_at(campos, target, opengl=True): |
| |
| |
| |
| if not opengl: |
| |
| forward_vector = safe_normalize(target - campos) |
| up_vector = np.array([0, 1, 0], dtype=np.float32) |
| right_vector = safe_normalize(np.cross(forward_vector, up_vector)) |
| up_vector = safe_normalize(np.cross(right_vector, forward_vector)) |
| else: |
| |
| forward_vector = safe_normalize(campos - target) |
| up_vector = np.array([0, 1, 0], dtype=np.float32) |
| right_vector = safe_normalize(np.cross(up_vector, forward_vector)) |
| up_vector = safe_normalize(np.cross(forward_vector, right_vector)) |
| R = np.stack([right_vector, up_vector, forward_vector], axis=1) |
| return R |
|
|
|
|
| def orbit_camera( |
| elevation, azimuth, radius=1, is_degree=True, target=None, opengl=True |
| ): |
| |
| |
| |
| |
| if is_degree: |
| elevation = np.deg2rad(elevation) |
| azimuth = np.deg2rad(azimuth) |
| x = radius * np.cos(elevation) * np.sin(azimuth) |
| y = -radius * np.sin(elevation) |
| z = radius * np.cos(elevation) * np.cos(azimuth) |
| if target is None: |
| target = np.zeros([3], dtype=np.float32) |
| campos = np.array([x, y, z]) + target |
| T = np.eye(4, dtype=np.float32) |
| T[:3, :3] = look_at(campos, target, opengl) |
| T[:3, 3] = campos |
| return T |
|
|
|
|
| class MiniCam: |
| def __init__(self, c2w, width, height, fovy, fovx, znear, zfar): |
| |
|
|
| self.image_width = width |
| self.image_height = height |
| self.FoVy = fovy |
| self.FoVx = fovx |
| self.znear = znear |
| self.zfar = zfar |
|
|
| w2c = np.linalg.inv(c2w) |
|
|
| |
| w2c[1:3, :3] *= -1 |
| w2c[:3, 3] *= -1 |
|
|
| self._R = w2c[:3, :3].T |
| self._T = w2c[:3, 3] |
|
|
| self.world_view_transform = torch.tensor(w2c).transpose(0, 1).cuda() |
| self.projection_matrix = ( |
| getProjectionMatrix( |
| znear=self.znear, zfar=self.zfar, fovX=self.FoVx, fovY=self.FoVy |
| ) |
| .transpose(0, 1) |
| .cuda() |
| ) |
|
|
| self.full_proj_transform = self.world_view_transform @ self.projection_matrix |
| self.camera_center = -torch.tensor(c2w[:3, 3]).cuda() |
|
|
| self._focal_y = height / (2 * np.tan(fovy / 2)) |
| self._focal_x = width / (2 * np.tan(fovx / 2)) |
|
|
| @property |
| def R(self): |
|
|
| return self._R |
|
|
| @property |
| def T(self): |
| return self._T |
|
|
| @property |
| def focal_x(self): |
| return self._focal_x |
|
|
| @property |
| def focal_y(self): |
| return self._focal_y |
|
|
|
|
| def getProjectionMatrix(znear, zfar, fovX, fovY): |
| tanHalfFovY = math.tan((fovY / 2)) |
| tanHalfFovX = math.tan((fovX / 2)) |
|
|
| P = torch.zeros(4, 4) |
|
|
| z_sign = 1.0 |
|
|
| P[0, 0] = 1 / tanHalfFovX |
| P[1, 1] = 1 / tanHalfFovY |
| P[3, 2] = z_sign |
| P[2, 2] = z_sign * zfar / (zfar - znear) |
| P[2, 3] = -(zfar * znear) / (zfar - znear) |
|
|
| return P |
|
|
|
|
| class OrbitCamera: |
| def __init__(self, W, H, r=2, fovy=60, near=0.01, far=100): |
| self.W = W |
| self.H = H |
| self.radius = r |
| self.fovy = np.deg2rad(fovy) |
| self.near = near |
| self.far = far |
| self.center = np.array([0, 0, 0], dtype=np.float32) |
| self.rot = R.from_matrix(np.eye(3)) |
| self.up = np.array([0, 1, 0], dtype=np.float32) |
|
|
| @property |
| def fovx(self): |
| return 2 * np.arctan(np.tan(self.fovy / 2) * self.W / self.H) |
|
|
| @property |
| def campos(self): |
| return self.pose[:3, 3] |
|
|
| |
| @property |
| def pose(self): |
| |
| res = np.eye(4, dtype=np.float32) |
| res[2, 3] = self.radius |
| |
| rot = np.eye(4, dtype=np.float32) |
| rot[:3, :3] = self.rot.as_matrix() |
| res = rot @ res |
| |
| res[:3, 3] -= self.center |
| return res |
|
|
| |
| @property |
| def view(self): |
| return np.linalg.inv(self.pose) |
|
|
| |
| @property |
| def perspective(self): |
| y = np.tan(self.fovy / 2) |
| aspect = self.W / self.H |
|
|
| return np.array( |
| [ |
| [1 / (y * aspect), 0, 0, 0], |
| [0, -1 / y, 0, 0], |
| [ |
| 0, |
| 0, |
| -(self.far + self.near) / (self.far - self.near), |
| -(2 * self.far * self.near) / (self.far - self.near), |
| ], |
| [0, 0, -1, 0], |
| ], |
| dtype=np.float32, |
| ) |
|
|
| |
| @property |
| def intrinsics(self): |
| focal = self.H / (2 * np.tan(self.fovy / 2)) |
| return np.array([focal, focal, self.W // 2, self.H // 2], dtype=np.float32) |
|
|
| @property |
| def mvp(self): |
| return self.perspective @ np.linalg.inv(self.pose) |
|
|
| def orbit(self, dx, dy): |
| |
| side = self.rot.as_matrix()[:3, 0] |
| rotvec_x = self.up * np.radians(-0.05 * dx) |
| rotvec_y = side * np.radians(-0.05 * dy) |
| self.rot = R.from_rotvec(rotvec_x) * R.from_rotvec(rotvec_y) * self.rot |
|
|
| def scale(self, delta): |
| self.radius *= 1.1 ** (-delta) |
|
|
| def pan(self, dx, dy, dz=0): |
| |
| self.center += 0.0005 * self.rot.as_matrix()[:3, :3] @ np.array([-dx, -dy, dz]) |
|
|
| def from_angle(self, elevation, azimuth, is_degree=True): |
| """set the camera pose from elevation & azimuth angle. |
| |
| Args: |
| elevation (float): elevation in (-90, 90), from +y to -y is (-90, 90) |
| azimuth (float): azimuth in (-180, 180), from +z to +x is (0, 90) |
| is_degree (bool, optional): whether the angles are in degree. Defaults to True. |
| """ |
| if is_degree: |
| elevation = np.deg2rad(elevation) |
| azimuth = np.deg2rad(azimuth) |
| x = self.radius * np.cos(elevation) * np.sin(azimuth) |
| y = -self.radius * np.sin(elevation) |
| z = self.radius * np.cos(elevation) * np.cos(azimuth) |
| campos = np.array([x, y, z]) |
| rot_mat = look_at(campos, np.zeros([3], dtype=np.float32)) |
| self.rot = R.from_matrix(rot_mat) |
|
|
|
|
| if __name__ == "__main__": |
| MiniCam |
|
|