# 脸型判断规则优化报告 > 基于 Mediapipe 468 点人脸关键点的脸型分类系统 > 优化日期:2026-07-28 --- ## 一、优化总览 ### 原始规则主要问题 | 问题 | 说明 | |------|------| | **规则冲突** | 7条 if 规则可能同时匹配,无优先级机制 | | **特征定义模糊** | `width_ratio`、`chin_narrowness` 等未给出精确计算方式 | | **关键点不足** | 额头宽度用 234/454(颧骨点)而非太阳穴点,导致测量不准 | | **无置信度** | 硬判断,混合脸型无处理 | | **阈值经验性** | 阈值未经统计校准,边界处容易误判 | | **缺少归一化** | 不同距离拍的照片结果不一致 | ### 优化策略 1. **精确特征提取**:增加关键点,所有测量归一化 2. **评分制分类**:每个脸型计算匹配度分数(0-100),取最高分 3. **置信度输出**:报告 Top-1 / Top-2 分差,判断是否为混合脸型 4. **优先级仲裁**:分数接近时按"特异性优先"原则仲裁 5. **鲁棒性增强**:clamp 防止除零、NaN,角度计算增加 3D 投影 --- ## 二、优化后的完整 Python 代码 ```python """ face_shape_classifier.py 基于 Mediapipe 468 点人脸关键点的脸型分类系统 支持的脸型:圆形脸 / 心形脸 / 菱形脸 / 鹅蛋脸 / 方形脸 / 长形脸 / 瓜子脸 分类策略:多维度特征提取 → 加权评分 → 置信度判断 """ import math import numpy as np from typing import Dict, Tuple, List, Optional # ============================================================ # 第一部分:关键点索引定义 # ============================================================ class FaceLandmarks: """Mediapipe 468 点关键点索引(仅列出脸型分析所需)""" # --- 中线关键点 --- FOREHEAD_TOP = 10 # 额头顶部(发际线附近) NOSE_BRIDGE = 1 # 鼻根(眉心位置) NOSE_TIP = 168 # 鼻尖 CHIN_BOTTOM = 152 # 下巴最低点(menton) # --- 太阳穴 / 额头两侧(额头宽度)--- LEFT_TEMPLE = 127 # 左太阳穴 RIGHT_TEMPLE = 356 # 右太阳穴 # --- 颧骨 / 脸颊最宽处 --- LEFT_CHEEK = 234 # 左颧弓最外侧 RIGHT_CHEEK = 454 # 右颧弓最外侧 # --- 下颌角(gonion 区域)--- LEFT_JAW_ANGLE = 172 # 左下颌角 RIGHT_JAW_ANGLE = 397 # 右下颌角 # --- 下巴两侧(下巴宽度)--- LEFT_CHIN = 136 # 左下巴缘 RIGHT_CHIN = 365 # 右下巴缘 # --- 嘴角(辅助参考)--- LEFT_MOUTH = 61 # 左嘴角 RIGHT_MOUTH = 291 # 右嘴角 # --- 眼角(辅助参考)--- LEFT_EYE_OUT = 33 # 左眼外角 RIGHT_EYE_OUT = 263 # 右眼外角 # --- 额头侧缘(辅助)--- LEFT_FOREHEAD = 50 # 左额侧 RIGHT_FOREHEAD = 280 # 右额侧 # ============================================================ # 第二部分:特征提取 # ============================================================ def extract_face_features(landmarks) -> Dict[str, float]: """ 从 Mediapipe 关键点中提取脸型特征向量。 参数: landmarks: Mediapipe 的 NormalizedLandmark 列表(468 点) 返回: features dict,包含以下归一化特征: - aspect_ratio: 面部长宽比(face_width / face_height) - jaw_angle: 下颌角度(度),越大越圆润 - taper_ratio: 额头→下巴收窄比例 - forehead_ratio: 额头宽度 / 面部宽度 - cheekbone_ratio: 颧骨宽度 / 面部宽度 - jaw_ratio: 下颌宽度 / 面部宽度 - chin_ratio: 下巴宽度 / 面部宽度 - chin_sharpness: 下巴尖锐度(下巴宽 / 下颌宽) - width_uniformity: 宽度均匀度(越小越方正) - face_curve_score: 面部曲线评分(越大越圆润) """ def pt(idx): """提取 3D 坐标""" lm = landmarks[idx] return np.array([lm.x, lm.y, lm.z]) def dist(p1, p2): """欧氏距离""" return float(np.linalg.norm(p1 - p2)) # --- 1. 提取关键点 --- forehead_top = pt(FaceLandmarks.FOREHEAD_TOP) chin_bottom = pt(FaceLandmarks.CHIN_BOTTOM) left_temple = pt(FaceLandmarks.LEFT_TEMPLE) right_temple = pt(FaceLandmarks.RIGHT_TEMPLE) left_cheek = pt(FaceLandmarks.LEFT_CHEEK) right_cheek = pt(FaceLandmarks.RIGHT_CHEEK) left_jaw = pt(FaceLandmarks.LEFT_JAW_ANGLE) right_jaw = pt(FaceLandmarks.RIGHT_JAW_ANGLE) left_chin = pt(FaceLandmarks.LEFT_CHIN) right_chin = pt(FaceLandmarks.RIGHT_CHIN) # --- 2. 基础距离 --- face_height = dist(forehead_top, chin_bottom) forehead_width = dist(left_temple, right_temple) cheekbone_width = dist(left_cheek, right_cheek) jaw_width = dist(left_jaw, right_jaw) chin_width = dist(left_chin, right_chin) # face_width 取颧骨宽度(通常是面部最宽处) face_width = cheekbone_width # 防止除零 eps = 1e-8 # --- 3. 计算下颌角度 --- # 以下巴底为顶点,向左下颌角和右下颌角各做一向量 # 角度越大 → 下颌越圆润(圆形/鹅蛋) # 角度越小 → 下颌越方正(方形) v_left = left_jaw - chin_bottom v_right = right_jaw - chin_bottom cos_val = np.dot(v_left, v_right) / (np.linalg.norm(v_left) * np.linalg.norm(v_right) + eps) cos_val = np.clip(cos_val, -1.0, 1.0) jaw_angle = math.degrees(math.acos(cos_val)) # --- 4. 计算衍生特征 --- aspect_ratio = face_width / (face_height + eps) taper_ratio = (forehead_width - chin_width) / (forehead_width + eps) # 归一化到面部宽度 forehead_ratio = forehead_width / (face_width + eps) cheekbone_ratio = cheekbone_width / (face_width + eps) # 始终 ≈ 1.0 jaw_ratio = jaw_width / (face_width + eps) chin_ratio = chin_width / (face_width + eps) # 下巴尖锐度:下巴宽 / 下颌宽 # 值越小 → 下巴越尖(瓜子/心形) # 值越大 → 下巴越平(方形/圆形) chin_sharpness = chin_width / (jaw_width + eps) # 宽度均匀度:额头、颧骨、下颌三者的差异程度 # 值越小 → 三者越接近(方形/圆形) # 值越大 → 差异越明显(菱形/心形/瓜子) widths = [forehead_width, cheekbone_width, jaw_width] width_uniformity = (max(widths) - min(widths)) / (max(widths) + eps) # 面部曲线评分:下巴到下颌角的距离 / 面部高度 # 距离越短 → 线条越弯曲(圆润),越长 → 越直(方正) jaw_midpoint = (left_jaw + right_jaw) / 2.0 jaw_to_chin = dist(jaw_midpoint, chin_bottom) face_curve_score = jaw_to_chin / (face_height + eps) # --- 5. 返回特征字典 --- features = { # 原始尺寸 'face_height': face_height, 'face_width': face_width, 'forehead_width': forehead_width, 'cheekbone_width': cheekbone_width, 'jaw_width': jaw_width, 'chin_width': chin_width, # 比例特征 'aspect_ratio': aspect_ratio, 'taper_ratio': taper_ratio, 'forehead_ratio': forehead_ratio, 'cheekbone_ratio': cheekbone_ratio, 'jaw_ratio': jaw_ratio, 'chin_ratio': chin_ratio, # 角度特征 'jaw_angle': jaw_angle, # 复合特征 'chin_sharpness': chin_sharpness, 'width_uniformity': width_uniformity, 'face_curve_score': face_curve_score, } return features # ============================================================ # 第三部分:评分制分类器 # ============================================================ def classify_face_shape( features: Dict[str, float], return_details: bool = False ) -> Tuple[str, float, Optional[Dict]]: """ 基于评分的脸型分类器。 策略: 每种脸型计算 0-100 的匹配度分数。 取最高分作为结果,返回置信度和详细得分。 参数: features: extract_face_features() 的输出 return_details: 是否返回详细评分 返回: (face_shape: str, confidence: float, details: dict | None) """ ar = features['aspect_ratio'] # 长宽比(宽/高) jaw = features['jaw_angle'] # 下颌角度 tap = features['taper_ratio'] # 额头→下巴收窄 fr = features['forehead_ratio'] # 额头宽/面宽 jr = features['jaw_ratio'] # 下颌宽/面宽 cr = features['chin_ratio'] # 下巴宽/面宽 cs = features['chin_sharpness'] # 下巴尖锐度 wu = features['width_uniformity'] # 宽度均匀度 fcs = features['face_curve_score'] # 面部曲线 fw = features['forehead_width'] cw = features['chin_width'] jw = features['jaw_width'] sw = features['cheekbone_width'] fh = features['face_height'] eps = 1e-8 scores: Dict[str, float] = {} # ========================================== # 1. 圆形脸 (Round) # ========================================== # 核心特征: # - 长宽比接近 1(脸几乎和宽一样长) # - 下颌角大(>135°,圆润线条) # - 额头≈颧骨≈下颌宽度(均匀) # - 下巴圆润不尖 # # 理想值:aspect_ratio ≈ 0.88-1.0, jaw_angle ≈ 140-160° # ========================================== s = 0.0 s += _score_range(ar, 0.85, 1.0, peak=0.92, max_points=30) # 长宽比 s += _score_range(jaw, 135, 165, peak=150, max_points=30) # 下颌角度 s += _score_below(wu, 0.10, max_points=20) # 宽度均匀 s += _score_range(cs, 0.65, 0.90, peak=0.75, max_points=10) # 下巴不尖 s += _score_range(tap, -0.05, 0.10, peak=0.02, max_points=10) # 几乎不收窄 scores['圆形脸'] = s # ========================================== # 2. 心形脸 (Heart) # ========================================== # 核心特征: # - 额头明显宽于下巴(taper > 0.2) # - 下巴尖细(chin_sharpness < 0.55) # - 颧骨与额头接近(不是颧骨最宽) # - 前额发际线较宽 # # 理想值:taper ≈ 0.25-0.40, chin_sharpness ≈ 0.35-0.55 # ========================================== s = 0.0 s += _score_above(tap, 0.20, max_points=25) # 额头宽于下巴 s += _score_below(cs, 0.55, max_points=25) # 下巴尖 s += _score_above(fw, sw * 0.92, max_points=15) # 额头≥颧骨的92% s += _score_range(jaw, 115, 150, peak=130, max_points=15) # 下颌适中偏圆 s += _score_range(ar, 0.75, 0.92, peak=0.82, max_points=10) # 长宽比适中 s += _score_above(cr, 0.0, max_points=10) # 下巴存在但窄 scores['心形脸'] = s # ========================================== # 3. 菱形脸 (Diamond) # ========================================== # 核心特征: # - 颧骨明显最宽(>额头和下颌的 105%+) # - 额头较窄(< 面宽的 90%) # - 下颌也较窄 # - 整体呈菱形/钻石形 # # 理想值:width_uniformity > 0.15 # ========================================== s = 0.0 s += _score_above(sw, fw * 1.05, max_points=25) # 颧骨>额头5%+ s += _score_above(sw, jw * 1.10, max_points=25) # 颧骨>下颌10%+ s += _score_below(fr, 0.92, max_points=15) # 额头偏窄 s += _score_below(jr, 0.92, max_points=15) # 下颌偏窄 s += _score_above(wu, 0.12, max_points=10) # 宽度不均匀 s += _score_range(ar, 0.72, 0.90, peak=0.80, max_points=10) # 长宽比适中 scores['菱形脸'] = s # ========================================== # 4. 鹅蛋脸 (Oval) # ========================================== # 核心特征: # - 长宽比适中(0.72-0.85,不过圆不过长) # - 轮廓柔和,下颌角度适中(125-155°) # - 额头略宽于下巴,但差距不大 # - 宽度从上到下平滑递减 # - 下巴圆润偏尖但不极端 # # 理想值:aspect_ratio ≈ 0.75-0.82 # ========================================== s = 0.0 s += _score_range(ar, 0.70, 0.85, peak=0.77, max_points=25) # 长宽比 s += _score_range(jaw, 125, 155, peak=138, max_points=20) # 下颌角度 s += _score_range(tap, 0.03, 0.20, peak=0.10, max_points=15) # 适度收窄 s += _score_range(cs, 0.50, 0.75, peak=0.62, max_points=15) # 下巴适中 s += _score_below(wu, 0.12, max_points=15) # 宽度比较均匀 s += _score_range(fcs, 0.15, 0.25, peak=0.19, max_points=10) # 曲线适中 scores['鹅蛋脸'] = s # ========================================== # 5. 方形脸 (Square) # ========================================== # 核心特征: # - 长宽比较大(接近等宽,ar > 0.80) # - 下颌角小(<135°,线条硬朗) # - 额头≈颧骨≈下颌(宽度均匀) # - 下巴偏平不尖 # # 理想值:aspect_ratio ≈ 0.85-0.95, jaw_angle ≈ 110-125° # ========================================== s = 0.0 s += _score_range(ar, 0.78, 0.95, peak=0.87, max_points=20) # 长宽比偏大 s += _score_below(jaw, 135, max_points=30) # 下颌角小 s += _score_below(wu, 0.10, max_points=20) # 宽度均匀 s += _score_above(cs, 0.62, max_points=15) # 下巴偏宽 s += _score_range(jr, 0.90, 1.05, peak=0.96, max_points=15) # 下颌宽接近面宽 scores['方形脸'] = s # ========================================== # 6. 长形脸 (Long/Oblong) # ========================================== # 核心特征: # - 长宽比低(< 0.72,脸明显比宽长很多) # - 面部高度 > 宽度的 1.4 倍 # - 额头略宽于下巴 # - 整体修长 # # 理想值:aspect_ratio ≈ 0.58-0.70 # ========================================== s = 0.0 s += _score_below(ar, 0.72, max_points=35) # 长宽比低 s += _score_above(fh, features['face_width'] * 1.35, max_points=20) # 高>宽*1.35 s += _score_range(tap, 0.0, 0.20, peak=0.08, max_points=10) # 适度收窄 s += _score_range(jaw, 120, 155, peak=135, max_points=15) # 下颌适中 s += _score_range(cs, 0.45, 0.72, peak=0.58, max_points=10) # 下巴适中 s += _score_range(wu, 0.02, 0.15, peak=0.08, max_points=10) # 宽度比较均匀 scores['长形脸'] = s # ========================================== # 7. 瓜子脸 (Melon Seed / V-shape) # ========================================== # 核心特征: # - 额头宽,逐渐收窄到尖下巴 # - 比心形脸更窄长(aspect_ratio < 0.82) # - 颧骨不超过额头 # - 下巴尖锐(V 线条) # - 整体线条流畅 # # 理想值:taper ≈ 0.20-0.35, chin_sharpness ≈ 0.30-0.55 # ========================================== s = 0.0 s += _score_above(tap, 0.15, max_points=20) # 额头宽于下巴 s += _score_below(ar, 0.82, max_points=15) # 偏长 s += _score_below(cs, 0.58, max_points=25) # 下巴尖 s += _score_below(sw, fw * 1.02, max_points=15) # 颧骨≤额头 s += _score_below(jw, fw * 0.95, max_points=15) # 下颌<额头 s += _score_range(jaw, 120, 155, peak=135, max_points=10) # 下颌适中 scores['瓜子脸'] = s # --- 选择最高分 --- ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) best_shape, best_score = ranked[0] second_shape, second_score = ranked[1] if len(ranked) > 1 else (None, 0) # 置信度:最高分 / 总分 total = sum(scores.values()) confidence = best_score / total if total > 0 else 0.0 # 判断是否混合脸型(Top-1 和 Top-2 分差太小) score_gap = best_score - second_score is_mixed = (score_gap < 8.0 and best_score > 30.0) details = { 'scores': scores, 'ranked': ranked, 'confidence': confidence, 'score_gap': score_gap, 'is_mixed': is_mixed, 'second_shape': second_shape, 'second_score': second_score, } if return_details else None return best_shape, confidence, details # ============================================================ # 第四部分:评分辅助函数 # ============================================================ def _score_range( value: float, low: float, high: float, peak: float, max_points: float = 10.0 ) -> float: """ 在 [low, high] 范围内评分,peak 处满分。 范围外线性衰减到 0。 使用三角窗函数(triangular window)。 例:_score_range(0.77, 0.70, 0.85, peak=0.77, max_points=25) → value == peak → 返回 25.0 → value == low → 返回 0.0(边界) → value 在 peak 和 low 之间 → 线性插值 """ if value < low or value > high: return 0.0 if value == peak: return max_points if value < peak: # 在 [low, peak] 区间线性上升 ratio = (value - low) / (peak - low + 1e-8) else: # 在 [peak, high] 区间线性下降 ratio = (high - value) / (high - peak + 1e-8) return max_points * ratio def _score_above(value: float, threshold: float, max_points: float = 10.0) -> float: """ value >= threshold 时给满分,低于则线性衰减。 衰减区间:[threshold * 0.7, threshold] """ if value >= threshold: return max_points floor = threshold * 0.7 if value <= floor: return 0.0 ratio = (value - floor) / (threshold - floor + 1e-8) return max_points * ratio def _score_below(value: float, threshold: float, max_points: float = 10.0) -> float: """ value <= threshold 时给满分,高于则线性衰减。 衰减区间:[threshold, threshold * 1.3] """ if value <= threshold: return max_points ceil = threshold * 1.3 if value >= ceil: return 0.0 ratio = (ceil - value) / (ceil - threshold + 1e-8) return max_points * ratio # ============================================================ # 第五部分:完整调用示例 # ============================================================ def classify_from_mediapipe(multi_face_landmarks) -> List[Dict]: """ 完整调用示例:从 Mediapipe 结果到脸型分类。 参数: multi_face_landmarks: mediapipe FaceMesh 的结果 result.multi_face_landmarks 返回: 每张脸的分类结果列表 """ results = [] for face_lms in multi_face_landmarks: features = extract_face_features(face_lms.landmark) shape, conf, details = classify_face_shape(features, return_details=True) results.append({ 'face_shape': shape, 'confidence': conf, 'features': features, 'details': details, }) return results # ============================================================ # 第六部分:混合脸型输出(可选) # ============================================================ def get_mixed_description(details: Dict) -> str: """ 当检测到混合脸型时,生成描述文本。 例:"鹅蛋脸(偏瓜子脸)" """ if not details or not details.get('is_mixed'): return "" shape1 = details['ranked'][0][0] shape2 = details['ranked'][1][0] return f"{shape1}(偏{shape2})" ``` --- ## 三、各脸型详细特征说明 ### 1. 圆形脸 (Round) | 特征 | 典型值 | 说明 | |------|--------|------| | aspect_ratio | 0.88-1.0 | 面部宽度和长度几乎相等 | | jaw_angle | 140-160° | 下颌线条圆润 | | width_uniformity | < 0.08 | 额头、颧骨、下颌宽度接近 | | chin_sharpness | 0.65-0.85 | 下巴圆润,不尖锐 | | taper_ratio | -0.05 ~ 0.08 | 额头到下巴几乎不收窄 | **视觉特征**:面部轮廓呈圆形,没有明显棱角,看起来年轻可爱。 ### 2. 心形脸 (Heart) | 特征 | 典型值 | 说明 | |------|--------|------| | taper_ratio | 0.25-0.40 | 额头明显宽于下巴 | | chin_sharpness | 0.35-0.55 | 下巴尖细 | | forehead_ratio | > 0.92 | 额头宽,接近面宽 | | jaw_angle | 120-145° | 下颌适中 | **视觉特征**:上宽下窄,额头饱满,下巴尖俏,像心形。 ### 3. 菱形脸 (Diamond) | 特征 | 典型值 | 说明 | |------|--------|------| | 颧骨宽度 | > 额头×1.05 | 颧骨明显最突出 | | 颧骨宽度 | > 下颌×1.10 | 远宽于下颌 | | forehead_ratio | < 0.92 | 额头偏窄 | | jaw_ratio | < 0.92 | 下颌偏窄 | | width_uniformity | > 0.12 | 宽度差异明显 | **视觉特征**:颧骨最宽,额头和下巴都偏窄,呈菱形/钻石轮廓。 ### 4. 鹅蛋脸 (Oval) | 特征 | 典型值 | 说明 | |------|--------|------| | aspect_ratio | 0.75-0.82 | 长宽比理想 | | jaw_angle | 130-148° | 轮廓柔和 | | taper_ratio | 0.05-0.15 | 适度收窄 | | chin_sharpness | 0.55-0.68 | 下巴圆润偏尖 | | width_uniformity | < 0.10 | 宽度比较均匀 | **视觉特征**:被认为是最理想的脸型,比例匀称,轮廓流畅。 ### 5. 方形脸 (Square) | 特征 | 典型值 | 说明 | |------|--------|------| | aspect_ratio | 0.85-0.92 | 接近等宽 | | jaw_angle | 108-128° | 下颌角明显,线条硬朗 | | width_uniformity | < 0.08 | 三处宽度接近 | | chin_sharpness | > 0.65 | 下巴偏平宽 | | jaw_ratio | > 0.92 | 下颌宽接近面宽 | **视觉特征**:额头、颧骨、下颌宽度接近,下颌角明显,给人干练印象。 ### 6. 长形脸 (Long/Oblong) | 特征 | 典型值 | 说明 | |------|--------|------| | aspect_ratio | 0.58-0.70 | 面部明显偏长 | | face_height/face_width | > 1.40 | 高度远超宽度 | | taper_ratio | 0.05-0.15 | 适度收窄 | | jaw_angle | 125-145° | 下颌适中 | **视觉特征**:面部修长,整体偏窄,额头较饱满。 ### 7. 瓜子脸 (Melon Seed / V-shape) | 特征 | 典型值 | 说明 | |------|--------|------| | taper_ratio | 0.20-0.35 | 额头宽于下巴 | | aspect_ratio | 0.65-0.80 | 偏长 | | chin_sharpness | 0.30-0.55 | V 形尖下巴 | | 颧骨 | ≤ 额头宽度 | 颧骨不突出 | | jaw_width | < 额头×0.95 | 下颌收窄 | **视觉特征**:额头较宽,向下逐渐收窄到尖下巴,整体呈瓜子形。 --- ## 四、关键参数含义与阈值设定理由 ### aspect_ratio(面部长宽比) ``` 计算方式:face_width / face_height ``` | 范围 | 脸型倾向 | 理由 | |------|----------|------| | < 0.70 | 长形脸 | 脸长明显大于宽 | | 0.70-0.85 | 鹅蛋/心形/瓜子 | 多数亚洲人的标准比例 | | 0.85-1.0 | 圆形/方形 | 脸宽接近脸长 | **设定理由**:根据 Farkas 面部测量数据,东亚人群面宽/面高比通常在 0.75-0.88 之间。0.85 和 0.70 是自然的分界点。 ### jaw_angle(下颌角度) ``` 计算方式:下巴底为顶点,向左右下颌角做向量,计算夹角 ``` | 范围 | 脸型倾向 | 理由 | |------|----------|------| | < 125° | 方形脸 | 下颌角锐利,线条硬朗 | | 125-140° | 鹅蛋/瓜子/心形 | 自然柔和 | | > 140° | 圆形脸 | 下颌圆润 | **设定理由**:下颌角是区分方形和圆形的关键。方形脸 gonion 角通常在 110-125°,圆形脸在 140-155°。 ### taper_ratio(额头→下巴收窄比例) ``` 计算方式:(forehead_width - chin_width) / forehead_width ``` | 范围 | 脸型倾向 | |------|----------| | < 0.05 | 圆形/方形(无收窄)| | 0.05-0.15 | 鹅蛋/长形(适度收窄)| | > 0.20 | 心形/瓜子(明显收窄)| ### chin_sharpness(下巴尖锐度) ``` 计算方式:chin_width / jaw_width ``` | 范围 | 脸型倾向 | |------|----------| | < 0.50 | 尖下巴(瓜子/心形)| | 0.50-0.65 | 适中(鹅蛋)| | > 0.65 | 宽下巴(圆形/方形)| --- ## 五、优化点说明 ### 5.1 从「硬规则」到「评分制」 **原始方案**:每条规则是独立的 if 判断,可能同时满足多条,也可能都不满足。 **优化方案**:每种脸型计算 0-100 的匹配度分数,取最高分。 ```python # 原始:可能冲突 if 0.85 <= width_ratio <= 1.0: # 圆形脸 ... if jaw_angle < 130: # 方形脸 ... # 同一张脸可能同时满足或都不满足! # 优化:评分制,必然有结果 scores = {'圆形脸': 72.5, '方形脸': 45.0, ...} # 取最高分 → 圆形脸,置信度 72.5/total ``` ### 5.2 三角窗评分函数 每个特征的贡献不是 0/1 的硬切换,而是使用**三角窗函数**平滑过渡: ``` 满分 /\ / \ / \ / \ _____/__ \____ low peak high ``` 好处:在阈值边界处不会产生跳变,结果更稳定。 ### 5.3 增加关键点精度 | 测量 | 原始方案 | 优化方案 | |------|----------|----------| | 额头宽度 | 234-454(颧骨点)| 127-356(太阳穴点)| | 下巴宽度 | 未明确 | 136-365(下巴缘)| | 下颌宽度 | 172-397 | 172-397(保持,下颌角)| **改进理由**:234/454 是颧弓最外侧点,用它们测"额头宽度"会把颧骨宽度误当额头宽度。改用 127/356 太阳穴点更准确。 ### 5.4 混合脸型检测 当 Top-1 和 Top-2 分差小于 8 分时,判定为混合脸型: ```python # 例:鹅蛋脸 65 分,瓜子脸 62 分 → 分差 3 < 8 # 输出:"鹅蛋脸(偏瓜子脸)" ``` ### 5.5 置信度输出 ```python confidence = best_score / total_score # > 0.25 → 高置信度,结果明确 # 0.18-0.25 → 中等,有一定混合 # < 0.18 → 低置信度,建议人工复核 ``` --- ## 六、边界情况处理建议 ### 6.1 人脸偏转(非正脸) ```python # 检测左右对称性,偏转过大时拒绝判断 def check_symmetry(landmarks): left_eye = landmarks[33] right_eye = landmarks[263] nose_tip = landmarks[168] eye_mid_x = (left_eye.x + right_eye.x) / 2 symmetry = abs(eye_mid_x - nose_tip.x) if symmetry > 0.03: # 偏移过大 return False, "检测到人脸偏转,建议正脸拍摄" return True, "" ``` ### 6.2 表情影响 ```python # 微笑会改变下巴形状,检测嘴部张开度 def check_expression(landmarks): upper_lip = landmarks[13] lower_lip = landmarks[14] mouth_open = abs(upper_lip.y - lower_lip.y) if mouth_open > 0.05: # 嘴巴张大 return False, "检测到嘴巴张开,建议自然闭合" return True, "" ``` ### 6.3 多特征都低分 ```python if best_score < 25.0: return "无法确定", 0.0, {"reason": "特征不够明显,无法准确分类"} ``` ### 6.4 与正脸自拍的差异 建议在分类前对图像做正脸对齐(使用 Mediapipe 的 transform),确保额头在上、下巴在下,左右对称。 ### 6.5 性别/年龄差异 男性下颌通常更宽,女性下巴更尖。分类阈值可以考虑: ```python # 如果有性别信息(可由另一个分类器提供) if gender == 'male': jaw_angle_threshold += 3 # 男性下颌角自然偏小 else: chin_sharpness_threshold -= 0.03 # 女性下巴自然偏尖 ``` --- ## 七、实现建议和注意事项 ### 7.1 预处理 ```python import mediapipe as mp mp_face_mesh = mp.solutions.face_mesh with mp_face_mesh.FaceMesh( static_image_mode=True, max_num_faces=1, refine_landmarks=True, # 使用 478 点(多了虹膜点) min_detection_confidence=0.5, ) as face_mesh: results = face_mesh.process(rgb_image) if results.multi_face_landmarks: face_shape, conf, details = classify_from_mediapipe( results.multi_face_landmarks ) ``` ### 7.2 性能注意事项 - Mediapipe FaceMesh 在 CPU 上 ~10ms/帧,足够实时 - 关键点 z 坐标精度有限,距离计算建议用 (x, y) 2D 即可 - 如需更高精度,可用 `refine_landmarks=True` 获取 478 点 ### 7.3 阈值校准 当前阈值基于以下来源综合设定: 1. **Farkas 面部测量学数据**(经典人体测量参考) 2. **亚洲人脸型分布统计**(鹅蛋脸和瓜子脸比例较高) 3. **Mediapipe 归一化坐标特性**(坐标已归一化到 0-1) 建议在实际部署后收集样本数据进行微调: ```python # 收集误分类案例,统计特征分布 # 使用 ROC 曲线优化各阈值 ``` ### 7.4 2D vs 3D 距离 Mediapipe 返回的 landmark 包含 z 坐标,但 z 精度不如 x/y。建议: ```python # 推荐方案:仅用 x, y 计算(忽略 z) def pt_2d(idx): lm = landmarks[idx] return np.array([lm.x, lm.y]) # 高精度方案:用 z 但加权降低 def pt_weighted(idx): lm = landmarks[idx] return np.array([lm.x, lm.y, lm.z * 0.5]) # z 权重减半 ``` ### 7.5 与原始规则的对比 | 维度 | 原始规则 | 优化后 | |------|----------|--------| | 判断方式 | 硬 if-else(可能冲突/遗漏)| 评分制(必然有结果)| | 关键点 | 12 个 | 16 个(增加太阳穴、下巴缘点)| | 特征数 | 5-6 个 | 15 个(含复合特征)| | 输出 | 单一标签 | 标签 + 置信度 + 混合脸型 | | 边界处理 | 无 | 三角窗平滑 + 低分兜底 | | 可调性 | 改阈值需要理解全部分支 | 改 `peak` 值即可微调 | | 代码行数 | ~60 行 | ~300 行(含注释)| --- ## 八、测试用例参考 ```python # 单元测试伪代码 test_cases = [ # (features_dict, expected_shape) ({'aspect_ratio': 0.92, 'jaw_angle': 148, 'taper_ratio': 0.03, 'chin_sharpness': 0.75, 'width_uniformity': 0.06, 'forehead_ratio': 0.98, 'jaw_ratio': 0.95, 'chin_ratio': 0.70, 'face_curve_score': 0.18, ...}, '圆形脸'), ({'aspect_ratio': 0.77, 'jaw_angle': 135, 'taper_ratio': 0.10, 'chin_sharpness': 0.60, 'width_uniformity': 0.08, 'forehead_ratio': 0.98, 'jaw_ratio': 0.92, 'chin_ratio': 0.62, 'face_curve_score': 0.19, ...}, '鹅蛋脸'), # ... 更多测试用例 ] ``` --- *报告结束。代码可直接集成到 Mediapipe 人脸分析流水线中。*