File size: 8,556 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
"""Structured3D (imChuling export, structured3d.jsonl: 22,894 rooms / 3,488 houses, official train/val/test).

Export (kits/convert_structured3d.py): meters, Z-up, room-local (floor polygon centroid, z from floor_z);
position = bbox_3d centroid (box center); the exported size/yaw come from a heuristic axis canonicalization
(row most aligned with world X), which discards the model frame. The raw bbox_3d fields are kept per object:
raw_basis rows = box axes in world, raw_coeffs = half extents (mm) along those rows. Row 2 is the model's up;
wall-contact statistics on beds / TVs / pictures / toilets put the back on raw +Y, so front = raw -Y.
Objects whose raw row 2 is not world +Z (~31k, almost all 'unknown'/'otherprop') are tilted relative to their
model pose -> tilted=True (raw_basis kept), boxed by the upright envelope of their rotated corners. Yaw follows
raw -Y when horizontal; when that front is near vertical, use raw X and mark front_known=False.
Category 'unknown' (41%: occluded in every view, or the corrupt
perspective_full_09.zip for scenes 1600-1799) keeps its box with category "unknown": build.prep drops it as a
generic label and rejects the room only if dropped floor boxes leave a real hole (hidden_floor_obstacle).
room_type 'undefined' -> None.
Boundary = floor polygon traced from annotation_3d junctions/lines.
Doors/windows: room_context.doors/windows (same room frame, position = opening centre on the wall centreline,
~0.1 m outside the floor polygon; width, height, no direction/thickness) -> structure boxes, see `openings`.
"""
import math
import os

import numpy as np

from fastfill.adapters.internscenes import tilted_bbox
from fastfill.adapters.unified import convert_room, iter_records

SOURCE = "Structured3D"


def _furniture(o):
    B = np.array(o["raw_basis"], float)
    ext = 2 * np.abs(np.array(o["raw_coeffs"], float)) / 1000
    f = {"furniture_category": o["category"],
         "furniture_instance_id": str(o["instance_id"]), "furniture_position": o["position"],
         "furniture_size": {"width": ext[1], "length": ext[0], "height": ext[2]}, "raw_basis": o["raw_basis"]}
    if B[2, 2] < 0.999:                                   # model up is not world up
        R = np.stack([-B[1], B[0], B[2]], axis=1)          # semantic front first
        size, yaw, k, _ = tilted_bbox(R, ext[[1, 0, 2]])
        return {**f, "furniture_size": dict(zip(("width", "length", "height"), size)),
                "furniture_rotation": {"x": None, "y": None, "z": yaw},
                **({"front_known": False} if k == 0 else {})}
    front = -B[1]
    return {**f, "furniture_rotation": {"x": 0, "y": 0, "z": math.degrees(math.atan2(front[1], front[0]))}}


OPENING_T = 0.10     # opening box thickness (the export gives none)
SAME_M = 0.05        # an object box centred this close (xy and z) to an opening, same size, is that opening's box


def _edge_deg(poly, x, y):
    """Direction (deg) of the floor-polygon edge nearest to (x, y)."""
    def dist(a, b):
        dx, dy = b[0] - a[0], b[1] - a[1]
        t = min(1.0, max(0.0, ((x - a[0]) * dx + (y - a[1]) * dy) / (dx * dx + dy * dy or 1)))
        return math.hypot(x - a[0] - t * dx, y - a[1] - t * dy)
    a, b = min(zip(poly, poly[1:] + poly[:1]), key=lambda e: dist(*e))
    return math.degrees(math.atan2(b[1] - a[1], b[0] - a[0]))


def openings(rc, objs):
    """room_context doors/windows -> (structure furniture, objs without the boxes they replace). Box = [width along
    the wall, OPENING_T, height] centred at the given position (z = centre -> bottom = sill), yaw = nearest floor
    edge. ~15% of openings already carry an object box (unknown / otherstructure, same centre and size): that box is
    replaced by the labelled opening instead of duplicated."""
    out, dup = [], set()
    for kind in ("door", "window"):
        for k, d in enumerate(rc.get(kind + "s") or []):
            p, wh = d["position"], sorted([d["width"], d["height"]])
            for i, o in enumerate(objs):
                q, ext = o["position"], sorted(2 * abs(c) / 1000 for c in o["raw_coeffs"])[1:]
                if math.hypot(q["x"] - p["x"], q["y"] - p["y"]) < SAME_M and abs(q["z"] - p["z"]) < SAME_M \
                        and all(abs(u - v) < 0.3 for u, v in zip(ext, wh)):
                    dup.add(i)
            out.append({"furniture_category": kind, "furniture_instance_id": f"{kind}_{k}", "structure": True,
                        "furniture_position": p,
                        "furniture_rotation": {"x": 0, "y": 0, "z": _edge_deg(rc["floor_polygon"], p["x"], p["y"])},
                        "furniture_size": {"width": d["width"], "length": OPENING_T, "height": d["height"]}})
    return out, [o for i, o in enumerate(objs) if i not in dup]


def convert(rec):
    rc, prov = rec["room_context"], rec["provenance"]
    ops, objs = openings(rc, rec["layout"]["floor_layout"]["objects"])
    room = {"room_id": rc["room_id"], "room_type": None if rc["room_type"] == "undefined" else rc["room_type"],
            "room_boundary": rc["floor_polygon"], "room_height": rc["ceiling_height_m"],
            "furniture": [_furniture(o) for o in objs] + ops}
    return convert_room(room, source=SOURCE, uid=rec["sample_id"], group=f"structured3d:{prov['source_house_id']}",
                        subset=prov["split"], boundary_type="polygon", center_z=True,
                        extra=lambda f: {"structure": True} if f.get("structure") else
                        {**({"raw_basis": f["raw_basis"]} if f["furniture_rotation"]["x"] is None else {}),
                         **({"front_known": False} if f.get("front_known") is False else {})},
                        meta={"front_known": True, "room_origin_house_m": rec["room_origin_house_m"],
                              "n_nearest_assigned": sum(o.get("assignment_method") == "nearest" for o in objs),
                              "n_unknown_category": sum(o["category"] == "unknown" for o in objs)})


def load(root):
    path = os.path.join(root, "imChuling__3D_Room_Collections/Structured3D_exported/structured3d.jsonl")
    for rec in iter_records(path):
        yield convert(rec)


if __name__ == "__main__":
    # bed rotated 90 deg: raw row0 = world +Y, row1 = world -X (back on raw +Y -> world -X) -> front world +X, yaw 0
    f = _furniture({"category": "bed", "instance_id": 1, "position": {"x": 0, "y": 0, "z": 0.5},
                    "raw_basis": [[0, 1, 0], [-1, 0, 0], [0, 0, 1]], "raw_coeffs": [800, 1000, 500]})
    assert abs(f["furniture_rotation"]["z"]) < 1e-9 and f["furniture_size"]["width"] == 2.0, f
    f = _furniture({"category": "unknown", "instance_id": 2, "position": {"x": 0, "y": 0, "z": 1},
                    "raw_basis": [[0, 0, -1], [0, 1, 0], [1, 0, 0]], "raw_coeffs": [1, 1, 1]})
    assert f["furniture_category"] == "unknown" and f["furniture_rotation"]["x"] is None
    # 4 x 3 room; door on the bottom wall already boxed as 'otherstructure' (replaced, not duplicated), window on the
    # right wall (x = 4) 0.1 m outside the floor, centre z 1.5, height 1.4 -> sill 0.8
    rec = {"sample_id": "t", "provenance": {"source_house_id": 0, "split": "train"}, "room_origin_house_m": [0, 0, 0],
           "room_context": {"room_id": "r", "room_type": "undefined", "ceiling_height_m": 2.8,
                            "floor_polygon": [[0, 0], [4, 0], [4, 3], [0, 3]],
                            "doors": [{"position": {"x": 1, "y": -0.06, "z": 1.1}, "width": 0.7, "height": 2.2}],
                            "windows": [{"position": {"x": 4.1, "y": 1.5, "z": 1.5}, "width": 1.6, "height": 1.4}]},
           "layout": {"floor_layout": {"objects": [
               {"category": "otherstructure", "instance_id": 9, "position": {"x": 1, "y": -0.06, "z": 1.1},
                "raw_basis": [[1, 0, 0], [0, 1, 0], [0, 0, 1]], "raw_coeffs": [350, 60, 1100]}]}}}
    ir = convert(rec)
    door, win = ir["objects"]
    close = lambda a, b: all(abs(x - y) < 1e-6 for x, y in zip(a, b))
    assert door["category"] == "door" and door["structure"] and close(door["pos"], [1, -0.06, 0]), door
    assert close(door["size"], [0.7, OPENING_T, 2.2]) and abs(door["yaw"]) < 1e-9, door
    assert win["category"] == "window" and close(win["pos"], [4.1, 1.5, 0.8]) and close(win["size"], [1.6, OPENING_T, 1.4])
    assert abs(win["yaw"] - math.pi / 2) < 1e-9 and not win["tilted"], win
    from fastfill.anchors import fixed_geometry
    assert {o["category"] for o in fixed_geometry(ir)} == {"door", "window"}
    print("structured3d.py self-check ok")