基于行空板与MediaPipe实现三庭五眼人脸美学分析系统

📅 2026/7/28 8:27:01 👁️ 阅读次数 📝 编程学习
基于行空板与MediaPipe实现三庭五眼人脸美学分析系统

1. 项目缘起:当传统美学标准遇上开源硬件

最近在捣鼓行空板,总想着用它做点有意思的、能和人交互的项目。有天刷手机,又看到关于“三庭五眼”这个传统美学标准的讨论,什么“标准美人脸”、“黄金比例”。我突然灵光一闪:这些标准说起来头头是道,但到底怎么量化?能不能让一块板子,通过摄像头“看”一眼,就给出一个客观的、基于“三庭五眼”理论的分析结果?这听起来比单纯的人脸识别打卡或者表情分析要有趣得多。

“三庭五眼”是咱们老祖宗总结出来的一套面部比例美学,简单说就是把脸的长度分成三个等分:从前额发际线到眉骨是上庭,眉骨到鼻底是中庭,鼻底到下巴是下庭,理想状态下这三庭应该基本相等。而“五眼”是指脸的宽度,以一只眼睛的长度为单位,理想的脸宽应该是五只眼睛的宽度,即两眼之间、两眼外侧到太阳穴各约等于一只眼睛的长度。

这个想法立刻吸引了我。用行空板来实现,优势很明显:它集成了摄像头、屏幕和强大的Python计算能力,完全可以做成一个独立的、便携的“颜值分析仪”。你可以把它放在桌上,站到合适的距离,它就能捕捉你的面部图像,自动定位关键点,计算比例,并在屏幕上直观地显示结果和分析。这不仅仅是一个技术Demo,更是一个融合了计算机视觉、人机交互和传统文化的有趣跨界尝试。无论是自己玩,还是作为展示项目,都很有话题性。

2. 核心原理拆解:从人脸到数据的转化链路

要让行空板“看懂”三庭五眼,我们需要把整个过程拆解成几个清晰的步骤。这本质上是一个典型的计算机视觉应用流水线。

2.1 人脸检测与关键点定位:找到测量的“尺子”

这是整个项目最基础也是最关键的一步。我们不能凭空去量“三庭五眼”,必须先在人脸上找到可靠的、可重复定位的“锚点”。

为什么选择MediaPipe Face Mesh?在行空板这样的嵌入式设备上,我们需要一个兼顾精度和效率的解决方案。OpenCV自带的Haar级联分类器虽然轻量,但只能框出人脸,无法提供精细的关键点。而一些重型的人脸关键点检测模型(如Dlib的68点模型)在树莓派级别的算力上可能会比较吃力。MediaPipe的Face Mesh模型是一个绝佳的选择。它提供了468个3D人脸关键点,精度足够高,并且经过谷歌优化,在移动设备和边缘设备上运行效率非常出色。最重要的是,它有现成的、易于使用的Python库,与行空板基于Debian的系统完美兼容。

这468个点覆盖了人脸的每一个细节,但对于“三庭五眼”的测量,我们只需要其中一小部分。我们需要精准地找到:

  • 发际线中点:通常取额头顶部中央的点。
  • 眉间点:左右眉毛中间,鼻根上方的点。
  • 鼻尖点:鼻子最突出的点。
  • 鼻下点:鼻小柱和上唇连接的点。
  • 颏下点:下巴最下方的点。
  • 左右眼的内眼角和外眼角:用于计算眼宽和眼距。
  • 左右面颊最外侧点:用于计算面宽。

MediaPipe返回的点是有固定索引的。例如,鼻尖的索引通常是1,左眼内眼角是33,外眼角是133。我们需要事先查阅MediaPipe的面部网格索引图,确定我们需要的每一个关键点对应的索引号。这是后续所有计算的基础。

2.2 “三庭五眼”的量化计算规则

有了关键点的像素坐标,我们就可以定义具体的计算规则了。这里需要注意,我们计算的是像素距离的比例,而不是真实的物理长度比例。但只要拍摄时人脸距离摄像头大致固定,且人脸没有严重倾斜,这个比例就具有参考价值。

三庭计算:

  1. 上庭高度:计算发际线中点眉间点的垂直距离(Y坐标差)。
  2. 中庭高度:计算眉间点鼻下点的垂直距离。
  3. 下庭高度:计算鼻下点颏下点的垂直距离。
  4. 三庭比例:分别计算上庭、中庭、下庭的高度,然后计算它们的比值。理想值为1:1:1。我们可以计算标准差或最大偏差来评估均衡度。

五眼计算:

  1. 单眼宽度:计算左眼外眼角左眼内眼角的水平距离(X坐标差)。通常取双眼宽度的平均值更稳定。
  2. 眼距:计算右眼内眼角左眼内眼角的水平距离。
  3. 面部两侧宽度:计算左眼外眼角左面颊外侧点的水平距离,以及右眼外眼角右面颊外侧点的水平距离。同样取平均值。
  4. 五眼比例:将“眼距”和两个“面部两侧宽度”分别除以“单眼宽度”,得到三个比值。理想状态下,这三个比值都应接近1。加上双眼本身(各占1眼宽),合起来就是“五眼”。所以理想比例是1(侧边): 1(眼): 1(眼距): 1(眼): 1(侧边)

注意:这些计算都基于2D图像坐标。如果头部有偏转,会影响测量的准确性。在实际项目中,我们可以通过判断双眼Y坐标是否大致水平来简单判断头部是否端正,并提示用户调整姿势。

2.3 结果可视化与交互设计

行空板自带屏幕,这为我们提供了丰富的交互可能。结果不能只是一堆枯燥的数字。

界面设计思路:

  1. 实时预览界面:屏幕大部分区域显示摄像头实时画面,并用醒目的点(如绿色圆点)和连线(如蓝色线条)实时标出我们用于计算的那些关键点,让用户直观看到“板子看到了什么”。
  2. 覆盖分析图形:在检测到稳定人脸后,可以在人脸上直接叠加绘制出“三庭”(三条水平线)和“五眼”(五条垂直线)的辅助分析线,就像美容设计师用的那种比例尺一样。
  3. 结果仪表盘:在屏幕一侧或底部开辟一个区域,用进度条、数字、星级或简单的表情符号来展示各项比例的分析结果。例如,用三个横向进度条分别表示上、中、下庭的接近理想值的程度;用五个纵向进度条表示“五眼”的符合度。
  4. 交互逻辑:设计一个简单的状态机。初始为“等待检测”状态,检测到人脸后进入“分析中”状态,持续分析几秒钟(如3秒)取平均值以稳定结果,然后进入“结果显示”状态。可以设置一个物理按键或屏幕虚拟按键来重置分析,进行下一次测量。

3. 行空板环境搭建与核心代码实现

理论清晰了,接下来就是动手环节。行空板默认搭载了Python和部分库,但我们还需要安装核心的MediaPipe。

3.1 软件环境准备

通过行空板的终端(SSH或直接连接屏幕键盘)进行操作。

# 首先更新软件包列表 sudo apt-get update # 安装必要的系统依赖,MediaPipe可能需要一些编译库 sudo apt-get install -y python3-pip libatlas-base-dev libjasper-dev libqtgui4 libqt4-test # 使用pip安装所需的Python库 # 使用国内镜像源加速下载 pip3 install opencv-python-headless -i https://pypi.tuna.tsinghua.edu.cn/simple pip3 install mediapipe -i https://pypi.tuna.tsinghua.edu.cn/simple

这里安装的是opencv-python-headless,这是一个不带GUI功能的OpenCV版本,更轻量,适合在行空板这种无桌面环境或通过SSH运行的环境。MediaPipe的安装包相对较大,需要耐心等待。

3.2 核心代码结构解析

下面是一个高度精简但结构完整的代码框架,展示了核心逻辑。

import cv2 import mediapipe as mp import numpy as np from dataclasses import dataclass from typing import Tuple, Optional # 定义关键点索引(根据MediaPipe面部网格图确认) @dataclass class FaceLandmarks: FOREHEAD_TOP: int = 10 # 发际线中点近似点 GLABELLA: int = 168 # 眉间点 NOSE_TIP: int = 1 # 鼻尖 SUB_NASALE: int = 2 # 鼻下点 CHIN_BOTTOM: int = 152 # 颏下点 LEFT_EYE_INNER: int = 33 # 左眼内眼角 LEFT_EYE_OUTER: int = 133 # 左眼外眼角 RIGHT_EYE_INNER: int = 362 # 右眼内眼角 RIGHT_EYE_OUTER: int = 263 # 右眼外眼角 LEFT_CHEEK: int = 234 # 左面颊外侧近似点 RIGHT_CHEEK: int = 454 # 右面颊外侧近似点 class BeautyAnalyzer: def __init__(self): self.landmark_indices = FaceLandmarks() # 初始化MediaPipe人脸网格模型,配置为静态图像模式(更准)或视频模式(更快) self.mp_face_mesh = mp.solutions.face_mesh self.face_mesh = self.mp_face_mesh.FaceMesh( static_image_mode=False, # 视频流模式 max_num_faces=1, # 只检测一张脸 refine_landmarks=True, # 细化眼部和嘴唇关键点 min_detection_confidence=0.5, min_tracking_confidence=0.5 ) self.mp_draw = mp.solutions.drawing_utils self.drawing_spec = mp_draw.DrawingSpec(thickness=1, circle_radius=1, color=(0, 255, 0)) def _get_landmark_coord(self, landmarks, idx) -> Optional[Tuple[int, int]]: """根据索引从MediaPipe结果中获取像素坐标""" if landmarks and idx < len(landmarks.landmark): lm = landmarks.landmark[idx] # 注意:landmark的x, y是归一化坐标(0-1),需要转换为图像像素坐标 # 转换在外部进行,这里只返回归一化坐标 return (lm.x, lm.y) return None def analyze_proportions(self, image_shape, landmarks): """核心分析函数""" h, w, _ = image_shape results = {} # 获取所有需要的点(归一化坐标) points = {} for name, idx in vars(self.landmark_indices).items(): coord = self._get_landmark_coord(landmarks, idx) if coord: # 转换为像素坐标 points[name] = (int(coord[0] * w), int(coord[1] * h)) else: points[name] = None # 计算三庭 if all(points.get(k) for k in ['FOREHEAD_TOP', 'GLABELLA', 'SUB_NASALE', 'CHIN_BOTTOM']): upper = abs(points['GLABELLA'][1] - points['FOREHEAD_TOP'][1]) middle = abs(points['SUB_NASALE'][1] - points['GLABELLA'][1]) lower = abs(points['CHIN_BOTTOM'][1] - points['SUB_NASALE'][1]) total = upper + middle + lower results['three_regions'] = { 'upper_ratio': upper / total, 'middle_ratio': middle / total, 'lower_ratio': lower / total, 'raw_pixels': (upper, middle, lower) } # 计算均衡度:理想是0.333,计算偏离度 ideal = 1/3 deviation = np.std([upper/total, middle/total, lower/total]) results['three_regions']['balance_score'] = max(0, 1 - deviation / ideal) # 简单评分 # 计算五眼 if all(points.get(k) for k in ['LEFT_EYE_OUTER', 'LEFT_EYE_INNER', 'RIGHT_EYE_INNER', 'RIGHT_EYE_OUTER', 'LEFT_CHEEK', 'RIGHT_CHEEK']): left_eye_width = abs(points['LEFT_EYE_OUTER'][0] - points['LEFT_EYE_INNER'][0]) right_eye_width = abs(points['RIGHT_EYE_OUTER'][0] - points['RIGHT_EYE_INNER'][0]) avg_eye_width = (left_eye_width + right_eye_width) / 2.0 eye_distance = abs(points['RIGHT_EYE_INNER'][0] - points['LEFT_EYE_INNER'][0]) left_face_width = abs(points['LEFT_EYE_OUTER'][0] - points['LEFT_CHEEK'][0]) right_face_width = abs(points['RIGHT_CHEEK'][0] - points['RIGHT_EYE_OUTER'][0]) avg_side_width = (left_face_width + right_face_width) / 2.0 # 计算比例 eye_dist_ratio = eye_distance / avg_eye_width left_side_ratio = left_face_width / avg_eye_width right_side_ratio = right_face_width / avg_eye_width results['five_eyes'] = { 'eye_width_avg': avg_eye_width, 'eye_distance_ratio': eye_dist_ratio, 'left_side_ratio': left_side_ratio, 'right_side_ratio': right_side_ratio, 'balance_score': self._calc_five_eyes_score(eye_dist_ratio, left_side_ratio, right_side_ratio) } return results, points def _calc_five_eyes_score(self, dist_ratio, left_ratio, right_ratio): """简单的五眼比例评分(理想值均为1)""" scores = [] for ratio in [dist_ratio, left_ratio, right_ratio]: # 偏离1越远,分数越低 score = max(0, 1 - abs(ratio - 1) * 0.5) # 系数可调 scores.append(score) return np.mean(scores) # 主循环示例 def main(): cap = cv2.VideoCapture(0) # 打开行空板摄像头 analyzer = BeautyAnalyzer() analysis_results = None stable_counter = 0 STABLE_THRESHOLD = 15 # 连续稳定15帧才确定结果 while cap.isOpened(): success, image = cap.read() if not success: break # MediaPipe处理需要RGB图像 image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = analyzer.face_mesh.process(image_rgb) if results.multi_face_landmarks: for face_landmarks in results.multi_face_landmarks: # 绘制面部网格(可选) # analyzer.mp_draw.draw_landmarks(image, face_landmarks, analyzer.mp_face_mesh.FACEMESH_TESSELATION, analyzer.drawing_spec) # 分析比例 current_results, landmark_points = analyzer.analyze_proportions(image.shape, face_landmarks) # 在图像上绘制关键点和比例线 # ... 这里添加绘制代码,用cv2.line, cv2.circle ... # 简单稳定性判断:如果连续多帧结果变化不大,则判定为稳定 if analysis_results is not None and self._is_result_stable(analysis_results, current_results): stable_counter += 1 else: stable_counter = 0 analysis_results = current_results if stable_counter >= STABLE_THRESHOLD: # 在图像上显示最终分析结果 # ... 添加显示文本的代码 ... pass # 将图像显示在行空板屏幕上(需根据行空板具体显示库调整,如使用Pygame或直接帧缓冲) # cv2.imshow('Beauty Analysis', image) # 行空板上可能需要使用其他方式显示,例如通过板载屏幕接口 if cv2.waitKey(5) & 0xFF == 27: # ESC退出 break cap.release() if __name__ == "__main__": main()

这段代码勾勒出了整个程序的骨架。BeautyAnalyzer类封装了核心的分析逻辑,主循环负责捕获摄像头图像、调用分析器、并根据结果进行绘制和状态判断。在实际部署时,你需要将OpenCV的显示窗口(cv2.imshow)替换为行空板原生屏幕的显示方法,这通常涉及到使用pygame或直接操作Linux帧缓冲设备。

4. 实操中的关键细节与避坑指南

把代码跑起来只是第一步,要让项目体验好、结果有参考性,还有很多细节需要打磨。这些都是我在实际调试中踩过的坑。

4.1 摄像头标定与拍摄距离的标准化

问题:在不同距离下拍摄,人脸在图像中的大小不同,计算出的像素距离绝对值毫无意义,只有比例值才有意义。但即便如此,距离过近会产生透视畸变(鼻子显得大,耳朵显得小),严重影响“五眼”中面部两侧宽度的测量。

解决方案

  1. 固定距离引导:在屏幕上绘制一个“人脸框”作为引导。提示用户调整位置,直到脸部轮廓大致贴合这个框。这个框的大小对应一个理想的拍摄距离(例如30-50厘米)。
  2. 头部姿态初步校正:在分析前,增加一个简单的姿态判断。计算双眼连线的倾斜角度,如果角度过大,则提示用户“请摆正头部”。可以通过np.arctan2(dy, dx)计算角度。
  3. 比例归一化:我们的所有计算都基于比例,这本身已经消除了一部分尺度影响。但为了更精确,可以引入一个参考尺度。例如,以“虹膜间距”(瞳孔中心距离)作为一个相对稳定的内部参考单位,将所有其他距离除以这个单位,得到更鲁棒的比例值。虹膜位置可以从MediaPipe更精细的眼部关键点中估算。

4.2 MediaPipe关键点索引的准确性与替代方案

问题:MediaPipe的468个点索引是固定的,但“发际线中点”、“面颊最外侧”这些点可能没有直接的索引对应。我们之前代码中用的索引(如10, 234, 454)是近似点,对于某些发型(如刘海)或脸型,误差会很大。

解决方案与备选方案

  1. 关键点插值与估算:对于发际线,可以尝试用额头上方一排点的平均位置来估算中点,而不是用一个固定点。对于面颊外侧,可以寻找面部轮廓点(MediaPipe也提供了面部轮廓的索引集合)中最左侧和最右侧的点。
  2. 使用Dlib作为精度对比:如果对精度要求极高,且行空板性能允许,可以同时集成Dlib的68点模型。Dlib的点位定义(特别是眉间点、鼻下点)在学术界更常用。你可以用MediaPipe做快速初筛和跟踪,用Dlib对截取的人脸ROI区域做一次精确的关键点检测。这属于“重型”方案,需要编译安装dlib库,对行空板是个挑战。
  3. 结果模糊化处理:向用户明确说明,这是一个基于2D图像的趣味性分析,受光线、角度、发型影响。在输出结果时,不要给出“89分”这样精确的数字,而是采用“上庭比例较为标准”、“眼距略宽于理想比例”这样的定性描述,或者用“非常接近”、“比较接近”、“略有差异”几个等级来呈现,体验会更好。

4.3 性能优化与实时性保障

问题:MediaPipe Face Mesh在行空板上全分辨率运行可能无法达到流畅的帧率(如30fps),导致体验卡顿。

优化策略

  1. 降低处理分辨率:这是最有效的办法。不需要用摄像头原生分辨率(如1080p)进行处理。将采集到的图像缩放到一个较小的尺寸(如640x480甚至320x240)再喂给MediaPipe,可以极大提升速度。显示时仍然使用原始分辨率或缩放回屏幕大小进行绘制。
    process_img = cv2.resize(image_rgb, (320, 240)) results = face_mesh.process(process_img) # 获取关键点后,需要将坐标缩放回原始图像尺寸 scale_x = image.shape[1] / 320.0 scale_y = image.shape[0] / 240.0
  2. 降低模型复杂度FaceMesh初始化时,refine_landmarks=True会启用更精细的眼部和嘴唇网格,如果不需要,可以设为False以提升速度。
  3. 非每帧检测:采用“检测”与“跟踪”分离的策略。每隔N帧(如10帧)进行一次完整的人脸网格检测(static_image_mode=False但相当于执行一次检测),在中间的帧里,依赖MediaPipe自带的跟踪功能(min_tracking_confidence)来更新关键点位置,这比每帧都做检测要快。
  4. 代码层面的优化:避免在循环中进行不必要的对象创建和销毁。将cv2.putText等绘制操作的开销降到最低,只绘制必要的信息。

4.4 光照与背景的影响处理

问题:强烈的侧光会在脸部形成浓重阴影,可能导致MediaPipe丢失部分关键点(如处于阴影中的眼睛内眼角)。杂乱或与人脸颜色相近的背景会影响人脸检测的初始阶段。

缓解措施

  1. 预处理:在将图像送入MediaPipe前,可以尝试简单的图像预处理。例如,使用cv2.equalizeHist(在灰度图上)进行直方图均衡化,增强对比度。或者使用高斯滤波cv2.GaussianBlur轻微平滑图像,减少噪声。
  2. 提示用户:在程序界面中,当检测置信度持续较低时,给出友好的文字提示:“请调整到光线均匀的地方”或“请避免背景过于杂乱”。
  3. 动态调整检测阈值:在光照条件未知的环境下,可以尝试动态调整min_detection_confidence。例如,如果连续多帧检测失败,可以适当降低阈值(如从0.5降到0.3)尝试重新捕获人脸,一旦捕获成功,再逐步提升回标准阈值。

5. 项目扩展与玩法升级

基础功能实现后,这个项目还有很多可以玩出花样的扩展方向。

1. 美学风格化结果输出不要只显示数字和进度条。可以设计一些有趣的、带有国风或漫画风格的视觉输出。例如:

  • 根据“三庭五眼”的综合评分,生成一句古文或诗词评语(如“庭宇均衡,眸若星距”)。
  • 在分析完成后,在屏幕一侧生成一个简笔画的“面相分析图”,用不同的线条颜色标注出接近理想比例和偏离较大的部分。
  • 结合一些轻松幽默的“颜值建议”(纯属娱乐),比如“中庭稍长,建议常带笑容,可缩短中庭视觉长度”。

2. 历史记录与对比分析利用行空板的存储空间(或连接一个U盘),将每次的分析结果(比例数据、时间戳、可选的照片缩略图)保存为一个JSON文件。之后可以开发一个简单的模式,回顾历史数据,查看比例随时间的变化(当然,成年人的骨骼比例基本固定,主要是娱乐)。或者增加一个“双人对比”模式,先后测量两个人,将结果并列显示。

3. 与物联网结合行空板本身具备物联网能力。你可以将分析结果(当然是匿名化的、概括性的数据)通过Wi-Fi上传到云端服务器,做一个全球(或全班)的“平均脸比例”统计看板。或者更简单地,当检测到比例特别接近理想值时(比如综合评分>0.95),控制一个连接到行空板GPIO的LED彩灯闪烁庆祝,增加仪式感。

4. 校准与个性化“美”本身是主观的。可以增加一个“校准”功能。让用户输入自己认为的“明星标准脸”照片,程序学习这张照片的关键点比例,并将其作为用户自定义的“理想标准”。后续的分析都将以这个自定义标准为基准,让结果更具个人意义。

这个项目从想法到实现,充满了探索的乐趣。它不仅仅是一个代码练习,更是一次对硬件能力、算法应用和交互设计的综合考验。当你看到行空板的小屏幕上清晰地勾勒出人脸比例线,并给出分析时,那种成就感是独一无二的。最重要的是,整个过程充满了可调整和优化的空间,每一个环节的改进都能带来可见的效果提升,这正是创客项目的魅力所在。