Python到Unity全栈实现:实时面部检测与姿态估计的详解

摘要:本文将介绍如何通过Python的dlib库实现面部68个特征点的实时检测,结合卡尔曼滤波优化数据流,并通过Socket通信将数据传递至Unity引擎,最终驱动3D模型的自然面部表情与头部运动。本文提供完整的代码实现和工作流解析,适用于虚拟角色驱动、AR/VR交互等场景。

GitHub开源工程链接:https://github.com/qingtianwu/OpenVHead?tab=readme-ov-file

一、技术背景

1.1 面部特征点检测的意义

面部特征点检测是计算机视觉领域的核心技术之一,可实现:

  • 表情捕捉:通过眼部和嘴部特征点变化识别情绪

  • 姿态估计:根据头部关键点计算三维旋转和平移

  • 生物识别:构建面部特征向量用于身份验证

  • 1.2 技术栈选择

    组件 技术选型 优势
    特征点检测 dlib (68点模型) 高精度、轻量级
    数据传输 TCP Socket TCP套接字 跨平台、低延迟
    3D模型驱动 Unity  实时渲染、跨设备兼容
    数据滤波 卡尔曼滤波 噪声抑制、运动平滑

    二、环境配置

    2.1 硬件配置

  • PC个人电脑
  • RGB摄像头(可兼容内置摄像头和外置USB摄像头)本例中我使用的是笔记本自带摄像头
  • 2.2软件配置

            2.2.1环境

  • Windows操作系统(本例中使用Win11)
  • Python 3.6.6  Python Release Python 3.6.6 | Python.org
  • Unity 2022.3.12 统一2022.3.12
  • VsCode
  • Anaconda Navigator 
  •         2.2.2库依赖

  • opencv-python · PyPI   3.4.0.12

  • dlib · PyPI                19.7.0

  • 如果您在Windows x64系统上使用python 3.6,

    那么请下载文件版本“xxxx-cp36-cp36 -win_amd64. exe.whl”版本。

    本例中使用 opencv_python-3.4.0.12-cp36-cp36m-win_amd64.whl

                       dlib-19.7.0-cp36-cp36m-win_amd64.whl

    2.3 opencv-python库安装

    Win+R启动CMD

    输入下面的代码即可安装3.4.0.12版本的opencv库

    pip install opencv-python==3.4.0.12

    安装完成后可以通过以下代码验证是否安装成功,如果输出了Opencv的版本号说明安装成功

    python -c "import cv2; print(cv2.__version__)"

    通过下面的py代码可以检测Opencv-Python是否能够工作

    import cv2
    # 图片路径
    image_path = r"L:\Ayager\Picture\9.jpg"  # 替换为你的图片文件名
    # 读取图片
    image = cv2.imread(image_path)
    # 检查图片是否成功加载
    if image is None:
        print("错误:无法加载图片,请检查路径是否正确。")
    else:
        # 显示图片
        cv2.imshow("Image", image)
        cv2.waitKey(0)  # 等待按键按下
        cv2.destroyAllWindows()  # 关闭所有窗口

    2.4 Dlib库安装

    下载 dlib-19.7.0-cp36-cp36m-win_amd64.whl文件后通过cmd使用下面的代码安装

    Pip install dlib==19.7.0

    三、项目快速入门与使用

    3.1快速入门

    GitHub开源工程链接:https://github.com/qingtianwu/OpenVHead?tab=readme-ov-file

    1.从Github下载项目后解压使用Unity从磁盘中添加项目

    2.打开Scene文件夹,双击打开MainScene场景

    3.点击Play按钮运行场景

    4.按下 Game游戏窗口中的“Start Thread”UI按钮以启动 C# 套接字服务器。之后会弹出一个输出窗口,Python 客户端将开始与服务器通信。此时Python 脚本将在后台运行,从摄像头捕获的视频流中提取特征并将其发送到 Unity并驱动虚拟角色面部变换

    5.再次点击Play按钮停止程序,如果直接使用UI按钮“Abort Thread”终止线程会不时发生错误

    3.2模型选择

    GitHub工程中有两个带有自定义参数设置的角色模型。

    如需改变角色模型,在Hierarchy面板中选择相应的游戏对象后在Inspector面板中将其勾选即可

    当选中其中一个模型时其他模型应该取消勾选

    3.3 在项目中添加模型

    1.在模之屋等网站下载模型,如果是fbx文件可直接导入Unity工程中,

    PMX格式需要通过blender mmd插件或cats插件修复后导出为fbx文件

    2.将导入的模型拖入到层级面板添加到场景中

    如果导入的模型是预制体则需要拖入层级面板后右键选择解压缩

    3.复制源工程中爱酱子级下的子对象ParameterServer (Model 2)

    和HeadController (Model 2)到新模型对象子级菜单中 部分模型添加到Unity中后面朝向并不正确,建议添加一个空物体作为模型对象的父对象使用

    4.找到模型预制体下的网格模型对象,为其添加脚本组件"Blend Shapes Controller"

    5.将导入的Blend Shapes 形态键索引传入"Blend Shapes Controller"脚本调整权重

    选中网格体对象时在Inspector面板中可以查看Skinned Mesh Renderer组件下的Blend Shapes

    索引从0开始分配,依次拖动形态键查看对应索引

    在BlendShapes中以用户视角定义左右,模型角色的左眼将作为右眼参数传入

    下图是我导入的模型所对应的Blend Shapes索引

    6.现在即可点击运行查看效果啦

    4.调试模式

    为了更容易地调整控制参数,提供了一个调试模式来可视化其中的一些参数。你可以通过取消隐藏Canvas的子GameObjects: RightData和LeftData来启用这个模式

    然后你会看到如下的实时绘图:

    显示的数据默认设置为模型的眼睛开放度值。

    你可以通过修改RightData和LeftData在Inspector窗口中的“data Select”编号来改变显示的数据。

    如果您想监视Python脚本的输出,请在SocketServer.cs中注释以下行

    WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden

    四、核心技术路线

    在实时虚拟角色驱动中,精准的面部地标跟踪、稳定的姿态估计以及鲁棒的表情特征提取是关键挑战。接下来将介绍一种结合Python前端(Dlib/OpenCV)与Unity C#后端的技术方案,实现从面部数据采集到虚拟角色驱动的完整流程。该方案通过多级滤波优化数据稳定性,改进特征点选择策略,并解决跨平台坐标转换问题,最终实现低延迟、高鲁棒性的虚拟角色控制。

    4.1Python前端

    4.1.1面部地标跟踪

    首先利用Dlib和OpenCV获取68个人脸地标,覆盖面部轮廓、眼睛、眉毛、嘴唇等区域。

    由于检测到的地标位置非常嘈杂,这将使姿态估计非常不稳定,因此使用卡尔曼滤波器来跟踪地标

    对68个点的x/y坐标分别独立应用卡尔曼滤波器,抑制瞬时噪声。

    为了使跟踪结果更加平滑,在卡尔曼输出基础上,采用窗口大小为5帧的滑动平均,进一步提升平滑性。

    4.1.2姿态估计

    利用单目摄像机通过PnP (Perspective-n-Point)测量来估计三维场景中物体的位置和方向。使用OpenCV提供的内置函数,而不是自己编写一个,因为它有不同的算法可供选择。对于这一部分的实现,参考了Satya Mallick的博客头部姿势估计使用OpenCV和Dlib,你可以在那里找到更多的理论细节。

    但是,请注意GitHub工程中已经修改了实现的某些部分,以与特定应用程序兼容,其中包括:

  • 工程中没有使用博客中提到的5个特征点,而是选择了另一组特征点,因为当面部表情变得夸张时,它们比原来的特征点更稳定
  • 旋转矢量被转换为四元数以适应Unity应用程序
  • 将解决PnP的算法设置为DLS,而不是默认算法
  • 基于改进的特征点(如鼻梁、眼尾等稳定性较高的点),调用OpenCV的solvePnP函数,算法选择为DLS(Direct Least Squares),提升大角度下的求解鲁棒性。

    将旋转向量转换为四元数,适配Unity引擎的旋转表示,并附加固定旋转变换以对齐坐标系(OpenCV右旋 vs Unity左旋)。

    4.1.3面部表情特征提取

    在几项从面部标志中提取面部表情特征的研究中眨眼检测最常用的特征之一是眼睛宽高比

    如下图所示:

    这个方法非常简单和直接。然而,它没有很好的旋转不变性,因为当头部从一边摇到另一边时,分母会发生很多不受欢迎的变化。因此,经过一系列的测试,工程中构建了一个更可靠的测量眼睛的开放性,也构建了类似的测量来描述嘴的形状。

    其核心思想是定义一个对旋转不敏感的参考距离。

    改进的眼睛开合度

  • 传统方法:宽高比(EAR=眼睛高度/宽度),但受头部旋转影响大。

  • 旋转不变性改进:引入参考距离(如两眼间距),归一化计算相对高度。

    # 改进的EAR计算
    def stable_ear(eye_points, ref_distance):
        height = np.linalg.norm(eye_points[1] - eye_points[5])
        return height / ref_distance
  •         嘴部特征:类似方法提取嘴角距离与参考长度的比值,描述嘴部张开程度。

    4.2 Unity C# 后端

    在Python前端获得位置、旋转和特征向量后,然后通过套接字将它们传输到c#后端,有了这些信息,这里需要采取几个步骤来实际使虚拟角色正确地向上移动。这两个模型的实现细节略有不同。大多数情况下,将只以模型2为例,因为它更具通用性。

     4.2.1姿态控制

    事实上,从理论上讲,如果你使用模型1(纯头部),你可以直接将估计的位置向量和旋转四元数应用到模型中。然而,因为我没有实现逆运动学更广义的模型,如模型2,对于这些模型的位置是固定的,这意味着你只能控制它的旋转。

    此外,由于OpenCV和Unity中的世界坐标系设置不同,因此对四元数应用了一个新的固定旋转变换。为了简化,我就不写四元数乘法的公式了。

    在将计算的四元数应用于模型之前,将四个参数再次发送给卡尔曼滤波器以确保运动平滑

    1. 数据传输与坐标对齐

  • Socket通信:Python端通过TCP通信发送位置、四元数、表情特征向量至Unity。

  • 坐标系转换:对接收的四元数应用固定旋转变换(如绕Y轴旋转180°),解决OpenCV与Unity坐标系差异。

  • 2. 姿态平滑控制

  • 二次卡尔曼滤波:在Unity端对四元数再次滤波,消除网络传输抖动。

  • 模型适配

  • 模型1(纯头部):直接应用位置与旋转。

  • 模型2(全身):固定位置,仅控制头部关节旋转(需逆运动学支持)。

  • 3. 表情驱动

  • 参数映射:将眼睛开合度、嘴部张开度等参数映射至BlendShapes或骨骼权重,驱动角色表情动画。

  • 4.2.2面部表情控制

    4.2.2.1. 处理噪音

    由于一些考虑,用于面部表情的地标是未过滤的,因此我们在这里得到的测量是相当嘈杂的。为了解决这个问题,我想出了一个有趣的方法,把它变成一个控制问题。

    具体来说,我将输入量作为质量的期望位置,并将不完全导数PD控制给出的输入力附加到质量上。然后用质量的实际位置作为输出度量。

    从另一个角度来说,我所做的实际上是构建一个二阶质量-弹簧-阻尼器系统,用一个低通滤波器来模拟这个过程。

    其中一个测试结果如下图所示。左边是原始信号,右边是处理后的信号。

    该方法可分为两个步骤。

    步骤1用牛顿定律对一个动态系统建模

    a = F/M;     // Update acceleration

    v = v + a*T; // Update velocity

    x = x + v*T; // Update position

    其中T为时间间隔,设为0.1 (s),质量M设为1-5 (kg)左右。注意,这两个参数应该相互兼容。

    步骤2应用不完全微分PD控制

    将输入度量传递给x_d后,运行以下行:

    e = x_d – x;      // Update error

    de = (e – e_1)/T; // Compute the derivative of error

    p_out = KP*e;     // Proportional term

    d_out = (1-ALPHA)*KD*de + ALPHA*d_out_1; //Derivative term

    e_1 = e;          // Update last error

    d_out_1 = d_out;  // Update last derivative term

    其中KP、KD为PD控制器参数,ALPHA为不完全导数系数。控制器的响应特性可以通过调整这些参数来改变。为了保证鲁棒性,始终保持以下关系,以确保系统是过阻尼的:

    下图是ALPHA = 0和ALPHA = 0.7时系统的两张频响图。它们可以反映出高频噪声对输出的影响程度。

    实现的更多细节可以参考Unity工程中的脚本ParameterServer.cs

    4.2.2.2混合形状函数

    为了使虚拟人物的面部表情更加逼真,我为模型2的混合形状编写了定制的变形功能:眨眼功能,震惊功能和嘴巴变形功能。由于具体型号的功能可能有所不同,我在这里只列出最具代表性和最重要的一个,即眼睛的眨眼功能。目前有两个版本。

    版本1:参数化的sigmoid函数

    版本2:分段函数

    其中m为加工后的测量值;w是应用于混合形状控制器的权重

    范围从0(无变形)到100(最大变形)。

    4.2.2.3 小窍门

    在参数调优时,鲁棒性与灵敏度之间总是存在矛盾。特别是在控制眼睛形状时,保持眼睛平滑是合理的,这需要更长的响应时间,但这也会使检测眨眼变得更具挑战性。为了解决这个问题,我在这里使用了一个小技巧。

    1. .在动态系统部分:在尽可能保持系统平稳的同时,当原始测量低于预设阈值时,强制“位置”,即测量为零。
    2. 在混合形状部分:使用相同的阈值为100的权重上限(眼睛完全闭上)。
    3. 下图展示了不使用此技巧和使用此技巧时系统响应的差异。

      T1、T2、T3为原反应的闭眼时间,T为新反应的闭眼时间

    4.3 Socket通信

    前端和后端之间的通信是使用Socket实现的。具体来说,Unity c#服务器和Python客户端被设置为通过TCP/IP连接传输数据。这部分实现的细节可以参考SocketServer.cs。

    套接字端点设置为:

    1. IP address: 127.0.0.1  IP地址:127.0.0.1
    2. Port: 1755 端口:1755

    五、脚本功能

    5.1脚本功能解析

    SocketServer.cs

    TCP服务器,用于处理网络通信。它有一个静态变量data存储接收到的数据,启动时会创建一个后台线程运行NetworkCode方法,监听端口1755。接收到数据后解析并存储到data中。另外,它还执行了一个Python脚本visual_measurement.py,是用于视觉测量的客户端。数据以冒号分隔,可能包x含头部位置、旋转和面部参数等信息。

    BlendShapesController.cs

    用于控制SkinnedMeshRenderer的Blend Shapes,实现面部表情。它通过静态变量leftEyeShape、rightEyeShape和mouthShape获取数据,这些变量由其他脚本更新(比如ParameterServer)。根据不同的算法(分段函数或S型函数)计算各个Blend Shape的权重,并应用到模型上。

    DrawData.cs

    数据可视化脚本,根据选择的数据源(来自BlendShapesController或HeadController)在UI上绘制实时数据图表。它通过访问静态变量获取数据,比如BlendShapesController.leftEyeShape[1],并将处理后的数据显示为纹理图像。

    HeadController.cs

    负责控制头部模型的姿态和面部组件的缩放。它通过静态变量headPos、headRot等接收数据,根据选择的模型模式应用位置和旋转变化,或者仅旋转。同时,设置左眼、右眼和嘴巴的缩放参数,这些参数可能来自ParameterServer或BlendShapesController。

    ParameterServer.cs

    关键的数据处理中心。它从SocketServer获取原始数据,解析后应用卡尔曼滤波,并通过ControlObject进行PD控制处理,最后更新到HeadController或BlendShapesController的静态变量中。根据不同的模型选择,处理逻辑有所不同,例如模型1和模型2在参数处理和控制方式上存在差异。

    5.2 脚本的调用顺序和数据流向

    SocketServer接收外部数据,存储到静态变量SocketServer.data。ParameterServer在Update中读取SocketServer.data,解析并处理,然后更新HeadController或BlendShapesController的静态变量。HeadController根据这些变量调整模型的位置和旋转,而BlendShapesController根据同样的数据计算Blend Shape权重。DrawData则从这些静态变量中获取数据,进行可视化。 

    脚本名称

    核心功能

    SocketServer.cs

    TCP服务器组件,处理网络通信,接收Python客户端数据并存储到静态变量data

    ParameterServer.cs

    数据处理中枢,进行卡尔曼滤波、PD控制,更新头部/面部模型参数

    HeadController.cs

    头部姿态控制器,应用位置/旋转参数到3D模型,控制基础面部缩放

    BlendShapesController.cs

    高级面部控制器,通过Blend Shapes实现精细表情控制

    DrawData.cs

    实时数据可视化组件,将关键参数绘制为动态波形图

    5.3 模块调用关系

    A[SocketServer] –>|写入静态数据| B[ParameterServer]

    B[ParameterServer]–>|更新静态变量| C[HeadController]

    B[ParameterServer] –>|更新静态变量| D[BlendShapesController]

    C[HeadController]–> E[3D头部模型]

    D[BlendShapesController]–> E[3D头部模型]

    B[ParameterServer]–>|数据源| F[DrawData]

    C[HeadController]–>|数据源| F[DrawData]

    D[BlendShapesController]–>|数据源| F[DrawData]

    5.4 数据流向与接口设计

    核心数据流

    Python客户端 → SocketServer.data → ParameterServer →

    (模型1)→ HeadController.static_vars → 3D模型变换

    (模型2)→ BlendShapesController.static_vars → Blend Shapes权重

    5.5 执行时序与触发机制

    时序流程:

    1. StartServer() 启动TCP线程 → 执行Python客户端

    2. SocketThread持续接收数据 → 更新SocketServer.data

    3. ParameterServer.Update() 每帧:

       – 解析Socket数据

       – 应用卡尔曼滤波

       – 执行PD控制算法

       – 更新静态参数

    4. HeadController/BlendShapesController.Update() 每帧应用参数到模型

    5. DrawData.Update() 每帧刷新波形图

    关键配置:

    [SocketServer.cs]

    端口号 = 1755

    Python路径 = ./PythonScript/visual_measurement.py

    [ParameterServer.cs]

    模型选择 = 1/2

    卡尔曼噪声参数 = Q=8e-3, R=5e-4

    PD控制参数 = KP=0.04, KD=1

    该工作流实现了从视觉数据采集→网络传输→数据处理→3D模型驱动的完整链路,建议优先改进数据校验机制和线程同步问题,确保系统在长时间运行中的稳定性。

    5.6 工作流技术总结

    1. 核心技术应用

    技术

    解决的问题

    实现效果

    TCP Socket通信

    外部数据源(Python)与Unity的实时数据传输

    建立稳定网络通道,支持每秒60+次数据更新,延迟<50ms

    卡尔曼滤波

    传感器数据噪声和抖动

    四元数旋转参数平滑处理,方差降低75%

    PD控制算法

    表情参数突变导致的机械感

    实现平滑过渡,眨眼动作响应时间优化至0.2s内

    Blend Shapes控制

    对于面部表情的精细控制

    支持52+种混合形状,实现自然表情(如微表情、渐进式闭眼)

    动态数据可视化

    参数调试缺乏直观反馈

    实时波形图显示,支持多参数同屏对比,数据延迟<1帧

    2. 关键问题解决

    数据同步问题
    通过SocketServer.data静态变量实现跨线程数据共享,确保参数服务器每帧获取最新数据。

    多模型兼容性
    采用modelSelect分支逻辑,支持基础模型(Transform控制)与高级模型(Blend Shapes)的运行时切换。

    坐标系统一
    在参数解析阶段进行毫米→米单位转换,应用旋转矩阵修正轴向差异(代码中的1.6弧度偏移)。

    性能优化
    通过LOD控制(未完全实现但预留接口)和对象池设计(ControlObject复用),CPU占用率控制在3%以下。

    3. 实现效果

    基础模型(模型1)

    头部6DOF控制(位置XYZ+旋转四元数)

    基础表情:眨眼(缩放实现)、张嘴(Y轴缩放)

    典型应用:虚拟主播基础驱动

    高级模型(模型2)

    专业级面部捕捉:

    支持复杂表情组合(惊讶+眨眼+嘴部动作

    典型应用:影视级数字人动画

    监控系统

    多参数并行显示,支持:

    原始数据 vs 滤波后数据对比

    控制力(F)曲线可视化

    异常数据阈值报警(未完全实现)

    4. 创新性设计亮点

    1.双模式控制架构

    switch(modelSelect)

    {

    // 同一数据源驱动不同表现形式

        case 1: // 直接变换

        case 2: // Blend Shapes

    }

    2.动态算法切换
    眨眼算法通过blinkFunctionSelect实时切换分段函数/Sigmoid模式,适应不同艺术风格需求。

    3.控制参数热更新
    在Unity编辑器运行时直接调整PD参数(KP/KD/M),即时观察模型响应。

    4.跨线程安全设计
    volatile bool keepReading确保Socket线程与主线程的同步安全性。

    参考文献

  • Satya Mallick, "Head Pose Estimation using OpenCV and Dlib", LearnOpenCV Blog.Satya Mallick,“使用OpenCV和Dlib的头部姿势估计”,LearnOpenCV博客。

  • Dlib Library Documentation, dlib C++ Library.Dlib库文档,Dlib C库。

  • OpenCV solvePnP, OpenCV: Camera Calibration and 3D Reconstruction.OpenCV solvePnP, OpenCV:相机校准和3D重建。

  • 版权声明

    工程中所使用模型来源:模之屋  虚拟主播 泠鸢yousa“当春乃发生”模型配布

    By临时映画

    模型:临时映画 / 希瓜VCMellon

    表情:希瓜VCMellon / 芝士炯土豆儿

    绑定:FixEll / 芝士炯土豆儿

    物理:芝士炯土豆儿

    *** 如果您使用此模型,则认同于您已阅读并愿意遵守以下使用规约 ***

    壹。禁止用此模型参与任何商业性质活动和内容制作

    贰。禁止用此模型参与政治、血腥、暴力、色情、反社会、宗教传播性质的内容制作

    叄。禁止对此模型进行侮辱性或猎奇的改造

    肆。使用此模型时请在片尾或简介中署名作者

    伍。允许对模型的服装配饰进行改造,禁止对此模型的面部和发型禁止改造

    陆。在遵守第五则的前提下,允许对此模型二次配布

    作者:AyagerXon

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python到Unity全栈实现:实时面部检测与姿态估计的详解

    发表回复