CADMorph / backend /src /cadmorph /classify /features.py
Brent Bolleman
initial build
085f6c4
Raw History Blame Contribute Delete
2.87 kB
"""Deterministic feature encoding of a DrawingGraph for the learned stages.
Features are pure functions of vector-exact entity fields (kind, bbox,
segment structure, style, text shape) — no pixels, no inference inputs.
The classifier additionally sees normalized sheet position; the Siamese
matcher does NOT (a moved entity must embed like its counterpart, R7).
"""
from __future__ import annotations
import torch
from cadmorph.models import DrawingEntity, DrawingGraph
KINDS = ("linework", "text", "dimension", "symbol", "hatch")
# GAT symbol-spotting label set (T020) — matches synthgen `semantic` labels.
CLASSES = ("dimension", "door", "furniture", "linework", "note", "room-label", "title", "wall")
BASE_DIM = 17
FEATURE_DIM = BASE_DIM + 2 # + normalized sheet position (classifier only)
def entity_features(
entity: DrawingEntity, graph: DrawingGraph, include_position: bool
) -> list[float]:
diag = graph.revision.sheet_diagonal
x0, y0, x1, y1 = entity.bbox
w, h = x1 - x0, y1 - y0
n_seg = len(entity.geometry)
seg_counts = {"line": 0, "rect": 0, "curve": 0, "quad": 0}
for seg in entity.geometry:
seg_counts[seg.kind] += 1
denom = max(n_seg, 1)
text = entity.text_payload or ""
style_width = entity.style.width if entity.style and entity.style.width else 0.0
feats = [
*[1.0 if entity.kind == k else 0.0 for k in KINDS],
w / diag,
h / diag,
min(w / (h + 1e-6), 10.0) / 10.0,
min(n_seg, 10) / 10.0,
seg_counts["line"] / denom,
seg_counts["rect"] / denom,
(seg_counts["curve"] + seg_counts["quad"]) / denom,
min(style_width, 20.0) / 20.0,
1.0 if text else 0.0,
min(len(text), 32) / 32.0,
(sum(c.isdigit() for c in text) / len(text)) if text else 0.0,
(sum(c.isupper() for c in text) / len(text)) if text else 0.0,
]
if include_position:
feats += [
((x0 + x1) / 2.0) / graph.revision.sheet_width,
((y0 + y1) / 2.0) / graph.revision.sheet_height,
]
return feats
def graph_tensors(
graph: DrawingGraph, include_position: bool
) -> tuple[torch.Tensor, torch.Tensor]:
"""(node features [n, F], edge_index [2, E] undirected) in canonical entity order."""
x = torch.tensor(
[entity_features(e, graph, include_position) for e in graph.entities],
dtype=torch.float32,
)
index = {e.entity_id: i for i, e in enumerate(graph.entities)}
pairs: list[tuple[int, int]] = []
for edge in graph.edges:
a, b = index[edge.source], index[edge.target]
pairs.append((a, b))
pairs.append((b, a))
if pairs:
edge_index = torch.tensor(sorted(set(pairs)), dtype=torch.long).t().contiguous()
else:
edge_index = torch.zeros((2, 0), dtype=torch.long)
return x, edge_index