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)
Exemple #2
0
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
Exemple #3
0
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
Exemple #4
0
    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