File size: 3,944 Bytes
e793773
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Camera-pose canonicalization shared by every SCoPE dataset.

All SCoPE datasets, regardless of their on-disk layout, converge on one camera
convention before the batch reaches the model:

1. Poses are OpenCV camera-to-world (c2w) matrices, shape ``[T, 3, 4]``.
2. The first camera is mapped to the identity, so every clip is expressed
   relative to its own first frame (``inv(c2w[0]) @ c2w``). This is the
   RealEstate10K convention; all datasets use it so the model never sees a
   dataset-specific world frame.
3. Translation is preprocessed by a per-clip near-distance depth: it is
   multiplied by ``trajectory_scale / near_depth``. ``near_depth`` comes from an
   offline estimate (``scripts/estimate_near_depth.py``) and is only a
   near-depth normalization that brings the translation magnitude into a
   comparable range across heterogeneous datasets. Scale itself is handled
   inside the model by a dedicated scale gate, not by this preprocessing.

The functions here are intentionally pure NumPy so the convention can be unit
tested without importing torch or decoding any video.
"""

from __future__ import annotations

import json
from pathlib import Path

import numpy as np


def to_c2w_44(poses: np.ndarray) -> np.ndarray:
    """Return homogeneous ``[T, 4, 4]`` c2w from ``[T, 3, 4]`` or ``[T, 4, 4]``."""
    poses = np.asarray(poses, dtype=np.float32)
    if poses.ndim != 3 or poses.shape[-2:] not in ((3, 4), (4, 4)):
        raise ValueError(f"Expected camera poses [T,3,4] or [T,4,4], got {poses.shape}")
    if poses.shape[-2:] == (4, 4):
        return poses
    bottom = np.broadcast_to(np.asarray([0, 0, 0, 1], dtype=poses.dtype), (poses.shape[0], 1, 4))
    return np.concatenate([poses, bottom], axis=1)


def first_camera_relative(poses: np.ndarray) -> np.ndarray:
    """Express all cameras relative to the first (first camera -> identity).

    Args:
        poses: OpenCV c2w, ``[T, 3, 4]`` or ``[T, 4, 4]``.

    Returns:
        ``[T, 3, 4]`` c2w with ``result[0]`` equal to identity.
    """
    c2w = to_c2w_44(poses)
    relative = np.linalg.inv(c2w[0])[None] @ c2w
    return relative[:, :3].astype(np.float32)


def scale_translation(
    poses34: np.ndarray, near_depth: float | None, trajectory_scale: float = 1.0
) -> np.ndarray:
    """Scale the translation column by ``trajectory_scale / near_depth``.

    When ``near_depth`` is ``None`` only ``trajectory_scale`` is applied. The
    rotation block is never touched.
    """
    poses34 = np.array(poses34, dtype=np.float32, copy=True)
    scale = float(trajectory_scale)
    if near_depth is not None:
        if not np.isfinite(near_depth) or near_depth <= 0:
            raise ValueError(f"near_depth must be finite and positive, got {near_depth}")
        scale = scale / float(near_depth)
    if scale != 1.0:
        poses34[:, :, 3] *= scale
    return poses34


def load_near_depth_map(near_depth_json: str | Path | None) -> dict[str, float] | None:
    """Load a ``clip_id -> near_depth`` map produced by estimate_near_depth.py.

    File layout::

        { "<clip_id>": {"near_depth": <float|null>, ...}, ... }

    Entries with null / non-finite / non-positive values are dropped. Returns
    ``None`` when no path is given or the file is missing.
    """
    if near_depth_json is None:
        return None
    path = Path(near_depth_json)
    if not path.exists():
        print(f"[near_depth] file not found: {path} - trajectory_scale used for all clips")
        return None
    raw = json.loads(path.read_text(encoding="utf-8"))
    out: dict[str, float] = {}
    for key, value in raw.items():
        near_depth = value.get("near_depth") if isinstance(value, dict) else value
        if near_depth is None:
            continue
        if not np.isfinite(near_depth) or near_depth <= 0:
            continue
        out[key] = float(near_depth)
    print(f"[near_depth] loaded {len(out)} clips from {path}")
    return out