class_name PoseFeatures extends RefCounted ## 关键点 → DTW 特征向量 —— 契约 contracts/skeleton-topology.md §5(冻结) ## ## 每帧特征 = [33 × (x, y, z), 9 个关节角余弦] = 108 维。 ## 低置信度点(conf < 0.5)对应的维度权重置 0,不参与代价计算。 ## ## 关节角三元组顺序冻结为: ## [shoulder_L, shoulder_R, elbow_L, elbow_R, hip_L, hip_R, knee_L, knee_R, spine] const KeypointFrame = preload("res://src/net/keypoint_frame.gd") const KEYPOINT_COUNT := 33 const VIRTUAL_NECK := 33 const VIRTUAL_PELVIS := 34 const ANGLE_COUNT := 9 const FEATURE_DIM := KEYPOINT_COUNT * 3 + ANGLE_COUNT # 108 ## 关节角 (a, b, c) 三元组索引 const ANGLE_TRIPLETS := [ [KeypointFrame.LEFT_ELBOW, KeypointFrame.LEFT_SHOULDER, KeypointFrame.LEFT_HIP], [KeypointFrame.RIGHT_ELBOW, KeypointFrame.RIGHT_SHOULDER, KeypointFrame.RIGHT_HIP], [KeypointFrame.LEFT_SHOULDER, KeypointFrame.LEFT_ELBOW, KeypointFrame.LEFT_WRIST], [KeypointFrame.RIGHT_SHOULDER, KeypointFrame.RIGHT_ELBOW, KeypointFrame.RIGHT_WRIST], [KeypointFrame.LEFT_SHOULDER, KeypointFrame.LEFT_HIP, KeypointFrame.LEFT_KNEE], [KeypointFrame.RIGHT_SHOULDER, KeypointFrame.RIGHT_HIP, KeypointFrame.RIGHT_KNEE], [KeypointFrame.LEFT_HIP, KeypointFrame.LEFT_KNEE, KeypointFrame.LEFT_ANKLE], [KeypointFrame.RIGHT_HIP, KeypointFrame.RIGHT_KNEE, KeypointFrame.RIGHT_ANKLE], [KeypointFrame.NOSE, VIRTUAL_NECK, VIRTUAL_PELVIS], ] const ANGLE_NAMES := [ "shoulder_L", "shoulder_R", "elbow_L", "elbow_R", "hip_L", "hip_R", "knee_L", "knee_R", "spine", ] ## 一帧特征:108 维值 + 108 维权重 class FeatureVector extends RefCounted: var values: PackedFloat32Array = PackedFloat32Array() var weights: PackedFloat32Array = PackedFloat32Array() var time_ms: float = 0.0 func _init() -> void: values.resize(PoseFeatures.FEATURE_DIM) weights.resize(PoseFeatures.FEATURE_DIM) values.fill(0.0) weights.fill(1.0) func dimension() -> int: return values.size() ## 由姿态帧构造特征向量 static func build(frame: KeypointFrame.PoseFrame, use_unmirrored: bool = true) -> FeatureVector: var out := FeatureVector.new() if frame == null: return out var points: PackedVector3Array = frame.unmirrored_points() if use_unmirrored else frame.points var confidence: PackedFloat32Array = frame.confidence out.time_ms = float(frame.received_at_ms) # ---- 33 × 3 坐标 ---- for i in KEYPOINT_COUNT: var p := points[i] if i < points.size() else Vector3.ZERO var c := confidence[i] if i < confidence.size() else 0.0 var w := 1.0 if c >= KeypointFrame.CONFIDENCE_THRESHOLD else 0.0 out.values[i * 3] = p.x out.values[i * 3 + 1] = p.y out.values[i * 3 + 2] = p.z out.weights[i * 3] = w out.weights[i * 3 + 1] = w out.weights[i * 3 + 2] = w # ---- 9 个关节角 ---- var neck := _midpoint(points, KeypointFrame.LEFT_SHOULDER, KeypointFrame.RIGHT_SHOULDER) var pelvis := _midpoint(points, KeypointFrame.LEFT_HIP, KeypointFrame.RIGHT_HIP) var positions := _extended_positions(points, neck, pelvis) for a in ANGLE_COUNT: var triple: Array = ANGLE_TRIPLETS[a] var idx := KEYPOINT_COUNT * 3 + a out.values[idx] = _angle_cos(positions[triple[0]], positions[triple[1]], positions[triple[2]]) # 三个端点均可信时该角度才参与代价计算 var trustworthy := ( _weight_for_point(confidence, triple[0]) > 0.0 and _weight_for_point(confidence, triple[1]) > 0.0 and _weight_for_point(confidence, triple[2]) > 0.0 ) out.weights[idx] = 1.0 if trustworthy else 0.0 return out ## 33 点 + neck(33) + pelvis(34) static func _extended_positions(points: PackedVector3Array, neck: Vector3, pelvis: Vector3) -> Array: var out: Array = [] out.resize(VIRTUAL_PELVIS + 1) for i in KEYPOINT_COUNT: out[i] = points[i] if i < points.size() else Vector3.ZERO out[VIRTUAL_NECK] = neck out[VIRTUAL_PELVIS] = pelvis return out static func _midpoint(points: PackedVector3Array, a: int, b: int) -> Vector3: var pa: Vector3 = points[a] if a < points.size() else Vector3.ZERO var pb: Vector3 = points[b] if b < points.size() else Vector3.ZERO return (pa + pb) * 0.5 static func _weight_for_point(confidence: PackedFloat32Array, index: int) -> float: if index == VIRTUAL_NECK or index == VIRTUAL_PELVIS or index >= KeypointFrame.KEYPOINT_COUNT: return 1.0 if index >= confidence.size(): return 0.0 return 1.0 if confidence[index] >= KeypointFrame.CONFIDENCE_THRESHOLD else 0.0 ## 向量 ba 与 bc 夹角的余弦,落在 [-1, 1] static func _angle_cos(a: Vector3, b: Vector3, c: Vector3) -> float: var ba := a - b var bc := c - b var denom := ba.length() * bc.length() if denom < 1e-9: return 0.0 return clampf(ba.dot(bc) / denom, -1.0, 1.0) ## 加权欧氏距离(逐维归一化,使不同维数可比) static func weighted_distance(a: FeatureVector, b: FeatureVector) -> float: var dim := mini(a.values.size(), b.values.size()) if dim == 0: return 0.0 var sum := 0.0 var weight_total := 0.0 for i in dim: var w: float = minf(a.weights[i], b.weights[i]) if w <= 0.0: continue var d := a.values[i] - b.values[i] sum += w * d * d weight_total += w if weight_total <= 0.0: return 0.0 return sqrt(sum / weight_total) ## 逐维绝对差(用于调试与可视化) static func dimension_deltas(a: FeatureVector, b: FeatureVector) -> PackedFloat32Array: var out := PackedFloat32Array() out.resize(mini(a.values.size(), b.values.size())) for i in out.size(): out[i] = absf(a.values[i] - b.values[i]) return out ## 关节角名称索引 → 中文标签(UI 展示用) static func angle_label(index: int) -> String: match index: 0: return "左肩" 1: return "右肩" 2: return "左肘" 3: return "右肘" 4: return "左髋" 5: return "右髋" 6: return "左膝" 7: return "右膝" 8: return "躯干" return "角度%d" % index