File size: 6,021 Bytes
3b18ebd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# -*- 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])