141 lines
3.9 KiB
Python
141 lines
3.9 KiB
Python
"""
|
|
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)
|