""" Chuẩn hoá theo người ký (signer-relative normalization) — làm keypoint bất biến với khung hình / khoảng cách / vị trí trong khung. Dùng CHUNG cho cả huấn luyện (scripts/train_clean.py --signer) lẫn suy luận (app/inference/pipeline.py). Chỉ có MỘT bản duy nhất để train và inference không bao giờ lệch nhau. Ý tưởng: căn mọi điểm về GIỮA HAI VAI và chia cho ĐỘ RỘNG VAI. Sau phép này, người đứng gần/xa, lệch trái/phải, khung rộng/hẹp đều cho cùng toạ độ — model chỉ còn thấy hình dạng + chuyển động của tay so với cơ thể. Bố cục 75 keypoint: bàn tay trái 0–20, tay phải 21–41, pose 42–74. Vai trong pose: landmark 11 (trái) → 42+11=53, landmark 12 (phải) → 42+12=54. """ from __future__ import annotations import numpy as np _LS = 53 # vai trái (pose landmark 11) _RS = 54 # vai phải (pose landmark 12) def signer_normalize(kp: np.ndarray) -> np.ndarray: """ kp: (T, 75, 3) — toạ độ thô (đã chuẩn hoá theo khung hình, điểm thiếu = 0). Trả về (T, 75, 3) đã căn giữa vai + chia độ rộng vai. Điểm thiếu giữ nguyên 0. Nếu không đủ dữ liệu vai để ước lượng (cả clip không thấy vai), trả về nguyên bản — thà không chuẩn hoá còn hơn chuẩn hoá sai. """ kp = np.asarray(kp, dtype=np.float32) ls = kp[:, _LS, :] # (T, 3) rs = kp[:, _RS, :] # (T, 3) detected = (np.any(ls != 0.0, axis=-1)) & (np.any(rs != 0.0, axis=-1)) # (T,) if detected.sum() == 0: return kp.copy() # Tâm = trung điểm hai vai, lấy trung bình qua các frame thấy đủ vai mid = ((ls[detected] + rs[detected]) / 2.0).mean(axis=0) # (3,) # Thang = độ rộng vai (khoảng cách x-y), trung bình qua các frame đó width = np.linalg.norm((ls[detected] - rs[detected])[:, :2], axis=1).mean() if width < 1e-6: return kp.copy() mask = np.any(kp != 0.0, axis=-1, keepdims=True) # (T, 75, 1) — điểm đã phát hiện normed = (kp - mid) / width return np.where(mask, normed, 0.0).astype(np.float32)