Fastfill-test / code /fastfill /anchors.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
17.7 kB
"""Object scope for training: drop structure, infer support anchors, keep a support-closed subset.
anchor = "floor" | "object" | "wall" | "ceiling" | "fixed". Adapters set anchor/parent from source evidence when the
source has it (placement flags, explicit parent ids); otherwise `infer_anchors` derives them from boxes:
ceiling hanging categories (pendant, chandelier, ...) whatever their z or source anchor (one at z=0 is
misplaced), or top within 10 cm of the room height above 1 m
floor bottom at most FLOOR_Z = 10 cm above z=0 (sunk boxes too, so build.prep can drop and count them), or
floor furniture (bed, table, sofa, ...) at most FURNITURE_FLOOR_Z = 50 cm up with nothing under it
(annotation z-noise, boxes that start above the legs; build.prep snaps them to the floor)
object bottom within 5 cm of the top of a floor/object box whose footprint contains its center;
seats, beds and rugs are never parents (bbox top = backrest / headboard; a rug is not a surface)
wall wall art (painting, mirror, ...) with no support below, or anything else elevated without one
fixed floor furniture that can hang (cabinet, shelf, vanity...) floating <= 50 cm with nothing below in a
designer (polygon) room: a wall-hung unit, out of scope to place, shown to the model as a fixed box
Box-level contact is weak evidence of support; build.py counts evidence and inferred supports separately.
`fixed_geometry` picks the structure boxes the model is told about (never placed): doors, windows, columns, stairs,
fireplaces, radiators, partitions, low beams... below FIXED_MAX_Z; surface finishes, sockets/drains, curtains and
slabs covering most of the room are left out, and a "wall" box only counts when it stands inside the room.
"""
import math
import re
from shapely.geometry import Point, Polygon
from fastfill.scene import footprint, head, norm_cat
FLOOR_Z = 0.10 # bottoms up to 10 cm above the floor count as standing on it (build.prep snaps them to z=0)
# Strong architectural words mark structure anywhere in the name; floor/wall/ceiling/column are usually modifiers
# (plant_floor, floor lamp, wall_shelving_unit, locker_column) and mark structure only in purely architectural
# names ("wall", "Wall design", "floor mold").
STRUCTURE = re.compile(r"\b(door|window|curtain|drape|blind|beam|pillar|stair|staircase|stairway|stairwell|railing|rail|"
r"handrail|banister|guardrail|balustrade|baseboard|radiator|opening|doorframe|partition|fireplace|"
r"chimney|otherstructure|structure|doorway|arch|archway|bulkhead|niche|fence|step|ramp|elevator|duct|"
r"drain|pipe|socket|outlet|tile)s?\b")
# furniture whose name borrows a structure word: fireplace_console, pillar_candle, clothes rail, portable_partition_panel
NOT_STRUCTURE = re.compile(r"\b(lamp|light|fan|cabinet|shelf|mat|rug|towel|chair|table|console|stool|candle|figurine|box|"
r"plant|planter|basket|display|station|clothes|freestanding|portable|ladder|bench|sofa|couch|"
r"bed|wardrobe|desk|seat|bookcase|bookshelf|dresser|sideboard|ottoman)s?\b")
SURFACE = r"(wall|floor|ceiling|column)s?"
ARCH = (r"(panel|paneling|design|combination|mold|molding|moulding|background|trim|skirting|board|tile|cladding|covering|"
r"decoration|sticker|styling|decorative|decor|structural|support|concrete|low|half|suspended)s?")
HANGING = re.compile(r"\b(pendant|chandelier|ceiling|downlight|hanging)s?\b")
NOT_HANGING = re.compile(r"\b(chair|stool|seat|swing)\b") # a hanging chair stands on its own frame
# soft covers draped over a bed or sofa (their boxes cover the bed): not objects to place, not floor obstacles
SOFT_COVERS = {"bedding", "comforter", "blanket", "quilt", "duvet", "bedspread", "bedcover", "coverlet"}
# floor furniture whose box bottom sits up to FURNITURE_FLOOR_Z high with nothing under it stands on the floor
# (annotation z-noise, boxes that start above the legs); without this it fell through to 'wall' and vanished
FLOOR_FURNITURE = {"bed", "sofa", "couch", "loveseat", "sectional", "table", "desk", "chair", "armchair", "stool", "bench",
"ottoman", "cabinet", "wardrobe", "dresser", "bookcase", "bookshelf", "sideboard", "nightstand",
"fridge", "refrigerator", "oven", "stove", "dishwasher", "washer", "dryer", "piano", "tub",
"bathtub", "toilet", "console", "stand", "chest", "armoire", "shelf", "cupboard"}
FURNITURE_FLOOR_Z = 0.50 # floating up to here with nothing below: a floor piece (snapped to z=0 by build.prep)
FURNITURE_SUNK_Z = 0.30 # any floor box sunk up to here: same (deeper boxes belong to a lower storey)
# furniture that cannot hang on a wall: floating with nothing below it is on the floor with a noisy z anywhere
GROUND_ONLY = {"bed", "sofa", "couch", "loveseat", "sectional", "chair", "armchair", "table", "desk", "fridge",
"refrigerator", "oven", "stove", "dishwasher", "stool", "bench", "ottoman", "piano", "tub", "bathtub",
"washer", "dryer", "wardrobe", "armoire", "bookcase", "dresser"}
SOFT_HANGINGS = {"curtain", "curtains", "drape", "drapes", "blind", "blinds", "shade", "shades", "screening", "valance"}
# structure that has no footprint of its own: finishes, fixtures, the room's own slabs
FINISH = {"floor", "ceiling", "tile", "tiles", "baseboard", "skirting", "trim", "mold", "molding", "moulding", "panel",
"paneling", "board", "boards", "cladding", "covering", "socket", "outlet", "drain", "pipe", "pipes", "design",
"decoration", "sticker", "background", "wallpaper"}
FIXED_MAX_Z = 1.5 # structure whose bottom is higher does not shape the floor layout (ceiling beams, ducts)
DIVIDERS = {"partition", "divider", "screen"} # a "partition panel" is an obstacle, not a wall finish
# scans: box z is noisy, so floating floor furniture is on the floor (not a wall-hung unit), whatever the boundary type
SCANNED = {"Scan2CAD", "MultiScan", "InternScenes_arkit", "InternScenes_scannet", "InternScenes_mp3d",
"InternScenes_3rscan"}
def hanging(category):
nc = norm_cat(category)
return bool(HANGING.search(nc)) and not NOT_HANGING.search(nc)
WALL_ART = re.compile(r"\b(painting|picture|poster|artwork|art|mirror|whiteboard|blackboard|frame)s?\b")
FLAT = {"rug", "carpet", "mat", "doormat", "bathmat", "placemat", "playmat", "floormat", "yogamat", "exercisemat",
"fitnessmat"}
FLAT |= {w + "s" for w in FLAT}
NO_TOP = {"sofa", "couch", "loveseat", "sectional", "bed", "armchair", "chair", "bench"} | FLAT # not a support surface
def is_structure(category):
c = re.sub(r"[^a-z]+", " ", (category or "").lower())
if NOT_STRUCTURE.search(c):
return False
return bool(STRUCTURE.search(c)) or (bool(re.search(rf"\b{SURFACE}\b", c))
and not re.sub(rf"\b({SURFACE}|{ARCH})\b", " ", c).strip())
def infer_anchors(room, tol=0.05):
objs = room["objects"]
fps = {o["id"]: Polygon(footprint(o)) for o in objs}
height = room.get("height")
hull = room.get("boundary_type") == "hull" or room.get("source") in SCANNED
for o in sorted(objs, key=lambda o: o["pos"][2]):
if hanging(o["category"]): # before the floor rule and any source anchor: a chandelier at z=0 is misplaced
o["anchor"] = "ceiling"
continue
if o.get("anchor"):
continue
z, nc = o["pos"][2], norm_cat(o["category"])
if z <= FLOOR_Z: # one-sided: sunk boxes stay floor so build.prep drops and counts them
o["anchor"] = "floor"
continue
c = Point(o["pos"][0], o["pos"][1])
below = [p for p in objs if p is not o and p.get("anchor") in ("floor", "object") and head(p) not in NO_TOP
and abs(p["pos"][2] + p["size"][2] - z) < tol and fps[p["id"]].contains(c)]
if below: # a frame standing on a dresser is on it; art above a sofa is not (NO_TOP)
o["anchor"], o["parent"] = "object", max(below, key=lambda p: p["pos"][2] + p["size"][2])["id"]
o["anchor_inferred"] = True
elif WALL_ART.search(nc):
o["anchor"] = "wall"
elif z <= FURNITURE_FLOOR_Z and head(o) in FLOOR_FURNITURE:
# nothing under it: on the floor with a noisy z (scans; anything that cannot hang), or a wall-hung
# unit in a designer room (vanity, floating TV console): shown as fixed, not placed
o["anchor"] = "floor" if hull or head(o) in GROUND_ONLY else "fixed"
elif height and z + o["size"][2] > height - 2 * tol and z > 1.0:
o["anchor"] = "ceiling"
else:
o["anchor"] = "wall"
return room
def fixed_geometry(room, band=0.10, intrusion=0.05):
"""Structure boxes to show the model as immovable (see module doc). Returns copies with tilted/anchor cleared.
Which of them survive next to the reference layout (covered, outside, slabs) is build.prep's decision."""
out = []
shell = Polygon(room["boundary"]).buffer(0) if room.get("boundary") else None
inner = shell.buffer(-band) if shell is not None else None
hull = room.get("boundary_type") == "hull"
for o in room["objects"]:
if not structural(o):
continue
words, h = set(norm_cat(o["category"]).split()), head(o)
finish = (h in FINISH or h.rstrip("s") in FINISH) and not words & DIVIDERS
if finish or words & SOFT_HANGINGS or o["pos"][2] >= FIXED_MAX_Z:
continue
fp = Polygon(footprint(o))
if shell is not None and fp.area >= 0.8 * shell.area: # a slab under another name
continue
# a "wall" box is the room's own wall unless it stands inside a polygon room (a partition); in a hull room
# the walls stand inside the hull by construction, so none of them is a partition
if h == "wall" and (hull or inner is None or fp.intersection(inner).area < intrusion):
continue
out.append({k: v for k, v in o.items() if k not in ("anchor", "parent", "tilted", "anchor_inferred")})
return out
def structural(o):
"""Source-flagged architecture, or a structure name on something that does not stand on another object (a
power 'outlet' strip or a lever 'arch' file on a desk is an object, not architecture)."""
return bool(o.get("structure")) or (is_structure(o["category"]) and not o.get("parent"))
def annotate(room):
"""Structure and soft covers removed, anchors inferred: every remaining box with anchor/parent set."""
objs = [dict(o) for o in room["objects"] if not (structural(o) or head(o) in SOFT_COVERS)]
return infer_anchors({**room, "objects": objs})["objects"]
def closed(objs, keep=("floor", "object")):
"""Objects whose anchor is in `keep` and whose support chain is entirely kept, in input order."""
kept, ids = [], set()
for o in sorted(objs, key=lambda o: o["pos"][2]): # parents sit lower than children
if o["anchor"] in keep and (o.get("parent") is None or o["parent"] in ids):
kept.append(o)
ids.add(o["id"])
order = {o["id"]: k for k, o in enumerate(objs)}
return sorted(kept, key=lambda o: order[o["id"]])
def scope_objects(room, keep=("floor", "object")):
"""Structure removed, anchors inferred, objects outside `keep` dropped together with their dependants."""
return {**room, "objects": closed(annotate(room), keep)}
if __name__ == "__main__":
room = {"height": 2.6, "objects": [
{"id": "desk", "category": "desk", "size": [1.2, .6, .75], "pos": [1, 1, 0], "yaw": 0},
{"id": "lamp", "category": "table lamp", "size": [.2, .2, .4], "pos": [1.2, 1, .75], "yaw": 0},
{"id": "pic", "category": "painting", "size": [.8, .05, .6], "pos": [1, 0, 1.4], "yaw": 0},
{"id": "cl", "category": "pendant lamp", "size": [.5, .5, .3], "pos": [2, 2, 2.0], "yaw": 0},
{"id": "sofa", "category": "sofa", "size": [2, .9, .8], "pos": [3, 3, 0], "yaw": 0},
{"id": "art", "category": "picture frame", "size": [.6, .05, .5], "pos": [3, 3, .8], "yaw": 0},
{"id": "vase", "category": "vase", "size": [.1, .1, .2], "pos": [1, 0, 2.0], "yaw": 0, "anchor": "object", "parent": "pic"},
{"id": "win", "category": "window", "size": [1, .1, 1.2], "pos": [3, 0, .9], "yaw": 0}]}
s = scope_objects(room)
assert [o["id"] for o in s["objects"]] == ["desk", "lamp", "sofa"], s
assert s["objects"][1]["parent"] == "desk" and s["objects"][1]["anchor"] == "object"
a = {o["id"]: o["anchor"] for o in infer_anchors({**room, "objects": [dict(o) for o in room["objects"]]})["objects"]}
assert a["pic"] == "wall" and a["cl"] == "ceiling" and a["art"] == "wall", a # art above a sofa is not "on" it
assert is_structure("window") and is_structure("door(screen door, screen)") and not is_structure("floor lamp")
for c in ("wall", "floor", "Wall design", "ceiling combination", "floor mold", "otherstructure", "room_partition",
"radiator", "support_column", "vertical core stair 1", "door(screen door, screen)"):
assert is_structure(c), c
for c in ("plant_floor", "floor_plant_large", "locker_column", "floor_bookshelf", "bookshelf_wall_unit",
"floor-standing air purifier", "wall_hung_toilet", "floor lamp", "wall_cabinet", "ceiling fan", "wall_mirror"):
assert not is_structure(c), c
sofa = {"id": "s", "category": "sofa", "size": [2, .9, .8], "pos": [1, 1, 0.07], "yaw": 0}
rug = {"id": "r", "category": "rug", "size": [3, 2, .02], "pos": [1, 1, 0], "yaw": 0}
got = {o["id"]: (o["anchor"], o.get("parent")) for o in infer_anchors({"objects": [dict(sofa), dict(rug)]})["objects"]}
assert got == {"s": ("floor", None), "r": ("floor", None)}, got # 7 cm up = standing, not on the rug
fx = {"boundary": [[0, 0], [5, 0], [5, 4], [0, 4]], "objects": [
{"id": "d", "category": "door", "size": [.9, .1, 2.1], "pos": [2, 0, 0], "yaw": 0},
{"id": "w", "category": "window", "size": [1.2, .1, 1.3], "pos": [4.98, 2, .9], "yaw": math.pi / 2},
{"id": "c", "category": "support_column", "size": [.3, .3, 2.8], "pos": [2.5, 2, 0], "yaw": 0, "structure": True},
{"id": "b", "category": "beam", "size": [5, .3, .3], "pos": [2.5, 2, 2.5], "yaw": 0},
{"id": "f", "category": "floor", "size": [5, 4, .02], "pos": [2.5, 2, 0], "yaw": 0},
{"id": "k", "category": "curtain", "size": [1.5, .1, 2.4], "pos": [4.95, 2, 0], "yaw": 0},
{"id": "s", "category": "socket", "size": [.08, .02, .08], "pos": [1, 0.01, .3], "yaw": 0},
{"id": "w1", "category": "wall", "size": [4, .1, 2.8], "pos": [2.5, 0.05, 0], "yaw": 0},
{"id": "w2", "category": "wall", "size": [2, .1, 2.8], "pos": [2.5, 2, 0], "yaw": 0},
{"id": "p", "category": "wall panels", "size": [3, .05, 2.8], "pos": [1.5, 0.02, 0], "yaw": 0},
{"id": "sc", "category": "window screening", "size": [1.5, .05, 2.4], "pos": [2, 3.98, 0], "yaw": 0},
{"id": "t", "category": "table", "size": [1.2, .6, .75], "pos": [1, 1, 0], "yaw": 0}]}
assert [o["id"] for o in fixed_geometry(fx)] == ["d", "w", "c", "w2"], fixed_geometry(fx)
assert [o["id"] for o in fixed_geometry({**fx, "boundary_type": "hull"})] == ["d", "w", "c"] # hull: walls are not partitions
assert not is_structure("step ladder") and is_structure("styling column") and is_structure("otherstructure")
assert is_structure("elevator shaft") and is_structure("banister") and not is_structure("floor lamp")
assert not is_structure("elevator_lobby_bench") and is_structure("stair_railing") and is_structure("bedroom_door")
panel = {"boundary": fx["boundary"], "objects": [{"id": "pp", "category": "low_partition_panel", "size": [.3, 1, 1.7],
"pos": [2.5, 2, 0], "yaw": 0}, {"id": "wp", "category": "wall panels",
"size": [3, .05, 2.8], "pos": [1.5, 0.02, 0], "yaw": 0}]}
assert [o["id"] for o in fixed_geometry(panel)] == ["pp"] # a partition panel is fixed, a wall panel is not
# a frame standing on a dresser is on it; a wall-hung cabinet floats -> fixed in a polygon room, floor in a hull
dr = {"id": "dr", "category": "dresser", "size": [1, .5, .8], "pos": [1, 1, 0], "yaw": 0}
fr = {"id": "fr", "category": "picture frame", "size": [.2, .05, .25], "pos": [1, 1, .8], "yaw": 0}
cb = {"id": "cb", "category": "bathroom cabinet", "size": [.6, .4, .5], "pos": [3, 3, .35], "yaw": 0}
bd = {"id": "bd", "category": "bed", "size": [2, 1.6, .5], "pos": [3, 1, .35], "yaw": 0}
got = {o["id"]: (o["anchor"], o.get("parent")) for o in annotate({"boundary_type": "polygon", "objects": [dr, fr, cb, bd]})}
assert got == {"dr": ("floor", None), "fr": ("object", "dr"), "cb": ("fixed", None), "bd": ("floor", None)}, got
got = {o["id"]: o["anchor"] for o in annotate({"boundary_type": "hull", "objects": [dict(cb)]})}
assert got == {"cb": "floor"}, got
assert annotate({"source": "Scan2CAD", "boundary_type": "polygon", "objects": [dict(cb)]})[0]["anchor"] == "floor"
strip = {"id": "ps", "category": "power_outlet_strip", "size": [.3, .06, .04], "pos": [1, 1, .8], "yaw": 0,
"anchor": "object", "parent": "dr"}
assert [o["id"] for o in annotate({"objects": [dict(dr), strip]})] == ["dr", "ps"] and not fixed_geometry(
{"boundary": fx["boundary"], "objects": [dict(dr), strip]})
print("anchors.py self-check ok")