""" Normalisasi landmark untuk konsistensi across different positions dan scales. Melakukan normalisasi koordinat landmark sesuai README. """ import numpy as np from typing import Optional class LandmarkNormalizer: """ Kelas untuk menormalisasi landmark tangan dan wajah. Proses normalisasi sesuai README: 1. Menggeser koordinat ke titik referensi 2. Scaling """ def __init__(self, scale_factor: Optional[float] = None): """ Inisialisasi normalizer. Args: scale_factor: Faktor skala tetap. Jika None, dihitung dari data. """ self.scale_factor = scale_factor def normalize_hand(self, landmarks: np.ndarray) -> np.ndarray: """ Normalisasi landmark satu tangan. Args: landmarks: Array shape (21, 3) dari landmark tangan Returns: Array landmark yang sudah dinormalisasi """ if landmarks is None or landmarks.shape != (21, 3): return np.zeros((21, 3)) # Buat copy normalized = landmarks.copy() # 1. Translation: Geser ke wrist (origin) - index 0 wrist = normalized[0] normalized -= wrist # 2. Scaling: Normalisasi berdasarkan distance dari wrist ke middle finger MCP (index 9) middle_mcp = normalized[9] distance = np.linalg.norm(middle_mcp) if distance > 1e-6: # Avoid division by zero normalized = normalized / distance if self.scale_factor is not None: normalized *= self.scale_factor return normalized def normalize_face(self, landmarks: np.ndarray) -> np.ndarray: """ Normalisasi landmark wajah. Args: landmarks: Array shape (60, 3) dari landmark wajah Returns: Array landmark yang sudah dinormalisasi """ if landmarks is None or len(landmarks) == 0: return np.zeros((60, 3)) # Buat copy normalized = landmarks.copy() # 1. Translation: Geser ke nose (center) nose = normalized[0] normalized -= nose # 2. Scaling: Normalisasi berdasarkan max distance from center max_distance = np.max(np.linalg.norm(normalized, axis=1)) if max_distance > 1e-6: normalized = normalized / max_distance if self.scale_factor is not None: normalized *= self.scale_factor return normalized def normalize_frame( self, left_hand: np.ndarray, right_hand: np.ndarray, face: np.ndarray ) -> np.ndarray: """ Normalisasi semua landmark dari satu frame. Args: left_hand: Array (21, 3) landmark tangan kiri right_hand: Array (21, 3) landmark tangan kanan face: Array (60, 3) landmark wajah Returns: Array 1D concatenation dari semua landmark yang sudah dinormalisasi """ norm_left = self.normalize_hand(left_hand) norm_right = self.normalize_hand(right_hand) norm_face = self.normalize_face(face) # Concatenate: 63 + 63 + 180 = 306 return np.concatenate([ norm_left.flatten(), # 63 values norm_right.flatten(), # 63 values norm_face.flatten() # 180 values ]) def normalize_landmarks( left_hand: np.ndarray, right_hand: np.ndarray, face: np.ndarray, scale_factor: Optional[float] = None ) -> np.ndarray: """ Fungsi convenience untuk normalisasi landmark. Args: left_hand: Landmark tangan kiri right_hand: Landmark tangan kanan face: Landmark wajah scale_factor: Optional scale factor Returns: Array landmark yang sudah dinormalisasi """ normalizer = LandmarkNormalizer(scale_factor=scale_factor) return normalizer.normalize_frame(left_hand, right_hand, face)