File size: 10,426 Bytes
e6f80f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""InternScenes layouts. This module = InternScenes-Gen (SOURCE InternScenes_gen) plus the shared reader for the
Real2Sim re-layouts of real scans (modules internscenes_{scannet,3rscan,arkit,mp3d}).

Export (kits/extract_internscenes*_json.py): meters, Z-up, bbox = [x, y, z, sx, sy, sz, a_z, a_x, a_y] with
position = box CENTER, sizes along the box local axes, ZXY Euler (R = Rz(a_z) Rx(a_x) Ry(a_y), EmbodiedScan /
pytorch3d convention). Floor is at z = 0.
Boundary: Real2Sim = 2D convex hull of the floor mesh (-> "hull"); Gen = boundary_points.json floor polygon.
Some boxes are stored with a 90/180 deg X/Y rotation (Y-up assets, books lying flat); they are upright boxes
with permuted axes and are re-expressed as yaw-only boxes (`upright`). Boxes tilted by more than TILT_TOL_DEG
are replaced by the yaw-aligned bounding box of their 8 corners (`tilted_bbox`: true footprint, height and bottom,
yaw = heading of the local X axis) with rotation x/y = None so convert_room flags them tilted; "tilt_deg" keeps
the angle between the local axis closest to vertical and world Z.
"""
import csv
import io
import json
import math
import os
import zipfile

import numpy as np
from shapely.geometry import Polygon

from fastfill.adapters.unified import convert_room, iter_records

SOURCE = "InternScenes_gen"
DIR = "imChuling__3D_Room_Collections/InternScenes_exported"
TILT_TOL_DEG = 5.0
MAX_SIZE_M = 20.0
# ponytail: one offset for all Real2Sim/Gen assets, set from the inspect_ir front test (see module docstrings)
FRONT_OFFSET_DEG = 0.0


def rot_zxy(az, ax, ay):
    cz, sz, cx, sx, cy, sy = math.cos(az), math.sin(az), math.cos(ax), math.sin(ax), math.cos(ay), math.sin(ay)
    rz = np.array([[cz, -sz, 0], [sz, cz, 0], [0, 0, 1]])
    rx = np.array([[1, 0, 0], [0, cx, -sx], [0, sx, cx]])
    ry = np.array([[cy, 0, sy], [0, 1, 0], [-sy, 0, cy]])
    return rz @ rx @ ry


def upright(R, size, tol_deg=TILT_TOL_DEG):
    """World-from-local rotation + local sizes -> (sizes [sx, sy, sz], yaw_deg, k) of the same box as an upright
    box, k = index of the source local axis that is vertical; None if no local axis is within tol of vertical.
    The new local X is the source local X when it stays horizontal (keeps its front), else the source local Y."""
    R = np.asarray(R, dtype=float)
    k = int(np.argmax(np.abs(R[2])))
    if abs(R[2, k]) < math.cos(math.radians(tol_deg)):
        return None
    hx, hy = [i for i in range(3) if i != k]          # k=2: (0,1)  k=1: (0,2)  k=0: (1,2)
    v = R[:, hx]
    return [size[hx], size[hy], size[k]], math.degrees(math.atan2(v[1], v[0])), k


def tilted_bbox(R, size):
    """World-from-local rotation + local sizes of a box that is NOT upright within tolerance -> (sizes [w, l, h] of
    the yaw-aligned bounding box of its 8 corners, yaw_deg, k, tilt_deg); k = local axis closest to vertical, tilt =
    its angle from world Z; yaw = heading of local X, or of local Y when X is the near-vertical axis (as upright()
    does: X's small horizontal part is then only the lean direction, and a box aligned to it is up to 3x too big)."""
    R = np.asarray(R, dtype=float)
    k = int(np.argmax(np.abs(R[2])))
    a = next((c for c in ((R[:2, 1], R[:2, 0]) if k == 0 else (R[:2, 0], R[:2, 1])) if np.hypot(*c) >= 1e-6), (1.0, 0.0))
    yaw = math.atan2(a[1], a[0])
    D = np.array([[math.cos(yaw), math.sin(yaw), 0], [-math.sin(yaw), math.cos(yaw), 0], [0, 0, 1]])   # u, v, z
    ext = np.abs(D @ R) @ np.asarray(size, dtype=float)      # max - min of the (origin-symmetric) corners along u, v, z
    return ext.tolist(), math.degrees(yaw), k, math.degrees(math.acos(min(1.0, abs(R[2, k]))))


def canon(f):
    """Exported furniture -> same dict with an upright {x:0,y:0,z:yaw} rotation and permuted sizes when the box is
    upright within tolerance; otherwise its yaw-aligned bounding box (`tilted_bbox`) with {x:None,y:None,z:yaw}
    (convert_room flags it tilted) and "tilt_deg". The centre is unchanged either way."""
    b = f["source_fields"]["bbox"]
    if max(b[3:6]) > MAX_SIZE_M:                 # corrupt box (2 Gen beds are 1910 m / 803014 m tall)
        return {**f, "furniture_size": None}
    R = rot_zxy(b[6], b[7], b[8])
    up = upright(R, b[3:6])
    if up is None:
        (sx, sy, sz), yaw, k, tilt = tilted_bbox(R, b[3:6])
        rot, extra = {"x": None, "y": None, "z": yaw}, {"tilt_deg": tilt}
    else:
        (sx, sy, sz), yaw, k = up
        rot, extra = {"x": 0.0, "y": 0.0, "z": yaw}, {}
    return {**f, "furniture_size": {"width": sx, "length": sy, "height": sz}, "furniture_rotation": rot,
            "vertical_axis": k, **extra}


def convert(scene, source, uid, group, boundary_type, meta):
    room = scene["rooms"][0]
    room = {**room, "furniture": [canon(f) for f in room["furniture"]]}
    ir = convert_room(room, source=source, uid=uid, group=group, subset=scene.get("source_subset"),
                      boundary_type=boundary_type, center_z=True, front_offset_deg=FRONT_OFFSET_DEG,
                      meta={"scene_id": scene["scene_id"], "source_layout": scene["source_layout"],
                            "source_dataset": scene["source_dataset"], "is_true_room": room.get("is_true_room"),
                            "tilt_tol_deg": TILT_TOL_DEG, "front_known": True,
                            "n_permuted_axes": sum(f.get("vertical_axis", 2) != 2 for f in room["furniture"]),
                            **meta})
    return ir


def load_real(root, fname, source, group_of, extra_meta=None):
    for s in iter_records(os.path.join(root, DIR, fname)):
        yield convert(s, source, uid=f"{source}::{s['scene_id']}", group=group_of(s), boundary_type="hull",
                      meta=(extra_meta(s) if extra_meta else {}))


def rscan_reference(root):
    """3RScan scan id (reference or rescan) -> reference scan id."""
    m = json.load(open(os.path.join(root, "imChuling__3D_Room_Collections/3RScan_exported/source_metadata/3RScan.json")))
    ref = {}
    for env in m:
        ref[env["reference"]] = env["reference"]
        for sc in env.get("scans", []):
            ref[sc["reference"]] = env["reference"]
    return ref


def arkit_visits(root):
    """ARKitScenes video_id -> visit_id (None when 'NA')."""
    z = zipfile.ZipFile(os.path.join(root, "imChuling__3D_Room_Collections/ARKitScenes_exported/source.zip"))
    rows = csv.DictReader(io.TextIOWrapper(z.open("source/metadata.csv"), encoding="utf-8"))
    return {r["video_id"]: (None if r["visit_id"] in ("", "NA") else r["visit_id"]) for r in rows}


def load(root):
    """Gen regions: the floor is not at z = 0 (0.10-0.15 m, floor slab); every region has >= 1 door and all
    doors of a region share one bottom height, so z is shifted by the median door bottom."""
    for s in iter_records(os.path.join(root, DIR, "internscenes_gen_exported.json")):
        rt, rid = s["rooms"][0]["room_type"], s["source_scene_name"]
        # boundary_points are wall samples every 0.7 m with the first point repeated: keep the corners only
        # (median 27 -> 6 vertices; every polygon is valid, max shift 1 cm)
        poly = Polygon(s["rooms"][0]["room_boundary"]).simplify(0.01)
        s["rooms"][0]["room_boundary"] = [list(c) for c in poly.exterior.coords[:-1]]
        ir = convert(s, SOURCE, uid=f"{SOURCE}::{rt}/{rid}", group=f"internscenes:gen/{rid}",   # one id = one generated house across room-type folders
                     boundary_type="polygon", meta={"asset_source": "Infinigen", "boundary_simplify_m": 0.01})
        doors = sorted(o["pos"][2] for o in ir["objects"] if o["category"] == "door")
        fz = doors[len(doors) // 2] if doors else None
        for o in ir["objects"]:
            o["pos"][2] -= fz or 0.0
        ir["meta"].update(floor_z=fz, floor_z_source="median door bottom" if doors else None)
        yield ir


if __name__ == "__main__":
    # book lying flat: local X vertical -> upright box, height = source width
    R = rot_zxy(0.3, 0.0, math.pi / 2)
    size, yaw, k = upright(R, [0.03, 0.2, 0.3])
    assert k == 0 and size == [0.2, 0.3, 0.03] and abs(yaw - math.degrees(0.3) - 90) < 1e-6, (size, yaw, k)
    # Y-up chair stored with a_x = -90: local Y vertical, local X keeps the yaw
    size, yaw, k = upright(rot_zxy(0.0, -math.pi / 2, math.radians(40)), [0.7, 0.8, 0.6])
    assert k == 1 and size == [0.7, 0.6, 0.8] and abs(yaw + 40) < 1e-6, (size, yaw)
    assert upright(rot_zxy(0.0, math.radians(10), 0.0), [1, 1, 1]) is None

    # canon: sizes [1.0, 0.2, 0.1], ZXY angles in degrees, centre z
    S = [1.0, 0.2, 0.1]

    def rec(ax, ay, az=0.0, z=0.0):
        return {"furniture_category": "box", "furniture_position": {"x": 1.0, "y": 1.0, "z": z},
                "source_fields": {"bbox": [1.0, 1.0, z, *S, math.radians(az), math.radians(ax), math.radians(ay)]}}

    def sz(c):
        return [c["furniture_size"][key] for key in ("width", "length", "height")]

    # (1) upright box yawed 30 deg: unchanged sizes, yaw 30
    c = canon(rec(0, 0, az=30))
    assert sz(c) == S and abs(c["furniture_rotation"]["z"] - 30) < 1e-9 and "tilt_deg" not in c, c
    # (2) tilted 30 deg about X: yaw-aligned bounding box, yaw 0
    s30, c30 = math.sin(math.radians(30)), math.cos(math.radians(30))
    h, l = 0.2 * s30 + 0.1 * c30, 0.2 * c30 + 0.1 * s30              # 0.1866, 0.2232
    c = canon(rec(30, 0))
    assert np.allclose(sz(c), [1.0, l, h]) and abs(c["furniture_rotation"]["z"]) < 1e-9, c
    assert c["furniture_rotation"]["x"] is None and c["vertical_axis"] == 2 and abs(c["tilt_deg"] - 30) < 1e-9, c
    # (3) lying flat (a_x = 90): permuted upright box via upright(), not the tilted path
    c = canon(rec(90, 0))
    assert np.allclose(sz(c), [1.0, 0.1, 0.2]) and c["vertical_axis"] == 1 and "tilt_deg" not in c, c
    assert c["furniture_rotation"]["x"] == 0.0 and abs(c["furniture_rotation"]["z"]) < 1e-9, c
    # (4) tilted box centred at z = 0.2 -> convert_room bottom = 0.2 - h/2, flagged tilted
    scene = {"scene_id": "s", "source_layout": "l", "source_dataset": "d",
             "rooms": [{"room_type": "t", "room_boundary": [[0, 0], [4, 0], [4, 4], [0, 4]], "furniture": [rec(30, 0, z=0.2)]}]}
    ir = convert(scene, "t", uid="u", group="g", boundary_type="polygon", meta={})
    o = ir["objects"][0]
    assert o["tilted"] and abs(o["pos"][2] - (0.2 - h / 2)) < 1e-9 and ir["meta"]["n_tilted"] == 1, (o, ir["meta"])
    print("internscenes.py self-check ok")