TIF_E4253362/backend/utils/normalization.py

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)