picpocket / facial_analyzer.py
chenchaoyun
fix
d11ff01
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()