2D numpy的蒙皮算法

自己制作一个rest_post  是等边三角形 poses 为一个直角三角形和一个钝角三角型

可以直接运行观察效果

import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import lsq_linear
from scipy.cluster.vq import vq, kmeans, whiten
import time
#点云配准算法
import numpy as np
def kabsch_2d(P, Q):
    """
    二维点云配准算法,使用Kabsch算法。
    inputs: P  N x 2 numpy矩阵,表示点集P中的二维坐标点
            Q  N x 2 numpy矩阵,表示点集Q中的二维坐标点
    return: A 3 x 2矩阵,前两行为旋转矩阵,最后一行为平移向量
    """
    if (P.size == 0 or Q.size == 0):
        raise ValueError("Empty matrices sent to kabsch")
    centroid_P = np.mean(P, axis=0)
    centroid_Q = np.mean(Q, axis=0)
    P_centered = P - centroid_P
    Q_centered = Q - centroid_Q
    H = P_centered.T.dot(Q_centered)
    U, S, V = np.linalg.svd(H)
    R = U.dot(V).T
    if np.linalg.det(R) < 0:
        V[1, :] *= -1
        R = U.dot(V).T
    t = centroid_Q - R.dot(centroid_P)
    return np.vstack((R, t))
def initialize_2d(poses, rest_pose, num_bones, iterations=5):
    """
    Uses the k-means algorithm to initialize bone transformations.

    inputs: poses       |num_poses| x |num_verts| x 2 matrix representing coordinates of vertices of each pose
            rest_pose   |num_verts| x 2 numpy matrix representing the coordinates of vertices in rest pose
            num_bones   Number of bones to initialize
            iterations  Number of iterations to run the k-means algorithm

    return: A |num_bones| x |num_poses| x 3 x 2 matrix representing the stacked Rotation and Translation
              for each pose, for each bone.
            A |num_bones| x 2 matrix representing the translations of the rest bones.
    """
    # rest_pose:建模姿态
    num_verts = rest_pose.shape[0]
    num_poses = poses.shape[0]
    # 每个骨骼每一帧的trans
    # bone_transforms: (num_bones, num_poses, 3, 2)
    bone_transforms = np.empty((num_bones, num_poses, 3, 2))   # [(R, T) for for each pose] for each bone
                                                               # 3rd dim has 3 rows for R and 1 row for T
    rest_bones_t = np.empty((num_bones, 2))                    # Translations for bones at rest pose
    rest_pose_corrected = np.empty((num_bones, num_verts, 2))  # Rest pose - mean of vertices attached to each bone

    # Use k-means to assign bones to vertices
    # 白化:rest_pose / 每一列的标准差,即x,y,z三个轴的标准差,这个只是为了kmeans用的
    whitened = whiten(rest_pose)
    # 直接将白化后的rest pose进行kmeans分组,分组的数量就是骨骼的数量
    # python用了两个函数来完成这一步
    # kmeans返回的是中心点
    codebook, _ = kmeans(whitened, num_bones)#聚类中心(初始骨骼点)
    # 将白化的初始顶点与中心点绑定,就完成了分组
    # vert_assignments: (num_verts,)
    vert_assignments, _ = vq(whitened, codebook) # 每个顶点对应骨骼点的索引[0,1,5,6,8,........]
    # 遍历所有骨骼
    # Compute initial random bone transformations
    for bone in range(num_bones):
        #  计算中心点,每个vert都有一个骨骼的assignment,每个骨骼的所有顶点作为一个cluster,来计算中心
        # rest pose下,当前bone对应cluster的中心
        # rest_bones_t:(num_bones, 2)
        rest_bones_t[bone] = np.mean(rest_pose[vert_assignments == bone], axis=0)
        # 这个corrected就是各个顶点相对于所属cluster的中心点的偏移
        # rest_pose: (num_verts, 3)
        # rest_pose_corrected: (num_bones, num_verts, 3)
        # 将rest pose中的各个顶点减去中心点,这里需要注意的就是,这里是计算的所有的顶点
        # 也就是说,这里是把全身所有的顶点相对于一根骨骼的cluster的中心的位置
        rest_pose_corrected[bone] = rest_pose - rest_bones_t[bone]
        #rest_pose_corrected[bone] = rest_pose - np.mean(rest_pose[vert_assignments == bone], axis=0)

        # 现在我们有了rest pose接着遍历每个pose
        for pose in range(num_poses):
            # 传入的相对于中心点的rest pose的坐标,以及当前这一帧pose的顶点数据
            # 对于一根骨骼,可能会有多个顶点绑定到这根骨骼上,因此对于一根骨骼而言,它对应了一个点集
            # 因此kabsch算法的目标,就是尝试去旋转和平移这根骨骼,使得这两个点集的差异最小
            # 计算该骨骼在当前这一帧下的transform
            # rest_pose_corrected里面是所有的顶点,但这里只取了属于这个骨骼的顶点,用kabsch算法来计算当前这根骨骼的transform
            bone_transforms[bone, pose] = kabsch_2d(rest_pose_corrected[bone, vert_assignments == bone], poses[pose, vert_assignments == bone])

    for it in range(iterations):
        # Re-assign bones to vertices using smallest reconstruction error from all poses
        # 这里重新构造了每个骨骼,每一帧,所有顶点的空间
        # constructed: (num_bones, num_poses, num_verts, 3)
        constructed = np.empty((num_bones, num_poses, num_verts, 2)) # |num_bones| x |num_poses| x |num_verts| x 3
        for bone in range(num_bones):
            # 遍历每根骨骼
            # 变换顶点到模型空间
            # 先旋转
            # 由于该矩阵是4行3列,这里取了矩阵前三行,即旋转矩阵。
            # 将rest pose的顶点位置设置到所属骨骼空间下,之后进行旋转
            # 顶点进行旋转之后,将顶点的yz两个轴对调了,变为右手系,上面构造bone_transforms的kabsch函数中也有变换手系的操作
            # 不过这里相当于rest pose下所有顶点在所有帧都进行了一次变换
            # bone_transforms: (num_bones, num_poses, 4, 3)
            Rp = bone_transforms[bone,:,:2,:].dot((rest_pose - rest_bones_t[bone]).T).transpose((0, 2, 1)) # |num_poses| x |num_verts| x 3
            # 再平移
            # R * p + T
            constructed[bone] = Rp + bone_transforms[bone, :, np.newaxis, 2, :]
        # 变换后的顶点位置与原始pose做比较
        # 计算2范数,即平方和再开方
        # 后面的axis=(1,3),1对应的是 poses, 3对应的是位置,也就是说,是针对这所有帧的顶点位置差异计算2范数
        # 即遍历num_bones和num_verts,所有元素平方求和再开根号
        # constructed: (num_bones, num_poses, num_verts, 3)
        # errs: (num_bones, num_verts)
        errs = np.linalg.norm(constructed - poses, axis=(1, 3))
        # print(errs.sum())
        # 上面我们计算了errors,其结构是(num_bones, num_verts),是每根骨骼,每个顶点(与是否隶属于该骨骼无关),在整个动画序列中的error
        # 对于每一根骨骼,按列,即寻找最小的顶点索引,即每个顶点的位置上保存了error最小的骨骼的索引。于是我们有了一个新的映射
        # (num_verts),
        vert_assignments = np.argmin(errs, axis=0)

        ## Visualization of vertex assignments for bone 0 over iterations
        ## Make 5 copies of an example pose mesh and call them test0, test1...
        #for i in range(num_verts):
        #    if vert_assignments[i] == 0:
        #        pm.select('test{0}.vtx[{1}]'.format(it, i), add=True)
        #print(vert_assignments)

        # For each bone, for each pose, compute new transform using kabsch
        for bone in range(num_bones):
            # 根据新的骨骼映射,重新计算cluster中心点
            rest_bones_t[bone] = np.mean(rest_pose[vert_assignments == bone], axis=0)
            # 计算所有顶点相对于每根骨骼的位置
            rest_pose_corrected[bone] = rest_pose - np.mean(rest_pose[vert_assignments == bone], axis=0)
            # 重新使用kabsch算法,来计算骨骼变换
            for pose in range(num_poses):
                # 这里依然是根据vert_assignments,以某根骨骼下的所有顶点为依据,计算该骨骼的transform
                bone_transforms[bone, pose] = kabsch_2d(rest_pose_corrected[bone, vert_assignments == bone], poses[pose, vert_assignments == bone])
    # 最后返回的是骨骼的动画数据,以及骨骼在rest pose下的位置
    return bone_transforms, rest_bones_t


def update_weight_map_2d(bone_transforms, rest_bones_t, poses, rest_pose, sparseness):
    """
    Update the bone-vertex weight map W by fixing bone transformations and using a least squares
    solver subject to non-negativity constraint, affinity constraint, and sparseness constraint.

    inputs: bone_transforms |num_bones| x |num_poses| x 4 x 3 matrix representing the stacked
                                Rotation and Translation for each pose, for each bone.
            rest_bones_t    |num_bones| x 3 matrix representing the translations of the rest bones
            poses           |num_poses| x |num_verts| x 3 matrix representing coordinates of vertices of each pose
            rest_pose       |num_verts| x 3 numpy matrix representing the coordinates of vertices in rest pose
            sparseness      Maximum number of bones allowed to influence a particular vertex

    return: A |num_verts| x |num_bones| weight map representing the influence of the jth bone on the ith vertex
    """
    num_verts = rest_pose.shape[0]
    num_poses = poses.shape[0]
    num_bones = bone_transforms.shape[0]

    W = np.empty((num_verts, num_bones))

    for v in range(num_verts):
        # 遍历所有顶点
        # For every vertex, solve a least squares problem
        Rp = np.empty((num_bones, num_poses, 2))
        # 变换当前这个顶点,用所有骨骼去变换,得到变换后的顶点
        for bone in range(num_bones):
            Rp[bone] = bone_transforms[bone,:,:2,:].dot(rest_pose[v] - rest_bones_t[bone]) # |num_bones| x |num_poses| x 3
        # R * p + T
        Rp_T = Rp + bone_transforms[:, :, 2, :] # |num_bones| x |num_poses| x 3
        # 这个就是变换后的顶点位置,这里相当于把所有pose的每个xyz都按行展开了
        # (num_bones, nun_poses, 3)->(num_poses, 3, num_bones) -> (3 * num_poses, num_bones)
        A = Rp_T.transpose((1, 2, 0)).reshape((2 * num_poses, num_bones)) # 3 * |num_poses| x |num_bones|
        # 这个是真正的顶点位置
        b = poses[:, v, :].reshape(2 * num_poses) # 3 * |num_poses| x 1
        # 计算x在什么时候,Ax - b的差异最小,用的是mse。这里用带bound的线性回归去做,因为所有权重的值只能在[0],1]之间
        # 最终得到了每根骨骼的权重
        # Bounds ensure non-negativity constraint and kind of affinity constraint
        w = lsq_linear(A, b, bounds=(0, 1), method='bvls').x  # |num_bones| x 1
        # 骨骼权重归一化
        w /= np.sum(w) # Ensure that w sums to 1 (affinity constraint)

        # 由于每一个顶点有一个最大权重的数量限制|K|,因此我们要抛弃其他的
        # Remove |B| - |K| bone weights with the least "effect"
        # 在这里重新reshape一下A*w,得到了拟合后的顶点
        # (3 * num_poses, num_bones) -> (num_poses, 3, num_bones)
        # 然后根据列1,求二范数,即遍历num_poses和num_bones,所有元素平方求和开根号,其实就是顶点位置的2范数
        effect = np.linalg.norm((A * w).reshape(num_poses, 2, num_bones), axis=1) # |num_poses| x |num_bones|
        # 然后又把num_poses给合并了,这里其实干的事情,就是去求当前顶点受到不同骨骼影响的程度,我们就可以知道应该干掉哪些骨骼了
        effect = np.sum(effect, axis=0) # |num_bones| x 1
        # 因为一个顶点最多只能被|k|根骨骼所影响,因此这里计算要被抛弃的骨骼数量
        num_discarded = max(num_bones - sparseness, 0)
        # np.argpartition是将数组内容分组,比num_discarded小的放前面,比它大的放后面,
        # 因此这里实际上就是抛弃掉最小的num_discarded个元素
        # 这里的effective保存了每个顶点所选取的骨骼的索引:数量为:|sparseness|
        effective = np.argpartition(effect, num_discarded)[num_discarded:] # |sparseness| x 1

        # 到现在位置,我们知道了每个顶点到底要绑定哪些骨骼
        # 只使用我们选取的那几根骨骼,重新线性回归来算权重
        # Run least squares again, but only use the most effective bones
        A_reduced = A[:, effective] # 3 * |num_poses| x |sparseness|
        w_reduced = lsq_linear(A_reduced, b, bounds=(0, 1), method='bvls').x # |sparseness| x 1
        w_reduced /= np.sum(w_reduced) # Ensure that w sums to 1 (affinity constraint)

        # 最终,得到了当前顶点的权重稀疏矩阵
        w_sparse = np.zeros(num_bones)
        # 我们选取的那几根骨骼,设置为重新算好的权重
        w_sparse[effective] = w_reduced
        w_sparse /= np.sum(w_sparse) # Ensure that w_sparse sums to 1 (affinity constraint)

        W[v] = w_sparse
    # 返回所有顶点的权重赋值关系
    # (num_verts, num_bones)
    return W
def update_bone_transforms_2d(W, bone_transforms, rest_bones_t, poses, rest_pose):
    """
    Updates the bone transformations by fixing the bone-vertex weight map and minimizing an
    objective function individually for each pose and each bone.

    inputs: W               |num_verts| x |num_bones| matrix: bone-vertex weight map. Rows sum to 1, sparse.
            bone_transforms |num_bones| x |num_poses| x 4 x 3 matrix representing the stacked
                                Rotation and Translation for each pose, for each bone.
            rest_bones_t    |num_bones| x 3 matrix representing the translations of the rest bones
            poses           |num_poses| x |num_verts| x 3 matrix representing coordinates of vertices of each pose
            rest_pose       |num_verts| x 3 numpy matrix representing the coordinates of vertices in rest pose

    return: |num_bones| x |num_poses| x 4 x 3 matrix representing the stacked
                                Rotation and Translation for each pose, for each bone.
    """
    num_bones = W.shape[1]
    num_poses = poses.shape[0]
    num_verts = W.shape[0]
    # 遍历所有pose
    for pose in range(num_poses):
        # 遍历当前pose下的所有骨骼
        for bone in range(num_bones):
            # 计算rest pose下所有顶点相对于当前骨骼的偏移
            # Represents the points in rest pose without this rest bone's translation
            p_corrected = rest_pose - rest_bones_t[bone] # |num_verts| x 3

            # Calculate q_i for all vertices by equation (6)
            # (num_bones, num_verts, 3)
            constructed = np.empty((num_bones, num_verts, 2)) # |num_bones| x |num_verts| x 3

            for bone2 in range(num_bones):
                # 遍历所有骨骼
                #  首先计算rest pose以bone2的
                # 提取出当前骨骼的旋转矩阵,将rest pose相对于当前骨骼来算出local position,将其旋转
                # can't use p_corrected before because we want to correct for every bone2 distinctly
                # (num_verts, 3)
                Rp = bone_transforms[bone2,pose,:2,:].dot((rest_pose - rest_bones_t[bone2]).T).T # |num_verts| x 3
                #  然后平移,这里就得到了每一帧动画,被每一根骨骼变换后的顶点位置
                # 由于有权重来表明了哪个顶点会被哪些骨骼所影响,所以对于任何一根骨骼,这里算全部顶点的
                # R * p + T
                constructed[bone2] = Rp + bone_transforms[bone2, pose, 2, :]
            # w * (R * p + T)
            # 这里把顶点乘以对应的权重,注意经过上一步,这里的权重大多数都是0了,只有重要的权重才有数值,数量最多为|K|
            # 因此这里得到的是每个顶点相对于其对应骨骼的加权位置,后面要求和,得到真正位置
            # (num_bones, num_verts, 3)->(num_verts, num_bones, 3)
            constructed = constructed.transpose((1, 0, 2)) * W[:, :, np.newaxis] # |num_verts| x |num_bones| x 3
            # 这里的操作,就是要将这些位置,干掉当前骨骼这一列,用于去掉当前骨骼的影响
            constructed = np.delete(constructed, bone, axis=1) # |num_verts| x |num_bones-1| x 3
            # 当前骨骼是我们关注的骨骼,因此q就是真正的顶点位置减去其他骨骼对该顶点的影响,得到的点云
            # 直观的理解,就是pose是真实的点云,而去掉当前骨骼的constructed points是一个估计后的点云,
            q = poses[pose] - np.sum(constructed, axis=1) # |num_verts| x 3

            # 而p是rest pose下的顶点相对于中心点的偏移的点云,这个中心点是一个加权平均,权重与该顶点的对于骨骼的w有关
            # p_star是rest pose下的中心点
            # q_star是当前动画的残差的中心点
            # Calculate p_star, q_star, p_bar, and q_bar for all verts by equation (8)
            p_star = np.sum(np.square(W[:, bone, np.newaxis]) * p_corrected, axis=0) # |num_verts| x 3 => 3 x 1
            p_star /= np.sum(np.square(W[:, bone])) # 3 x 1

            q_star = np.sum(W[:, bone, np.newaxis] * q, axis=0) # |num_verts| x 3 => 3 x 1
            q_star /= np.sum(np.square(W[:, bone])) # 3 x 1
            # 至此得到了两个点云,
            p_bar = p_corrected - p_star # |num_verts| x 3
            q_bar = q - W[:, bone, np.newaxis] * q_star # |num_verts| x 3
            # 下面进行点云配准,得到骨骼的旋转和平移
            # Perform SVD by equation (9)
            P = (p_bar * W[:, bone, np.newaxis]).T # 3 x |num_verts|
            Q = q_bar.T # 3 x |num_verts|
            # 点云配准得到旋转矩阵
            U, S, V = np.linalg.svd(np.matmul(P, Q.T))

            # Calculate rotation R and translation t by equation (10)
            R = U.dot(V).T # 3 x 3
            t = q_star - R.dot(p_star) # 3 x 1

            bone_transforms[bone, pose, :2, :] = R
            bone_transforms[bone, pose, 2, :] = t
    # 最终返回,每根骨骼在每一帧的pose
    return bone_transforms

def reconstruction_err_2d(poses, rest_pose, bone_transforms, rest_bones_t, W):
    num_bones = bone_transforms.shape[0]
    num_verts = W.shape[0]
    num_poses = poses.shape[0]

    # 计算rest pose本地相对于各个骨骼的偏移
    p_corrected = rest_pose[np.newaxis, :, :] - rest_bones_t[:, np.newaxis, :] # |num_bones| x |num_verts| x 2

    constructions = np.empty((num_bones, num_poses, num_verts, 2)) # |num_bones| x |num_poses| x |num_verts| x 2

    # 进行旋转变换
    for bone in range(num_bones):
        constructions[bone] = np.einsum('ijk,lk->ilj', bone_transforms[bone, :, :2, :], p_corrected[bone]) # |num_poses| x |num_verts| x 2

    # 加上偏移
    constructions += bone_transforms[:, :, np.newaxis, 2, :] # |num_bones| x |num_poses| x |num_verts| x 2

    # 算上权重,得到最终拟合顶点
    constructions *= (W.T)[:, np.newaxis, :, np.newaxis] # |num_bones| x |num_poses| x |num_verts| x 2

    # 真正pose减去拟合的pose,得到残差errors
    constructions = np.sum(constructions, axis=0)
    errors = poses - constructions # |num_poses| x |num_verts| x 2

    #画图
    plt.figure(figsize=(6, 6))
    plt.scatter(constructions[1, :, 0], constructions[1, :, 1], s=5, color='blue')
    plt.xlabel('X')
    plt.ylabel('Y')
    plt.title('Points Inside Equilateral Triangle')
    plt.xlim(1, 2)
    plt.ylim(1, 2)
    plt.grid(True)
    plt.show()
    # 算二范数作为最终error
    return np.mean(np.linalg.norm(errors, axis=2))

def SSDR_2d(poses, rest_pose, num_bones, sparseness=4, max_iterations=20):
    """
      Computes the Smooth Skinning Decomposition with Rigid bones

      inputs: poses           |num_poses| x |num_verts| x 2 matrix representing coordinates of vertices of each pose
              rest_pose       |num_verts| x 2 numpy matrix representing the coordinates of vertices in rest pose
              num_bones       number of bones to create
              sparseness      max number of bones influencing a single vertex

      return: An i x j matrix of bone-vertex weights, where i = # vertices and j = # bones
              A length-B list of (length-t lists of bone transformations [R_j | T_j] ), one list for each bone
              A list of bone translations for the bones at rest
      """
    start_time = time.time()
    # 初始化,给定动画帧poses,和t pose,以及需要多少根骨骼,返回所有骨骼的变换矩阵,以及各个骨骼的中心点
    bone_transforms, rest_bones_t = initialize_2d(poses, rest_pose, num_bones)
    print("init success")
    for _ in range(max_iterations):
        W = update_weight_map_2d(bone_transforms, rest_bones_t, poses, rest_pose, sparseness)
        bone_transforms = update_bone_transforms_2d(W, bone_transforms, rest_bones_t, poses, rest_pose)
        print("Reconstruction error:", reconstruction_err_2d(poses, rest_pose, bone_transforms, rest_bones_t, W))

    end_time = time.time()
    print("Done. Calculation took {0} seconds".format(end_time - start_time))
    print("Avg reconstruction error:", reconstruction_err_2d(poses, rest_pose, bone_transforms, rest_bones_t, W))
    return W, bone_transforms, rest_bones_t
if __name__ == '__main__':
    # 定义等边三角形的顶点
    vertex1_equilateral = np.array([0, 0])
    vertex2_equilateral = np.array([1, 0])
    vertex3_equilateral = np.array([0.5, np.sqrt(3) / 2])

    # 定义直角三角形的顶点
    vertex1_right = np.array([0, 0])
    vertex2_right = np.array([0, 1])
    vertex3_right = np.array([1, 0])

    # 定义钝角三角形的顶点
    vertex1_blunt = np.array([0, 0])
    vertex2_blunt = np.array([1, 0])
    vertex3_blunt = np.array([0.2, 0.8])

    # 生成500个等间距的点在三角形内部(等边三角形)
    num_points = 40
    t = np.linspace(0, 1, num_points)
    points_inside_equilateral = np.array(
        [(1 - a - b) * vertex1_equilateral + a * vertex2_equilateral + b * vertex3_equilateral
         for a in t for b in t if a + b <= 1])
    points_inside_equilateral = points_inside_equilateral.astype(float)

    # 生成500个等间距的点在三角形内部(直角三角形)
    points_inside_right_triangle = np.array([(1 - a - b) * vertex1_right + a * vertex2_right + b * vertex3_right
                                             for a in t for b in t if a + b <= 1])
    points_inside_right_triangl = points_inside_right_triangle.astype(float)
    # 生成500个等间距的点在三角形内部(钝角三角形)
    points_inside_blunt_triangle = np.array([(1 - a - b) * vertex1_blunt + a * vertex2_blunt + b * vertex3_blunt
                                             for a in t for b in t if a + b <= 1])
    points_inside_blunt_triangle= points_inside_blunt_triangle.astype(float)

    #动态的动作
    poses = np.zeros((2, 820, 2))
    poses[0] = points_inside_right_triangle+1
    poses[1]=points_inside_blunt_triangle+1
    #静态的动作
    rest_pose = points_inside_equilateral+1
    SSDR_2d(poses,rest_pose,10,8,25)

这副图是由等边三角形驱动生成

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值