chenchaoyun commited on
Commit ·
0d573f0
1
Parent(s): 1de256e
refactor: 重构人脸特征打分与雷达图指标算法,修正物理关键点映射并增加方差拉伸对比度以提升雷达图的个性和起伏度
Browse files- config.py +4 -0
- facial_analyzer.py +113 -86
config.py
CHANGED
|
@@ -394,6 +394,10 @@ FACE_SCORE_MAX_IMAGES = int(os.environ.get("FACE_SCORE_MAX_IMAGES", 5)) # 颜
|
|
| 394 |
FEMALE_AGE_ADJUSTMENT = int(os.environ.get("FEMALE_AGE_ADJUSTMENT", 3)) # 默认减3岁
|
| 395 |
FEMALE_AGE_ADJUSTMENT_THRESHOLD = int(os.environ.get("FEMALE_AGE_ADJUSTMENT_THRESHOLD", 20)) # 年龄阈值,默认20岁
|
| 396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
# 配置日志
|
| 398 |
if ENABLE_LOGGING:
|
| 399 |
logging.basicConfig(
|
|
|
|
| 394 |
FEMALE_AGE_ADJUSTMENT = int(os.environ.get("FEMALE_AGE_ADJUSTMENT", 3)) # 默认减3岁
|
| 395 |
FEMALE_AGE_ADJUSTMENT_THRESHOLD = int(os.environ.get("FEMALE_AGE_ADJUSTMENT_THRESHOLD", 20)) # 年龄阈值,默认20岁
|
| 396 |
|
| 397 |
+
# 局部美学分数与总颜值的融合及对比度拉伸配置
|
| 398 |
+
FACIAL_FUSION_ALPHA = float(os.environ.get("FACIAL_FUSION_ALPHA", 0.35)) # 全局颜值分融合权重 (旧版硬编码0.6)
|
| 399 |
+
FACIAL_SPREAD_FACTOR = float(os.environ.get("FACIAL_SPREAD_FACTOR", 2.5)) # 五官与雷达图得分对比度方差拉伸因子
|
| 400 |
+
|
| 401 |
# 配置日志
|
| 402 |
if ENABLE_LOGGING:
|
| 403 |
logging.basicConfig(
|
facial_analyzer.py
CHANGED
|
@@ -74,11 +74,14 @@ class FacialFeatureAnalyzer:
|
|
| 74 |
# 获取第一个面部的关键点
|
| 75 |
face_landmarks = results.multi_face_landmarks[0]
|
| 76 |
|
|
|
|
|
|
|
|
|
|
| 77 |
# 将MediaPipe的468个关键点转换为类似dlib 68点的格式
|
| 78 |
points = self._convert_mediapipe_to_dlib_format(face_landmarks, face_image.shape)
|
| 79 |
|
| 80 |
return self._analyze_features_from_landmarks(
|
| 81 |
-
points, face_image.shape, language, age_str, gender, beauty_score, emotion
|
| 82 |
)
|
| 83 |
|
| 84 |
except Exception as e:
|
|
@@ -93,92 +96,90 @@ class FacialFeatureAnalyzer:
|
|
| 93 |
"""
|
| 94 |
h, w = image_shape[:2]
|
| 95 |
|
| 96 |
-
# MediaPipe关键点索引到dlib 68点的映射
|
| 97 |
-
# 这个映射基于MediaPipe Face Mesh的标准索引
|
| 98 |
mediapipe_to_dlib_map = {
|
| 99 |
# 面部轮廓 (0-16)
|
| 100 |
-
0:
|
| 101 |
-
1:
|
| 102 |
-
2:
|
| 103 |
-
3:
|
| 104 |
-
4:
|
| 105 |
-
5:
|
| 106 |
-
6:
|
| 107 |
-
7: 148, # 右
|
| 108 |
-
8: 152, #
|
| 109 |
-
9: 377, # 左
|
| 110 |
-
10:
|
| 111 |
-
11:
|
| 112 |
-
12:
|
| 113 |
-
13:
|
| 114 |
-
14:
|
| 115 |
-
15:
|
| 116 |
-
16:
|
| 117 |
|
| 118 |
# 右眉毛 (17-21)
|
| 119 |
-
17:
|
| 120 |
-
18: 63, # 右眉毛
|
| 121 |
-
19: 105, # 右眉毛
|
| 122 |
-
20: 66, # 右眉毛
|
| 123 |
21: 107, # 右眉毛内端
|
| 124 |
|
| 125 |
# 左眉毛 (22-26)
|
| 126 |
22: 336, # 左眉毛内端
|
| 127 |
-
23: 296, # 左眉毛
|
| 128 |
-
24: 334, # 左眉毛
|
| 129 |
-
25: 293, # 左眉毛
|
| 130 |
-
26:
|
| 131 |
|
| 132 |
# 鼻梁 (27-30)
|
| 133 |
27: 168, # 鼻梁顶
|
| 134 |
-
28:
|
| 135 |
-
29:
|
| 136 |
-
30:
|
| 137 |
|
| 138 |
-
# 鼻翼 (31-35)
|
| 139 |
-
31:
|
| 140 |
-
32:
|
| 141 |
33: 2, # 鼻尖
|
| 142 |
-
34:
|
| 143 |
-
35:
|
| 144 |
|
| 145 |
# 右眼 (36-41)
|
| 146 |
36: 33, # 右眼外角
|
| 147 |
-
37:
|
| 148 |
-
38:
|
| 149 |
-
39:
|
| 150 |
-
40:
|
| 151 |
-
41:
|
| 152 |
|
| 153 |
# 左眼 (42-47)
|
| 154 |
42: 362, # 左眼内角
|
| 155 |
-
43:
|
| 156 |
-
44:
|
| 157 |
-
45:
|
| 158 |
-
46:
|
| 159 |
-
47:
|
| 160 |
|
| 161 |
# 嘴部轮廓 (48-67)
|
| 162 |
-
48:
|
| 163 |
-
49:
|
| 164 |
-
50:
|
| 165 |
-
51:
|
| 166 |
-
52:
|
| 167 |
-
53:
|
| 168 |
-
54:
|
| 169 |
-
55:
|
| 170 |
-
56:
|
| 171 |
-
57:
|
| 172 |
-
58:
|
| 173 |
-
59:
|
| 174 |
-
60:
|
| 175 |
-
61:
|
| 176 |
-
62:
|
| 177 |
-
63:
|
| 178 |
-
64:
|
| 179 |
-
65:
|
| 180 |
-
66:
|
| 181 |
-
67:
|
| 182 |
}
|
| 183 |
|
| 184 |
# 转换关键点
|
|
@@ -209,6 +210,7 @@ class FacialFeatureAnalyzer:
|
|
| 209 |
gender: str = "Female",
|
| 210 |
beauty_score: float = 7.0,
|
| 211 |
emotion: str = "neutral",
|
|
|
|
| 212 |
) -> Dict[str, Any]:
|
| 213 |
"""基于68个关键点分析五官"""
|
| 214 |
try:
|
|
@@ -238,7 +240,7 @@ class FacialFeatureAnalyzer:
|
|
| 238 |
|
| 239 |
# 3. 提取基础测量数据用于雷达图
|
| 240 |
points_np = np.array(landmarks)
|
| 241 |
-
face_measurements = self._get_face_measurements(points_np)
|
| 242 |
|
| 243 |
# 4. 计算雷达图各项原始指标(十分制)
|
| 244 |
raw_symmetry = self._calculate_facial_symmetry(face_measurements, points_np)
|
|
@@ -250,23 +252,42 @@ class FacialFeatureAnalyzer:
|
|
| 250 |
# 5. 识别分制并归一化为十分制颜值分,作为拉平目标
|
| 251 |
is_hundred_scale = beauty_score > 15.0
|
| 252 |
beauty_score_10 = beauty_score / 10.0 if is_hundred_scale else beauty_score
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
-
# 6.
|
| 256 |
-
|
| 257 |
for k, val in raw_scores.items():
|
| 258 |
-
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
harmony_score = round(max(0.0, min(10.0, raw_harmony * (1 - alpha) + beauty_score_10 * alpha)), 2)
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
"
|
| 265 |
-
"
|
| 266 |
-
"
|
| 267 |
-
"
|
|
|
|
| 268 |
}
|
| 269 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
# 计算颜值巅峰曲线
|
| 271 |
beauty_timeline = self._calculate_beauty_timeline(
|
| 272 |
age_str, beauty_score, scores, radar_analysis
|
|
@@ -410,13 +431,16 @@ class FacialFeatureAnalyzer:
|
|
| 410 |
else:
|
| 411 |
width_score = max(0.3, 0.15 / mouth_ratio) # 太大时渐减
|
| 412 |
|
| 413 |
-
# 2. 唇厚度评分 -
|
| 414 |
lip_thickness = abs(lower_lip_center[1] - upper_lip_center[1])
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
|
|
|
|
|
|
|
|
|
| 418 |
else:
|
| 419 |
-
thickness_score = 0.5 # 太薄给
|
| 420 |
|
| 421 |
# 3. 嘴部对称性评分 - 更宽松
|
| 422 |
mouth_center_x = (left_corner[0] + right_corner[0]) / 2
|
|
@@ -624,7 +648,7 @@ class FacialFeatureAnalyzer:
|
|
| 624 |
traceback.print_exc() # ← 打印完整堆栈,包括确切行号
|
| 625 |
return 6.21
|
| 626 |
|
| 627 |
-
def _get_face_measurements(self, points: np.ndarray) -> Dict[str, float]:
|
| 628 |
"""提取面部关键测量数据"""
|
| 629 |
measurements = {}
|
| 630 |
|
|
@@ -670,9 +694,12 @@ class FacialFeatureAnalyzer:
|
|
| 670 |
measurements["mouth_height"] = np.max(mouth[:, 1]) - np.min(mouth[:, 1])
|
| 671 |
|
| 672 |
# 关键垂直距离
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
|
|
|
|
|
|
|
|
|
| 676 |
measurements["middle_face_height"] = (
|
| 677 |
measurements["nose_tip"][1] - measurements["left_eye_center"][1]
|
| 678 |
)
|
|
|
|
| 74 |
# 获取第一个面部的关键点
|
| 75 |
face_landmarks = results.multi_face_landmarks[0]
|
| 76 |
|
| 77 |
+
# 提取发际线顶部中心(10号点)的y像素坐标
|
| 78 |
+
forehead_y = int(face_landmarks.landmark[10].y * face_image.shape[0])
|
| 79 |
+
|
| 80 |
# 将MediaPipe的468个关键点转换为类似dlib 68点的格式
|
| 81 |
points = self._convert_mediapipe_to_dlib_format(face_landmarks, face_image.shape)
|
| 82 |
|
| 83 |
return self._analyze_features_from_landmarks(
|
| 84 |
+
points, face_image.shape, language, age_str, gender, beauty_score, emotion, forehead_y=forehead_y
|
| 85 |
)
|
| 86 |
|
| 87 |
except Exception as e:
|
|
|
|
| 96 |
"""
|
| 97 |
h, w = image_shape[:2]
|
| 98 |
|
|
|
|
|
|
|
| 99 |
mediapipe_to_dlib_map = {
|
| 100 |
# 面部轮廓 (0-16)
|
| 101 |
+
0: 162, # 右耳根下
|
| 102 |
+
1: 234, # 右脸颊上
|
| 103 |
+
2: 93, # 右脸颊
|
| 104 |
+
3: 58, # 右脸颊下
|
| 105 |
+
4: 172, # 右下颌角上
|
| 106 |
+
5: 136, # 右下颌角
|
| 107 |
+
6: 149, # 右下颌骨
|
| 108 |
+
7: 148, # 右下颌偏下
|
| 109 |
+
8: 152, # 下巴最底端
|
| 110 |
+
9: 377, # 左下颌偏下
|
| 111 |
+
10: 378, # 左下颌骨
|
| 112 |
+
11: 365, # 左下颌角
|
| 113 |
+
12: 397, # 左下颌角上
|
| 114 |
+
13: 288, # 左脸颊下
|
| 115 |
+
14: 323, # 左脸颊
|
| 116 |
+
15: 454, # 左脸颊上
|
| 117 |
+
16: 389, # 左耳根下
|
| 118 |
|
| 119 |
# 右眉毛 (17-21)
|
| 120 |
+
17: 71, # 右眉毛外端
|
| 121 |
+
18: 63, # 右眉毛外
|
| 122 |
+
19: 105, # 右眉毛中
|
| 123 |
+
20: 66, # 右眉毛内
|
| 124 |
21: 107, # 右眉毛内端
|
| 125 |
|
| 126 |
# 左眉毛 (22-26)
|
| 127 |
22: 336, # 左眉毛内端
|
| 128 |
+
23: 296, # 左眉毛内
|
| 129 |
+
24: 334, # 左眉毛中
|
| 130 |
+
25: 293, # 左眉毛外
|
| 131 |
+
26: 301, # 左眉毛外端
|
| 132 |
|
| 133 |
# 鼻梁 (27-30)
|
| 134 |
27: 168, # 鼻梁顶
|
| 135 |
+
28: 197, # 鼻梁中上
|
| 136 |
+
29: 5, # 鼻梁中下
|
| 137 |
+
30: 4, # 鼻梁底/鼻尖上
|
| 138 |
|
| 139 |
+
# 鼻底/鼻翼 (31-35)
|
| 140 |
+
31: 75, # 右鼻翼外侧
|
| 141 |
+
32: 97, # 右鼻孔偏下
|
| 142 |
33: 2, # 鼻尖
|
| 143 |
+
34: 326, # 左鼻孔偏下
|
| 144 |
+
35: 305, # 左鼻翼外侧
|
| 145 |
|
| 146 |
# 右眼 (36-41)
|
| 147 |
36: 33, # 右眼外角
|
| 148 |
+
37: 160, # 右眼上眼睑外
|
| 149 |
+
38: 158, # 右眼上眼睑内
|
| 150 |
+
39: 133, # 右眼内角
|
| 151 |
+
40: 153, # 右眼下眼睑内
|
| 152 |
+
41: 144, # 右眼下眼睑外
|
| 153 |
|
| 154 |
# 左眼 (42-47)
|
| 155 |
42: 362, # 左眼内角
|
| 156 |
+
43: 385, # 左眼上眼睑内
|
| 157 |
+
44: 387, # 左眼上眼睑外
|
| 158 |
+
45: 263, # 左眼外角
|
| 159 |
+
46: 373, # 左眼下眼睑外
|
| 160 |
+
47: 380, # 左眼下眼睑内
|
| 161 |
|
| 162 |
# 嘴部轮廓 (48-67)
|
| 163 |
+
48: 61, # 右嘴角
|
| 164 |
+
49: 39, # 右上唇外
|
| 165 |
+
50: 37, # 右上唇中
|
| 166 |
+
51: 0, # 上唇中央顶
|
| 167 |
+
52: 267, # 左上唇中
|
| 168 |
+
53: 269, # 左上唇外
|
| 169 |
+
54: 291, # 左嘴角
|
| 170 |
+
55: 405, # 左下唇外
|
| 171 |
+
56: 314, # 左下唇中
|
| 172 |
+
57: 17, # 下唇中央底
|
| 173 |
+
58: 84, # 右下唇中
|
| 174 |
+
59: 181, # 右下唇外
|
| 175 |
+
60: 78, # 右嘴角内侧
|
| 176 |
+
61: 82, # 上唇内侧右
|
| 177 |
+
62: 13, # 上唇内侧中央
|
| 178 |
+
63: 312, # 上唇内侧左
|
| 179 |
+
64: 308, # 左嘴角内侧
|
| 180 |
+
65: 317, # 下唇内侧左
|
| 181 |
+
66: 14, # 下唇内侧中央
|
| 182 |
+
67: 87 # 下唇内侧右
|
| 183 |
}
|
| 184 |
|
| 185 |
# 转换关键点
|
|
|
|
| 210 |
gender: str = "Female",
|
| 211 |
beauty_score: float = 7.0,
|
| 212 |
emotion: str = "neutral",
|
| 213 |
+
forehead_y: int = None,
|
| 214 |
) -> Dict[str, Any]:
|
| 215 |
"""基于68个关键点分析五官"""
|
| 216 |
try:
|
|
|
|
| 240 |
|
| 241 |
# 3. 提取基础测量数据用于雷达图
|
| 242 |
points_np = np.array(landmarks)
|
| 243 |
+
face_measurements = self._get_face_measurements(points_np, forehead_y=forehead_y)
|
| 244 |
|
| 245 |
# 4. 计算雷达图各项原始指标(十分制)
|
| 246 |
raw_symmetry = self._calculate_facial_symmetry(face_measurements, points_np)
|
|
|
|
| 252 |
# 5. 识别分制并归一化为十分制颜值分,作为拉平目标
|
| 253 |
is_hundred_scale = beauty_score > 15.0
|
| 254 |
beauty_score_10 = beauty_score / 10.0 if is_hundred_scale else beauty_score
|
| 255 |
+
|
| 256 |
+
# 使用新配置的融合权重与拉伸因子
|
| 257 |
+
alpha = float(getattr(config, "FACIAL_FUSION_ALPHA", 0.35))
|
| 258 |
+
spread_factor = float(getattr(config, "FACIAL_SPREAD_FACTOR", 2.5))
|
| 259 |
|
| 260 |
+
# 6. 融合总颜值分和局部几何特征
|
| 261 |
+
fused_scores = {}
|
| 262 |
for k, val in raw_scores.items():
|
| 263 |
+
fused_scores[k] = val * (1 - alpha) + beauty_score_10 * alpha
|
| 264 |
|
| 265 |
+
# 计算融合得分均值并做对比度方差拉伸,以放大各项间的区分度
|
| 266 |
+
mean_fused_score = sum(fused_scores.values()) / len(fused_scores)
|
| 267 |
+
scores = {}
|
| 268 |
+
for k, val in fused_scores.items():
|
| 269 |
+
spread_val = mean_fused_score + (val - mean_fused_score) * spread_factor
|
| 270 |
+
scores[k] = round(max(1.0, min(10.0, spread_val)), 2)
|
| 271 |
+
|
| 272 |
+
# 整体协调性得分不需要拉伸,直接取融合值
|
| 273 |
harmony_score = round(max(0.0, min(10.0, raw_harmony * (1 - alpha) + beauty_score_10 * alpha)), 2)
|
| 274 |
|
| 275 |
+
# 融合原始雷达图指标
|
| 276 |
+
fused_radar = {
|
| 277 |
+
"symmetry": raw_symmetry * (1 - alpha) + beauty_score_10 * alpha,
|
| 278 |
+
"proportions": raw_proportions * (1 - alpha) + beauty_score_10 * alpha,
|
| 279 |
+
"contour": raw_contour * (1 - alpha) + beauty_score_10 * alpha,
|
| 280 |
+
"eyes_brows": raw_eyes_brows * (1 - alpha) + beauty_score_10 * alpha,
|
| 281 |
+
"nose_mouth": raw_nose_mouth * (1 - alpha) + beauty_score_10 * alpha,
|
| 282 |
}
|
| 283 |
|
| 284 |
+
# 同样对雷达图指标进行方差拉伸
|
| 285 |
+
mean_radar = sum(fused_radar.values()) / len(fused_radar)
|
| 286 |
+
radar_analysis = {}
|
| 287 |
+
for k, val in fused_radar.items():
|
| 288 |
+
spread_val = mean_radar + (val - mean_radar) * spread_factor
|
| 289 |
+
radar_analysis[k] = round(max(1.0, min(10.0, spread_val)), 2)
|
| 290 |
+
|
| 291 |
# 计算颜值巅峰曲线
|
| 292 |
beauty_timeline = self._calculate_beauty_timeline(
|
| 293 |
age_str, beauty_score, scores, radar_analysis
|
|
|
|
| 431 |
else:
|
| 432 |
width_score = max(0.3, 0.15 / mouth_ratio) # 太大时渐减
|
| 433 |
|
| 434 |
+
# 2. 唇厚度评分 - 重构为相对比例(理想比例占面部裁剪高度的 5%)
|
| 435 |
lip_thickness = abs(lower_lip_center[1] - upper_lip_center[1])
|
| 436 |
+
face_height = max(image_shape[0], 1)
|
| 437 |
+
lip_ratio = lip_thickness / face_height
|
| 438 |
+
|
| 439 |
+
# 只要厚度比例在正常范围内就给满分或温和打分
|
| 440 |
+
if lip_ratio >= 0.01: # 有一定厚度(大于1%)
|
| 441 |
+
thickness_score = min(1.0, lip_ratio / 0.05) # 5%为满分
|
| 442 |
else:
|
| 443 |
+
thickness_score = 0.5 # 太薄给兜底分
|
| 444 |
|
| 445 |
# 3. 嘴部对称性评分 - 更宽松
|
| 446 |
mouth_center_x = (left_corner[0] + right_corner[0]) / 2
|
|
|
|
| 648 |
traceback.print_exc() # ← 打印完整堆栈,包括确切行号
|
| 649 |
return 6.21
|
| 650 |
|
| 651 |
+
def _get_face_measurements(self, points: np.ndarray, forehead_y: int = None) -> Dict[str, float]:
|
| 652 |
"""提取面部关键测量数据"""
|
| 653 |
measurements = {}
|
| 654 |
|
|
|
|
| 694 |
measurements["mouth_height"] = np.max(mouth[:, 1]) - np.min(mouth[:, 1])
|
| 695 |
|
| 696 |
# 关键垂直距离
|
| 697 |
+
if forehead_y is not None:
|
| 698 |
+
measurements["forehead_height"] = measurements["left_eye_center"][1] - forehead_y
|
| 699 |
+
else:
|
| 700 |
+
measurements["forehead_height"] = measurements["left_eye_center"][1] - np.min(
|
| 701 |
+
points[:, 1]
|
| 702 |
+
)
|
| 703 |
measurements["middle_face_height"] = (
|
| 704 |
measurements["nose_tip"][1] - measurements["left_eye_center"][1]
|
| 705 |
)
|