def test_poisson_disk_sampling(self): import point_cloud_utils as pcu import numpy as np # v is a nv by 3 NumPy array of vertices # f is an nf by 3 NumPy array of face indexes into v # n is a nv by 3 NumPy array of vertex normals v, f, n = pcu.read_obj(os.path.join(self.test_path, "cube_twist.obj")) bbox = np.max(v, axis=0) - np.min(v, axis=0) bbox_diag = np.linalg.norm(bbox) # Generate very dense random samples on the mesh (v, f, n) # Note that this function works with no normals, just pass in an empty array np.array([], dtype=v.dtype) # v_dense is an array with shape (100*v.shape[0], 3) where each row is a point on the mesh (v, f) # n_dense is an array with shape (100*v.shape[0], 3) where each row is a the normal of a point in v_dense v_dense, n_dense = pcu.sample_mesh_random(v, f, n, num_samples=v.shape[0] * 100) # Downsample v_dense to be from a blue noise distribution: # # v_poisson is a downsampled version of v where points are separated by approximately # `radius` distance, use_geodesic_distance indicates that the distance should be measured on the mesh. # # n_poisson are the corresponding normals of v_poisson v_poisson, n_poisson = pcu.sample_mesh_poisson_disk( v_dense, f, n_dense, radius=0.01 * bbox_diag, use_geodesic_distance=True)
def sample_mesh_poisson_disk(hm: HybridMesh, sample_num: int) -> PointCloud: """ Uses poisson disk sampling and maps existing labels using a KDTree. It can not be guaranteed that the requested number of sample points can be generated. It can differ from the requested number by a small amount (around +-2%). Args: hm: The MeshCloud from which the samples should be generated sample_num: Requested number (approximate!) of sample points. Returns: PointCloud consisting of sampled points. """ vertices = hm.vertices.astype(float) s_vertices, s_normals = pcu.sample_mesh_poisson_disk(vertices, hm.faces, np.array([]), ceil(sample_num)) # map labels from input cloud to sample labels = None types = None tree = cKDTree(hm.vertices) dist, ind = tree.query(s_vertices, k=1) if len(hm.labels) > 0: labels = hm.labels[ind] if len(hm.types) > 0: types = hm.types[ind] result = PointCloud(vertices=s_vertices.reshape(-1, 3), labels=labels, types=types) return result
def sample_points_poisson_disk_radius(tri_mesh, radius=0.01, use_geodesic_distance=True, best_choice_sampling=True): """ Generates samples so that them are approximately evenly separated. :param tri_mesh: mesh to sample :param radius: the desired separation :param use_geodesic_distance: :param best_choice_sampling: :return: """ vertices = np.asarray(tri_mesh.vertices) triangles = np.asarray(tri_mesh.faces) normals = np.asarray(tri_mesh.vertex_normals, order='C') v_poisson, n_poisson = pcu.sample_mesh_poisson_disk( vertices, triangles, normals, num_samples=-1, radius=radius, use_geodesic_distance=use_geodesic_distance, best_choice_sampling=best_choice_sampling, random_seed=0) return v_poisson, n_poisson
def test_mesh_sampling(self): import point_cloud_utils as pcu import numpy as np # v is a nv by 3 NumPy array of vertices # f is an nf by 3 NumPy array of face indexes into v # n is a nv by 3 NumPy array of vertex normals if they are specified, otherwise an empty array v, f, n = pcu.read_obj(os.path.join(self.test_path, "cube_twist.obj")) bbox = np.max(v, axis=0) - np.min(v, axis=0) bbox_diag = np.linalg.norm(bbox) f_idx1, bc1 = pcu.sample_mesh_random(v, f, num_samples=1000, random_seed=1234567) f_idx2, bc2 = pcu.sample_mesh_random(v, f, num_samples=1000, random_seed=1234567) f_idx3, bc3 = pcu.sample_mesh_random(v, f, num_samples=1000, random_seed=7654321) self.assertTrue(np.all(f_idx1 == f_idx2)) self.assertTrue(np.all(bc1 == bc2)) self.assertFalse(np.all(f_idx1 == f_idx3)) self.assertFalse(np.all(bc1 == bc3)) # Generate very dense random samples on the mesh (v, f) f_idx, bc = pcu.sample_mesh_random(v, f, num_samples=v.shape[0] * 4) v_dense = (v[f[f_idx]] * bc[:, np.newaxis]).sum(1) s_idx = pcu.downsample_point_cloud_poisson_disk(v_dense, 0, 0.1*bbox_diag, random_seed=1234567) s_idx2 = pcu.downsample_point_cloud_poisson_disk(v_dense, 0, 0.1*bbox_diag, random_seed=1234567) s_idx3 = pcu.downsample_point_cloud_poisson_disk(v_dense, 0, 0.1 * bbox_diag, random_seed=7654321) self.assertTrue(np.all(s_idx == s_idx2)) if s_idx3.shape == s_idx.shape: self.assertFalse(np.all(s_idx == s_idx3)) else: self.assertFalse(s_idx.shape == s_idx3.shape) # Ensure we can request more samples than vertices and get something reasonable s_idx_0 = pcu.downsample_point_cloud_poisson_disk(v_dense, 2*v_dense.shape[0], random_seed=1234567) s_idx = pcu.downsample_point_cloud_poisson_disk(v_dense, 1000, random_seed=1234567) s_idx2 = pcu.downsample_point_cloud_poisson_disk(v_dense, 1000, random_seed=1234567) s_idx3 = pcu.downsample_point_cloud_poisson_disk(v_dense, 1000, random_seed=7654321) self.assertTrue(np.all(s_idx == s_idx2)) if s_idx3.shape == s_idx.shape: self.assertFalse(np.all(s_idx == s_idx3)) else: self.assertFalse(s_idx.shape == s_idx3.shape) f_idx1, bc1 = pcu.sample_mesh_poisson_disk(v, f, num_samples=1000, random_seed=1234567, use_geodesic_distance=True, oversampling_factor=5.0) f_idx2, bc2 = pcu.sample_mesh_poisson_disk(v, f, num_samples=1000, random_seed=1234567, use_geodesic_distance=True, oversampling_factor=5.0) f_idx3, bc3 = pcu.sample_mesh_poisson_disk(v, f, num_samples=1000, random_seed=7654321, use_geodesic_distance=True, oversampling_factor=5.0) self.assertTrue(np.all(f_idx1 == f_idx2)) self.assertTrue(np.all(bc1 == bc2)) if f_idx1.shape == f_idx3.shape: self.assertFalse(np.all(f_idx1 == f_idx3)) if bc1.shape == bc3.shape: self.assertFalse(np.all(bc1 == bc3)) f_idx1, bc1 = pcu.sample_mesh_poisson_disk(v, f, num_samples=-1, radius=0.01*bbox_diag, random_seed=1234567, oversampling_factor=5.0) f_idx2, bc2 = pcu.sample_mesh_poisson_disk(v, f, num_samples=-1, radius=0.01*bbox_diag, random_seed=1234567, oversampling_factor=5.0) f_idx3, bc3 = pcu.sample_mesh_poisson_disk(v, f, num_samples=-1, radius=0.01*bbox_diag, random_seed=7654321, oversampling_factor=5.0) self.assertTrue(np.all(f_idx1 == f_idx2)) self.assertTrue(np.all(bc1 == bc2)) if f_idx1.shape == f_idx3.shape: self.assertFalse(np.all(f_idx1 == f_idx3)) if bc1.shape == bc3.shape: self.assertFalse(np.all(bc1 == bc3))
def downsample_point_cloud(point_cloud, normals, target_num_pts, max_iters=4096, max_retries=5): """ Given a point cloud of shape [n, d] where each row is a point, downsample it to have num_pts points which are as evenly separated as possible. This function uses binary search over the radius parameter for Poisson Disk Sampling to find a radius yielding exactly the desired number of points. If the point cloud cannot be downsampled, the function throws as RuntimeError :param point_cloud: An array of shape [n, d] where each row, point_cloud[i, :], is a point :param normals: Normals at each point in the point cloud :param target_num_pts: The target number of points to downsample to :param max_iters: The maximum number of binary search iterations to find a valid down-sampling :param max_retries: The maximum number of retries for binary search :return: A downsampled point cloud """ V = point_cloud N = normals if V.shape[0] < target_num_pts: raise ValueError( "Cannot downsample point cloud with %d points to a target " "of %d points" % (V.shape[0], target_num_pts)) if V.shape[0] == target_num_pts: return V bbox = np.max(V, axis=0) - np.min(V, axis=0) bbox_diag = np.linalg.norm(bbox) F = np.zeros(V.shape, dtype=np.int32) pts_range = [target_num_pts, target_num_pts] radius_range = [0.00001 * bbox_diag, 0.3 * bbox_diag] Pdown = np.zeros([0, 3]) Ndown = np.zeros([0, 3]) success = False for _ in range(max_retries): num_iters = 0 while not (pts_range[0] <= Pdown.shape[0] <= pts_range[1]): mid = radius_range[0] + 0.5 * (radius_range[1] - radius_range[0]) Pdown, Ndown = sample_mesh_poisson_disk(V, F, N, radius=mid) if Pdown.shape[0] < pts_range[0]: radius_range[1] = mid elif Pdown.shape[0] > pts_range[1]: radius_range[0] = mid num_iters += 1 if num_iters > max_iters: break if num_iters > max_iters: success = False continue else: success = True break if not success: raise RuntimeError("Failed to downsample point cloud. Try again") return Pdown, Ndown