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()