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
Download code/fastfill/v2/remask.py from XXXpilar/Fastfill-test: direct link, hf CLI and curl.
- Browser
- Download file 19.3 kB
-
https://huggingface.co/XXXpilar/Fastfill-test/resolve/main/code/fastfill/v2/remask.py
- Command line
-
hf download hf://XXXpilar/Fastfill-test/code/fastfill/v2/remask.py
-
curl -L -o remask.py https://huggingface.co/XXXpilar/Fastfill-test/resolve/main/code/fastfill/v2/remask.py
19.3 kB
| """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 | |
| 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 | |