Download graph.py from dYang1/SignBridge-TSL-STGCN: direct link, hf CLI and curl.
- Browser
- Download file 6.02 kB
-
https://huggingface.co/dYang1/SignBridge-TSL-STGCN/resolve/main/graph.py
- Command line
-
hf download hf://dYang1/SignBridge-TSL-STGCN/graph.py
-
curl -L -o graph.py https://huggingface.co/dYang1/SignBridge-TSL-STGCN/resolve/main/graph.py
6.02 kB
| # -*- coding: utf-8 -*- | |
| """55 節點骨架圖定義(ST-GCN 用)。 | |
| 節點順序與 extract_features.py 完全一致: | |
| 0..11 pose,對應 MediaPipe 的 [11,12,13,14,15,16,17,18,19,20,21,22] | |
| 12..32 左手 21 點 | |
| 33..53 右手 21 點 | |
| 54 虛擬頸點(雙肩中點) | |
| pose 那 12 點的實際意義: | |
| slot 0=左肩(11) 1=右肩(12) 2=左肘(13) 3=右肘(14) 4=左腕(15) 5=右腕(16) | |
| slot 6=左小指(17) 7=右小指(18) 8=左食指(19) 9=右食指(20) 10=左拇指(21) 11=右拇指(22) | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import config as C | |
| # ---------------------------------------------------------------- 節點編號 | |
| L_SHOULDER, R_SHOULDER = 0, 1 | |
| L_ELBOW, R_ELBOW = 2, 3 | |
| L_WRIST, R_WRIST = 4, 5 | |
| L_PINKY, R_PINKY = 6, 7 | |
| L_INDEX, R_INDEX = 8, 9 | |
| L_THUMB, R_THUMB = 10, 11 | |
| NECK = C.CENTER_IDX # 54 | |
| LH0 = C.LHAND_SLICE.start # 12,左手腕 | |
| RH0 = C.RHAND_SLICE.start # 33,右手腕 | |
| # ---------------------------------------------------------------- 邊 | |
| # 上半身骨架 | |
| POSE_EDGES = [ | |
| (NECK, L_SHOULDER), (NECK, R_SHOULDER), | |
| (L_SHOULDER, L_ELBOW), (L_ELBOW, L_WRIST), | |
| (R_SHOULDER, R_ELBOW), (R_ELBOW, R_WRIST), | |
| (L_WRIST, L_PINKY), (L_WRIST, L_INDEX), (L_WRIST, L_THUMB), | |
| (R_WRIST, R_PINKY), (R_WRIST, R_INDEX), (R_WRIST, R_THUMB), | |
| ] | |
| # MediaPipe 單手 21 點的標準連接 | |
| HAND_EDGES = [ | |
| (0, 1), (1, 2), (2, 3), (3, 4), # 拇指 | |
| (0, 5), (5, 6), (6, 7), (7, 8), # 食指 | |
| (5, 9), (9, 10), (10, 11), (11, 12), # 中指 | |
| (9, 13), (13, 14), (14, 15), (15, 16), # 無名指 | |
| (13, 17), (17, 18), (18, 19), (19, 20), # 小指 | |
| (0, 17), # 掌根 | |
| ] | |
| def build_edges(): | |
| edges = list(POSE_EDGES) | |
| for off in (LH0, RH0): | |
| edges += [(a + off, b + off) for a, b in HAND_EDGES] | |
| # 把 pose 的手腕接到手部骨架的手腕,讓手臂和手掌是連通的 | |
| edges += [(L_WRIST, LH0), (R_WRIST, RH0)] | |
| return edges | |
| # ---------------------------------------------------------------- 鄰接矩陣 | |
| def _hop_distance(num_nodes: int, edges, max_hop: int = 1) -> np.ndarray: | |
| A = np.zeros((num_nodes, num_nodes)) | |
| for i, j in edges: | |
| A[i, j] = 1 | |
| A[j, i] = 1 | |
| hop = np.full((num_nodes, num_nodes), np.inf) | |
| powers = [np.linalg.matrix_power(A, d) for d in range(max_hop + 1)] | |
| arrive = (np.stack(powers) > 0) | |
| for d in range(max_hop, -1, -1): | |
| hop[arrive[d]] = d | |
| return hop | |
| def _normalize(A: np.ndarray) -> np.ndarray: | |
| """D^-1 A,避免度數高的節點主導。""" | |
| deg = A.sum(axis=0) | |
| Dinv = np.zeros_like(A) | |
| nz = deg > 0 | |
| Dinv[nz, nz] = deg[nz] ** -1 | |
| return A @ Dinv | |
| def build_adjacency(num_nodes: int = C.NUM_POINTS, strategy: str = "spatial") -> np.ndarray: | |
| """回傳 (K, V, V) 的鄰接矩陣堆疊。 | |
| spatial(ST-GCN 原論文的分割方式)分成三組: | |
| 0 根節點:自己 + 與自己離頸點等距的鄰居 | |
| 1 向心:比自己更靠近頸點的鄰居(代表軀幹方向的運動) | |
| 2 離心:比自己更遠離頸點的鄰居(代表末端手指的運動) | |
| 對手語很合理——同一個手勢,重點常在末端相對於軀幹怎麼動。 | |
| """ | |
| edges = build_edges() | |
| hop = _hop_distance(num_nodes, edges, max_hop=1) | |
| adjacency = (hop <= 1).astype(float) # 含自環 | |
| norm = _normalize(adjacency) | |
| dist_to_center = _hop_distance(num_nodes, edges, max_hop=num_nodes)[NECK] | |
| if strategy == "uniform": | |
| return norm[None, ...] | |
| if strategy != "spatial": | |
| raise ValueError(f"未知的 strategy: {strategy}") | |
| root = np.zeros_like(norm) | |
| close = np.zeros_like(norm) | |
| far = np.zeros_like(norm) | |
| for i in range(num_nodes): | |
| for j in range(num_nodes): | |
| if hop[j, i] != 1 and i != j: | |
| continue | |
| di, dj = dist_to_center[i], dist_to_center[j] | |
| if dj == di: | |
| root[j, i] = norm[j, i] | |
| elif dj > di: | |
| far[j, i] = norm[j, i] | |
| else: | |
| close[j, i] = norm[j, i] | |
| return np.stack([root, close, far]) | |
| def build_parents(num_nodes: int = C.NUM_POINTS): | |
| """以頸點為根做 BFS,回傳 (parents, bfs_order)。 | |
| parents[v] = v 的父節點索引,根節點為 -1。 | |
| 掌根那圈迴路在 BFS 時自然被展開成樹,不影響。 | |
| 骨向量與肢段縮放都需要這個樹狀結構。 | |
| """ | |
| adj = [[] for _ in range(num_nodes)] | |
| for i, j in build_edges(): | |
| adj[i].append(j) | |
| adj[j].append(i) | |
| parents = np.full(num_nodes, -1, dtype=int) | |
| visited = np.zeros(num_nodes, dtype=bool) | |
| order, queue = [], [NECK] | |
| visited[NECK] = True | |
| while queue: | |
| v = queue.pop(0) | |
| order.append(v) | |
| for u in adj[v]: | |
| if not visited[u]: | |
| visited[u] = True | |
| parents[u] = v | |
| queue.append(u) | |
| return parents, order | |
| def sanity_check(): | |
| """檢查圖是連通的、沒有孤立節點。""" | |
| edges = build_edges() | |
| deg = np.zeros(C.NUM_POINTS, dtype=int) | |
| for i, j in edges: | |
| deg[i] += 1 | |
| deg[j] += 1 | |
| isolated = np.where(deg == 0)[0] | |
| hop = _hop_distance(C.NUM_POINTS, edges, max_hop=C.NUM_POINTS) | |
| unreachable = np.where(~np.isfinite(hop[NECK]))[0] | |
| return { | |
| "num_edges": len(edges), | |
| "isolated_nodes": isolated.tolist(), | |
| "unreachable_from_neck": unreachable.tolist(), | |
| "max_hop_from_neck": float(np.nanmax(hop[NECK][np.isfinite(hop[NECK])])), | |
| } | |
| if __name__ == "__main__": | |
| print(sanity_check()) | |
| A = build_adjacency() | |
| print("鄰接矩陣 shape:", A.shape, " 每組非零數:", [int((a > 0).sum()) for a in A]) | |