dYang1's picture
Release SignBridge-TSL-STGCN with compliance Model Card
3b18ebd verified
Raw History Blame Contribute Delete
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])