File size: 11,108 Bytes
3de34d4
25abb1d
 
3de34d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25abb1d
 
 
 
 
 
3de34d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25abb1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3de34d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
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