Instructions to use XXXpilar/Fastfill-test with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use XXXpilar/Fastfill-test with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 19,302 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 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | """Remasking decoding (Stage 2 of masked layout training): place a request's objects over several rounds.
MaskGIT / SceneNAT confidence-first iterative decoding at the object level, for a ``pose_conditioning`` model:
1. ``model.encode`` once (backbone + slot pooling); every round only ``model.decode`` runs.
2. Each round decodes every slot with the poses committed so far given (``given_pose`` / ``given_pose_mask``,
``masked_layout.pose_features`` of the committed pose, as in training). Each open slot picks its argmax cell;
its confidence is the softmax probability of that cell under the raw logits (SceneNAT's score of the sampled
token). The most confident open slots are committed so that ``floor(N * cos(pi/2 * (r + 1) / R))`` slots stay
open after round ``r`` of ``R`` (at least one commit per round; none open after the last round).
3. With ``legal`` the cell choice is restricted, per slot, to its least-cost cells under ``cell_costs`` (the
expected-geometry costs: footprint outside the floor, a door's clearance zone for floor objects that can block,
the room's fixed objects, and, round by round, the footprints of the objects already committed whose height interval overlaps the
slot's, as the scorer's collision check counts them). Costs use the slot's own predicted size, yaw and z. Slots of one round are committed in confidence order and each re-checks its cell against the
footprints committed just before it. A slot whose every cell costs something takes its cheapest cells. The
bottom centre decoded in the chosen cell is kept when it is as cheap as the cell's best candidate position
(``cell_costs.cell_positions``), else it snaps to the nearest least-cost candidate.
4. With ``retract`` (SceneNAT's sampler) a committed slot can be taken back: each round ranks the open slots by
their confidence now and the committed ones by their confidence when committed, keeps the most confident so
that the schedule's number stays open, and masks the others again (pose, footprint and score dropped), to be
re-predicted with the poses kept so far. Slots given by the request are never retracted.
5. A ``support_head`` model's support prediction (floor or not) stands in for an undeclared ``support_parent``
in deciding which objects block doors, and its ``support_logits`` ride along with the committed rows.
6. Supports: an object standing on another one (its declared ``support_parent``, a hard ``on`` constraint, or, for a
``support_head`` model, its predicted parent) takes its cell without the legality filter and blocks nothing while
the rounds run (its parent's footprint is no obstacle to it); after the last round ``attach_supports`` puts it on
its parent like ``evaluate.spread_grid_xy`` does: bottom on the parent's top (or kept inside the parent, between
its bottom and ``INNER_M`` below its top: a book on a shelf), centre inside the parent's footprint (the nearest
point of it, in the parent's frame, when the decoded one is outside), parents before their children. One declared
(or predicted) on the floor stands on it. Without this the rounds treated a lamp's own nightstand as an occupied
cell and committed the lamp elsewhere in the room, and argmax serialization never put it back.
The result is a predictions dict like the model's: every slot carries the rows (cell logits restricted to its
legal cells, residuals, z, size, yaw logits and residuals) of the round that committed it, so
``evaluate.serialize_predictions(..., grid_decode="argmax")`` writes exactly the committed poses. ``rounds=1``
without ``legal`` is the plain argmax decode.
Pose features need each slot's yaw symmetry order and K1 flag; batches collated from labelled rows carry them
(``yaw_symmetry_order`` / ``size_axis_swap_allowed``), a bare condition gets the collate defaults (order 1, no swap).
"""
from __future__ import annotations
import math
import numpy as np
import torch
from fastfill.v2.cell_costs import (THIN_HEIGHT_M, RoomRaster, box_corners, cell_cost_maps, cell_cost_options, cell_positions,
footprint_offsets, vertical_extent)
from fastfill.v2.geometry import decode_grid_position, decode_yaw
from fastfill.v2.masked_layout import pose_features
from fastfill.v2.support import decode_support_parents
from fastfill.v2.validation import effective_support_requests
FLOOR_PREDICTION_TOLERANCE_M = .15 # a predicted bottom this close to the floor stands on it
INNER_M = .05 # a child more than this below its parent's top (and not below its bottom) stays inside the parent
ILLEGAL_LOGIT = -1e4
ROW_KEYS = ("position_normalized", "size", "yaw_logits", "yaw_residuals", "position_cell_logits", "position_cell_residuals")
def open_after(total: int, round_index: int, rounds: int) -> int:
"""Slots left open after round ``round_index`` (0-based) of ``rounds``: MaskGIT's cosine schedule."""
return math.floor(total * math.cos(math.pi / 2 * (round_index + 1) / rounds))
def _point_cost(raster: RoomRaster, points: np.ndarray, size, yaw: float, blocks: bool, z_range, options: dict) -> np.ndarray:
"""Cost (as in ``cell_cost_maps``) of the footprint with its bottom centre at each of ``points`` (k x 2)."""
samples = points[:, None, :] + footprint_offsets(size[:2], yaw, options)[None]
outside, door = raster.lookup(samples)
cost = outside.mean(-1) + door.mean(-1) * blocks
occupied = raster.occupied(z_range)
if occupied is not None:
cost = cost + raster.lookup_occupied(samples, occupied).mean(-1)
return cost
def _snap(raster, candidates, decoded, size, yaw, blocks, z_range, options) -> np.ndarray:
"""The decoded bottom centre if it costs no more than the cell's best candidate, else the nearest of the
least-cost candidates: the cell was chosen for some legal position in it, the residual may miss it."""
points = np.vstack((decoded[None], candidates))
cost = _point_cost(raster, points, size, yaw, blocks, z_range, options)
best = cost.min()
if cost[0] <= best + 1e-6:
return decoded
legal = np.flatnonzero(cost <= best + 1e-6)
return points[legal[np.argmin(np.linalg.norm(points[legal] - decoded, axis=1))]]
def _blocks(condition_object: dict, z_world: float, floor_z, height: float, predicted_floor: bool | None = None) -> bool:
"""Floor object that can block a door or collide with other floor objects: a declared support wins, then the
support head's prediction (floor or not), else a predicted bottom near the floor; thin objects (carpets) never
block."""
declared = condition_object.get("support_parent")
if declared is not None:
on_floor = declared == "floor"
elif predicted_floor is not None:
on_floor = predicted_floor
else:
on_floor = isinstance(floor_z, (int, float)) and abs(z_world - floor_z) <= FLOOR_PREDICTION_TOLERANCE_M
return on_floor and height > THIN_HEIGHT_M
def support_parents(condition: dict, support_logits=None) -> list:
"""One parent per request (slot order): the declared one (``support_parent`` or a hard ``on``), else the support
head's prediction when ``support_logits`` (n x classes) is given, else None."""
requests = effective_support_requests(condition)
if support_logits is None:
return [r.get("support_parent") for r in requests]
return decode_support_parents(support_logits[:len(requests)].detach().cpu(), requests)
def attach_supports(out: dict, batch: dict, parents: list) -> int:
"""Put every slot whose parent is another request or a fixed object on that parent, and every slot whose parent is
"floor" on the floor, in ``out["position_normalized"]`` (in place, parents first); returns the slots moved.
``parents``: per row, ``support_parents`` of its condition."""
origin, scale = batch["origin"].cpu().double(), batch["scale"].cpu().double()
yaw = decode_yaw(out["yaw_logits"].float(), out["yaw_residuals"].float()).cpu().double()
moved = 0
for row, condition in enumerate(batch["conditions"]):
ids = [o["id"] for o in condition["objects"]]
slot = {ident: i for i, ident in enumerate(ids)}
fixed = {f["id"]: f for f in condition["room"].get("fixed_objects", ())}
floor = condition["room"].get("floor_z_m")
world = (out["position_normalized"][row, :len(ids)].cpu().double() * scale[row] + origin[row]).numpy()
size = out["size"][row, :len(ids)].float().cpu().double().numpy()
def depth(i):
chain, seen = 0, set()
while parents[row][i] in slot and i not in seen:
seen.add(i)
i, chain = slot[parents[row][i]], chain + 1
return chain
for i in sorted(range(len(ids)), key=depth):
parent = parents[row][i]
before = world[i].copy()
if parent == "floor" and isinstance(floor, (int, float)):
world[i, 2] = floor
elif parent in slot or parent in fixed:
if parent in slot:
j = slot[parent]
centre, box, angle = world[j], size[j], float(yaw[row, j])
else:
f = fixed[parent]
centre, box, angle = np.asarray(f["bottom_center_m"], float), np.asarray(f["size_local_m"], float), f["yaw_rad"]
c, s = math.cos(angle), math.sin(angle)
d = world[i, :2] - centre[:2]
local = np.array((c * d[0] + s * d[1], c * d[1] - s * d[0]))
local = np.clip(local, -box[:2] / 2, box[:2] / 2) # the nearest point of the parent's footprint
world[i, :2] = centre[:2] + np.array((c * local[0] - s * local[1], s * local[0] + c * local[1]))
top = centre[2] + box[2]
if not centre[2] <= world[i, 2] < top - INNER_M: # else: inside the parent, e.g. on a shelf
world[i, 2] = top
if not np.allclose(before, world[i]):
moved += 1
out["position_normalized"][row, :len(ids)] = torch.from_numpy((world - origin[row].numpy()) / scale[row].numpy()).to(
out["position_normalized"].dtype).to(out["position_normalized"].device)
return moved
@torch.no_grad()
def remask_predictions(model, batch: dict, *, rounds: int = 4, legal: bool = True, retract: bool = False,
door_depth_m: float = .8, stats: dict | None = None) -> dict:
"""Decode a collated batch over ``rounds`` rounds (see the module docstring). ``stats`` (a dict) receives
``decode_calls``, ``commits_per_round`` and ``retractions_per_round``."""
if rounds < 1:
raise ValueError("rounds must be at least 1")
if model.config.position_head != "grid_residual":
raise ValueError("remasking decoding needs the grid_residual position head")
if rounds > 1 and not model.config.pose_conditioning:
raise ValueError("remasking decoding over several rounds needs a model built with pose_conditioning")
model.eval()
grid = model.config.position_grid
slot_mask = batch["slot_mask"].bool()
b, n = slot_mask.shape
encoded = model.encode(batch["input_ids"], batch["attention_mask"], batch["object_spans"], batch["slot_mask"])
fixed = {key: batch[key] for key in ("fixed_position_normalized", "fixed_position_mask", "fixed_size", "fixed_size_mask")
if key in batch}
device = slot_mask.device
pinned = batch.get("given_pose_mask", torch.zeros_like(slot_mask)).bool() & slot_mask # given by the request
committed = pinned.clone()
features = batch.get("given_pose", torch.zeros((b, n, 8), device=device)).clone().float()
out = None
options = cell_cost_options({"grid": grid, "door_depth_m": door_depth_m})
rasters = [RoomRaster(c["room"], options, .1) if legal else None for c in batch["conditions"]]
base_blockers = [list(r.blockers) if legal else [] for r in rasters] # the room's fixed objects
footprints = [{} for _ in range(b)] # committed slot -> its blocker
candidates = [cell_positions(c["room"], options) if legal else None for c in batch["conditions"]]
origin, scale = batch["origin"].cpu().double(), batch["scale"].cpu().double()
symmetry = batch.get("yaw_symmetry_order", torch.ones((b, n), dtype=torch.long)).to(device)
swap = batch.get("size_axis_swap_allowed", torch.zeros((b, n), dtype=torch.bool)).to(device)
total = (slot_mask & ~pinned).sum(1).tolist()
score = [{} for _ in range(b)] # committed slot -> its confidence when committed (SceneNAT keeps it)
log = {"decode_calls": 0, "commits_per_round": [], "retractions_per_round": [], "attached": 0}
for r in range(rounds):
kwargs = dict(fixed)
if model.config.pose_conditioning:
kwargs.update(given_pose=features.to(encoded["slots"].dtype), given_pose_mask=committed)
pred = model.decode(encoded, **kwargs)
log["decode_calls"] += 1
if out is None:
out = {key: pred[key].clone() for key in (*ROW_KEYS, *(("support_logits",) if "support_logits" in pred else ()))}
out["slot_mask"] = pred["slot_mask"]
raw = pred["position_cell_logits"].float()
yaw = decode_yaw(pred["yaw_logits"].float(), pred["yaw_residuals"].float())
support = pred.get("support_logits")
commits = retractions = 0
for row in range(b):
open_slots = [i for i in range(n) if slot_mask[row, i] and not committed[row, i]]
if not open_slots:
continue
objects, room = batch["conditions"][row]["objects"], batch["conditions"][row]["room"]
size = pred["size"][row].float().cpu().double().numpy()
yaw_row = yaw[row].cpu().double().numpy()
z_world = (pred["position_normalized"][row, :, 2].float().cpu().double() * scale[row, 2] + origin[row, 2]).numpy()
predicted_floor = (support[row].argmax(-1) == 0).tolist() if support is not None else [None] * n
parents = support_parents(batch["conditions"][row], support[row] if support is not None else None)
on_object = {i for i, p in enumerate(parents) if p is not None and p not in ("floor", "wall")}
blocks = {i: _blocks(objects[i], z_world[i], room.get("floor_z_m"), size[i, 2], predicted_floor[i])
for i in open_slots}
z_range = {i: vertical_extent(z_world[i], size[i, 2], options) for i in open_slots}
version = [0] # bumped whenever the occupied footprints change
def restricted(i):
if not legal or i in on_object: # placed on its parent after the rounds (attach_supports)
return raw[row, i]
costs, _ = cell_cost_maps(room, [(size[i, :2].tolist(), float(yaw_row[i]), blocks[i], z_range[i])], options,
raster=rasters[row])
cost = torch.from_numpy(costs[0]).to(raw.device)
return torch.where(cost <= cost.min() + 1e-6, raw[row, i], torch.full_like(raw[row, i], ILLEGAL_LOGIT))
if retract and legal and footprints[row]:
# rank the open slots against the room and its fixed objects only: committed footprints may be
# retracted below, the slots that commit are re-checked against the ones kept
rasters[row].blockers = list(base_blockers[row])
version[0] += 1
logits = {i: restricted(i) for i in open_slots}
seen = version[0]
probability = raw[row].softmax(-1)
confidence = {i: float(probability[i, int(logits[i].argmax())]) for i in open_slots}
keep_open = 0 if r == rounds - 1 else open_after(total[row], r, rounds)
if retract:
# SceneNAT: the kept set is the most confident of the open slots (scored now) and the committed ones
# (scored when committed); a committed slot outside it is masked again and re-predicted later
pool = {**score[row], **confidence}
keep = set(sorted(pool, key=lambda k: -pool[k])[:max(0, len(pool) - keep_open)])
for i in [i for i in score[row] if i not in keep]:
committed[row, i] = False
features[row, i] = 0.
del score[row][i]
footprints[row].pop(i, None)
retractions += 1
if legal and footprints[row]:
rasters[row].blockers = base_blockers[row] + list(footprints[row].values())
version[0] += 1
chosen = sorted((i for i in open_slots if i in keep), key=lambda k: -confidence[k])
else:
count = max(1, len(open_slots) - keep_open)
chosen = sorted(open_slots, key=lambda k: -confidence[k])[:count]
for i in chosen:
if legal and version[0] != seen:
logits[i] = restricted(i) # footprints committed (or retracted) since the logits were computed
cell_logits = logits[i]
xy = decode_grid_position(cell_logits[None], pred["position_cell_residuals"][row, i][None].float(), grid)[0]
if legal and i not in on_object:
lo, sc = origin[row, :2].numpy(), scale[row, :2].numpy()
world = _snap(rasters[row], candidates[row][int(cell_logits.argmax())], xy.cpu().double().numpy() * sc + lo,
size[i], float(yaw_row[i]), blocks[i], z_range[i], options)
xy = torch.from_numpy((world - lo) / sc).to(xy)
position = torch.cat((xy.to(pred["position_normalized"].dtype), pred["position_normalized"][row, i, 2:]))
for key in out:
if key != "slot_mask":
out[key][row, i] = pred[key][row, i]
out["position_cell_logits"][row, i] = cell_logits.to(out["position_cell_logits"].dtype)
out["position_normalized"][row, i] = position
features[row, i] = pose_features(position[None].float(), pred["size"][row, i][None].float(),
yaw[row, i][None], symmetry[row, i][None], swap[row, i][None])[0]
committed[row, i] = True
score[row][i] = confidence[i]
commits += 1
if legal and i not in on_object:
world = position[:2].cpu().double().numpy() * scale[row, :2].numpy() + origin[row, :2].numpy()
rasters[row].block(box_corners(world, size[i, :2], float(yaw_row[i])), z_range[i])
footprints[row][i] = rasters[row].blockers[-1]
version[0] += 1
log["commits_per_round"].append(commits)
log["retractions_per_round"].append(retractions)
if not (slot_mask & ~committed).any():
break
log["attached"] = attach_supports(out, batch, [support_parents(c, out["support_logits"][row] if "support_logits" in out else None)
for row, c in enumerate(batch["conditions"])])
if stats is not None:
stats.update(log)
return out
|