Download backend/src/cadmorph/classify/features.py from bolleman/CADMorph: direct link, hf CLI and curl.
- Browser
- Download file 2.87 kB
-
https://huggingface.co/spaces/bolleman/CADMorph/resolve/main/backend/src/cadmorph/classify/features.py
- Command line
-
hf download hf://spaces/bolleman/CADMorph/backend/src/cadmorph/classify/features.py
-
curl -L -o features.py https://huggingface.co/spaces/bolleman/CADMorph/resolve/main/backend/src/cadmorph/classify/features.py
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 | |