File size: 4,954 Bytes
0514ca2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# Copyright (C) 2024-present Naver Corporation. All rights reserved.
# Licensed under CC BY-NC-SA 4.0 (non-commercial use only).
#
# --------------------------------------------------------
# Various 3D/2D projection utils, useful to sample virtual cameras.
# --------------------------------------------------------
import numpy as np

class EquirectangularProjection:
    """
    Convention for the central pixel of the equirectangular map similar to OpenCV perspective model:
        +X from left to right
        +Y from top to bottom
        +Z going outside the camera
    EXCEPT that the top left corner of the image is assumed to have (0,0) coordinates (OpenCV assumes (-0.5,-0.5))
    """

    def __init__(self, height, width):
        self.height = height
        self.width = width
        self.u_scaling = (2 * np.pi) / self.width
        self.v_scaling = np.pi / self.height

    def unproject(self, u, v):
        """
        Args:
            u, v: 2D coordinates
        Returns:
            unnormalized 3D rays.
        """
        longitude = self.u_scaling * u - np.pi
        minus_latitude = self.v_scaling * v - np.pi/2

        cos_latitude = np.cos(minus_latitude)
        x, z = np.sin(longitude) * cos_latitude, np.cos(longitude) * cos_latitude
        y = np.sin(minus_latitude)

        rays = np.stack([x, y, z], axis=-1)
        return rays

    def project(self, rays):
        """
        Args:
            rays: Bx3 array of 3D rays.
        Returns:
            u, v: tuple of 2D coordinates.
        """
        rays = rays / np.linalg.norm(rays, axis=-1, keepdims=True)
        x, y, z = [rays[..., i] for i in range(3)]

        longitude = np.arctan2(x, z)
        minus_latitude = np.arcsin(y)

        u = (longitude + np.pi) * (1.0 / self.u_scaling)
        v = (minus_latitude + np.pi/2) * (1.0 / self.v_scaling)
        return u, v


class PerspectiveProjection:
    """
    OpenCV convention:
    World space:
        +X from left to right
        +Y from top to bottom
        +Z going outside the camera
    Pixel space:
        +u from left to right
        +v from top to bottom
    EXCEPT that the top left corner of the image is assumed to have (0,0) coordinates (OpenCV assumes (-0.5,-0.5)).
    """

    def __init__(self, K, height, width):
        self.height = height
        self.width = width
        self.K = K
        self.Kinv = np.linalg.inv(K)

    def project(self, rays):
        uv_homogeneous = np.einsum("ik, ...k -> ...i", self.K, rays)
        uv = uv_homogeneous[..., :2] / uv_homogeneous[..., 2, None]
        return uv[..., 0], uv[..., 1]

    def unproject(self, u, v):
        uv_homogeneous = np.stack((u, v, np.ones_like(u)), axis=-1)
        rays = np.einsum("ik, ...k -> ...i", self.Kinv, uv_homogeneous)
        return rays


class RotatedProjection:
    def __init__(self, base_projection, R_to_base_projection):
        self.base_projection = base_projection
        self.R_to_base_projection = R_to_base_projection

    @property
    def width(self):
        return self.base_projection.width

    @property
    def height(self):
        return self.base_projection.height

    def project(self, rays):
        if self.R_to_base_projection is not None:
            rays = np.einsum("ik, ...k -> ...i", self.R_to_base_projection, rays)
        return self.base_projection.project(rays)

    def unproject(self, u, v):
        rays = self.base_projection.unproject(u, v)
        if self.R_to_base_projection is not None:
            rays = np.einsum("ik, ...k -> ...i", self.R_to_base_projection.T, rays)
        return rays

def get_projection_rays(projection, noise_level=0):
    """
    Return a 2D map of 3D rays corresponding to the projection.
    If noise_level > 0, add some jittering noise to these rays.
    """
    grid_u, grid_v = np.meshgrid(0.5 + np.arange(projection.width), 0.5 + np.arange(projection.height))
    if noise_level > 0:
        grid_u += np.clip(0, noise_level * np.random.uniform(-0.5, 0.5, size=grid_u.shape), projection.width)
        grid_v += np.clip(0, noise_level * np.random.uniform(-0.5, 0.5, size=grid_v.shape), projection.height)
    return projection.unproject(grid_u, grid_v)

def compute_camera_intrinsics(height, width, hfov):
    f = width/2 / np.tan(hfov/2 * np.pi/180)
    cu, cv = width/2, height/2
    return f, cu, cv

def colmap_to_opencv_intrinsics(K):
    """
    Modify camera intrinsics to follow a different convention.
    Coordinates of the center of the top-left pixels are by default:
    - (0.5, 0.5) in Colmap
    - (0,0) in OpenCV
    """
    K = K.copy()
    K[0, 2] -= 0.5
    K[1, 2] -= 0.5
    return K

def opencv_to_colmap_intrinsics(K):
    """
    Modify camera intrinsics to follow a different convention.
    Coordinates of the center of the top-left pixels are by default:
    - (0.5, 0.5) in Colmap
    - (0,0) in OpenCV
    """
    K = K.copy()
    K[0, 2] += 0.5
    K[1, 2] += 0.5
    return K