
项目名称:基于深度学习的人脸表情捕捉及卡通表情重建 来源:第二届 3D 视觉创新应用竞赛 作者:徐冠宇、彭飞 相机型号:Astra S / Astra 系列深度相机 支持设备:Jetson Nano 源代码链接:https://github.com/3DCVdeveloper/3DReconstruction_Cartoon-emoticons
一句话看懂这个项目 🚀
这是一个用开源软件和低成本硬件跑通的“人脸表情捕捉 -> 表情参数计算 -> Blender 卡通角色重建”项目。它不依赖昂贵的商业面捕系统,而是通过 Astra 深度相机采集人脸视频,用 MediaPipe Face Mesh 检测人脸关键点,再把头部、嘴部、眉毛、眼睛等表情变化映射到 Blender 角色 Vincent 的控制器上,最终让卡通角色跟随真人做出相似表情。
项目已经初步跑通完整流程,并且采用前后端分离设计:后端负责视觉检测、滤波和参数计算;前端负责驱动三维软件中的角色控制器。因此,当前前端使用 Blender,但未来理论上可以迁移到 3DMax、Maya、Unity、UE4/UE 等软件环境中。
为什么这个项目值得看 👀
传统面部捕捉通常意味着昂贵硬件、商业软件授权和复杂棚拍流程。这个项目的亮点在于,它把“可用的面捕链路”压缩到一个更轻、更开放的方案里:
| 维度 | 项目做法 | 价值 |
|---|---|---|
| 硬件 | Astra 深度相机 + Jetson Nano + 自制面捕头盔 | 降低实验和部署门槛 |
| 软件 | Python、OpenCV、MediaPipe、Blender | 全流程开源,不依赖商业软件 |
| 算法 | 人脸关键点检测、PnP 头姿估计、卡尔曼滤波、关键点映射 | 兼顾实时性、稳定性和可解释性 |
| 架构 | 后端视觉算法与前端三维软件驱动分离 | 便于迁移到其他动画/游戏引擎 |
| 应用 | 虚拟偶像、动画制作、直播表情驱动、交互娱乐 | 具备清晰的扩展空间 |
项目整体流程 🧩
项目可以拆成五个连续环节:

- 使用 Astra 相机采集人脸视频流,并保存为连续图片帧。
- 使用 MediaPipe Face Mesh 从每一帧中提取高密度人脸关键点。
- 对关键点序列进行卡尔曼滤波和移动均值滤波,降低逐帧抖动。
- 将关键点变化转换为头部姿态、嘴部张合、眉毛抬降、眼睛开合等控制参数。
- 在 Blender 中驱动 Vincent 卡通角色的骨骼控制器,并插入关键帧生成动画。
技术架构:后端抓“表情”,前端让角色“动起来” ⚙️
理解这个项目,最重要的是先抓住它的“数据流”。它不是直接把摄像头画面塞进 Blender,而是先把视频转换成稳定的人脸运动数据,再由 Blender 读取这些数据并驱动角色。
可以把系统拆成三层:
| 层级 | 输入 | 输出 | 作用 |
|---|---|---|---|
| 采集层 | Astra 相机视频流 | 连续图片帧 | 把真人表情变成可离线处理的数据 |
| 视觉算法层 | 图片帧 | 人脸关键点 .npy |
检测、跟踪、平滑面部运动 |
| 角色驱动层 | 平滑后的关键点 | Blender 骨骼关键帧 | 把表情参数映射到卡通角色控制器 |
后端主要由三个脚本组成:
| 文件 | 作用 |
|---|---|
main1_capture_astra.py |
调用 OpenCV 读取 Astra 相机画面,采集并保存图片帧 |
main2_meidapipe_gen_npy.py |
使用 MediaPipe Face Mesh 提取人脸关键点,并保存为 .npy 数据 |
main3_kalmanfilter_read_npy_gen_npy.py |
对关键点序列进行卡尔曼滤波、移动均值滤波,并生成平滑后的关键点数据 |
前端主要由两个 Blender 脚本组成:
| 文件 | 作用 |
|---|---|
OpenCVAnim.py |
在 Blender 工具栏中注册 “OpenCV Animation” 工具入口 |
OpenCVAnimOperator(blender中使用的代码).py |
读取图片和关键点数据,计算表情控制参数,驱动角色骨骼并插入关键帧 |
当前数据目录中包含 1800 张原始采集图片,以及 4 组关键点序列数据:原始关键点、卡尔曼滤波结果、移动均值滤波结果、卡尔曼叠加均值滤波结果。代码中实际关键点数组形状为 (1800, 478, 2),这是因为 MediaPipe 在开启 refine_landmarks=True 后会额外加入眼部虹膜等精细点位;项目说明中常称为 468 点,本质上仍属于 Face Mesh 高密度人脸关键点方案。
用伪代码看,项目主流程可以理解为下面这样:
# 1. 采集:相机视频流 -> 图片序列
frames = capture_from_astra(camera_id=1, size=(640, 480), frame_count=1800)
save_images(frames, "data/images_origin")
# 2. 检测:图片序列 -> 人脸关键点
landmarks = []
for image in images:
face_mesh = mediapipe_face_mesh(image)
landmarks.append(face_mesh.pixel_points) # shape: (478, 2)
save_npy("data/landmarks_nx468x2.npy", landmarks)
# 3. 平滑:原始关键点 -> 稳定关键点
landmarks_smooth = kalman_filter(landmarks)
save_npy("data/landmarks_nx468x2_kalmanFilter.npy", landmarks_smooth)
# 4. 驱动:关键点 -> Blender 控制器
for frame_points in landmarks_smooth:
controls = map_landmarks_to_controls(frame_points)
apply_to_blender_rig("RIG-Vincent", controls)

这段流程说明了项目架构的核心价值:视觉算法层只输出通用关键点数据,不绑定某一个三维软件;Blender 层只负责消费这些数据并控制角色。也正因为这样,后续如果要迁移到 Unity、UE 或 Maya,主要改的是“角色驱动层”,前面的采集和关键点处理逻辑可以保留。
关键技术 1:高密度人脸关键点,让表情有“骨架” 🧠
人脸表情捕捉的第一步,是把普通图像变成机器能计算的结构化数据。项目对比了 dlib、OpenCV LBF、OpenPose、MediaPipe 等方案后,选择 MediaPipe Face Mesh,核心原因是它的点位密度更高,能覆盖嘴唇、眼皮、眉毛、鼻梁、脸颊和脸部轮廓等区域。
在这个项目里,每一帧图像会被转换成一个二维关键点数组。可以把它理解为:
# 单帧人脸关键点
one_frame_landmarks.shape == (478, 2)
# 含义:第 i 个关键点在图像中的像素坐标
x, y = one_frame_landmarks[i]
# 1800 帧组成一段面部运动序列
all_landmarks.shape == (1800, 478, 2)
这一步的关键不是“检测到脸”这么简单,而是把表情拆成可计算的几何关系。例如:
mouth_width = distance(point_291, point_61) # 左右嘴角距离
mouth_height = distance(point_0, point_17) # 上下嘴唇距离
eye_open = distance(point_386, point_374) # 左眼上下眼皮距离
brow_raise = distance(point_334, point_168) # 眉毛到鼻梁参考点距离
这些距离本身还不是动画参数,但它们已经把“张嘴、闭眼、挑眉”这类表情动作从图像变成了数值。后面的滤波、归一化、映射,本质上都是围绕这些数值做加工。
也就是说,Face Mesh 在这里承担的是“表情传感器”的角色:摄像头看到的是画面,算法输出的是每一块面部区域的运动轨迹。
关键技术 2:滤波不是锦上添花,而是面捕稳定性的底座 📈
逐帧检测的人脸关键点会天然抖动。原因包括相机噪声、光照变化、检测模型误差、脸部局部遮挡、头部微小运动等。如果直接用原始关键点驱动角色,卡通表情会出现“抽动感”,尤其是眼皮、眉毛、嘴角这类小幅运动部位。
项目实现了三类滤波结果:
| 数据版本 | 说明 | 特点 |
|---|---|---|
origin |
原始关键点 | 响应快,但噪点多 |
kalmanFilter |
卡尔曼滤波 | 平滑明显,延迟较小 |
meanFilter |
移动均值滤波 | 平滑明显,但实时性弱一些 |
kalmanAndMeanFilter |
卡尔曼 + 移动均值 | 更稳,但延迟更明显 |
项目最终更偏向使用卡尔曼滤波结果。原因很直接:面捕是一个强实时任务,表情不能只追求平滑,还必须跟得上真人动作。卡尔曼滤波通过“预测 + 观测修正”的方式,在保留响应速度的同时压低抖动,比单纯滑动平均更适合驱动交互式动画。
卡尔曼滤波的直觉可以这样理解:
# 预测:根据上一帧状态,估计这一帧关键点应该在哪里
predicted_state = F @ previous_state
# 观测:MediaPipe 在当前帧检测到的位置
observed_state = current_landmarks
# 修正:在“预测值”和“观测值”之间取一个更可信的结果
smoothed_state = predicted_state + K @ (observed_state - predicted_state)
如果观测值突然跳动,但预测趋势比较稳定,滤波器不会完全相信这次跳动;如果连续多帧都显示关键点确实移动了,滤波器又会逐渐跟上动作。这正好符合面部捕捉的需求:压掉偶发噪声,同时保留真实表情变化。
项目里的移动均值滤波则更容易理解:
# 取最近 5 帧关键点的平均值
smoothed_point = mean([point_t4, point_t3, point_t2, point_t1, point_t])
移动均值能让曲线更平滑,但它天然会带来延迟。眨眼、快速张嘴这类动作如果延迟太明显,角色就会显得“慢半拍”。因此,项目把卡尔曼滤波作为更适合实时表情驱动的主方案。
关键技术 3:头部姿态用 PnP,表情细节用关键点映射 🎯

项目把面部运动拆成两类处理:头部刚性运动和面部柔性运动。这个拆分很重要,因为“转头”和“张嘴”在数学上不是同一种问题。
第一类是头部刚性运动。
头部整体点头、摇头、歪头,本质上可以近似看作一个刚体在三维空间里的姿态变化。项目选取鼻尖、下巴、左右眼角、左右脸颊等 6 个关键点,通过 PnP 估计头部相对于相机的旋转和平移。
PnP 可以理解为:已知几个点在真实三维头部模型中的位置,也知道它们在当前图像中的二维位置,反推出“头相对于相机是怎么转、怎么移的”。
# 3D 参考点:来自人脸模型或深度相机点云标定
model_points_3d = [
nose_tip_3d,
chin_3d,
left_eye_corner_3d,
right_eye_corner_3d,
left_cheek_3d,
right_cheek_3d,
]
# 2D 观测点:来自当前帧 Face Mesh
image_points_2d = [
landmarks[4], # 鼻尖
landmarks[152], # 下巴
landmarks[263], # 左眼角
landmarks[33], # 右眼角
landmarks[454], # 左脸颊
landmarks[234], # 右脸颊
]
success, rotation, translation = cv2.solvePnP(
model_points_3d,
image_points_2d,
camera_matrix,
dist_coeffs
)
更值得注意的是,项目并不是简单地把当前欧拉角减去初始欧拉角,而是计算两个位姿矩阵之间的相对变化:
# first_rt:第一帧头部位姿
# current_rt:当前帧头部位姿
relative_rt = inverse(first_rt) @ current_rt
relative_euler = rotation_matrix_to_euler(relative_rt)
这样做更符合三维空间变换逻辑,因为旋转不是普通数字相减,旋转顺序和坐标系都会影响结果。
第二类是面部柔性运动。
嘴巴、眉毛、眼皮并不是刚体,它们需要根据关键点之间的相对距离来映射。例如:
| 表情部位 | 关键点逻辑 | Blender 控制对象 |
|---|---|---|
| 头部姿态 | 6 点 PnP 求解头部旋转 | head_fk |
| 嘴部 | 上下唇距离、左右嘴角距离 | mouth_ctrl |
| 左右眉毛 | 眉毛点到鼻梁参考点的距离 | brow_ctrl_L、brow_ctrl_R |
| 左右眼皮 | 上下眼皮关键点距离 | eyelid_up_ctrl_L/R、eyelid_low_ctrl_L/R |
这种方案被称为“关键点映射”。它的好处是可解释、可控、美术侧工作量相对较低;缺点是需要针对角色控制器做调试,映射比例也要根据模型绑定效果校准。
项目中的映射逻辑可以概括为三步:
# 1. 从关键点计算一个面部动作量
mouth_height = distance(landmarks[0], landmarks[17])
# 2. 根据历史最小值和最大值,把动作量归一化到 0-1
mouth_open_ratio = normalize_to_0_1("mouth_height", mouth_height)
# 3. 映射到 Blender 控制器允许的位移范围
bones["mouth_ctrl"].location[2] = -mouth_open_ratio * 0.06
这段映射体现了项目的工程取舍:算法不直接修改模型顶点,而是驱动已经绑定好的控制器。这样更接近动画制作流程,也更容易迁移到其他角色资产上。
为什么选择“关键点映射”,而不是 Blendshape?🛠️
面部动画常见路线有两种:Blendshape 和关键点映射。
| 技术路线 | 核心思想 | 优点 | 代价 |
|---|---|---|---|
| Blendshape | 预先制作多个表情形变,再按权重混合 | 效果稳定、行业成熟、适合高质量制作 | 需要较多美术资产和表情形变制作 |
| 关键点映射 | 直接把人脸关键点变化映射到角色控制器 | 可解释、可调试、原型迭代快 | 需要针对角色绑定调映射关系 |
本项目选择关键点映射,是因为它更适合低成本、开源、可实验的项目目标:只要角色已经有基本绑定和控制器,就可以根据关键点距离、归一化比例、缩放范围来驱动角色。换句话说,它用算法调参替代了一部分美术资产准备工作,更适合快速验证从人脸到卡通角色的完整链路。
如果用一段更直观的伪代码表达二者差异,可以这样看:
# Blendshape 思路:输出多个表情形变权重
avatar.blendshape["Smile"] = 0.72
avatar.blendshape["BlinkLeft"] = 0.30
avatar.blendshape["JawOpen"] = 0.55
# 关键点映射思路:输出控制器的位移或旋转
bones["mouth_ctrl"].location = (mouth_width_value, 0, mouth_open_value)
bones["brow_ctrl_L"].location[2] = brow_left_value
bones["head_fk"].rotation_euler = head_pose_value
这个项目的重点不是追求电影级面捕效果,而是跑通一条“能采集、能计算、能驱动、能迁移”的开放链路。因此,关键点映射是更贴合项目目标的路线。
角色驱动:Vincent 如何“学会”真人表情 😄
项目使用 Blender 官方免费卡通模型 Vincent,并重点调试了 8 个控制器:
| 控制器 | 驱动内容 |
|---|---|
head_fk |
整个头部姿态 |
mouth_ctrl |
嘴部横向张合、纵向张合 |
brow_ctrl_L |
左眉毛升降 |
brow_ctrl_R |
右眉毛升降 |
eyelid_up_ctrl_L |
左上眼皮 |
eyelid_low_ctrl_L |
左下眼皮 |
eyelid_up_ctrl_R |
右上眼皮 |
eyelid_low_ctrl_R |
右下眼皮 |
脚本会持续读取图像帧和对应关键点,计算当前帧的控制参数,然后写入 Blender 骨骼控制器,并通过 keyframe_insert 插入关键帧。这样就把视频中的真人表情转化成了可编辑、可回放的三维动画数据。
角色驱动的核心逻辑可以简化成下面这样:
for frame_index, landmarks in enumerate(landmark_sequence):
# 头部:PnP 结果映射到头部旋转
bones["head_fk"].rotation_euler = estimate_head_rotation(landmarks)
bones["head_fk"].keyframe_insert(data_path="rotation_euler")
# 嘴部:嘴角宽度和上下唇距离映射到 mouth_ctrl
bones["mouth_ctrl"].location[0] = map_mouth_width(landmarks)
bones["mouth_ctrl"].location[2] = map_mouth_height(landmarks)
bones["mouth_ctrl"].keyframe_insert(data_path="location")
# 眉毛和眼皮:局部距离映射到上下位移
bones["brow_ctrl_L"].location[2] = map_left_brow(landmarks)
bones["eyelid_up_ctrl_L"].location[2] = map_left_upper_eyelid(landmarks)
这里的“控制器”可以理解为角色脸上的操纵杆。算法不需要知道 Vincent 模型每个顶点怎么变形,只需要推动这些操纵杆,Blender 中已有的绑定系统就会负责让面部网格产生对应表情。这也是项目能保持前后端分离的关键原因。
开发与测试流程 🧪
项目测试过程可以按以下步骤复现:
- 运行
main1_capture_astra.py,用 Astra 相机采集视频流并保存图片。
- 运行
main2_meidapipe_gen_npy.py,从图片集中提取人脸关键点。
- 运行
main3_kalmanfilter_read_npy_gen_npy.py,生成滤波后的关键点数据。
- 打开
vincent.blend。
- 在 Blender 的 Script 标签页中加载
OpenCVAnimOperator(blender中使用的代码).py和OpenCVAnim(blender中使用的代码).py。
- 将脚本中的
FOLDER_ROOT_DATA修改为本机data文件夹的绝对路径。
- 切换到 Layout 标签页,点击
Capture,开始驱动卡通表情重建。
项目价值总结 🌟
这个项目最有价值的地方,不只是“让卡通脸动起来”,而是证明了一条轻量、开放、可迁移的表情捕捉路线:
- 它用开源工具替代商业闭源软件,降低学习、复现和二次开发门槛。
- 它用高密度人脸关键点和滤波算法解决了表情捕捉的核心数据问题。
- 它用 PnP 处理头部刚性运动,用关键点映射处理局部柔性表情,技术分层清楚。
- 它通过 Blender 控制器驱动角色,而不是直接操作模型点,保留了迁移到其他三维软件或引擎的可能性。
- 它用低成本硬件和自制面捕头盔完成实验,适合教学、竞赛、原型验证和独立创作。
如果继续迭代,项目可以进一步增强实时推理性能、加入更精细的嘴型/表情语义识别、适配更多角色绑定规范,并封装为通用插件或实时虚拟直播工具。对于虚拟偶像、动画制作、实时互动娱乐来说,这类开源面捕方案很可能会成为创作者手里越来越重要的一块“轻量基础设施”。
