|
|
import traceback |
|
|
from typing import List, Dict, Any |
|
|
|
|
|
import cv2 |
|
|
import numpy as np |
|
|
|
|
|
import config |
|
|
from config import logger, DLIB_AVAILABLE |
|
|
|
|
|
if DLIB_AVAILABLE: |
|
|
import mediapipe as mp |
|
|
|
|
|
|
|
|
class FacialFeatureAnalyzer: |
|
|
"""五官分析器""" |
|
|
|
|
|
def __init__(self): |
|
|
self.face_mesh = None |
|
|
if DLIB_AVAILABLE: |
|
|
try: |
|
|
|
|
|
mp_face_mesh = mp.solutions.face_mesh |
|
|
self.face_mesh = mp_face_mesh.FaceMesh( |
|
|
static_image_mode=True, |
|
|
max_num_faces=1, |
|
|
refine_landmarks=True, |
|
|
min_detection_confidence=0.5, |
|
|
min_tracking_confidence=0.5 |
|
|
) |
|
|
logger.info("MediaPipe face landmark detector loaded successfully") |
|
|
except Exception as e: |
|
|
logger.error(f"Failed to load MediaPipe model: {e}") |
|
|
|
|
|
def analyze_facial_features( |
|
|
self, face_image: np.ndarray, face_box: List[int] |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
分析五官特征 |
|
|
:param face_image: 人脸图像 |
|
|
:param face_box: 人脸边界框 [x1, y1, x2, y2] |
|
|
:return: 五官分析结果 |
|
|
""" |
|
|
if not DLIB_AVAILABLE or self.face_mesh is None: |
|
|
return self._basic_facial_analysis(face_image) |
|
|
|
|
|
try: |
|
|
|
|
|
rgb_image = cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB) |
|
|
|
|
|
|
|
|
results = self.face_mesh.process(rgb_image) |
|
|
|
|
|
if not results.multi_face_landmarks: |
|
|
logger.warning("No facial landmarks detected") |
|
|
return self._basic_facial_analysis(face_image) |
|
|
|
|
|
|
|
|
face_landmarks = results.multi_face_landmarks[0] |
|
|
|
|
|
|
|
|
points = self._convert_mediapipe_to_dlib_format(face_landmarks, face_image.shape) |
|
|
|
|
|
return self._analyze_features_from_landmarks(points, face_image.shape) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Facial feature analysis failed: {e}") |
|
|
traceback.print_exc() |
|
|
return self._basic_facial_analysis(face_image) |
|
|
|
|
|
def _convert_mediapipe_to_dlib_format(self, face_landmarks, image_shape): |
|
|
""" |
|
|
将MediaPipe的468个关键点转换为类似dlib 68点的格式 |
|
|
MediaPipe到dlib的关键点映射 |
|
|
""" |
|
|
h, w = image_shape[:2] |
|
|
|
|
|
|
|
|
|
|
|
mediapipe_to_dlib_map = { |
|
|
|
|
|
0: 234, |
|
|
1: 132, |
|
|
2: 172, |
|
|
3: 136, |
|
|
4: 150, |
|
|
5: 149, |
|
|
6: 176, |
|
|
7: 148, |
|
|
8: 152, |
|
|
9: 377, |
|
|
10: 400, |
|
|
11: 378, |
|
|
12: 379, |
|
|
13: 365, |
|
|
14: 397, |
|
|
15: 361, |
|
|
16: 454, |
|
|
|
|
|
|
|
|
17: 70, |
|
|
18: 63, |
|
|
19: 105, |
|
|
20: 66, |
|
|
21: 107, |
|
|
|
|
|
|
|
|
22: 336, |
|
|
23: 296, |
|
|
24: 334, |
|
|
25: 293, |
|
|
26: 300, |
|
|
|
|
|
|
|
|
27: 168, |
|
|
28: 8, |
|
|
29: 9, |
|
|
30: 10, |
|
|
|
|
|
|
|
|
31: 151, |
|
|
32: 134, |
|
|
33: 2, |
|
|
34: 363, |
|
|
35: 378, |
|
|
|
|
|
|
|
|
36: 33, |
|
|
37: 7, |
|
|
38: 163, |
|
|
39: 144, |
|
|
40: 145, |
|
|
41: 153, |
|
|
|
|
|
|
|
|
42: 362, |
|
|
43: 382, |
|
|
44: 381, |
|
|
45: 380, |
|
|
46: 374, |
|
|
47: 373, |
|
|
|
|
|
|
|
|
48: 78, |
|
|
49: 95, |
|
|
50: 88, |
|
|
51: 178, |
|
|
52: 87, |
|
|
53: 14, |
|
|
54: 317, |
|
|
55: 318, |
|
|
56: 308, |
|
|
57: 324, |
|
|
58: 318, |
|
|
59: 16, |
|
|
60: 17, |
|
|
61: 18, |
|
|
62: 200, |
|
|
63: 199, |
|
|
64: 175, |
|
|
65: 84, |
|
|
66: 17, |
|
|
67: 314, |
|
|
} |
|
|
|
|
|
|
|
|
points = [] |
|
|
for i in range(68): |
|
|
if i in mediapipe_to_dlib_map: |
|
|
mp_idx = mediapipe_to_dlib_map[i] |
|
|
if mp_idx < len(face_landmarks.landmark): |
|
|
landmark = face_landmarks.landmark[mp_idx] |
|
|
x = int(landmark.x * w) |
|
|
y = int(landmark.y * h) |
|
|
points.append((x, y)) |
|
|
else: |
|
|
|
|
|
points.append((w//2, h//2)) |
|
|
else: |
|
|
|
|
|
points.append((w//2, h//2)) |
|
|
|
|
|
return points |
|
|
|
|
|
def _analyze_features_from_landmarks( |
|
|
self, landmarks: List[tuple], image_shape: tuple |
|
|
) -> Dict[str, Any]: |
|
|
"""基于68个关键点分析五官""" |
|
|
try: |
|
|
|
|
|
jawline = landmarks[0:17] |
|
|
left_eyebrow = landmarks[17:22] |
|
|
right_eyebrow = landmarks[22:27] |
|
|
nose = landmarks[27:36] |
|
|
left_eye = landmarks[36:42] |
|
|
right_eye = landmarks[42:48] |
|
|
mouth = landmarks[48:68] |
|
|
|
|
|
|
|
|
scores = { |
|
|
"eyes": self._score_eyes(left_eye, right_eye, image_shape), |
|
|
"nose": self._score_nose(nose, image_shape), |
|
|
"mouth": self._score_mouth(mouth, image_shape), |
|
|
"eyebrows": self._score_eyebrows( |
|
|
left_eyebrow, right_eyebrow, image_shape |
|
|
), |
|
|
"jawline": self._score_jawline(jawline, image_shape), |
|
|
} |
|
|
|
|
|
|
|
|
harmony_score = self._calculate_harmony_new(landmarks, image_shape) |
|
|
|
|
|
harmony_score = self._adjust_harmony_score(harmony_score) |
|
|
|
|
|
return { |
|
|
"facial_features": scores, |
|
|
"harmony_score": round(harmony_score, 2), |
|
|
"overall_facial_score": round(sum(scores.values()) / len(scores), 2), |
|
|
"analysis_method": "mediapipe_landmarks", |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Landmark analysis failed: {e}") |
|
|
return self._basic_facial_analysis(None) |
|
|
|
|
|
def _adjust_harmony_score(self, score: float) -> float: |
|
|
"""整体协调性分值温和拉升:当低于阈值时往阈值靠拢一点。""" |
|
|
try: |
|
|
if not getattr(config, "HARMONY_ADJUST_ENABLED", False): |
|
|
return round(float(score), 2) |
|
|
thr = float(getattr(config, "HARMONY_ADJUST_THRESHOLD", 8.0)) |
|
|
gamma = float(getattr(config, "HARMONY_ADJUST_GAMMA", 0.5)) |
|
|
gamma = max(0.0001, min(1.0, gamma)) |
|
|
s = float(score) |
|
|
if s < thr: |
|
|
s = thr - gamma * (thr - s) |
|
|
return round(min(10.0, max(0.0, s)), 2) |
|
|
except Exception: |
|
|
try: |
|
|
return round(float(score), 2) |
|
|
except Exception: |
|
|
return 6.21 |
|
|
|
|
|
def _score_eyes( |
|
|
self, left_eye: List[tuple], right_eye: List[tuple], image_shape: tuple |
|
|
) -> float: |
|
|
"""眼部评分""" |
|
|
try: |
|
|
|
|
|
left_width = abs(left_eye[3][0] - left_eye[0][0]) |
|
|
right_width = abs(right_eye[3][0] - right_eye[0][0]) |
|
|
|
|
|
|
|
|
left_height = abs(left_eye[1][1] - left_eye[5][1]) |
|
|
right_height = abs(right_eye[1][1] - right_eye[5][1]) |
|
|
|
|
|
|
|
|
width_symmetry = 1 - min( |
|
|
abs(left_width - right_width) / max(left_width, right_width), 0.5 |
|
|
) |
|
|
|
|
|
|
|
|
height_symmetry = 1 - min( |
|
|
abs(left_height - right_height) / max(left_height, right_height), 0.5 |
|
|
) |
|
|
|
|
|
|
|
|
avg_eye_width = (left_width + right_width) / 2 |
|
|
face_width = image_shape[1] |
|
|
ideal_ratio = 0.08 |
|
|
size_score = max( |
|
|
0, 1 - abs(avg_eye_width / face_width - ideal_ratio) / ideal_ratio |
|
|
) |
|
|
|
|
|
|
|
|
avg_eye_height = (left_height + right_height) / 2 |
|
|
aspect_ratio = avg_eye_width / max(avg_eye_height, 1) |
|
|
ideal_aspect = 3.0 |
|
|
aspect_score = max(0, 1 - abs(aspect_ratio - ideal_aspect) / ideal_aspect) |
|
|
|
|
|
final_score = ( |
|
|
width_symmetry * 0.3 |
|
|
+ height_symmetry * 0.3 |
|
|
+ size_score * 0.25 |
|
|
+ aspect_score * 0.15 |
|
|
) * 10 |
|
|
return round(max(0, min(10, final_score)), 2) |
|
|
except: |
|
|
return 6.21 |
|
|
|
|
|
def _score_nose(self, nose: List[tuple], image_shape: tuple) -> float: |
|
|
"""鼻部评分""" |
|
|
try: |
|
|
|
|
|
nose_tip = nose[3] |
|
|
nose_bridge_top = nose[0] |
|
|
left_nostril = nose[1] |
|
|
right_nostril = nose[5] |
|
|
|
|
|
|
|
|
straightness = 1 - min( |
|
|
abs(nose_tip[0] - nose_bridge_top[0]) / (image_shape[1] * 0.1), 1.0 |
|
|
) |
|
|
|
|
|
|
|
|
nose_width = abs(right_nostril[0] - left_nostril[0]) |
|
|
face_width = image_shape[1] |
|
|
ideal_nose_ratio = 0.06 |
|
|
width_score = max( |
|
|
0, |
|
|
1 - abs(nose_width / face_width - ideal_nose_ratio) / ideal_nose_ratio, |
|
|
) |
|
|
|
|
|
|
|
|
nose_length = abs(nose_tip[1] - nose_bridge_top[1]) |
|
|
face_height = image_shape[0] |
|
|
ideal_length_ratio = 0.08 |
|
|
length_score = max( |
|
|
0, |
|
|
1 |
|
|
- abs(nose_length / face_height - ideal_length_ratio) |
|
|
/ ideal_length_ratio, |
|
|
) |
|
|
|
|
|
final_score = ( |
|
|
straightness * 0.4 + width_score * 0.35 + length_score * 0.25 |
|
|
) * 10 |
|
|
return round(max(0, min(10, final_score)), 2) |
|
|
except: |
|
|
return 6.21 |
|
|
|
|
|
def _score_mouth(self, mouth: List[tuple], image_shape: tuple) -> float: |
|
|
"""嘴部评分 - 大幅优化,更宽松的评分标准""" |
|
|
try: |
|
|
|
|
|
left_corner = mouth[0] |
|
|
right_corner = mouth[6] |
|
|
|
|
|
|
|
|
upper_lip_center = mouth[3] |
|
|
lower_lip_center = mouth[9] |
|
|
|
|
|
|
|
|
base_score = 6.0 |
|
|
|
|
|
|
|
|
mouth_width = abs(right_corner[0] - left_corner[0]) |
|
|
face_width = image_shape[1] |
|
|
mouth_ratio = mouth_width / face_width |
|
|
|
|
|
|
|
|
if 0.04 <= mouth_ratio <= 0.15: |
|
|
width_score = 1.0 |
|
|
elif mouth_ratio < 0.04: |
|
|
width_score = max(0.3, mouth_ratio / 0.04) |
|
|
else: |
|
|
width_score = max(0.3, 0.15 / mouth_ratio) |
|
|
|
|
|
|
|
|
lip_thickness = abs(lower_lip_center[1] - upper_lip_center[1]) |
|
|
|
|
|
if lip_thickness > 3: |
|
|
thickness_score = min(1.0, lip_thickness / 25) |
|
|
else: |
|
|
thickness_score = 0.5 |
|
|
|
|
|
|
|
|
mouth_center_x = (left_corner[0] + right_corner[0]) / 2 |
|
|
face_center_x = image_shape[1] / 2 |
|
|
center_deviation = abs(mouth_center_x - face_center_x) / face_width |
|
|
|
|
|
if center_deviation < 0.02: |
|
|
symmetry_score = 1.0 |
|
|
elif center_deviation < 0.05: |
|
|
symmetry_score = 0.8 |
|
|
else: |
|
|
symmetry_score = max(0.5, 1 - center_deviation * 10) |
|
|
|
|
|
|
|
|
|
|
|
corner_height_diff = abs(left_corner[1] - right_corner[1]) |
|
|
if corner_height_diff < face_width * 0.02: |
|
|
shape_score = 1.0 |
|
|
else: |
|
|
shape_score = max(0.6, 1 - corner_height_diff / (face_width * 0.02)) |
|
|
|
|
|
|
|
|
feature_score = ( |
|
|
width_score * 0.3 |
|
|
+ thickness_score * 0.25 |
|
|
+ symmetry_score * 0.25 |
|
|
+ shape_score * 0.2 |
|
|
) |
|
|
|
|
|
|
|
|
final_score = base_score + feature_score * 4 |
|
|
|
|
|
return round(max(4.0, min(10, final_score)), 2) |
|
|
except Exception as e: |
|
|
return 6.21 |
|
|
|
|
|
def _score_eyebrows( |
|
|
self, left_brow: List[tuple], right_brow: List[tuple], image_shape: tuple |
|
|
) -> float: |
|
|
"""眉毛评分 - 改进算法""" |
|
|
try: |
|
|
|
|
|
left_length = abs(left_brow[-1][0] - left_brow[0][0]) |
|
|
right_length = abs(right_brow[-1][0] - right_brow[0][0]) |
|
|
|
|
|
|
|
|
length_symmetry = 1 - min( |
|
|
abs(left_length - right_length) / max(left_length, right_length), 0.5 |
|
|
) |
|
|
|
|
|
|
|
|
left_peak_y = min([p[1] for p in left_brow]) |
|
|
left_ends_y = (left_brow[0][1] + left_brow[-1][1]) / 2 |
|
|
left_arch = max(0, left_ends_y - left_peak_y) |
|
|
|
|
|
right_peak_y = min([p[1] for p in right_brow]) |
|
|
right_ends_y = (right_brow[0][1] + right_brow[-1][1]) / 2 |
|
|
right_arch = max(0, right_ends_y - right_peak_y) |
|
|
|
|
|
|
|
|
arch_symmetry = 1 - min( |
|
|
abs(left_arch - right_arch) / max(left_arch, right_arch, 1), 0.5 |
|
|
) |
|
|
|
|
|
|
|
|
avg_arch = (left_arch + right_arch) / 2 |
|
|
face_height = image_shape[0] |
|
|
ideal_arch_ratio = 0.015 |
|
|
arch_ratio = avg_arch / face_height |
|
|
arch_score = max( |
|
|
0, 1 - abs(arch_ratio - ideal_arch_ratio) / ideal_arch_ratio |
|
|
) |
|
|
|
|
|
|
|
|
density_score = min(1.0, (len(left_brow) + len(right_brow)) / 10) |
|
|
|
|
|
final_score = ( |
|
|
length_symmetry * 0.3 |
|
|
+ arch_symmetry * 0.3 |
|
|
+ arch_score * 0.25 |
|
|
+ density_score * 0.15 |
|
|
) * 10 |
|
|
return round(max(0, min(10, final_score)), 2) |
|
|
except: |
|
|
return 6.21 |
|
|
|
|
|
def _score_jawline(self, jawline: List[tuple], image_shape: tuple) -> float: |
|
|
"""下颌线评分 - 改进算法""" |
|
|
try: |
|
|
jaw_points = [(p[0], p[1]) for p in jawline] |
|
|
|
|
|
|
|
|
left_jaw = jaw_points[2] |
|
|
jaw_tip = jaw_points[8] |
|
|
right_jaw = jaw_points[14] |
|
|
|
|
|
|
|
|
left_dist = ( |
|
|
(left_jaw[0] - jaw_tip[0]) ** 2 + (left_jaw[1] - jaw_tip[1]) ** 2 |
|
|
) ** 0.5 |
|
|
right_dist = ( |
|
|
(right_jaw[0] - jaw_tip[0]) ** 2 + (right_jaw[1] - jaw_tip[1]) ** 2 |
|
|
) ** 0.5 |
|
|
symmetry = 1 - min( |
|
|
abs(left_dist - right_dist) / max(left_dist, right_dist), 0.5 |
|
|
) |
|
|
|
|
|
|
|
|
left_angle_y = abs(left_jaw[1] - jaw_tip[1]) |
|
|
right_angle_y = abs(right_jaw[1] - jaw_tip[1]) |
|
|
avg_angle = (left_angle_y + right_angle_y) / 2 |
|
|
|
|
|
|
|
|
face_height = image_shape[0] |
|
|
ideal_angle_ratio = 0.08 |
|
|
angle_ratio = avg_angle / face_height |
|
|
angle_score = max( |
|
|
0, 1 - abs(angle_ratio - ideal_angle_ratio) / ideal_angle_ratio |
|
|
) |
|
|
|
|
|
|
|
|
smoothness_score = 0.8 |
|
|
|
|
|
final_score = ( |
|
|
symmetry * 0.4 + angle_score * 0.35 + smoothness_score * 0.25 |
|
|
) * 10 |
|
|
return round(max(0, min(10, final_score)), 2) |
|
|
except: |
|
|
return 6.21 |
|
|
|
|
|
def _calculate_harmony(self, landmarks: List[tuple], image_shape: tuple) -> float: |
|
|
"""计算五官协调性""" |
|
|
try: |
|
|
|
|
|
face_height = max([p[1] for p in landmarks]) - min( |
|
|
[p[1] for p in landmarks] |
|
|
) |
|
|
face_width = max([p[0] for p in landmarks]) - min([p[0] for p in landmarks]) |
|
|
|
|
|
|
|
|
ratio = face_height / face_width if face_width > 0 else 1 |
|
|
golden_ratio = 1.618 |
|
|
harmony = 1 - abs(ratio - golden_ratio) / golden_ratio |
|
|
|
|
|
return max(0, min(10, harmony * 10)) |
|
|
except: |
|
|
return 6.21 |
|
|
|
|
|
def _calculate_harmony_new( |
|
|
self, landmarks: List[tuple], image_shape: tuple |
|
|
) -> float: |
|
|
""" |
|
|
计算五官协调性 - 优化版本 |
|
|
基于多个美学比例和对称性指标 |
|
|
""" |
|
|
try: |
|
|
logger.info(f"face landmarks={len(landmarks)}") |
|
|
if len(landmarks) < 68: |
|
|
return 6.21 |
|
|
|
|
|
|
|
|
points = np.array(landmarks) |
|
|
|
|
|
|
|
|
face_measurements = self._get_face_measurements(points) |
|
|
|
|
|
|
|
|
scores = [] |
|
|
|
|
|
|
|
|
golden_score = self._calculate_golden_ratios(face_measurements) |
|
|
logger.info(f"Golden ratio score={golden_score}") |
|
|
scores.append(("golden_ratio", golden_score, 0.10)) |
|
|
|
|
|
|
|
|
symmetry_score = self._calculate_facial_symmetry(face_measurements, points) |
|
|
logger.info(f"Symmetry score={symmetry_score}") |
|
|
scores.append(("symmetry", symmetry_score, 0.40)) |
|
|
|
|
|
|
|
|
proportion_score = self._calculate_classical_proportions(face_measurements) |
|
|
logger.info(f"Three courts five eyes ratio={proportion_score}") |
|
|
scores.append(("proportions", proportion_score, 0.05)) |
|
|
|
|
|
|
|
|
spacing_score = self._calculate_feature_spacing(face_measurements) |
|
|
logger.info(f"Facial feature spacing harmony={spacing_score}") |
|
|
scores.append(("spacing", spacing_score, 0)) |
|
|
|
|
|
|
|
|
contour_score = self._calculate_contour_harmony(points) |
|
|
logger.info(f"Facial contour harmony={contour_score}") |
|
|
scores.append(("contour", contour_score, 0.05)) |
|
|
|
|
|
|
|
|
feature_score = self._calculate_feature_proportions(face_measurements) |
|
|
logger.info(f"Eye-nose-mouth proportion harmony={feature_score}") |
|
|
scores.append(("features", feature_score, 0.40)) |
|
|
|
|
|
|
|
|
final_score = sum(score * weight for _, score, weight in scores) |
|
|
logger.info(f"Weighted average final score={final_score}") |
|
|
return max(0, min(10, final_score)) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error calculating facial harmony: {e}") |
|
|
traceback.print_exc() |
|
|
return 6.21 |
|
|
|
|
|
def _get_face_measurements(self, points: np.ndarray) -> Dict[str, float]: |
|
|
"""提取面部关键测量数据""" |
|
|
measurements = {} |
|
|
|
|
|
|
|
|
face_contour = points[0:17] |
|
|
|
|
|
|
|
|
left_eyebrow = points[17:22] |
|
|
right_eyebrow = points[22:27] |
|
|
|
|
|
|
|
|
left_eye = points[36:42] |
|
|
right_eye = points[42:48] |
|
|
|
|
|
|
|
|
nose = points[27:36] |
|
|
|
|
|
|
|
|
mouth = points[48:68] |
|
|
|
|
|
|
|
|
measurements["face_width"] = np.max(face_contour[:, 0]) - np.min( |
|
|
face_contour[:, 0] |
|
|
) |
|
|
measurements["face_height"] = np.max(points[:, 1]) - np.min(points[:, 1]) |
|
|
|
|
|
|
|
|
measurements["left_eye_width"] = np.max(left_eye[:, 0]) - np.min(left_eye[:, 0]) |
|
|
measurements["right_eye_width"] = np.max(right_eye[:, 0]) - np.min( |
|
|
right_eye[:, 0] |
|
|
) |
|
|
measurements["eye_distance"] = np.min(right_eye[:, 0]) - np.max(left_eye[:, 0]) |
|
|
measurements["left_eye_center"] = np.mean(left_eye, axis=0) |
|
|
measurements["right_eye_center"] = np.mean(right_eye, axis=0) |
|
|
|
|
|
|
|
|
measurements["nose_width"] = np.max(nose[:, 0]) - np.min(nose[:, 0]) |
|
|
measurements["nose_height"] = np.max(nose[:, 1]) - np.min(nose[:, 1]) |
|
|
measurements["nose_tip"] = points[33] |
|
|
|
|
|
|
|
|
measurements["mouth_width"] = np.max(mouth[:, 0]) - np.min(mouth[:, 0]) |
|
|
measurements["mouth_height"] = np.max(mouth[:, 1]) - np.min(mouth[:, 1]) |
|
|
|
|
|
|
|
|
measurements["forehead_height"] = measurements["left_eye_center"][1] - np.min( |
|
|
points[:, 1] |
|
|
) |
|
|
measurements["middle_face_height"] = ( |
|
|
measurements["nose_tip"][1] - measurements["left_eye_center"][1] |
|
|
) |
|
|
measurements["lower_face_height"] = ( |
|
|
np.max(points[:, 1]) - measurements["nose_tip"][1] |
|
|
) |
|
|
|
|
|
return measurements |
|
|
|
|
|
def _calculate_golden_ratios(self, measurements: Dict[str, float]) -> float: |
|
|
"""计算黄金比例相关得分""" |
|
|
golden_ratio = 1.618 |
|
|
scores = [] |
|
|
|
|
|
|
|
|
if measurements["face_width"] > 0: |
|
|
face_ratio = measurements["face_height"] / measurements["face_width"] |
|
|
score = 1 - abs(face_ratio - golden_ratio) / golden_ratio |
|
|
scores.append(max(0, score)) |
|
|
|
|
|
|
|
|
total_height = ( |
|
|
measurements["forehead_height"] |
|
|
+ measurements["middle_face_height"] |
|
|
+ measurements["lower_face_height"] |
|
|
) |
|
|
|
|
|
if total_height > 0: |
|
|
upper_ratio = measurements["forehead_height"] / total_height |
|
|
middle_ratio = measurements["middle_face_height"] / total_height |
|
|
lower_ratio = measurements["lower_face_height"] / total_height |
|
|
|
|
|
|
|
|
ideal_ratio = 1 / 3 |
|
|
upper_score = 1 - abs(upper_ratio - ideal_ratio) / ideal_ratio |
|
|
middle_score = 1 - abs(middle_ratio - ideal_ratio) / ideal_ratio |
|
|
lower_score = 1 - abs(lower_ratio - ideal_ratio) / ideal_ratio |
|
|
|
|
|
scores.extend( |
|
|
[max(0, upper_score), max(0, middle_score), max(0, lower_score)] |
|
|
) |
|
|
|
|
|
return np.mean(scores) * 10 if scores else 7.0 |
|
|
|
|
|
def _calculate_facial_symmetry( |
|
|
self, measurements: Dict[str, float], points: np.ndarray |
|
|
) -> float: |
|
|
"""计算面部对称性""" |
|
|
|
|
|
face_center_x = np.mean(points[:, 0]) |
|
|
|
|
|
|
|
|
symmetry_pairs = [ |
|
|
(17, 26), |
|
|
(18, 25), |
|
|
(19, 24), |
|
|
(36, 45), |
|
|
(39, 42), |
|
|
(31, 35), |
|
|
(48, 54), |
|
|
(4, 12), |
|
|
(5, 11), |
|
|
(6, 10), |
|
|
] |
|
|
|
|
|
symmetry_scores = [] |
|
|
|
|
|
for left_idx, right_idx in symmetry_pairs: |
|
|
if left_idx < len(points) and right_idx < len(points): |
|
|
left_point = points[left_idx] |
|
|
right_point = points[right_idx] |
|
|
|
|
|
|
|
|
left_dist = abs(left_point[0] - face_center_x) |
|
|
right_dist = abs(right_point[0] - face_center_x) |
|
|
|
|
|
|
|
|
vertical_diff = abs(left_point[1] - right_point[1]) |
|
|
|
|
|
|
|
|
if left_dist + right_dist > 0: |
|
|
horizontal_symmetry = 1 - abs(left_dist - right_dist) / ( |
|
|
left_dist + right_dist |
|
|
) |
|
|
vertical_symmetry = 1 - vertical_diff / measurements.get( |
|
|
"face_height", 100 |
|
|
) |
|
|
|
|
|
symmetry_scores.append( |
|
|
(horizontal_symmetry + vertical_symmetry) / 2 |
|
|
) |
|
|
|
|
|
return np.mean(symmetry_scores) * 10 if symmetry_scores else 7.0 |
|
|
|
|
|
def _calculate_classical_proportions(self, measurements: Dict[str, float]) -> float: |
|
|
"""计算经典美学比例 (三庭五眼等)""" |
|
|
scores = [] |
|
|
|
|
|
|
|
|
if measurements["face_width"] > 0: |
|
|
eye_width_avg = ( |
|
|
measurements["left_eye_width"] + measurements["right_eye_width"] |
|
|
) / 2 |
|
|
ideal_eye_count = 5 |
|
|
actual_eye_count = ( |
|
|
measurements["face_width"] / eye_width_avg if eye_width_avg > 0 else 5 |
|
|
) |
|
|
|
|
|
eye_proportion_score = ( |
|
|
1 - abs(actual_eye_count - ideal_eye_count) / ideal_eye_count |
|
|
) |
|
|
scores.append(max(0, eye_proportion_score)) |
|
|
|
|
|
|
|
|
if measurements.get("left_eye_width", 0) > 0: |
|
|
eye_spacing_ratio = ( |
|
|
measurements["eye_distance"] / measurements["left_eye_width"] |
|
|
) |
|
|
ideal_spacing_ratio = 1.0 |
|
|
|
|
|
spacing_score = ( |
|
|
1 - abs(eye_spacing_ratio - ideal_spacing_ratio) / ideal_spacing_ratio |
|
|
) |
|
|
scores.append(max(0, spacing_score)) |
|
|
|
|
|
|
|
|
if ( |
|
|
measurements.get("left_eye_width", 0) > 0 |
|
|
and measurements.get("nose_width", 0) > 0 |
|
|
): |
|
|
nose_eye_ratio = measurements["nose_width"] / measurements["left_eye_width"] |
|
|
ideal_nose_eye_ratio = 0.8 |
|
|
|
|
|
nose_score = ( |
|
|
1 - abs(nose_eye_ratio - ideal_nose_eye_ratio) / ideal_nose_eye_ratio |
|
|
) |
|
|
scores.append(max(0, nose_score)) |
|
|
|
|
|
return np.mean(scores) * 10 if scores else 7.0 |
|
|
|
|
|
def _calculate_feature_spacing(self, measurements: Dict[str, float]) -> float: |
|
|
"""计算五官间距协调性""" |
|
|
scores = [] |
|
|
|
|
|
|
|
|
eye_nose_distance = abs( |
|
|
measurements["left_eye_center"][1] - measurements["nose_tip"][1] |
|
|
) |
|
|
if measurements.get("face_height", 0) > 0: |
|
|
eye_nose_ratio = eye_nose_distance / measurements["face_height"] |
|
|
ideal_ratio = 0.15 |
|
|
score = 1 - abs(eye_nose_ratio - ideal_ratio) / ideal_ratio |
|
|
scores.append(max(0, score)) |
|
|
|
|
|
|
|
|
nose_mouth_distance = abs( |
|
|
measurements["nose_tip"][1] - np.mean([measurements.get("mouth_height", 0)]) |
|
|
) |
|
|
if measurements.get("face_height", 0) > 0: |
|
|
nose_mouth_ratio = nose_mouth_distance / measurements["face_height"] |
|
|
ideal_ratio = 0.12 |
|
|
score = 1 - abs(nose_mouth_ratio - ideal_ratio) / ideal_ratio |
|
|
scores.append(max(0, score)) |
|
|
|
|
|
return np.mean(scores) * 10 if scores else 7.0 |
|
|
|
|
|
def _calculate_contour_harmony(self, points: np.ndarray) -> float: |
|
|
"""计算面部轮廓协调性""" |
|
|
try: |
|
|
face_contour = points[0:17] |
|
|
|
|
|
|
|
|
smoothness_scores = [] |
|
|
|
|
|
for i in range(1, len(face_contour) - 1): |
|
|
|
|
|
p1, p2, p3 = face_contour[i - 1], face_contour[i], face_contour[i + 1] |
|
|
|
|
|
v1 = p1 - p2 |
|
|
v2 = p3 - p2 |
|
|
|
|
|
|
|
|
cos_angle = np.dot(v1, v2) / ( |
|
|
np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-8 |
|
|
) |
|
|
angle = np.arccos(np.clip(cos_angle, -1, 1)) |
|
|
|
|
|
|
|
|
smoothness = 1 - abs(angle - np.pi / 2) / (np.pi / 2) |
|
|
smoothness_scores.append(max(0, smoothness)) |
|
|
|
|
|
return np.mean(smoothness_scores) * 10 if smoothness_scores else 7.0 |
|
|
|
|
|
except: |
|
|
return 6.21 |
|
|
|
|
|
def _calculate_feature_proportions(self, measurements: Dict[str, float]) -> float: |
|
|
"""计算眼鼻口等五官内部比例协调性""" |
|
|
scores = [] |
|
|
|
|
|
|
|
|
left_eye_ratio = measurements.get("left_eye_width", 1) / max( |
|
|
measurements.get("left_eye_width", 1) * 0.3, 1 |
|
|
) |
|
|
right_eye_ratio = measurements.get("right_eye_width", 1) / max( |
|
|
measurements.get("right_eye_width", 1) * 0.3, 1 |
|
|
) |
|
|
|
|
|
|
|
|
ideal_eye_ratio = 3.0 |
|
|
left_eye_score = 1 - abs(left_eye_ratio - ideal_eye_ratio) / ideal_eye_ratio |
|
|
right_eye_score = 1 - abs(right_eye_ratio - ideal_eye_ratio) / ideal_eye_ratio |
|
|
|
|
|
scores.extend([max(0, left_eye_score), max(0, right_eye_score)]) |
|
|
|
|
|
|
|
|
if measurements.get("mouth_height", 0) > 0: |
|
|
mouth_ratio = measurements["mouth_width"] / measurements["mouth_height"] |
|
|
ideal_mouth_ratio = 3.5 |
|
|
mouth_score = 1 - abs(mouth_ratio - ideal_mouth_ratio) / ideal_mouth_ratio |
|
|
scores.append(max(0, mouth_score)) |
|
|
|
|
|
|
|
|
if measurements.get("nose_height", 0) > 0: |
|
|
nose_ratio = measurements["nose_height"] / measurements["nose_width"] |
|
|
ideal_nose_ratio = 1.5 |
|
|
nose_score = 1 - abs(nose_ratio - ideal_nose_ratio) / ideal_nose_ratio |
|
|
scores.append(max(0, nose_score)) |
|
|
|
|
|
return np.mean(scores) * 10 if scores else 7.0 |
|
|
|
|
|
def _basic_facial_analysis(self, face_image) -> Dict[str, Any]: |
|
|
"""基础五官分析 (当dlib不可用时)""" |
|
|
return { |
|
|
"facial_features": { |
|
|
"eyes": 7.0, |
|
|
"nose": 7.0, |
|
|
"mouth": 7.0, |
|
|
"eyebrows": 7.0, |
|
|
"jawline": 7.0, |
|
|
}, |
|
|
"harmony_score": 7.0, |
|
|
"overall_facial_score": 7.0, |
|
|
"analysis_method": "basic_estimation", |
|
|
} |
|
|
|
|
|
def draw_facial_landmarks(self, face_image: np.ndarray) -> np.ndarray: |
|
|
""" |
|
|
在人脸图像上绘制特征点 |
|
|
:param face_image: 人脸图像 |
|
|
:return: 带特征点标记的人脸图像 |
|
|
""" |
|
|
if not DLIB_AVAILABLE or self.face_mesh is None: |
|
|
|
|
|
return face_image.copy() |
|
|
|
|
|
try: |
|
|
|
|
|
annotated_image = face_image.copy() |
|
|
|
|
|
|
|
|
rgb_image = cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB) |
|
|
|
|
|
|
|
|
results = self.face_mesh.process(rgb_image) |
|
|
|
|
|
if not results.multi_face_landmarks: |
|
|
logger.warning("No facial landmarks detected for drawing") |
|
|
return annotated_image |
|
|
|
|
|
|
|
|
face_landmarks = results.multi_face_landmarks[0] |
|
|
|
|
|
|
|
|
h, w = face_image.shape[:2] |
|
|
for landmark in face_landmarks.landmark: |
|
|
x = int(landmark.x * w) |
|
|
y = int(landmark.y * h) |
|
|
|
|
|
cv2.circle(annotated_image, (x, y), 1, (0, 255, 0), -1) |
|
|
|
|
|
|
|
|
cv2.line(annotated_image, (x-2, y), (x+2, y), (0, 255, 0), 1) |
|
|
cv2.line(annotated_image, (x, y-2), (x, y+2), (0, 255, 0), 1) |
|
|
|
|
|
return annotated_image |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Failed to draw facial landmarks: {e}") |
|
|
return face_image.copy() |
|
|
|