chenchaoyun commited on
Commit
0d573f0
·
1 Parent(s): 1de256e

refactor: 重构人脸特征打分与雷达图指标算法,修正物理关键点映射并增加方差拉伸对比度以提升雷达图的个性和起伏度

Browse files
Files changed (2) hide show
  1. config.py +4 -0
  2. 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: 234, # 下巴最低点
101
- 1: 132, # 右脸颊
102
- 2: 172, # 右脸颊
103
- 3: 136, # 右脸颊
104
- 4: 150, # 右颧骨
105
- 5: 149, # 右太阳穴
106
- 6: 176, # 右额头边缘
107
- 7: 148, # 右额头
108
- 8: 152, # 额头中央
109
- 9: 377, # 左额头
110
- 10: 400, # 左额头边缘
111
- 11: 378, # 左太阳穴
112
- 12: 379, # 左颧骨
113
- 13: 365, # 左脸颊
114
- 14: 397, # 左脸颊
115
- 15: 361, # 左脸颊
116
- 16: 454, # 下巴
117
 
118
  # 右眉毛 (17-21)
119
- 17: 70, # 右眉毛外端
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: 300, # 左眉毛外端
131
 
132
  # 鼻梁 (27-30)
133
  27: 168, # 鼻梁顶
134
- 28: 8, # 鼻梁
135
- 29: 9, # 鼻梁
136
- 30: 10, # 鼻梁底
137
 
138
- # 鼻翼 (31-35)
139
- 31: 151, # 右鼻翼
140
- 32: 134, # 右鼻孔
141
  33: 2, # 鼻尖
142
- 34: 363, # 左鼻孔
143
- 35: 378, # 左鼻翼
144
 
145
  # 右眼 (36-41)
146
  36: 33, # 右眼外角
147
- 37: 7, # 右眼上眼睑
148
- 38: 163, # 右眼上眼睑
149
- 39: 144, # 右眼内角
150
- 40: 145, # 右眼下眼睑
151
- 41: 153, # 右眼下眼睑
152
 
153
  # 左眼 (42-47)
154
  42: 362, # 左眼内角
155
- 43: 382, # 左眼上眼睑
156
- 44: 381, # 左眼上眼睑
157
- 45: 380, # 左眼外角
158
- 46: 374, # 左眼下眼睑
159
- 47: 373, # 左眼下眼睑
160
 
161
  # 嘴部轮廓 (48-67)
162
- 48: 78, # 右嘴角
163
- 49: 95, # 右上唇
164
- 50: 88, # 上唇右侧
165
- 51: 178, # 上唇中央
166
- 52: 87, # 上唇中
167
- 53: 14, # 上唇中央左
168
- 54: 317, # 上唇
169
- 55: 318, # 左
170
- 56: 308, # 左嘴角
171
- 57: 324, # 下唇
172
- 58: 318, # 下唇左侧
173
- 59: 16, # 下唇中央左
174
- 60: 17, # 下唇中央
175
- 61: 18, # 中央
176
- 62: 200, #
177
- 63: 199, # 右下
178
- 64: 175, # 嘴角内
179
- 65: 84, # 唇内
180
- 66: 17, # 下唇内中央
181
- 67: 314, # 唇内
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
- alpha = 0.6 # 颜值总分占 60% 权重,局部几何细节占 40% 权重
 
 
 
254
 
255
- # 6. 统一向分靠拢,得出最终返回的并进行四舍五入
256
- scores = {}
257
  for k, val in raw_scores.items():
258
- scores[k] = round(max(0.0, min(10.0, val * (1 - alpha) + beauty_score_10 * alpha)), 2)
259
 
 
 
 
 
 
 
 
 
260
  harmony_score = round(max(0.0, min(10.0, raw_harmony * (1 - alpha) + beauty_score_10 * alpha)), 2)
261
 
262
- radar_analysis = {
263
- "symmetry": round(max(0.0, min(10.0, raw_symmetry * (1 - alpha) + beauty_score_10 * alpha)), 2),
264
- "proportions": round(max(0.0, min(10.0, raw_proportions * (1 - alpha) + beauty_score_10 * alpha)), 2),
265
- "contour": round(max(0.0, min(10.0, raw_contour * (1 - alpha) + beauty_score_10 * alpha)), 2),
266
- "eyes_brows": round(max(0.0, min(10.0, raw_eyes_brows * (1 - alpha) + beauty_score_10 * alpha)), 2),
267
- "nose_mouth": round(max(0.0, min(10.0, raw_nose_mouth * (1 - alpha) + beauty_score_10 * alpha)), 2),
 
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
- if lip_thickness > 3: # 像素值,有一定厚度
417
- thickness_score = min(1.0, lip_thickness / 25) # 25像素为满分
 
 
 
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
- measurements["forehead_height"] = measurements["left_eye_center"][1] - np.min(
674
- points[:, 1]
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
  )