Files

216 lines
7.5 KiB
Python

import numpy as np
from typing import Optional, Tuple, List, Union
from ..core.resources.model_loader import ModelOBJ
class MediapipeBaseLandmarks:
"""
Class for handling base landmark positions and face topology.
Coordinates are stored in normalized 0-1 range.
"""
_instance: Optional[ModelOBJ] = None
_boundary_faces: Optional[np.ndarray] = None
_boundary_landmarks: Optional[np.ndarray] = None
_current_size: Optional[Union[int, Tuple[int, int]]] = None
@classmethod
def _get_model_instance(cls) -> ModelOBJ:
"""
Get or create ModelOBJ instance using singleton pattern
Returns:
ModelOBJ: Instance of model loader
"""
if cls._instance is None:
cls._instance = ModelOBJ()
return cls._instance
@classmethod
def create_boundary_triangles(cls, size) -> np.ndarray:
"""
Create additional triangles between face oval and image boundaries
Args:
size: Target size (int or tuple of width, height)
Returns:
numpy.ndarray: Array of boundary face indices
"""
if isinstance(size, int):
width = height = size
else:
width, height = size
print(f"Creating boundary triangles for image size: {width}x{height}")
boundary_points = []
boundary_triangles = []
edges = {
'top': {
'landmarks': [54, 103, 67, 109, 10, 338, 297, 332, 284],
'start': (0, 0),
'middle': (0.5, 0),
'end': (1, 0),
'segments': 8
},
'right': {
'landmarks': [284, 251, 389, 356, 454, 323, 361, 288, 397, 365],
'start': (1, 0),
'middle': (1, 0.5),
'end': (1, 1),
'segments': 9
},
'bottom': {
'landmarks': [365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136],
'start': (1, 1),
'middle': (0.5, 1),
'end': (0, 1),
'segments': 10
},
'left': {
'landmarks': [136, 172, 58, 132, 93, 234, 127, 162, 21, 54],
'start': (0, 1),
'middle': (0, 0.5),
'end': (0, 0),
'segments': 9
}
}
current_point_idx = cls._get_model_instance().num_landmarks
for edge_name, edge_info in edges.items():
print(f"Processing {edge_name} edge")
landmarks = edge_info['landmarks']
n_segments = edge_info['segments']
print(f"Creating {n_segments} segments for {len(landmarks)} landmarks")
# Generate normalized edge points
edge_points = cls._interpolate_edge_points(
edge_info['start'],
edge_info['end'],
n_segments
)
# Create quads and convert to triangles
for i in range(n_segments):
# Define quad vertices
quad = [
landmarks[i], # Current landmark
landmarks[i + 1], # Next landmark
current_point_idx + i + 1, # Next edge point
current_point_idx + i # Current edge point
]
# Convert quad to two triangles
boundary_triangles.extend([
[quad[0], quad[1], quad[2]], # First triangle
[quad[0], quad[2], quad[3]] # Second triangle
])
# Add edge points to boundary points list
boundary_points.extend(edge_points)
current_point_idx += len(edge_points)
print(f"Created {len(boundary_triangles)} triangles from {len(boundary_points)} boundary points")
cls._boundary_landmarks = np.array(boundary_points)
cls._boundary_faces = np.array(boundary_triangles)
return cls._boundary_faces
@staticmethod
def _interpolate_edge_points(start_pos: Tuple[float, float],
end_pos: Tuple[float, float],
n_segments: int) -> List[Tuple[float, float]]:
"""
Interpolate points along an edge
Args:
start_pos: Start position (x, y)
end_pos: End position (x, y)
n_segments: Number of segments to create
Returns:
List[Tuple[float, float]]: List of interpolated points
"""
points = []
for i in range(n_segments + 1):
t = i / n_segments
x = start_pos[0] + t * (end_pos[0] - start_pos[0])
y = start_pos[1] + t * (end_pos[1] - start_pos[1])
points.append((x, y))
return points
@classmethod
def get_face_triangles(cls, size=None, x_scale: float = 1.0, y_translation: float = 0.0) -> Tuple[
np.ndarray, np.ndarray]:
"""
Get face triangulation including boundary triangles with optional transformations
Args:
size: Target size (int or tuple)
x_scale: Horizontal scaling factor (0.5 to 1.0)
y_translation: Vertical translation (-0.5 to 0.5)
Returns:
Tuple[np.ndarray, np.ndarray]: (triangles array, landmarks array)
"""
model = cls._get_model_instance()
landmarks = model.get_transformed_landmarks(x_scale, y_translation)
faces = model.get_faces()
# Create boundary triangles if needed (using normalized coordinates)
if cls._boundary_faces is None:
cls.create_boundary_triangles(size)
if size is not None:
if isinstance(size, tuple):
scaled_landmarks = landmarks * np.array(size)
if cls._boundary_landmarks is not None:
boundary_landmarks = cls._boundary_landmarks * np.array(size)
if y_translation != 0:
boundary_landmarks = boundary_landmarks.copy()
boundary_landmarks[:, 1] += y_translation * size[1]
else:
scaled_landmarks = landmarks * size
if cls._boundary_landmarks is not None:
boundary_landmarks = cls._boundary_landmarks * size
if y_translation != 0:
boundary_landmarks = boundary_landmarks.copy()
boundary_landmarks[:, 1] += y_translation * size
if cls._boundary_faces is not None:
all_points = np.vstack([scaled_landmarks, boundary_landmarks])
all_triangles = np.vstack([faces, cls._boundary_faces])
return all_triangles, all_points
return faces, landmarks
@classmethod
def get_base_landmarks(cls, size=None, x_scale: float = 1.0, y_translation: float = 0.0) -> np.ndarray:
"""
Get base landmark positions with optional transformations.
Args:
size: Target size (int or tuple)
x_scale: Horizontal scaling factor (0.5 to 1.0)
y_translation: Vertical translation (-0.5 to 0.5)
Returns:
numpy.ndarray: Base landmarks with applied transformations
"""
model = cls._get_model_instance()
landmarks = model.get_transformed_landmarks(x_scale, y_translation)
if size is not None:
if isinstance(size, tuple):
landmarks = landmarks * np.array(size)
else:
landmarks = landmarks * size
return landmarks