Make a rest_post yourself. It is an equilateral triangle. The poses are a right triangle and an obtuse triangle.
You can run it directly to observe the effect
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 #PointCloudRegistrationAlgorithm import numpy as np def kabsch_2d(P, Q): """ Two-dimensional point cloud registration algorithm, using Kabsch algorithm. inputs: P N x 2 numpy matrix, representing the two-dimensional coordinate points in the point set P Q N x 2 numpy matrix, representing the two-dimensional coordinate points in the point set Q return: A 3 x 2 matrix, the first two lines are rotation matrices, and the last line is translation vector """ 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 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: Modeling pose num_verts = rest_pose.shape[0] num_poses = poses.shape[0] # trans for each bone per frame # 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 # Whitening: rest_pose / The standard deviation of each column, that is, the standard deviation of the three axes of x, y, and z. This is only used for kmeans whitened = whiten(rest_pose) # Directly group the whitened rest poses into kmeans groups. The number of groups is the number of bones. # python uses two functions to complete this step # kmeans returns the center point codebook, _ = kmeans(whitened, num_bones)#Clustering center (initial bone point) # Bind the whitened initial vertex to the center point to complete the grouping #vert_assignments: (num_verts,) vert_assignments, _ = vq(whitened, codebook) #The index of the bone point corresponding to each vertex [0,1,5,6,8,.....] # Traverse all bones # Compute initial random bone transformations for bone in range(num_bones): # Calculate the center point. Each vert has a bone assignment. All vertices of each bone are used as a cluster to calculate the center. # In rest pose, the current bone corresponds to the center of the cluster #rest_bones_t:(num_bones, 2) rest_bones_t[bone] = np.mean(rest_pose[vert_assignments == bone], axis=0) # This corrected is the offset of each vertex relative to the center point of the cluster it belongs to. # rest_pose: (num_verts, 3) # rest_pose_corrected: (num_bones, num_verts, 3) # Subtract the center point from each vertex in the rest pose. What needs to be noted here is that here are all the calculated vertices. # In other words, here is the position of all the vertices of the body relative to the center of the cluster of a bone. 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) # Now we have the rest pose and then iterate through each pose for pose in range(num_poses): # The coordinates of the rest pose passed in relative to the center point, and the vertex data of the pose in the current frame # For a bone, there may be multiple vertices bound to this bone, so for a bone, it corresponds to a point set # Therefore, the goal of the kabsch algorithm is to try to rotate and translate this bone to minimize the difference between the two point sets. # Calculate the transform of the bone in the current frame # rest_pose_corrected contains all the vertices, but here only the vertices belonging to this bone are taken, and the kabsch algorithm is used to calculate the transform of the current bone. 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 # This reconstructs the space of each bone, each frame, and all vertices. # 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): # Traverse each bone #Transform vertices into model space # Rotate first # Since the matrix has 4 rows and 3 columns, the first three rows of the matrix are taken here, which is the rotation matrix. # Set the vertex position of the rest pose to the corresponding bone space, and then rotate it # After the vertex is rotated, the yz axes of the vertex are reversed and become right-handed. The kabsch function constructing bone_transforms above also has the operation of transforming the hand system. # But this is equivalent to all vertices in rest pose being transformed once in all frames. # 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 #Move again # R * p + T constructed[bone] = Rp + bone_transforms[bone, :, np.newaxis, 2, :] # Compare the transformed vertex position with the original pose # Calculate the 2 norm, that is, the sum of squares and then the square root # The following axis=(1,3), 1 corresponds to poses, 3 corresponds to position, that is, the 2 norm is calculated based on the difference in vertex positions of all frames. # That is, traverse num_bones and num_verts, sum the squares of all elements and then take the root sign # 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()) # Above we calculated errors. Its structure is (num_bones, num_verts), which is the error of each bone and each vertex (regardless of whether it belongs to the bone) in the entire animation sequence. # For each bone, search for the smallest vertex index by column, that is, the index of the bone with the smallest error is stored at the position of each vertex. So we have a new mapping # (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): # Recalculate the cluster center point based on the new bone mapping rest_bones_t[bone] = np.mean(rest_pose[vert_assignments == bone], axis=0) # Calculate the position of all vertices relative to each bone rest_pose_corrected[bone] = rest_pose - np.mean(rest_pose[vert_assignments == bone], axis=0) # Reuse kabsch algorithm to calculate bone transformation for pose in range(num_poses): # This is still based on vert_assignments and all vertices under a certain bone to calculate the transform of the bone. bone_transforms[bone, pose] = kabsch_2d(rest_pose_corrected[bone, vert_assignments == bone], poses[pose, vert_assignments == bone]) #The final return is the animation data of the skeleton and the position of the skeleton in 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): # Traverse all vertices # For every vertex, solve a least squares problem Rp = np.empty((num_bones, num_poses, 2)) # Transform the current vertex, use all bones to transform, and get the transformed vertex 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 # This is the transformed vertex position. This is equivalent to expanding each xyz of all poses by rows. # (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| # This is the real vertex position b = poses[:, v, :].reshape(2 * num_poses) # 3 * |num_poses| x 1 # Calculate when x has the smallest difference between Ax - b, using mse. Linear regression with bound is used here, because the values of all weights can only be between [0], 1] #Finally got the weight of each bone # 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 # Bone weight normalization w /= np.sum(w) # Ensure that w sums to 1 (affinity constraint) # Since each vertex has a maximum weight limit |K|, we have to discard other # Remove |B| - |K| bone weights with the least "effect" # Reshape A*w here and get the fitted vertices. # (3 * num_poses, num_bones) -> (num_poses, 3, num_bones) # Then based on column 1, find the second norm, that is, traverse num_poses and num_bones, and sum up the square root of all elements, which is actually the 2 norm of the vertex position. effect = np.linalg.norm((A * w).reshape(num_poses, 2, num_bones), axis=1) # |num_poses| x |num_bones| # Then merged num_poses. What we actually do here is to find out the degree to which the current vertex is affected by different bones. Then we can know which bones should be killed. effect = np.sum(effect, axis=0) # |num_bones| x 1 # Because a vertex can only be affected by |k| root bones at most, the number of bones to be discarded is calculated here. num_discarded = max(num_bones - sparseness, 0) # np.argpartition is to group the contents of the array into groups. Those smaller than num_discarded are placed in front, and those larger than num_discarded are placed in the back. # So here is actually discarding the smallest num_discarded elements # The effective here saves the index of the selected bone for each vertex: the number is: |sparseness| effective = np.argpartition(effect, num_discarded)[num_discarded:] # |sparseness| x 1 # At this point, we know which bones are bound to each vertex. # Only use the few bones we selected and re-linear regression to calculate the weights # 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) # Finally, the weight sparse matrix of the current vertex is obtained w_sparse = np.zeros(num_bones) # The bones we selected are set to the recalculated weights w_sparse[effective] = w_reduced w_sparse /= np.sum(w_sparse) # Ensure that w_sparse sums to 1 (affinity constraint) W[v] = w_sparse # Return the weight assignment relationship of all vertices # (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] # Traverse all poses for pose in range(num_poses): # Traverse all bones under the current pose for bone in range(num_bones): # Calculate the offset of all vertices relative to the current bone in 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): # Traverse all bones # First calculate the rest pose based on bone2 # Extract the rotation matrix of the current bone, calculate the local position relative to the rest pose relative to the current bone, and rotate it # 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 # Then translate, here you get the vertex position transformed by each bone for each frame of animation. # Since there are weights to indicate which vertices will be affected by which bones, for any bone, all vertices are counted here. # R * p + T constructed[bone2] = Rp + bone_transforms[bone2, pose, 2, :] # w * (R * p + T) # Here, multiply the vertices by the corresponding weights. Note that after the previous step, most of the weights here are 0. Only important weights have numerical values, and the maximum number is |K| # So what is obtained here is the weighted position of each vertex relative to its corresponding bone. The sum is then required to obtain the true position. # (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 # The operation here is to remove these positions from the current bone column to remove the influence of the current bone. constructed = np.delete(constructed, bone, axis=1) # |num_verts| x |num_bones-1| x 3 # The current bone is the bone we are paying attention to, so q is the real vertex position minus the influence of other bones on the vertex, and the resulting point cloud # Intuitive understanding is that pose is a real point cloud, and the constructed points without the current skeleton are an estimated point cloud. q = poses[pose] - np.sum(constructed, axis=1) # |num_verts| x 3 # And p is the point cloud of the offset of the vertex in the rest pose relative to the center point. This center point is a weighted average, and the weight is related to the w of the vertex to the bone. # p_star is the center point under rest pose # q_star is the center point of the residual of the current animation # 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 # So far we have obtained two point clouds, p_bar = p_corrected - p_star # |num_verts| x 3 q_bar = q - W[:, bone, np.newaxis] * q_star # |num_verts| x 3 # Next, perform point cloud registration to obtain the rotation and translation of the bones. # 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| # Point cloud registration to obtain rotation matrix 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 # Finally return, the pose of each bone in each frame 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] # Calculate the local offset of the rest pose relative to each bone 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 #Perform rotation transformation 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 #Add offset constructions + = bone_transforms[:, :, np.newaxis, 2, :] # |num_bones| x |num_poses| x |num_verts| x 2 # Calculate the weights to get the final fitting vertex constructions *= (W.T)[:, np.newaxis, :, np.newaxis] # |num_bones| x |num_poses| x |num_verts| x 2 # Subtract the fitted pose from the real pose to get the residual errors constructions = np.sum(constructions, axis=0) errors = poses - constructions # |num_poses| x |num_verts| x 2 #draw图 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() # Calculate the second norm as the final 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() # Initialization, given animation frame poses, t pose, and how many bones are needed, return the transformation matrix of all bones and the center point of each bone 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__': # Define the vertices of an equilateral triangle vertex1_equilateral = np.array([0, 0]) vertex2_equilateral = np.array([1, 0]) vertex3_equilateral = np.array([0.5, np.sqrt(3) / 2]) # Define the vertices of the right triangle vertex1_right = np.array([0, 0]) vertex2_right = np.array([0, 1]) vertex3_right = np.array([1, 0]) # Define the vertices of an obtuse triangle vertex1_blunt = np.array([0, 0]) vertex2_blunt = np.array([1, 0]) vertex3_blunt = np.array([0.2, 0.8]) # Generate 500 equally spaced points inside the triangle (equilateral triangle) 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) # Generate 500 equally spaced points inside the triangle (right triangle) 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) # Generate 500 equally spaced points inside the triangle (obtuse triangle) 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) #dynamic action poses = np.zeros((2, 820, 2)) poses[0] = points_inside_right_triangle + 1 poses[1]=points_inside_blunt_triangle + 1 #static action rest_pose = points_inside_equilateral + 1 SSDR_2d(poses,rest_pose,10,8,25)
This image is driven by equilateral triangles
The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Algorithm skill tree Home page Overview 57032 people are learning the system