160 lines
6.0 KiB
Python
160 lines
6.0 KiB
Python
import pandas as pd
|
|
import numpy as np
|
|
from typing import Dict, List, Tuple, Optional
|
|
|
|
|
|
class LandmarkMappings:
|
|
"""
|
|
Class for mapping and transforming facial landmarks between MediaPipe and Dlib coordinate spaces.
|
|
Supports interpolated control points derived from Dlib landmark pairs.
|
|
"""
|
|
|
|
# Base landmark pairs mapping MediaPipe to Dlib indices
|
|
# Format: (mediapipe_idx, dlib_idx)
|
|
LANDMARKS_PAIRS = {
|
|
"left_eye": [
|
|
(133, 40), # Left eye outer corner
|
|
(33, 37) # Left eye inner corner
|
|
],
|
|
"right_eye": [
|
|
(362, 43), # Right eye outer corner
|
|
(263, 46) # Right eye inner corner
|
|
],
|
|
"mouth": [
|
|
(0, 52), # Upper lip top
|
|
(17, 58), # Bottom lip bottom
|
|
(61, 49), # Left mouth corner
|
|
(291, 55), # Right mouth corner
|
|
(13, 63), # Upper lip bottom
|
|
(14, 67), # Bottom lip top
|
|
(78, 61), # Left mouth inner corner
|
|
(308, 65) # Right mouth inner corner
|
|
],
|
|
"nose_edge": [
|
|
(6, 28), # Nose bridge top
|
|
(4, 31) # Nose tip
|
|
],
|
|
"face_oval": [
|
|
(152, 9), # Left jawline
|
|
(127, 1), # Chin left
|
|
(356, 17), # Right jawline
|
|
(172, 5), # Mid jawline left
|
|
(397, 13) # Mid jawline right
|
|
]
|
|
}
|
|
|
|
# Virtual points interpolated from Dlib landmark pairs
|
|
# Format: mediapipe_idx: (dlib_idx1, dlib_idx2)
|
|
VIRTUAL_PAIRS = {
|
|
"9": (22, 23)
|
|
}
|
|
|
|
@classmethod
|
|
def interpolate_point(cls, dlib_landmarks: pd.DataFrame, dlib_idx1: int, dlib_idx2: int) -> Optional[
|
|
Tuple[float, float]]:
|
|
"""
|
|
Interpolate a point between two Dlib landmarks.
|
|
|
|
Args:
|
|
dlib_landmarks: DataFrame with Dlib landmarks
|
|
dlib_idx1: First Dlib landmark index
|
|
dlib_idx2: Second Dlib landmark index
|
|
|
|
Returns:
|
|
Tuple[float, float]: Interpolated (x, y) coordinates or None if error
|
|
"""
|
|
try:
|
|
# Get the two Dlib points
|
|
point1 = dlib_landmarks[dlib_landmarks['index'] == dlib_idx1]
|
|
point2 = dlib_landmarks[dlib_landmarks['index'] == dlib_idx2]
|
|
|
|
if point1.empty or point2.empty:
|
|
print(f"Warning: Could not find Dlib landmarks {dlib_idx1} and/or {dlib_idx2}")
|
|
return None
|
|
|
|
# Calculate midpoint
|
|
x = (point1['x'].iloc[0] + point2['x'].iloc[0]) / 2
|
|
y = (point1['y'].iloc[0] + point2['y'].iloc[0]) / 2
|
|
|
|
return float(x), float(y)
|
|
|
|
except Exception as e:
|
|
print(f"Error interpolating point between Dlib landmarks {dlib_idx1} and {dlib_idx2}: {str(e)}")
|
|
return None
|
|
|
|
@classmethod
|
|
def get_control_points(cls, dlib_landmarks: pd.DataFrame) -> Optional[pd.DataFrame]:
|
|
"""
|
|
Generate control points DataFrame based on Dlib landmarks.
|
|
Includes both direct mappings and interpolated points.
|
|
|
|
Args:
|
|
dlib_landmarks: DataFrame containing Dlib landmark coordinates with columns [x, y, index]
|
|
|
|
Returns:
|
|
pd.DataFrame: DataFrame containing mapped MediaPipe landmark positions with columns [x, y, index]
|
|
or None if input data is invalid
|
|
"""
|
|
try:
|
|
# Validate input DataFrame
|
|
required_columns = {'x', 'y', 'index'}
|
|
if not all(col in dlib_landmarks.columns for col in required_columns):
|
|
print("Error: Input DataFrame missing required columns [x, y, index]")
|
|
return None
|
|
|
|
# Prepare data structures for control points
|
|
control_points = {
|
|
'x': [],
|
|
'y': [],
|
|
'index': []
|
|
}
|
|
|
|
# Process regular landmark pairs
|
|
print("Processing direct landmark mappings...")
|
|
for feature_group, pairs in cls.LANDMARKS_PAIRS.items():
|
|
for mp_idx, dlib_idx in pairs:
|
|
dlib_point = dlib_landmarks[dlib_landmarks['index'] == dlib_idx]
|
|
|
|
if dlib_point.empty:
|
|
print(f"Warning: Dlib landmark {dlib_idx} not found for MediaPipe index {mp_idx}")
|
|
continue
|
|
|
|
control_points['x'].append(float(dlib_point['x'].iloc[0]))
|
|
control_points['y'].append(float(dlib_point['y'].iloc[0]))
|
|
control_points['index'].append(mp_idx)
|
|
|
|
# Process interpolated virtual points
|
|
print("Processing interpolated control points...")
|
|
for mp_idx, (dlib_idx1, dlib_idx2) in cls.VIRTUAL_PAIRS.items():
|
|
interpolated = cls.interpolate_point(dlib_landmarks, dlib_idx1, dlib_idx2)
|
|
if interpolated is not None:
|
|
x, y = interpolated
|
|
control_points['x'].append(x)
|
|
control_points['y'].append(y)
|
|
control_points['index'].append(int(mp_idx))
|
|
print(f"Added interpolated point for MediaPipe {mp_idx} at ({x:.1f}, {y:.1f})")
|
|
|
|
# Create DataFrame with all control points
|
|
control_df = pd.DataFrame(control_points)
|
|
|
|
# Sort by MediaPipe landmark index for consistency
|
|
control_df = control_df.sort_values('index').reset_index(drop=True)
|
|
|
|
print(f"Successfully mapped {len(control_df)} control points "
|
|
f"({len(control_df) - len(cls.VIRTUAL_PAIRS)} regular, "
|
|
f"{len(cls.VIRTUAL_PAIRS)} interpolated)")
|
|
|
|
return control_df
|
|
|
|
except Exception as e:
|
|
print(f"Error generating control points: {str(e)}")
|
|
return None
|
|
|
|
# TODO: filter_trackers()
|
|
def filter_trackers(self, distance):
|
|
"""
|
|
return list of base mediapipe landmarks ids as a list of trackers to use for cv2.goodFeaturesToTrack
|
|
based on given distance
|
|
"""
|
|
trackers = []
|
|
return trackers |