Spaces:
Paused
Paused
| 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: | |
| # 初始化MediaPipe Face Mesh | |
| 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: | |
| # MediaPipe需要RGB图像 | |
| 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] | |
| # 将MediaPipe的468个关键点转换为类似dlib 68点的格式 | |
| 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关键点索引到dlib 68点的映射 | |
| # 这个映射基于MediaPipe Face Mesh的标准索引 | |
| mediapipe_to_dlib_map = { | |
| # 面部轮廓 (0-16) | |
| 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-21) | |
| 17: 70, # 右眉毛外端 | |
| 18: 63, # 右眉毛 | |
| 19: 105, # 右眉毛 | |
| 20: 66, # 右眉毛 | |
| 21: 107, # 右眉毛内端 | |
| # 左眉毛 (22-26) | |
| 22: 336, # 左眉毛内端 | |
| 23: 296, # 左眉毛 | |
| 24: 334, # 左眉毛 | |
| 25: 293, # 左眉毛 | |
| 26: 300, # 左眉毛外端 | |
| # 鼻梁 (27-30) | |
| 27: 168, # 鼻梁顶 | |
| 28: 8, # 鼻梁 | |
| 29: 9, # 鼻梁 | |
| 30: 10, # 鼻梁底 | |
| # 鼻翼 (31-35) | |
| 31: 151, # 右鼻翼 | |
| 32: 134, # 右鼻孔 | |
| 33: 2, # 鼻尖 | |
| 34: 363, # 左鼻孔 | |
| 35: 378, # 左鼻翼 | |
| # 右眼 (36-41) | |
| 36: 33, # 右眼外角 | |
| 37: 7, # 右眼上眼睑 | |
| 38: 163, # 右眼上眼睑 | |
| 39: 144, # 右眼内角 | |
| 40: 145, # 右眼下眼睑 | |
| 41: 153, # 右眼下眼睑 | |
| # 左眼 (42-47) | |
| 42: 362, # 左眼内角 | |
| 43: 382, # 左眼上眼睑 | |
| 44: 381, # 左眼上眼睑 | |
| 45: 380, # 左眼外角 | |
| 46: 374, # 左眼下眼睑 | |
| 47: 373, # 左眼下眼睑 | |
| # 嘴部轮廓 (48-67) | |
| 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 # 调整理想比例,原来0.15太大 | |
| 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 | |
| # 1. 嘴宽评分 - 更宽松的标准 | |
| mouth_width = abs(right_corner[0] - left_corner[0]) | |
| face_width = image_shape[1] | |
| mouth_ratio = mouth_width / face_width | |
| # 设置更宽的合理范围 (0.04-0.15) | |
| 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) # 太大时渐减 | |
| # 2. 唇厚度评分 - 简化并放宽标准 | |
| lip_thickness = abs(lower_lip_center[1] - upper_lip_center[1]) | |
| # 只要厚度不是极端值就给高分 | |
| if lip_thickness > 3: # 像素值,有一定厚度 | |
| thickness_score = min(1.0, lip_thickness / 25) # 25像素为满分 | |
| else: | |
| thickness_score = 0.5 # 太薄给中等分数 | |
| # 3. 嘴部对称性评分 - 更宽松 | |
| 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: # 偏差小于2% | |
| symmetry_score = 1.0 | |
| elif center_deviation < 0.05: # 偏差小于5% | |
| symmetry_score = 0.8 | |
| else: | |
| symmetry_score = max(0.5, 1 - center_deviation * 10) # 最低0.5分 | |
| # 4. 嘴唇形状评分 - 简化 | |
| # 检查嘴角是否在合理位置 | |
| 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)) | |
| # 5. 综合评分 - 调整权重,给基础分更大权重 | |
| 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 # 最高10分 | |
| return round(max(4.0, min(10, final_score)), 2) # 最低4分,最高10分 | |
| 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]) # 眉峰(y坐标最小) | |
| 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]) | |
| # 理想比例约为1.618 | |
| 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: # 假设使用68点面部关键点 | |
| return 6.21 | |
| # 转换为numpy数组便于计算 | |
| points = np.array(landmarks) | |
| # 1. 面部基础测量 | |
| face_measurements = self._get_face_measurements(points) | |
| # 2. 计算多个协调性指标 | |
| scores = [] | |
| # 黄金比例评分 (权重: 20%) | |
| golden_score = self._calculate_golden_ratios(face_measurements) | |
| logger.info(f"Golden ratio score={golden_score}") | |
| scores.append(("golden_ratio", golden_score, 0.10)) | |
| # 对称性评分 (权重: 25%) | |
| symmetry_score = self._calculate_facial_symmetry(face_measurements, points) | |
| logger.info(f"Symmetry score={symmetry_score}") | |
| scores.append(("symmetry", symmetry_score, 0.40)) | |
| # 三庭五眼比例 (权重: 20%) | |
| 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)) | |
| # 五官间距协调性 (权重: 15%) | |
| spacing_score = self._calculate_feature_spacing(face_measurements) | |
| logger.info(f"Facial feature spacing harmony={spacing_score}") | |
| scores.append(("spacing", spacing_score, 0)) | |
| # 面部轮廓协调性 (权重: 10%) | |
| contour_score = self._calculate_contour_harmony(points) | |
| logger.info(f"Facial contour harmony={contour_score}") | |
| scores.append(("contour", contour_score, 0.05)) | |
| # 眼鼻口比例协调性 (权重: 10%) | |
| 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 = {} | |
| # 面部轮廓点 (0-16) | |
| face_contour = points[0:17] | |
| # 眉毛点 (17-26) | |
| left_eyebrow = points[17:22] | |
| right_eyebrow = points[22:27] | |
| # 眼睛点 (36-47) | |
| left_eye = points[36:42] | |
| right_eye = points[42:48] | |
| # 鼻子点 (27-35) | |
| nose = points[27:36] | |
| # 嘴巴点 (48-67) | |
| 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 | |
| # 理想比例约为 1:1:1 | |
| 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 # 理想情况下面宽应该等于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 # 理想鼻宽约为眼宽的80% | |
| 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 | |
| ) | |
| # 理想眼部长宽比约为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() | |
| # MediaPipe需要RGB图像 | |
| 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() | |