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 “三庭五眼”的量化计算规则有了关键点的像素坐标我们就可以定义具体的计算规则了。这里需要注意我们计算的是像素距离的比例而不是真实的物理长度比例。但只要拍摄时人脸距离摄像头大致固定且人脸没有严重倾斜这个比例就具有参考价值。三庭计算上庭高度计算发际线中点到眉间点的垂直距离Y坐标差。中庭高度计算眉间点到鼻下点的垂直距离。下庭高度计算鼻下点到颏下点的垂直距离。三庭比例分别计算上庭、中庭、下庭的高度然后计算它们的比值。理想值为1:1:1。我们可以计算标准差或最大偏差来评估均衡度。五眼计算单眼宽度计算左眼外眼角到左眼内眼角的水平距离X坐标差。通常取双眼宽度的平均值更稳定。眼距计算右眼内眼角到左眼内眼角的水平距离。面部两侧宽度计算左眼外眼角到左面颊外侧点的水平距离以及右眼外眼角到右面颊外侧点的水平距离。同样取平均值。五眼比例将“眼距”和两个“面部两侧宽度”分别除以“单眼宽度”得到三个比值。理想状态下这三个比值都应接近1。加上双眼本身各占1眼宽合起来就是“五眼”。所以理想比例是1侧边: 1眼: 1眼距: 1眼: 1侧边。注意这些计算都基于2D图像坐标。如果头部有偏转会影响测量的准确性。在实际项目中我们可以通过判断双眼Y坐标是否大致水平来简单判断头部是否端正并提示用户调整姿势。2.3 结果可视化与交互设计行空板自带屏幕这为我们提供了丰富的交互可能。结果不能只是一堆枯燥的数字。界面设计思路实时预览界面屏幕大部分区域显示摄像头实时画面并用醒目的点如绿色圆点和连线如蓝色线条实时标出我们用于计算的那些关键点让用户直观看到“板子看到了什么”。覆盖分析图形在检测到稳定人脸后可以在人脸上直接叠加绘制出“三庭”三条水平线和“五眼”五条垂直线的辅助分析线就像美容设计师用的那种比例尺一样。结果仪表盘在屏幕一侧或底部开辟一个区域用进度条、数字、星级或简单的表情符号来展示各项比例的分析结果。例如用三个横向进度条分别表示上、中、下庭的接近理想值的程度用五个纵向进度条表示“五眼”的符合度。交互逻辑设计一个简单的状态机。初始为“等待检测”状态检测到人脸后进入“分析中”状态持续分析几秒钟如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_modeFalse, # 视频流模式 max_num_faces1, # 只检测一张脸 refine_landmarksTrue, # 细化眼部和嘴唇关键点 min_detection_confidence0.5, min_tracking_confidence0.5 ) self.mp_draw mp.solutions.drawing_utils self.drawing_spec mp_draw.DrawingSpec(thickness1, circle_radius1, 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 摄像头标定与拍摄距离的标准化问题在不同距离下拍摄人脸在图像中的大小不同计算出的像素距离绝对值毫无意义只有比例值才有意义。但即便如此距离过近会产生透视畸变鼻子显得大耳朵显得小严重影响“五眼”中面部两侧宽度的测量。解决方案固定距离引导在屏幕上绘制一个“人脸框”作为引导。提示用户调整位置直到脸部轮廓大致贴合这个框。这个框的大小对应一个理想的拍摄距离例如30-50厘米。头部姿态初步校正在分析前增加一个简单的姿态判断。计算双眼连线的倾斜角度如果角度过大则提示用户“请摆正头部”。可以通过np.arctan2(dy, dx)计算角度。比例归一化我们的所有计算都基于比例这本身已经消除了一部分尺度影响。但为了更精确可以引入一个参考尺度。例如以“虹膜间距”瞳孔中心距离作为一个相对稳定的内部参考单位将所有其他距离除以这个单位得到更鲁棒的比例值。虹膜位置可以从MediaPipe更精细的眼部关键点中估算。4.2 MediaPipe关键点索引的准确性与替代方案问题MediaPipe的468个点索引是固定的但“发际线中点”、“面颊最外侧”这些点可能没有直接的索引对应。我们之前代码中用的索引如10, 234, 454是近似点对于某些发型如刘海或脸型误差会很大。解决方案与备选方案关键点插值与估算对于发际线可以尝试用额头上方一排点的平均位置来估算中点而不是用一个固定点。对于面颊外侧可以寻找面部轮廓点MediaPipe也提供了面部轮廓的索引集合中最左侧和最右侧的点。使用Dlib作为精度对比如果对精度要求极高且行空板性能允许可以同时集成Dlib的68点模型。Dlib的点位定义特别是眉间点、鼻下点在学术界更常用。你可以用MediaPipe做快速初筛和跟踪用Dlib对截取的人脸ROI区域做一次精确的关键点检测。这属于“重型”方案需要编译安装dlib库对行空板是个挑战。结果模糊化处理向用户明确说明这是一个基于2D图像的趣味性分析受光线、角度、发型影响。在输出结果时不要给出“89分”这样精确的数字而是采用“上庭比例较为标准”、“眼距略宽于理想比例”这样的定性描述或者用“非常接近”、“比较接近”、“略有差异”几个等级来呈现体验会更好。4.3 性能优化与实时性保障问题MediaPipe Face Mesh在行空板上全分辨率运行可能无法达到流畅的帧率如30fps导致体验卡顿。优化策略降低处理分辨率这是最有效的办法。不需要用摄像头原生分辨率如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降低模型复杂度FaceMesh初始化时refine_landmarksTrue会启用更精细的眼部和嘴唇网格如果不需要可以设为False以提升速度。非每帧检测采用“检测”与“跟踪”分离的策略。每隔N帧如10帧进行一次完整的人脸网格检测static_image_modeFalse但相当于执行一次检测在中间的帧里依赖MediaPipe自带的跟踪功能min_tracking_confidence来更新关键点位置这比每帧都做检测要快。代码层面的优化避免在循环中进行不必要的对象创建和销毁。将cv2.putText等绘制操作的开销降到最低只绘制必要的信息。4.4 光照与背景的影响处理问题强烈的侧光会在脸部形成浓重阴影可能导致MediaPipe丢失部分关键点如处于阴影中的眼睛内眼角。杂乱或与人脸颜色相近的背景会影响人脸检测的初始阶段。缓解措施预处理在将图像送入MediaPipe前可以尝试简单的图像预处理。例如使用cv2.equalizeHist在灰度图上进行直方图均衡化增强对比度。或者使用高斯滤波cv2.GaussianBlur轻微平滑图像减少噪声。提示用户在程序界面中当检测置信度持续较低时给出友好的文字提示“请调整到光线均匀的地方”或“请避免背景过于杂乱”。动态调整检测阈值在光照条件未知的环境下可以尝试动态调整min_detection_confidence。例如如果连续多帧检测失败可以适当降低阈值如从0.5降到0.3尝试重新捕获人脸一旦捕获成功再逐步提升回标准阈值。5. 项目扩展与玩法升级基础功能实现后这个项目还有很多可以玩出花样的扩展方向。1. 美学风格化结果输出不要只显示数字和进度条。可以设计一些有趣的、带有国风或漫画风格的视觉输出。例如根据“三庭五眼”的综合评分生成一句古文或诗词评语如“庭宇均衡眸若星距”。在分析完成后在屏幕一侧生成一个简笔画的“面相分析图”用不同的线条颜色标注出接近理想比例和偏离较大的部分。结合一些轻松幽默的“颜值建议”纯属娱乐比如“中庭稍长建议常带笑容可缩短中庭视觉长度”。2. 历史记录与对比分析利用行空板的存储空间或连接一个U盘将每次的分析结果比例数据、时间戳、可选的照片缩略图保存为一个JSON文件。之后可以开发一个简单的模式回顾历史数据查看比例随时间的变化当然成年人的骨骼比例基本固定主要是娱乐。或者增加一个“双人对比”模式先后测量两个人将结果并列显示。3. 与物联网结合行空板本身具备物联网能力。你可以将分析结果当然是匿名化的、概括性的数据通过Wi-Fi上传到云端服务器做一个全球或全班的“平均脸比例”统计看板。或者更简单地当检测到比例特别接近理想值时比如综合评分0.95控制一个连接到行空板GPIO的LED彩灯闪烁庆祝增加仪式感。4. 校准与个性化“美”本身是主观的。可以增加一个“校准”功能。让用户输入自己认为的“明星标准脸”照片程序学习这张照片的关键点比例并将其作为用户自定义的“理想标准”。后续的分析都将以这个自定义标准为基准让结果更具个人意义。这个项目从想法到实现充满了探索的乐趣。它不仅仅是一个代码练习更是一次对硬件能力、算法应用和交互设计的综合考验。当你看到行空板的小屏幕上清晰地勾勒出人脸比例线并给出分析时那种成就感是独一无二的。最重要的是整个过程充满了可调整和优化的空间每一个环节的改进都能带来可见的效果提升这正是创客项目的魅力所在。