File size: 6,063 Bytes
632d805
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Per-object metadata scale for converted MOVi vertex data (data.object_scale_file).

data_generation/movi_to_sim_data.py wrote every object's vertices as R v_asset + p and never applied the metadata
`size`, so stored meshes do not have the simulated dimensions (MOVi-Sphere: all spheres ~1.0 m across, simulated radii
0.35 / 0.7 m; resting mesh bottoms at z ~ -0.15 / +0.2 m instead of the floor). With a scale file from
data_generation/build_object_scales.py, every array read from <scene>/frame_<t>/obj_<i>_pos.npy is scaled about the
object origin p_t (vis_traj.npy[0, t, i, :3], equal to the metadata positions):

    v' = p_t + size_i (v - p_t)

after which resting objects of every size have their lowest vertex at z ~ 0.002 m. Anchor and FPS-order caches are
unchanged (farthest-point sampling is invariant to a uniform scaling about any point). Without a scale file nothing
changes.
"""
import json
from pathlib import Path
from typing import Dict, Optional

import numpy as np

try:
    from data.scene_frames import load_frame
except ImportError:                        # imported as src.data.object_scale
    from src.data.scene_frames import load_frame


class ObjectScale:
    def __init__(self, scale_file: str):
        data = json.load(open(scale_file))
        self.scale_file = str(scale_file)
        # converted dataset the sizes belong to (scene names movi_<n> repeat across MOVi-A/B/Sphere)
        self.converted = data.get('converted')
        self.scales: Dict[str, np.ndarray] = {k: np.asarray(v, dtype=np.float64) for k, v in data['scales'].items()}
        self.fingerprint = {k: np.asarray(v, dtype=np.float64) for k, v in (data.get('fingerprint') or {}).items()}
        if 'radius0' not in data:
            raise ValueError(f'{scale_file} has no radius0 entries (the guard against vertex data that already carries the '
                             f'size); rebuild it with data_generation/build_object_scales.py')
        self.radius0 = {k: np.asarray(v, dtype=np.float64) for k, v in data['radius0'].items()}
        for name, table in (('scales', self.scales), ('fingerprint', self.fingerprint), ('radius0', self.radius0)):
            bad = [k for k, v in table.items() if not np.isfinite(v).all() or (name != 'fingerprint' and not (v > 0).all())]
            if bad:
                raise ValueError(f'{scale_file}: non-finite or non-positive {name} for {len(bad)} scene(s), e.g. {bad[:3]}')
        self._origins: Dict[str, np.ndarray] = {}

    def origins(self, scene_dir: Path) -> np.ndarray:
        key = scene_dir.name
        if key not in self._origins:
            o = np.load(scene_dir / 'vis_traj.npy')[0, :, :, :3].astype(np.float64)   # (T, Nobj, 3)
            if not np.isfinite(o).all():
                raise ValueError(f'ObjectScale: non-finite object origins in {scene_dir / "vis_traj.npy"}')
            self._origins[key] = o
        return self._origins[key]

    def apply(self, vertex_path: Path, V: np.ndarray) -> np.ndarray:
        """Scaled copy of V, the array stored at <scene>/frame_<t>/obj_<i>_pos.npy (V itself is never modified)."""
        vertex_path = Path(vertex_path)
        frame_dir = vertex_path.parent
        scene_dir = frame_dir.parent
        if not frame_dir.name.startswith('frame_') or not vertex_path.name.startswith('obj_'):
            raise ValueError(f'ObjectScale: not a per-frame vertex file: {vertex_path}')
        if scene_dir.name not in self.scales:
            raise KeyError(f'ObjectScale: scene {scene_dir.name} missing from {self.scale_file}')
        t = int(frame_dir.name.split('_')[1])
        i = int(vertex_path.name.split('_')[1])
        s = self.scales[scene_dir.name][i]
        p = self.origins(scene_dir)[t, i]
        return (p + s * (V.astype(np.float64) - p)).astype(V.dtype)


def from_config(config: dict) -> Optional[ObjectScale]:
    f = (config.get('data', {}) or {}).get('object_scale_file')
    return ObjectScale(f) if f else None


def check_object_counts(object_scale: 'ObjectScale', data_path, scenes) -> None:
    """For every scene: as many sizes as objects in frame_00000; object 0's frame-0 origin equal to the sidecar's
    fingerprint (MOVi-A and MOVi-Sphere share seeds and hence object counts, so the counts alone cannot tell them
    apart); and every object's largest frame-0 vertex distance from its origin equal to the sidecar's radius0, i.e. the
    stored vertices are still the unscaled ones the sizes apply to (data re-converted with the size would otherwise be
    scaled twice)."""
    import os
    bad = []
    for sc in scenes:
        if sc not in object_scale.scales:
            bad.append(f'{sc}: missing')
            continue
        frame0 = load_frame(Path(data_path) / sc, 0)          # either layout (src/data/scene_frames.py)
        n = len(frame0)
        if n != len(object_scale.scales[sc]):
            bad.append(f'{sc}: {n} objects vs {len(object_scale.scales[sc])} sizes')
            continue
        if sc not in object_scale.fingerprint or sc not in object_scale.radius0:
            bad.append(f'{sc}: no fingerprint / radius0 in the sidecar')
            continue
        p0 = np.load(os.path.join(str(data_path), sc, 'vis_traj.npy'))[0, 0, :, :3].astype(np.float64)   # (Nobj, 3)
        if not np.abs(p0[0] - object_scale.fingerprint[sc]).max() <= 1e-4:     # (a NaN fails this test too)
            bad.append(f'{sc}: frame-0 origin {p0[0].round(4).tolist()} vs fingerprint {object_scale.fingerprint[sc].tolist()}')
            continue
        r = np.array([np.linalg.norm(frame0[o].astype(np.float64) - p0[o], axis=1).max() for o in range(n)])
        if not np.abs(r - object_scale.radius0[sc]).max() <= 1e-4:            # (a NaN fails this test too)
            bad.append(f'{sc}: frame-0 vertex radii {r.round(4).tolist()} vs radius0 {object_scale.radius0[sc].tolist()} '
                       f'(sizes {object_scale.scales[sc].round(4).tolist()})')
    if bad:
        raise ValueError(f'{object_scale.scale_file}: {len(bad)} scene(s) do not match the data, e.g. {bad[:3]}')