Fastfill-test / code /fastfill /adapters /multiscan.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
7.81 kB
"""MultiScan (XXXpilar/multiscan-clean repackaging): 273 scans of 117 real spaces, one scan = one space.
objects.csv: obb_center (box CENTER), obb_half_extents (HALF sizes along the box axes), obb_axes = 9 floats =
the three box axes one after another (world directions), plus annotated `front` / `up` vectors; `up` is box
axis 1 for 10,957/10,957 objects and `front` is one of the box axes. Z-up meters, scan-local frame whose floor
is NOT at z = 0. regions.csv floor_height is the TOP of the floor-slab AABB (slabs are 0.07-0.46 m thick scan
noise) and furniture bottoms sit a median 0.09 m below it, so the floor level used is the median bottom of the
objects that relations.csv marks rests_on_floor (>= 1 per scan; geometric relation of the repackaging).
Boundary: regions.csv poly_loop is the convex hull of the floor-slab OBBs, but floors are annotated only where
the scan saw them (22% of in-scope objects stick out of it by a median 0.24 m). The boundary used is the convex
hull of poly_loop + the wall-segment endpoints (structure only) -> "hull" when every floor-standing
non-architectural object fits inside it (+5 cm); otherwise the room gets boundary_type "proxy" with that hull
extended by those objects' footprints (object extent, cannot prove in-bounds validity).
height = ceiling_height - floor level, only when height_reliable. Group = multiscan:<scene_id> (rescans _NN share it).
"""
import csv
import json
import math
import os
import numpy as np
from shapely.geometry import MultiPoint, Polygon
from fastfill.adapters.internscenes import TILT_TOL_DEG, tilted_bbox, upright
from fastfill.adapters.unified import convert_room
from fastfill.scene import footprint
SOURCE = "MultiScan"
DIR = "XXXpilar__multiscan-clean"
def box(o):
"""objects.csv row -> unified furniture dict (bottom-center position, upright yaw from the annotated front)."""
axes = np.array(json.loads(o["obb_axes"]), dtype=float).reshape(3, 3) # rows = box axes
size = [2 * v for v in json.loads(o["obb_half_extents"])]
c = json.loads(o["obb_center"])
fr = np.array(json.loads(o["front"]), dtype=float)
d = axes @ fr
f = int(np.argmax(np.abs(d)))
order = [f] + [i for i in range(3) if i != f] # front axis first = local +X
R = np.stack([axes[i] * (np.sign(d[f]) if i == f else 1) for i in order], axis=1)
up = upright(R, [size[i] for i in order])
if up is None: # tilted: keep a non-zero tilt
sz, yaw, k, tilt = tilted_bbox(R, [size[i] for i in order])
rot = {"x": tilt, "y": 0.0, "z": yaw}
else:
sz, yaw, k = up
rot = {"x": 0.0, "y": 0.0, "z": yaw}
bottom = c[2] - sz[2] / 2
return {"furniture_category": o["category"].replace("_", " "), "furniture_instance_id": f"o{o['object_id']}",
"furniture_position": {"x": c[0], "y": c[1], "z": bottom}, "furniture_rotation": rot,
"furniture_size": {"width": sz[0], "length": sz[1], "height": sz[2]},
"front_vertical": k == 0,
"structure": o["is_architectural"] == "True" or o["is_opening"] == "True"}
def boundary(g, furn, tol=0.05):
"""(polygon, type, n_outside) - see module docstring."""
walls = json.loads(g["wall_segments"] or "[]")
pts = json.loads(g["poly_loop"]) + [w[k] for w in walls for k in ("start", "end")]
hull = MultiPoint([tuple(p) for p in pts]).convex_hull
fps = [Polygon(footprint({"pos": [f["furniture_position"]["x"], f["furniture_position"]["y"]],
"size": [f["furniture_size"]["width"], f["furniture_size"]["length"]],
"yaw": math.radians(f["furniture_rotation"]["z"])}))
for f in furn if not f["structure"] and f["furniture_position"]["z"] < tol]
out = [fp for fp in fps if not hull.buffer(tol).contains(fp)]
if out:
hull = MultiPoint(list(hull.exterior.coords) + [c for fp in out for c in fp.exterior.coords]).convex_hull
return [list(c) for c in hull.exterior.coords[:-1]], ("proxy" if out else "hull"), len(out)
def load(root):
base = os.path.join(root, DIR)
scans = {r["scan_id"]: r for r in csv.DictReader(open(os.path.join(base, "scans.csv")))}
objs, on_floor = {}, {}
for o in csv.DictReader(open(os.path.join(base, "objects.csv"))):
objs.setdefault(o["scan_id"], []).append(o)
for r in csv.DictReader(open(os.path.join(base, "relations.csv"))):
if r["relation"] == "rests_on_floor":
on_floor.setdefault(r["scan_id"], set()).add(r["subject_id"])
for g in csv.DictReader(open(os.path.join(base, "regions.csv"))):
sid, s = g["scan_id"], scans[g["scan_id"]]
bottoms = [json.loads(o["aabb_min"])[2] for o in objs.get(sid, []) if o["object_id"] in on_floor.get(sid, ())]
fz = float(np.median(bottoms)) if bottoms else float(g["floor_height"])
furn = [box(o) for o in objs.get(sid, [])]
for f in furn:
f["furniture_position"]["z"] -= fz
poly, btype, n_out = boundary(g, furn)
# source flags is_architectural (wall/floor/ceiling/beam/pillar/windowsill/...) and is_opening are kept as
# "structure": build never places them but must see them (a floor pillar rejects the room)
room = {"room_id": sid, "room_type": s["room_type"].lower(), "room_boundary": poly,
# extrusion_height = ceiling - floor_height (slab top); measure from the floor level used for z
"room_height": float(g["ceiling_height"]) - fz if g["height_reliable"] == "True" else None,
"furniture": furn}
yield convert_room(room, source=SOURCE, uid=f"{SOURCE}::{sid}", group=f"multiscan:{s['scene_id']}",
boundary_type=btype, extra=lambda f: {
**({"structure": True} if f["structure"] else {}),
**({"front_known": False} if f["front_vertical"] else {})},
meta={"scan_id": sid, "scene_id": s["scene_id"], "device": s["device"],
"floor_z": fz, "floor_z_source": "median bottom of rests_on_floor objects" if bottoms
else "regions.floor_height", "floor_height": float(g["floor_height"]), "poly_source": g["poly_source"],
"boundary_source": "convex hull of floor slabs + wall segments"
+ (" + floor objects outside it" if n_out else ""), "n_floor_objects_outside_structure": n_out,
"height_reliable": g["height_reliable"] == "True", "front_known": True,
"front_source": "annotated front vector", "tilt_tol_deg": TILT_TOL_DEG,
"n_front_vertical": sum(f["front_vertical"] for f in furn)})
if __name__ == "__main__":
# bag from scene_00000_00: up = axis 1, front = axis 2 -> yaw of the front vector, size [2*h2, 2*h0, 2*h1]
row = {"obb_axes": "[0.9861942396120026, 0.16559263798884283, 0, 0, 0, 1, 0.16559263798884283, -0.9861942396120026, 0]",
"obb_half_extents": "[0.126, 0.166, 0.0795]", "obb_center": "[0, 0, 1]", "category": "shoe_box",
"front": "[0.16559263798884283, -0.9861942396120026, 0]", "object_id": "2",
"is_architectural": "False", "is_opening": "False"}
b = box(row)
assert b["furniture_category"] == "shoe box" and b["furniture_rotation"]["x"] == 0
assert abs(b["furniture_rotation"]["z"] - math.degrees(math.atan2(-0.98619, 0.16559))) < 1e-3
assert [round(v, 3) for v in b["furniture_size"].values()] == [0.159, 0.252, 0.332]
assert abs(b["furniture_position"]["z"] - (1 - 0.166)) < 1e-9
print("multiscan.py self-check ok")