Spaces:
Running on Zero
Running on Zero
| from numpy import ndarray | |
| from typing import Optional, Tuple | |
| import numpy as np | |
| import scipy | |
| def assert_ndarray(arr, name: str="arr", shape: Optional[Tuple[int, ...]]=None, dtype=None): | |
| if not isinstance(arr, np.ndarray): | |
| raise ValueError(f"{name} must be a numpy.ndarray or None, got {type(arr)}") | |
| if shape is not None: | |
| # shape may contain None as wildcard | |
| if len(shape) != arr.ndim: | |
| raise ValueError(f"{name}: expected shape length {len(shape)} but array ndim is {arr.ndim}") | |
| for i, (exp, actual) in enumerate(zip(shape, arr.shape)): | |
| if exp > 0 and exp != actual: | |
| raise ValueError(f"{name} shape mismatch at axis {i}: expected {exp}, got {actual}") | |
| if dtype is not None: | |
| if not np.issubdtype(arr.dtype, dtype): | |
| raise ValueError(f"{name} dtype must be {dtype}, got {arr.dtype}") | |
| def assert_list(arr, name: str="arr", dtype=None): | |
| if not isinstance(arr, list): | |
| raise ValueError(f"found type {type(arr)}, expect a list") | |
| if dtype is not None: | |
| for x in arr: | |
| if not isinstance(x, dtype): | |
| raise ValueError(f"found type {type(x)} in {name}, expect all to be {dtype}") | |
| def linear_blend_skinning( | |
| vertices: ndarray, | |
| matrix_local: ndarray, | |
| matrix: ndarray, | |
| skin: ndarray, | |
| pad: int=1, | |
| value: float=1.0, | |
| ) -> ndarray: | |
| """ | |
| Args: | |
| vertices: (N, 4-pad) | |
| matrix_local: (J, 4, 4) | |
| matrix: (J, 4, 4) | |
| skin: (N, J) | |
| pad: 0 or 1 | |
| value: value to pad | |
| Returns: | |
| (N, 3) vertices using LBS algorithm: Skinning with dual quaternions, Kavan, 2007 | |
| """ | |
| J = matrix_local.shape[0] | |
| N = vertices.shape[0] | |
| assert_ndarray(vertices, name='vertices', shape=(N, 3)) | |
| assert_ndarray(matrix_local, name="matrix_local", shape=(J, 4, 4)) | |
| assert_ndarray(matrix, name="matrix", shape=(J, 4, 4)) | |
| assert_ndarray(skin, name="skin", shape=(N, J)) | |
| assert vertices.shape[-1] + pad == 4 | |
| # (4, N) | |
| padded = np.pad(vertices, ((0, 0), (0, pad)), 'constant', constant_values=(0, value)).T | |
| # (J, 4, 4) | |
| trans = matrix @ np.linalg.inv(matrix_local) | |
| weighted_per_bone_matrix = [] | |
| # (J, N) | |
| mask = (skin > 0).T | |
| for i in range(J): | |
| offset = np.zeros((4, N), dtype=np.float32) | |
| offset[:, mask[i]] = (trans[i] @ padded[:, mask[i]]) * skin.T[i, mask[i]] | |
| weighted_per_bone_matrix.append(offset) | |
| weighted_per_bone_matrix = np.stack(weighted_per_bone_matrix) | |
| g = np.sum(weighted_per_bone_matrix, axis=0) | |
| final = g[:3, :] / (np.sum(skin, axis=1) + 1e-8) | |
| return final.T | |
| def axis_angle_to_matrix(axis_angle: ndarray) -> ndarray: | |
| """ | |
| Turn axis angle representation to matrix representation. | |
| """ | |
| res = np.pad(scipy.spatial.transform.Rotation.from_rotvec(axis_angle).as_matrix(), ((0, 0), (0, 1), (0, 1)), 'constant', constant_values=((0, 0), (0, 0), (0, 0))) | |
| assert res.ndim == 3 | |
| res[:, -1, -1] = 1 | |
| return res | |
| def sample_surface( | |
| num_samples: int, | |
| vertices: ndarray, | |
| faces: ndarray, | |
| mask: Optional[ndarray]=None, | |
| ) -> Tuple[ndarray, ndarray, ndarray]: | |
| ''' | |
| Randomly pick samples proportional to face area. | |
| See sample_surface: https://github.com/mikedh/trimesh/blob/main/trimesh/sample.py | |
| Args: | |
| mask: (num_faces,), only sample points on the faces where value is True. | |
| Return: | |
| vertex_samples: sampled vertices | |
| original_face_index: on which face is sampled | |
| random_lengths: sampled vectors on face | |
| ''' | |
| original_face_indices = np.arange(len(faces)) | |
| # sample according to mask | |
| if mask is not None: | |
| assert_ndarray(arr=mask, name="mask", shape=(faces.shape[0],)) | |
| original_face_indices = original_face_indices[mask] | |
| faces = faces[mask] | |
| # get face area | |
| offset_0 = vertices[faces[:, 1]] - vertices[faces[:, 0]] | |
| offset_1 = vertices[faces[:, 2]] - vertices[faces[:, 0]] | |
| # TODO: change to correct uniform sampling... | |
| face_weight = np.linalg.norm(np.cross(offset_0, offset_1, axis=-1), axis=-1) | |
| weight_cum = np.cumsum(face_weight, axis=0) | |
| face_pick = np.random.rand(num_samples) * weight_cum[-1] | |
| face_index = np.searchsorted(weight_cum, face_pick) | |
| # face_weight = np.cross(offset_0, offset_1, axis=-1) | |
| # face_weight = (face_weight * face_weight).sum(axis=1) | |
| # weight_cum = np.cumsum(face_weight, axis=0) | |
| # face_pick = np.random.rand(num_samples) * weight_cum[-1] | |
| # face_index = np.searchsorted(weight_cum, face_pick) | |
| # map face_index back to original indices | |
| original_face_index = original_face_indices[face_index] | |
| # pull triangles into the form of an origin + 2 vectors | |
| tri_origins = vertices[faces[:, 0]] | |
| tri_vectors = vertices[faces[:, 1:]] | |
| tri_vectors -= np.tile(tri_origins, (1, 2)).reshape((-1, 2, 3)) | |
| # pull the vectors for the faces we are going to sample from | |
| tri_origins = tri_origins[face_index] | |
| tri_vectors = tri_vectors[face_index] | |
| # randomly generate two 0-1 scalar components to multiply edge vectors b | |
| random_lengths = np.random.rand(len(tri_vectors), 2, 1) | |
| random_test = random_lengths.sum(axis=1).reshape(-1) > 1.0 | |
| random_lengths[random_test] -= 1.0 | |
| random_lengths = np.abs(random_lengths) | |
| sample_vector = (tri_vectors * random_lengths).sum(axis=1) | |
| vertex_samples = sample_vector + tri_origins | |
| return vertex_samples, original_face_index, random_lengths | |
| def sample_barycentric( | |
| vertex_group: ndarray, | |
| faces: ndarray, | |
| face_index: ndarray, | |
| random_lengths: ndarray, | |
| ) -> ndarray: | |
| v_origins = vertex_group[faces[face_index, 0]] | |
| v_vectors = vertex_group[faces[face_index, 1:]] | |
| v_vectors -= v_origins[:, np.newaxis, :] | |
| sample_vector = (v_vectors * random_lengths).sum(axis=1) | |
| v_samples = sample_vector + v_origins | |
| return v_samples | |
| def sample_vertex_groups( | |
| vertices: ndarray, | |
| faces: ndarray, | |
| num_samples: int, | |
| num_vertex_samples: Optional[int]=None, | |
| vertex_normals: Optional[ndarray]=None, | |
| face_normals: Optional[ndarray]=None, | |
| vertex_groups: Optional[ndarray]=None, | |
| face_mask: Optional[ndarray]=None, | |
| shuffle: bool=True, | |
| same: bool=False, | |
| ) -> Tuple[ndarray, ndarray|None, ndarray|None]: | |
| """ | |
| Choose num_samples samples on the mesh and get their positions and normals. | |
| If vertex_group is provided, get its weights using barycentric sampling. | |
| Return: | |
| sampled_vertices, sampled_normals, sampled_vertex_groups | |
| Args: | |
| vertices: (N, 3) | |
| faces: (F, 3) | |
| num_samples: how many samples | |
| num_vertex_samples: | |
| At most num_vertex_samples unique vertices to be included, | |
| these points will be concatenated in the last (if shuffle is False). | |
| vertex_normals: (N, 3), sampled_normals will be None if not provided | |
| face_normals: (N, 3), sampled_normals will be None if not provided | |
| vertex_groups: (N, m), sampled_vertex_groups will be None if not provided | |
| face_mask: | |
| (F,) or (F, m), if shape is (F,), use the same mask across all | |
| vertex groups. Only sample on faces where value is True. | |
| shuffle: shuffle samples in the end | |
| same: | |
| Sample on the same locations, only useful when using mutiple | |
| vertex groups and mask is None or shape of (F,). | |
| """ | |
| if num_vertex_samples is None: | |
| num_vertex_samples = 0 | |
| if num_vertex_samples > num_samples: | |
| raise ValueError(f"num_vertex_samples cannot be larger than num_samples, found: {num_vertex_samples} > {num_samples}") | |
| def get_mask_perm(mask: Optional[ndarray]): | |
| if mask is None: | |
| vertex_mask = np.arange(vertices.shape[0]) | |
| else: | |
| vertex_mask = np.unique(mask) | |
| perm = np.random.permutation(vertex_mask.shape[0]) | |
| return vertex_mask[perm[:num_vertex_samples]] | |
| if vertex_groups is not None: | |
| if vertex_groups.ndim == 1: | |
| assert_ndarray(arr=vertex_groups, name="vertex_groups", shape=(vertices.shape[0],)) | |
| vertex_groups = vertex_groups[:, None] | |
| else: | |
| assert_ndarray(arr=vertex_groups, name="vertex_groups", shape=(vertices.shape[0], -1)) | |
| vertex_groups = vertex_groups | |
| if vertex_groups is not None: | |
| if face_mask is not None: | |
| if face_mask.ndim == 1: | |
| assert_ndarray(arr=face_mask, name="mask", shape=(faces.shape[0],)) | |
| else: | |
| assert_ndarray(arr=face_mask, name="mask", shape=(faces.shape[0], vertex_groups.shape[1])) | |
| list_sampled_vertices = [] | |
| list_sampled_normals = [] | |
| list_sampled_vertex_groups = [] | |
| perm = None | |
| _mask = None | |
| same = same and (face_mask is None or (face_mask is not None and face_mask.ndim != 2)) | |
| for i in range(vertex_groups.shape[1]): | |
| if face_mask is not None: | |
| if face_mask.ndim == 1: | |
| perm = get_mask_perm(faces[face_mask]) | |
| _mask = face_mask | |
| else: | |
| perm = get_mask_perm(faces[face_mask[:, i]]) | |
| _mask = face_mask[:, i] | |
| else: | |
| perm = get_mask_perm(None) | |
| _mask = None | |
| _num_samples = num_samples - len(perm) | |
| face_vertices, face_index, random_lengths = sample_surface( | |
| num_samples=_num_samples, | |
| vertices=vertices, | |
| faces=faces, | |
| mask=_mask, | |
| ) | |
| list_sampled_vertices.append(np.concatenate([vertices[perm], face_vertices], axis=0)) | |
| if vertex_normals is not None and face_normals is not None: | |
| list_sampled_normals.append(np.concatenate([vertex_normals[perm], face_normals[face_index]], axis=0)) | |
| if same: | |
| g = sample_barycentric( | |
| vertex_group=vertex_groups, | |
| faces=faces, | |
| face_index=face_index, | |
| random_lengths=random_lengths, | |
| ) | |
| list_sampled_vertex_groups.append(np.concatenate([vertex_groups[perm], g], axis=0)) | |
| break | |
| g = sample_barycentric( | |
| vertex_group=vertex_groups[:, i:i+1], | |
| faces=faces, | |
| face_index=face_index, | |
| random_lengths=random_lengths, | |
| )[:, 0] | |
| list_sampled_vertex_groups.append(np.concatenate([vertex_groups[:, i][perm], g], axis=0)) | |
| sampled_vertices = np.stack(list_sampled_vertices, axis=1) | |
| if len(list_sampled_normals) > 0: | |
| sampled_normals = np.stack(list_sampled_normals, axis=1) | |
| else: | |
| sampled_normals = None | |
| if same: | |
| sampled_vertex_groups = list_sampled_vertex_groups[0] | |
| else: | |
| sampled_vertex_groups = np.stack(list_sampled_vertex_groups, axis=1) | |
| else: # otherwise only sample vertices and normals | |
| if face_mask is not None: | |
| assert_ndarray(arr=face_mask, name="mask", shape=(faces.shape[0],)) | |
| perm = get_mask_perm(faces[face_mask]) | |
| else: | |
| perm = get_mask_perm(None) | |
| num_samples -= len(perm) | |
| n_vertex = vertices[perm] | |
| face_vertices, face_index, random_lengths = sample_surface( | |
| num_samples=num_samples, | |
| vertices=vertices, | |
| faces=faces, | |
| mask=face_mask, | |
| ) | |
| sampled_vertices = np.concatenate([n_vertex, face_vertices], axis=0) | |
| if vertex_normals is not None and face_normals is not None: | |
| sampled_normals = np.concatenate([vertex_normals[perm], face_normals[face_index]], axis=0) | |
| else: | |
| sampled_normals = None | |
| sampled_vertex_groups = None | |
| return sampled_vertices, sampled_normals, sampled_vertex_groups |