视频转3D骨骼动画的核心思路并不复杂:从普通单目视频中提取人体关键点,把这些二维点提升为三维坐标,再映射到目标骨骼上并导出动画数据。真正影响效果的是每个环节里的细节,比如关键点抖动、深度估计误差、不同骨骼长度比例不一致导致的穿模。下面会按照数据流顺序把流程拆开,从姿态估计选型到最终导出BVH和FBX,给出可落地的处理方式。

一、选择合适的姿态估计方案
目前适合视频转骨骼动画的姿态估计方案主要分两类。一类是轻量级实时方案,以MediaPipe Pose和Holistic为代表,优点是在普通CPU或低端GPU上就能跑,API稳定,3D关键点可以直接拿到。另一类是精度更高的学术模型,例如ROMP、HybrIK、MotionBERT,它们通常基于SMPL人体参数模型,输出带形状参数的网格和关节旋转,适合对动作质量要求高的项目。
如果只是快速验证流程,建议先用MediaPipe。它返回33个3D关键点,其中x、y、z是经过归一化的世界坐标,原点位于人体中心,单位与图像尺寸相关,因此后续需要乘以参考尺度。ROMP这类模型则直接输出SMPL的关节点和旋转,适合导出到Blender后使用官方插件或脚本重定向。选择时主要看目标引擎和模型格式:Unity和Unreal对BVH支持较好,Blender对SMPL和FBX更友好。
下面是一个使用MediaPipe提取单帧3D关键点的示例,注意坐标在关键点对象中为归一化值,实际使用时需要结合图像宽高进行换算。
import cv2
import mediapipe as mp
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(static_image_mode=True, model_complexity=2)
image = cv2.imread('input_frame.jpg')
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = pose.process(image_rgb)
if results.pose_landmarks:
h, w = image.shape[:2]
for idx, lm in enumerate(results.pose_landmarks.landmark):
if lm.visibility < 0.5:
continue
x = lm.x * w
y = lm.y * h
z = lm.z * w # z与x同尺度,用来保持比例
print(idx, x, y, z)
二、把2D关键点提升为3D骨骼数据
直接从单张图像预测三维坐标属于病态问题,因为相机没有深度信息。MediaPipe的做法是先检测2D关键点,再通过回归模型输出带深度估计的3D坐标。这种方式在正对镜头时表现稳定,但转身、快速遮挡、自遮挡较多时,肩膀和手腕的深度会明显漂移。要减少这类误差,可以结合多视角视频、平滑滤波,或者使用时序模型如MotionBERT来利用前后帧约束。
另一个常见问题是坐标空间混乱。MediaPipe的world_landmarks以人体中心为原点,Blender和Unity使用的是世界坐标或局部骨骼坐标。直接把这些点当局部坐标用,会导致角色比例异常。正确做法是先记录TPose或第一帧作为参考姿态,计算各骨骼的参考长度,再对每一帧做比例映射。比如左臂在参考帧中的长度为0.35个单位,目标模型左臂长度为0.42个单位,就把所有左臂相关关键点乘上1.2的缩放系数。
要在此基础上生成骨骼动画,不能只保存全局坐标,而是要计算父子关节之间的旋转量。以肘关节为例,先算出上臂向量和下臂向量,再用四元数表示两个向量之间的旋转。这样得到的是局部旋转,可以直接写入BVH或FBX,不受角色位移影响。代码思路如下:
import numpy as np
def vector_to_quaternion(src, dst):
src = src / np.linalg.norm(src)
dst = dst / np.linalg.norm(dst)
cross = np.cross(src, dst)
dot = np.dot(src, dst)
w = np.sqrt((1 + dot) * 2)
if w < 1e-8:
return np.array([0, 0, 0, 1])
return np.array([cross[0] / w, cross[1] / w, cross[2] / w, w / 2])
# shoulder, elbow, wrist为同一帧中的三个3D点
upper_arm = elbow - shoulder
forearm = wrist - elbow
rotation = vector_to_quaternion(upper_arm, forearm)
print(rotation)
三、骨骼重定向与动画数据导出
重定向是把源骨骼动作迁移到目标模型上的关键步骤。源骨骼通常来自姿态估计器的标准人体拓扑,目标骨骼可能是游戏角色的自定义骨架。两者关节数量、朝向、父级关系都不相同。直接按序号复制位置会得到非常奇怪的动作,必须先建立关节映射,比如把MediaPipe的11号肩部、13号肘部、15号手腕对应到目标角色的Shoulder_L、Elbow_L、Wrist_L。
建立映射后,大多数引擎使用的人形骨骼都遵循TPose或APose作为绑定姿势。需要在目标模型加载时读取每个关节的绑定矩阵和父级关系,再把源关节的旋转转换到目标关节所在坐标系。一个简单的做法是:先在TPose下计算源骨骼每个关节相对父级的旋转,然后计算目标骨骼相同关节相对父级的绑定旋转,最后对每帧执行旋转差值迁移。用四元数插值可以避免欧拉角万向锁问题。
导出格式方面,BVH适合Unity、MotionBuilder和轻量级预览,但只保存骨骼层级和旋转,不包含网格。FBX则能同时携带网格、材质和动画,更适合Blender和Unreal。如果使用Blender,可以借助Python脚本读取关键点数据并创建动作,或者使用社区工具把BVH导入后重定向。下面是一个最小BVH文件头示例,展示如何定义三关节骨骼和一段动画帧数据:
def write_bvh_header():
header = (
'HIERARCHY\n'
'ROOT Hips\n'
'{\n'
' OFFSET 0.0 0.0 0.0\n'
' CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n'
' JOINT Chest\n'
' {\n'
' OFFSET 0.0 12.0 0.0\n'
' CHANNELS 3 Zrotation Xrotation Yrotation\n'
' End Site\n'
' {\n'
' OFFSET 0.0 24.0 0.0\n'
' }\n'
' }\n'
'}\n'
)
return header
print(write_bvh_header())
四、处理抖动、滑步与导出前校验
从单目视频得到的动画往往存在高频抖动,表现为手部小范围震颤、脊柱不正常晃动。不能直接把原始关键点写进动画文件。通常需要在旋转域或位置域做平滑,推荐使用One Euro滤波或Savitzky-Golay滤波。One Euro滤波对实时输入友好,能根据帧间隔动态调整截止频率;Savitzky-Golay适合离线批量处理,能较好保留动作拐点。下面示例对6通道旋转做Savitzky-Golay平滑:
from scipy.signal import savgol_filter
import numpy as np
# rotations形状为(帧数, 6),每行是x,y,z位置和z,x,y旋转
rotations = np.load('raw_rotations.npy')
smoothed = savgol_filter(rotations, window_length=9, polyorder=2, axis=0)
# 脚部接触约束:当脚部高度低于阈值时,将水平位移置零
contact_mask = rotations[:, 1] < 3.0
smoothed[contact_mask, 0] = 0
smoothed[contact_mask, 2] = 0
脚部滑步是视频动捕最常见的穿帮。原因是脚部关键点在低对比度或遮挡时横向漂移,而模型没有地面接触约束。可以通过检测脚部最低点,当关节高度低于阈值时认为该脚着地,在该时间段内冻结水平位移,并把垂直高度稳定在地面高度。对于走路和跑步,还可以结合步态周期检测,只对支撑相做约束。
导出前还要做一次比例校验:把目标模型导入Blender或Unity,播放一段包含TPose、转身和手臂交叉的动作,检查肘部是否过度弯曲、膝盖是否反向、脚底是否插入地面。如果发现整体骨骼缩放不对,不要直接改模型,而应在重定向阶段调整源骨骼到目标骨骼的比例因子。这样能保持后续动画数据的一致性。