Fastfill-test / code /fastfill /adapters /structured3d.py
XXXpilar's picture
FastFill v4 base (step 6552): LoRA adapter, geometry heads, tokenizer, training code, predict.py
e6f80f3 verified
Raw History Blame Contribute Delete
8.56 kB
"""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")