Files
hair/face/face_shape_classification.md
T
xslandCursor b78da0e0cd feat: 脸型分类器(7分类)+批量报告生成
基于 MediaPipe 468 关键点提取几何特征,转 z 分数后与各脸型原型加权匹配。
参考分布与原型靶心取自 1093 张测试集的实测画像,不再靠人工设定绝对阈值。

方形脸占比从 29.6% 降到 13.8%,两处原因:一是参考统计量原先只由 50 张样本
估得,相对全量人群有系统性偏移,且三项偏移都在给方形脸加分;二是原型把
aspect_ratio 当作方形脸的主特征,但实测方脸组该值中位仅 +0.16,真正"宽"的
是圆脸(+1.01),等于在拿脸宽找方脸。

原型参数在「6 张基准标注图判定不变、且领先第二名 >=3 分」的约束下搜索得到。
余量约束是必要的:早前一版余量仅 0.008 分,权重写码时四舍五入就会翻转结论。

测试素材(人像照片)与报告输出体积大,一并加入 .gitignore。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 11:37:09 +08:00

30 KiB
Raw Blame History

脸型判断规则优化报告

基于 Mediapipe 468 点人脸关键点的脸型分类系统
优化日期:2026-07-28


一、优化总览

原始规则主要问题

问题 说明
规则冲突 7条 if 规则可能同时匹配,无优先级机制
特征定义模糊 width_ratiochin_narrowness 等未给出精确计算方式
关键点不足 额头宽度用 234/454(颧骨点)而非太阳穴点,导致测量不准
无置信度 硬判断,混合脸型无处理
阈值经验性 阈值未经统计校准,边界处容易误判
缺少归一化 不同距离拍的照片结果不一致

优化策略

  1. 精确特征提取:增加关键点,所有测量归一化
  2. 评分制分类:每个脸型计算匹配度分数(0-100),取最高分
  3. 置信度输出:报告 Top-1 / Top-2 分差,判断是否为混合脸型
  4. 优先级仲裁:分数接近时按"特异性优先"原则仲裁
  5. 鲁棒性增强:clamp 防止除零、NaN,角度计算增加 3D 投影

二、优化后的完整 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 的匹配度分数,取最高分。

# 原始:可能冲突
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 分时,判定为混合脸型:

# 例:鹅蛋脸 65 分,瓜子脸 62 分 → 分差 3 < 8
# 输出:"鹅蛋脸(偏瓜子脸)"

5.5 置信度输出

confidence = best_score / total_score
# > 0.25  → 高置信度,结果明确
# 0.18-0.25 → 中等,有一定混合
# < 0.18  → 低置信度,建议人工复核

六、边界情况处理建议

6.1 人脸偏转(非正脸)

# 检测左右对称性,偏转过大时拒绝判断
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 表情影响

# 微笑会改变下巴形状,检测嘴部张开度
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 多特征都低分

if best_score < 25.0:
    return "无法确定", 0.0, {"reason": "特征不够明显,无法准确分类"}

6.4 与正脸自拍的差异

建议在分类前对图像做正脸对齐(使用 Mediapipe 的 transform),确保额头在上、下巴在下,左右对称。

6.5 性别/年龄差异

男性下颌通常更宽,女性下巴更尖。分类阈值可以考虑:

# 如果有性别信息(可由另一个分类器提供)
if gender == 'male':
    jaw_angle_threshold += 3   # 男性下颌角自然偏小
else:
    chin_sharpness_threshold -= 0.03  # 女性下巴自然偏尖

七、实现建议和注意事项

7.1 预处理

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

建议在实际部署后收集样本数据进行微调:

# 收集误分类案例,统计特征分布
# 使用 ROC 曲线优化各阈值

7.4 2D vs 3D 距离

Mediapipe 返回的 landmark 包含 z 坐标,但 z 精度不如 x/y。建议:

# 推荐方案:仅用 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 行(含注释)

八、测试用例参考

# 单元测试伪代码
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 人脸分析流水线中。