奇异值分解(SVD)在3D骨骼旋转中的应用:原理与实现
引言
在做在三维计算机视觉的人物姿态识别的项目过程中,遇到一个问题,摄像头只有一个,只采集了一个正面的数据,但是在实际测试过程中,摄像头的位置可能是人物的其他角度,比如30度或者随机。为了做数据增强,将这些采集的数据3D骨骼数据,全部做每个角度旋转的数据增强。

一、 实现思路
在 3D 空间中,对骨骼数据进行旋转通常有两种场景:
- 已知目标角度:直接构造旋转矩阵并相乘。
- 已知目标姿态(但不知具体角度):通过 SVD 求解最优旋转矩阵(Kabsch 算法)。
该项目符合场景 1 的设定,但使用场景 2 的算法流程。具体思路如下:
- 定义数据 :构建一个 N×3N \times 3N×3 的矩阵存储 3D 骨骼关节坐标。
- 中心化:将骨骼中心点平移至原点,消除平移分量,仅保留旋转信息。
- 构造目标 :根据指定角度(如 30°)构造真实的旋转矩阵 RtrueR_{true}Rtrue,生成旋转后的目标数据 YYY。
- SVD 求解 :计算原始数据与目标数据的交叉协方差矩阵 H=XTYH = X^T YH=XTY,对其进行 SVD 分解,按公式 R=VUTR = V U^TR=VUT 提取纯旋转矩阵。
- 应用旋转 :将求得的旋转矩阵 RRR 乘回原始数据,完成旋转对齐。
二、 代码实现
以下代码仅定义了一个变量 skeleton 作为 3D 骨骼数据,其余均为过程变量。
python
import numpy as np
==
1. 定义 3D 骨骼数据 (6个关节, xyz坐标)
==
skeleton = np.array([
[0, 0, 0], # 髋部
[0, 10, 0], # 脊柱
[5, 10, 0], # 右肩
[-5, 10, 0], # 左肩
[5, 20, 0], # 右肘
[-5, 20, 0] # 左肘
], dtype=float)
==
2. 中心化(消除平移,聚焦旋转)
==
centroid = skeleton.mean(axis=0)
skeleton_centered = skeleton - centroid
==
3. 设定目标角度并构造真实旋转矩阵
==
angle = np.radians(30) # 转为弧度
绕 Y 轴旋转的矩阵(右手系顺时针)
R_true = np.array([
[np.cos(angle), 0, np.sin(angle)],
[0, 1, 0],
[-np.sin(angle), 0, np.cos(angle)]
])
生成旋转后的目标数据(模拟已知目标姿态)
skeleton_target = skeleton_centered @ R_true.T
==
4. 利用 SVD 求解旋转矩阵 (Kabsch 算法)
==
4.1 计算交叉协方差矩阵 H
H = skeleton_centered.T @ skeleton_target
4.2 对 H 进行 SVD 分解
U, S, Vt = np.linalg.svd(H)
V = Vt.T
4.3 组合出纯旋转矩阵 R_svd
R_svd = V @ U.T
4.4 确保是纯旋转(行列式为 +1,避免反射)
if np.linalg.det(R_svd) < 0:
V[:, -1] *= -1
R_svd = V @ U.T
==
5. 应用 SVD 求得的旋转矩阵
==
skeleton_rotated = skeleton_centered @ R_svd.T + centroid
验证结果
print("SVD 求得的旋转矩阵是否与真实矩阵一致:", np.allclose(R_svd, R_true))
三、 原理解析
Q1: 代码中的 @ 运算符是什么意思?
@ 是 Python 中用于矩阵乘法的运算符。
- 对于 NumPy 数组,
A @ B等价于np.dot(A, B)。 - 它执行的是线性代数中的矩阵乘法(行乘列求和),而非逐元素相乘(
*运算符)。 - 在代码中,
skeleton_centered.T @ skeleton_target执行了 (3×N)×(N×3)(3 \times N) \times (N \times 3)(3×N)×(N×3) 的乘法,得到一个 3×33 \times 33×3 的矩阵。
Q2: np.linalg.svd 背后的数学原理是什么?
np.linalg.svd(H) 对矩阵 HHH 进行奇异值分解,将其拆解为三个矩阵的乘积:
H=UΣVT H = U \Sigma V^T H=UΣVT
- UUU:左奇异向量矩阵(m×mm \times mm×m,正交矩阵)。
- Σ\SigmaΣ:奇异值矩阵(m×nm \times nm×n,对角线为非负实数,按降序排列)。
- VTV^TVT:右奇异向量矩阵的转置(n×nn \times nn×n,正交矩阵)。
数值计算原理 :SVD 通常通过求解 HTHH^T HHTH 和 HHTH H^THHT 的特征值与特征向量来间接获得。对 HTHH^T HHTH 做特征分解,其特征向量构成 VVV,特征值的平方根构成 Σ\SigmaΣ;对 HHTH H^THHT 做特征分解,其特征向量构成 UUU。
Q3: 为什么在进行奇异值分解之前要计算协方差矩阵 HHH?
计算 H=XTYH = X^T YH=XTY 是为了将"寻找最优旋转"这一几何问题转化为"最大化矩阵迹"的代数问题。
- 目标函数 :我们希望找到旋转矩阵 RRR 使得 ∥XR−Y∥F2\| X R - Y \|_F^2∥XR−Y∥F2 最小。
- 化简 :展开后可得,最小化该误差等价于最大化 tr(RT(XTY))\text{tr}(R^T (X^T Y))tr(RT(XTY)),即 tr(RTH)\text{tr}(R^T H)tr(RTH)。
- SVD 的作用 :数学上已证明,对于正交矩阵 RRR,使得 tr(RTH)\text{tr}(R^T H)tr(RTH) 最大化的解是 R=VUTR = V U^TR=VUT(其中 H=UΣVTH = U \Sigma V^TH=UΣVT)。因此,计算 HHH 是为了给 SVD 提供一个能够解码出最优旋转的"关系矩阵"。
Q4: 为什么协方差矩阵是 H = skeleton_centered.T @ skeleton_target 这样计算的?
这涉及矩阵乘法的维度与内积的几何意义。
- 维度匹配 :
skeleton_centered是 N×3N \times 3N×3,转置后为 3×N3 \times N3×N。与目标 N×3N \times 3N×3 相乘,得到 3×33 \times 33×3 的矩阵 HHH。 - 元素含义 :HHH 的第 iii 行第 jjj 列元素为 Hij=∑k=1NXkiYkjH_{ij} = \sum_{k=1}^N X_{ki} Y_{kj}Hij=∑k=1NXkiYkj。这表示源点集的第 iii 个坐标轴与目标点集的第 jjj 个坐标轴之间的内积。
- 协方差本质 :在中心化后,内积正比于两个向量之间的协方差。HHH 打包了原始坐标系与目标坐标系之间所有轴的对应关系,描述了从一个空间到另一个空间的线性变换强度与方向。
Q5: 为什么需要检查 np.linalg.det(R_svd) < 0 并修正?
SVD 分解得到的 UUU 和 VVV 是正交矩阵,因此 R=VUTR = V U^TR=VUT 也是正交矩阵,但它可能包含反射(即行列式为 −1-1−1)。
- 纯旋转要求 :在三维空间中,纯旋转矩阵的行列式必须为 +1+1+1。
- 修正方法 :若 det(VUT)<0\det(V U^T) < 0det(VUT)<0,则将 VVV 的最后一列乘以 −1-1−1(等价于在 Σ\SigmaΣ 中引入一个负号),重新计算 R=VUTR = V U^TR=VUT,此时行列式变为 +1+1+1,确保得到的是纯旋转而非反射变换。