示例#1
0
    def _conductord_winding_squared(self, pts, distance):
        """Compute and returned the signed distances between particles and mesh.

        
        The distances are computed via the functions of winding number and squared distance provided by PyMesh.
        PyMesh's distance_to_mesh function needs to compute BVH engine every time it is called.
        The PyMesh library has been modified so distance_to_mesh can take BVH engine as an argument
        to avoid this time consuming step called every time.

        Args:
          pts: numpy.ndarray of Nx3 storing the positions of particles for query. 
          distance: A list to store the computed distance

        Return:
          distance: A list. Points outside conductor have positive values, and those inside have negative values.
        """
        winding_number = pymesh.compute_winding_number(self.surface, pts)
        winding_number[np.abs(winding_number) < self._winding_fuzz] = 0.0
        pts_inside = winding_number > 0.

        try:
            distance[:], _, _ = pymesh.distance_to_mesh(self.surface,
                                                        pts,
                                                        bvh=self._bvh)
        except TypeError:
            distance[:], _, _ = pymesh.distance_to_mesh(self.surface, pts)

        distance[:] = np.sqrt(distance)
        distance[pts_inside] *= -1
示例#2
0
    def _conductord_winding_squared(self, pts, distance):
        """Compute and returned the signed distances between particles and mesh.

        
        The distances are computed via the functions of winding number and squared distance provided by PyMesh.
        PyMesh's distance_to_mesh function needs to compute BVH engine every time it is called.
        The PyMesh library has been modified so distance_to_mesh can take BVH engine as an argument
        to avoid this time consuming step called every time.

        Args:
          pts: numpy.ndarray of Nx3 storing the positions of particles for query. 
          distance: A list to store the computed distance

        Return:
          distance: A list. Points outside conductor have positive values, and those inside have negative values.
        """
        winding_number = pymesh.compute_winding_number(self.surface, pts)
        winding_number[np.abs(winding_number) < self._winding_fuzz] = 0.0
        pts_inside = winding_number > 0.

        try:
            distance[:], _, _ = pymesh.distance_to_mesh(self.surface, pts, bvh=self._bvh)
        except TypeError:
            distance[:], _, _ = pymesh.distance_to_mesh(self.surface, pts)

        distance[:] = np.sqrt(distance)
        distance[pts_inside] *= -1
示例#3
0
def pick_points(mesh, mesh_pymesh, object_name):
    """
  Function for inteactively picking points
  """
    print("")
    print("1) Pick points using [shift + left click]")
    print("   Press [shift + right click] to undo point picking")
    print("2) After picking points, press q for close the window")
    # sample points on the mesh surface to be picked from
    pcd = mesh.sample_points_poisson_disk(10000)
    pcd.estimate_normals()

    # create interactive visualizer
    vis = o3dv.VisualizerWithEditing()
    vis.create_window(object_name)
    # vis.add_geometry(mesh)
    vis.add_geometry(pcd)
    vis.run()  # user picks points
    vis.destroy_window()
    print("")
    pt_idxs = vis.get_picked_points()

    # get the picked points and normals of the faces closest to those points
    pts = np.asarray(pcd.points)[pt_idxs]
    _, face_idxs, _ = pymesh.distance_to_mesh(mesh_pymesh, pts)
    normals = np.asarray(mesh.triangle_normals)[face_idxs]
    # normals = np.asarray(pcd.normals)[pt_idxs]
    if normals.ndim == 1:
        normals = normals[np.newaxis, :]
    return pts, normals
示例#4
0
    def test_boundary_pts_geogram(self):
        mesh = generate_box_mesh(np.array([0, 0, 0]), np.array([1, 1, 1]))
        pts = np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])

        if "geogram" in BVH.available_engines:
            sq_dist, face_idx, closest_pts = distance_to_mesh(
                mesh, pts, "geogram")
            self.assert_array_equal(sq_dist, np.zeros(2))
示例#5
0
    def test_boundary_pts_cgal(self):
        mesh = generate_box_mesh(
                np.array([0, 0, 0]), np.array([1, 1, 1]));
        pts = np.array([
            [0.0, 0.0, 0.0],
            [1.0, 1.0, 1.0] ]);

        sq_dist, face_idx, closest_pts = distance_to_mesh(mesh, pts, "cgal");
        self.assert_array_equal(sq_dist, np.zeros(2));
示例#6
0
    def test_boundary_pts_cgal(self):
        mesh = generate_box_mesh(
                np.array([0, 0, 0]), np.array([1, 1, 1]));
        pts = np.array([
            [0.0, 0.0, 0.0],
            [1.0, 1.0, 1.0] ]);

        sq_dist, face_idx, closest_pts = distance_to_mesh(mesh, pts, "cgal");
        self.assert_array_equal(sq_dist, np.zeros(2));
示例#7
0
def process_marker_locations(mesh_o3d, mesh_pymesh, markers):
    """
  snaps marker locations to object surface
  and computes surface normals at marker locations
  """
    dist2, face_idxs, nbrs = pymesh.distance_to_mesh(mesh_pymesh, markers)
    normals = np.asarray(mesh_o3d.triangle_normals)[face_idxs]

    # snap to surface
    vs = markers - nbrs
    vs /= np.linalg.norm(vs, axis=1, keepdims=True)
    markers -= np.sqrt(dist2)[:, np.newaxis] * vs
    return markers, normals
示例#8
0
def calculate_rmse_and_density(ground_truth_mesh, cropped_pointcloud,
                               depth_scale, camera_angle):
    # need to get the reference mesh and the pointcloud in the same units
    squared_distances, _, _ = pymesh.distance_to_mesh(
        ground_truth_mesh.reference_mesh,
        np.asarray(cropped_pointcloud.points) / depth_scale)

    rmse = np.sqrt(np.sum(squared_distances) / len(squared_distances))
    distance_thresh = 2  # mm of distance
    threshold = distance_thresh**2
    num_valid_pixels = len(squared_distances[squared_distances < threshold])

    valid_pattern_surface_area = ground_truth_mesh.get_pattern_surface_area(
        camera_angle=camera_angle)

    return rmse, num_valid_pixels / valid_pattern_surface_area
示例#9
0
 def preprocess_to_find_kp_uv(
     self,
     kp3d,
     faces,
     verts,
     verts_sphere,
 ):
     mesh = pymesh.form_mesh(verts, faces)
     dist, face_ind, closest_pts = pymesh.distance_to_mesh(mesh, kp3d)
     dist_to_verts = np.square(kp3d[:, None, :] - verts[None, :, :]).sum(-1)
     closest_pts = closest_pts / np.linalg.norm(
         closest_pts, axis=1, keepdims=1)
     min_inds = np.argmin(dist_to_verts, axis=1)
     kp_verts_sphere = verts_sphere[min_inds]
     kp_uv = geom_utils.convert_3d_to_uv_coordinates(closest_pts)
     return kp_uv
def measure_distances_on_surface_non_registered_pymesh(
        source_obj_file,
        destination_obj_file,
        measure_on_source_vertices=ALL_POINTS):
    import pymesh

    #source_mesh = np.array(read_mesh(source_obj_file))
    destination_mesh = pymesh.load_mesh(destination_obj_file)
    #for index_source in range(len(source_mesh)):
    # if index in list of given vertices or if list empty measure all distances
    #	if (measure_on_source_vertices==ALL_POINTS or index_source in measure_on_source_vertices):
    if measure_on_source_vertices == ALL_POINTS:
        measure_on_source_vertices = range(destination_mesh.num_vertices)
    source_points = get_vertex_positions(source_obj_file,
                                         measure_on_source_vertices)
    squared_distances, face_indices, closest_points = pymesh.distance_to_mesh(
        destination_mesh, source_points)
    distances = [math.sqrt(d2) for d2 in squared_distances]
    return distances
示例#11
0
def voxelized_pointcloud_boundary_sampling(path, sigmas, res, inp_points,
                                           sample_points):
    try:
        out_voxelization_file = path + '/voxelized_point_cloud_{}res_{}points.npz'.format(
            res, inp_points)

        off_path = path + '/mesh.off'
        mesh = trimesh.load(off_path)
        py_mesh = pymesh.load_mesh(off_path)

        # =====================
        # Voxelized point cloud
        # =====================

        if not os.path.exists(out_voxelization_file):

            bb_min = -0.5
            bb_max = 0.5

            point_cloud = mesh.sample(inp_points)

            # Grid Points used for computing occupancies
            grid_points = create_grid_points_from_bounds(
                bb_min, bb_max, args.res)

            # KDTree creation for fast querying nearest neighbour to points on the point cloud
            kdtree = KDTree(grid_points)
            _, idx = kdtree.query(point_cloud)

            occupancies = np.zeros(len(grid_points), dtype=np.int8)
            occupancies[idx] = 1
            compressed_occupancies = np.packbits(occupancies)

            np.savez(out_voxelization_file,
                     point_cloud=point_cloud,
                     compressed_occupancies=compressed_occupancies,
                     bb_min=bb_min,
                     bb_max=bb_max,
                     res=res)
            print('Finished Voxelized point cloud {}'.format(path))

        # ==================
        # Boundary Sampling
        # ==================
        for sigma in sigmas:
            out_sampling_file = path + '/pymesh_boundary_{}_samples.npz'.format(
                sigma)

            if not os.path.exists(out_sampling_file):
                points = mesh.sample(sample_points)
                if sigma == 0:
                    boundary_points = points
                else:
                    boundary_points = points + sigma * np.random.randn(
                        sample_points, 3)

                # Transform the boundary points to grid coordinates
                grid_coords = boundary_points.copy()
                grid_coords[:,
                            0], grid_coords[:,
                                            2] = boundary_points[:,
                                                                 2], boundary_points[:,
                                                                                     0]

                grid_coords = 2 * grid_coords

                # distance field calculation
                if sigma == 0:
                    df = np.zeros(boundary_points.shape[0])
                else:
                    df = np.sqrt(
                        pymesh.distance_to_mesh(py_mesh, boundary_points)[0])
                np.savez(out_sampling_file,
                         points=boundary_points,
                         df=df,
                         grid_coords=grid_coords)

                print(
                    'Finished boundary sampling {}'.format(out_sampling_file))

    except Exception as err:
        print('Error with {}: {}'.format(path, traceback.format_exc()))
示例#12
0
 def contains_point(self, x, epsilon=0.001):
     (dists, faces, pts) = pymesh.distance_to_mesh(self.component_mesh, [x])
     if dists == 0.0:
         return True
     return False
示例#13
0
def map_shape_to_ico_sphere(mapping, uv_map_size=(1001, 1001), transform=None):
    vshape = mapping['vshape'].transpose(1,0).astype(np.float)
    vsphere = mapping['vsphere'].transpose(1,0).astype(np.float)
    faces = mapping['face'].transpose(1,0).astype(np.int)
    if transform is not None:
        vshape = np.dot(vshape, transform.transpose())

   
    mesh_sphere = pymesh.form_mesh(vsphere, faces)
    mesh_shape = pymesh.form_mesh(vshape, faces)

    pymesh.meshio.save_mesh('test_shape.obj', mesh_shape)
    pymesh.meshio.save_mesh('test_sphere.obj', mesh_sphere)
    # # pdb.set_trace()
    map_H = uv_map_size[1]
    map_W = uv_map_size[0]
    x = np.arange(0, 1 + 1.0/(map_W-1), 1.0/(map_W-1))
    y = np.arange(0, 1 + 1.0/(map_H-1), 1.0/(map_H-1))

    xx, yy = np.meshgrid(x, y, indexing='xy')

    map_uv = np.stack([xx, yy], axis=2)
    map_uv  = map_uv.reshape(-1, 2)
    map_3d = geom_utils.convert_uv_to_3d_coordinates(map_uv)
    
    # fig = plt.figure()
    # ax = fig.add_subplot(111, projection='3d')
    dist , face_inds, closest_points = pymesh.distance_to_mesh(mesh_sphere, map_3d)
    # face_ind = face_inds.reshape(map_H, map_W,)
    
    barycentric_coordinates = convert_to_barycentric_coordinates(faces, vsphere, face_inds, map_3d)
    
    # ax.scatter(map_3d[:,0],map_3d[:,1], map_3d[:,2] ,'r')
    # ax.set_xlabel('X Label')
    # ax.set_ylabel('Y Label')
    # ax.set_zlabel('Z Label')
    uv_cords = geom_utils.convert_3d_to_uv_coordinates(vsphere)

    # plt.savefig('3d_uv_points.png')
    face_verts_ind = faces[face_inds]
    uv_vertsA =  uv_cords[face_verts_ind[:, 0]]
    uv_vertsB =  uv_cords[face_verts_ind[:, 1]]
    uv_vertsC =  uv_cords[face_verts_ind[:, 2]]
    
    new_uv = uv_vertsA*barycentric_coordinates[:,None,0] + uv_vertsB*barycentric_coordinates[:,None, 0] + uv_vertsC*barycentric_coordinates[:,None, 2]
    dist = dist.reshape(map_H, map_W)
    barycentric_coordinates = barycentric_coordinates.reshape(map_H, map_W, 3)
    face_inds = face_inds.reshape(map_H, map_W)
    map_3d = map_3d.reshape(map_H, map_W, -1)
    closest_points = closest_points.reshape(map_H, map_W, -1)
    map_uv = map_uv.reshape(map_H, map_W,2)
    stuff = {}
    stuff['map_3d'] = map_3d
    stuff['sphere_verts'] = vsphere 
    stuff['verts'] = vshape
    stuff['faces'] = faces
    stuff['uv_verts'] = uv_cords
    stuff['uv_map'] = map_uv
    stuff['bary_cord'] = barycentric_coordinates
    stuff['face_inds'] = face_inds

    return stuff
示例#14
0
def map_cmr_mean_to_uv_image(uv_map_size=(1001, 1001)):
    cub_cache_dir = '/home/nileshk/CMR/birds3d/cachedir/cub/'
    anno_sfm_path = osp.join(cub_cache_dir, 'sfm', 'anno_train.mat')
    anno_sfm = sio.loadmat(anno_sfm_path, struct_as_record=False, squeeze_me=True)
    sfm_mean_shape = (np.transpose(anno_sfm['S']), anno_sfm['conv_tri']-1)
    cmr_mean_shape_path = osp.join('/home/nileshk/CorrespNet/icn/cachedir/cub/', 'uv', 'cmr_mean.mat')
    cmr_mean = sio.loadmat(cmr_mean_shape_path)
    verts_proj, faces = cmr_mean['verts'].squeeze(), cmr_mean['faces'].squeeze()
    bird_mesh = pymesh.form_mesh(verts_proj, faces)
    # pymesh.meshio.save_mesh('cmr_bird.obj', bird_mesh)
    
    verts = verts_sphere = verts_proj/np.linalg.norm(verts_proj, axis=1, keepdims=True)
    mesh_sphere = pymesh.form_mesh(verts_sphere, faces)
    # pymesh.meshio.save_mesh('cmr_sphere.obj', sphere_mesh)
    # verts, faces = create_sphere(3)
    # verts_proj = project_verts_on_mesh(verts, sfm_mean_shape[0], sfm_mean_shape[1])
    # mesh_sphere = pymesh.form_mesh(verts, faces)
    # mesh_shape = pymesh.form_mesh(verts_proj, faces)

    # pymesh.meshio.save_mesh('test_sphere.obj', mesh_sphere)
    # uv = np.zeros((uv_map_size[1], uv_map_size[0], 2))
    # u_step = 1.0/1920
    # v_step = 1.0/960
    map_H = uv_map_size[1]
    map_W = uv_map_size[0]
    x = np.arange(0, 1 + 1.0/(map_W-1), 1.0/(map_W-1))
    y = np.arange(0, 1 + 1.0/(map_H-1), 1.0/(map_H-1))

    xx, yy = np.meshgrid(x, y, indexing='xy')

    map_uv = np.stack([xx, yy], axis=2)
    map_uv  = map_uv.reshape(-1, 2)
    map_3d = geom_utils.convert_uv_to_3d_coordinates(map_uv)
    
    # # fig = plt.figure()
    # # ax = fig.add_subplot(111, projection='3d')
    dist , face_inds, closest_points = pymesh.distance_to_mesh(mesh_sphere, map_3d)
    # # face_ind = face_inds.reshape(map_H, map_W,)
    
    barycentric_coordinates = convert_to_barycentric_coordinates(faces, verts, face_inds, map_3d)
    
    # # ax.scatter(map_3d[:,0],map_3d[:,1], map_3d[:,2] ,'r')
    # # ax.set_xlabel('X Label')
    # # ax.set_ylabel('Y Label')
    # # ax.set_zlabel('Z Label')
    uv_cords = geom_utils.convert_3d_to_uv_coordinates(verts)
    # # plt.savefig('3d_uv_points.png')
    # face_verts_ind = faces[face_inds]
    # uv_vertsA =  uv_cords[face_verts_ind[:, 0]]
    # uv_vertsB =  uv_cords[face_verts_ind[:, 1]]
    # uv_vertsC =  uv_cords[face_verts_ind[:, 2]]
    
    # new_uv = uv_vertsA*barycentric_coordinates[:,None,0] + uv_vertsB*barycentric_coordinates[:,None, 0] + uv_vertsC*barycentric_coordinates[:,None, 2]
    # dist = dist.reshape(map_H, map_W)
    # barycentric_coordinates = barycentric_coordinates.reshape(map_H, map_W, 3)
    # map_3d = map_3d.reshape(map_H, map_W, -1)
    # closest_points = closest_points.reshape(map_H, map_W, -1)

    face_inds = face_inds.reshape(map_H, map_W)
    map_uv = map_uv.reshape(map_H, map_W,2)
    stuff = {}
    stuff['sphere_verts'] = verts 
    stuff['verts'] = verts_proj 
    stuff['faces'] = faces
    stuff['uv_verts'] = uv_cords
    stuff['uv_map'] = map_uv
    # stuff['bary_cord'] = barycentric_coordinates
    stuff['face_inds'] = face_inds

    stuff['S'] = sfm_mean_shape[0]
    stuff['conv_tri'] = sfm_mean_shape[1]
    return stuff
示例#15
0
def map_shape_to_ico_sphere(uv_map_size=(1001, 1001)):
    p3d_cache_dir = '/home/nileshk/CorrespNet/icn/cachedir/p3d/'
    anno_sfm_path = osp.join(p3d_cache_dir, 'sfm', '{}_train.mat'.format(p3d_class))
    anno_sfm = sio.loadmat(anno_sfm_path, struct_as_record=False, squeeze_me=True)
    sfm_mean_shape = (np.transpose(anno_sfm['S']), anno_sfm['conv_tri']-1)
    verts, faces = create_sphere(3)
    verts_proj = project_verts_on_mesh(verts, sfm_mean_shape[0], sfm_mean_shape[1])
    mesh_sphere = pymesh.form_mesh(verts, faces)
    mesh_shape = pymesh.form_mesh(verts_proj, faces)

    # pymesh.meshio.save_mesh('test_sphere.obj', mesh_sphere)
    # pymesh.meshio.save_mesh('{}.obj'.format(p3d_class), mesh_shape)
    # uv = np.zeros((uv_map_size[1], uv_map_size[0], 2))
    # u_step = 1.0/1920
    # v_step = 1.0/960
    map_H = uv_map_size[1]
    map_W = uv_map_size[0]
    x = np.arange(0, 1 + 1.0/(map_W-1), 1.0/(map_W-1))
    y = np.arange(0, 1 + 1.0/(map_H-1), 1.0/(map_H-1))

    xx, yy = np.meshgrid(x, y, indexing='xy')

    map_uv = np.stack([xx, yy], axis=2)
    map_uv  = map_uv.reshape(-1, 2)
    map_3d = geom_utils.convert_uv_to_3d_coordinates(map_uv)
    
    # fig = plt.figure()
    # ax = fig.add_subplot(111, projection='3d')
    dist , face_inds, closest_points = pymesh.distance_to_mesh(mesh_sphere, map_3d)
    # face_ind = face_inds.reshape(map_H, map_W,)
    
    barycentric_coordinates = convert_to_barycentric_coordinates(faces, verts, face_inds, map_3d)
    
    # ax.scatter(map_3d[:,0],map_3d[:,1], map_3d[:,2] ,'r')
    # ax.set_xlabel('X Label')
    # ax.set_ylabel('Y Label')
    # ax.set_zlabel('Z Label')
    uv_cords = geom_utils.convert_3d_to_uv_coordinates(verts)
    # plt.savefig('3d_uv_points.png')
    face_verts_ind = faces[face_inds]
    uv_vertsA =  uv_cords[face_verts_ind[:, 0]]
    uv_vertsB =  uv_cords[face_verts_ind[:, 1]]
    uv_vertsC =  uv_cords[face_verts_ind[:, 2]]
    
    new_uv = uv_vertsA*barycentric_coordinates[:,None,0] + uv_vertsB*barycentric_coordinates[:,None, 0] + uv_vertsC*barycentric_coordinates[:,None, 2]
    dist = dist.reshape(map_H, map_W)
    barycentric_coordinates = barycentric_coordinates.reshape(map_H, map_W, 3)
    face_inds = face_inds.reshape(map_H, map_W)
    map_3d = map_3d.reshape(map_H, map_W, -1)
    closest_points = closest_points.reshape(map_H, map_W, -1)
    map_uv = map_uv.reshape(map_H, map_W,2)
    stuff = {}
    stuff['sphere_verts'] = verts 
    stuff['verts'] = verts_proj 
    # stuff['verts'] = verts ## Deliberate change, restore later
    stuff['faces'] = faces
    stuff['uv_verts'] = uv_cords
    stuff['uv_map'] = map_uv
    stuff['bary_cord'] = barycentric_coordinates
    stuff['face_inds'] = face_inds

    stuff['S'] = sfm_mean_shape[0]
    stuff['conv_tri'] = sfm_mean_shape[1]
    return stuff
示例#16
0
def compute_vertices_to_mesh_distances(groundtruth_vertices,
                                       grundtruth_landmark_points,
                                       predicted_mesh_vertices,
                                       predicted_mesh_faces,
                                       predicted_mesh_landmark_points,
                                       out_filename):
    """
    This script computes the reconstruction error between an input mesh and a ground truth mesh.
    :param groundtruth_vertices: An n x 3 numpy array of vertices from a ground truth scan.
    :param grundtruth_landmark_points: A 7 x 3 list with annotations of the ground truth scan.
    :param predicted_mesh_vertices: An m x 3 numpy array of vertices from a predicted mesh.
    :param predicted_mesh_faces: A k x 3 numpy array of vertex indices composing the predicted mesh.
    :param predicted_mesh_landmark_points: A 7 x 3 list containing the annotated 3D point locations in the predicted mesh.
    :param out_filename: Filename to write the resulting distances to (e.g. F1008_A_distances.txt).
    :return: A list of distances (errors), one for each vertex in the groundtruth mesh, and the associated vertex index in the ground truth scan.

    The grundtruth_landmark_points and predicted_mesh_landmark_points have to contain points in the following order:
    (1) right eye outer corner, (2) right eye inner corner, (3) left eye inner corner, (4) left eye outer corner,
    (5) nose bottom, (6) right mouth corner, (7) left mouth corner.
    """

    # Do procrustes based on the 7 points:
    # The ground truth scan is in mm, so by aligning the prediction to the ground truth, we get meaningful units.
    d, Z, tform = procrustes(np.array(grundtruth_landmark_points),
                             np.array(predicted_mesh_landmark_points),
                             scaling=True,
                             reflection='best')
    # Use tform to transform all vertices in predicted_mesh_vertices to the ground truth reference space:
    predicted_mesh_vertices_aligned = []
    for v in predicted_mesh_vertices:
        s = tform['scale']
        R = tform['rotation']
        t = tform['translation']
        transformed_vertex = s * np.dot(v, R) + t
        predicted_mesh_vertices_aligned.append(transformed_vertex)

    # Compute the mask: A circular area around the center of the face. Take the nose-bottom and go upwards a bit:
    nose_bottom = np.array(grundtruth_landmark_points[4])
    nose_bridge = (np.array(grundtruth_landmark_points[1]) + np.array(
        grundtruth_landmark_points[2])) / 2  # between the inner eye corners
    face_centre = nose_bottom + 0.3 * (nose_bridge - nose_bottom)
    # Compute the radius for the face mask:
    outer_eye_dist = np.linalg.norm(
        np.array(grundtruth_landmark_points[0]) -
        np.array(grundtruth_landmark_points[3]))
    nose_dist = np.linalg.norm(nose_bridge - nose_bottom)
    mask_radius = 1.2 * (outer_eye_dist + nose_dist) / 2

    # Find all the vertex indices in the ground truth scan that lie within the mask area:
    vertex_indices_mask = [
    ]  # vertex indices in the source mesh (the ground truth scan)
    points_on_groundtruth_scan_to_measure_from = []
    for vertex_idx, vertex in enumerate(groundtruth_vertices):
        dist = np.linalg.norm(
            vertex - face_centre
        )  # We use Euclidean distance for the mask area for now.
        if dist <= mask_radius:
            vertex_indices_mask.append(vertex_idx)
            points_on_groundtruth_scan_to_measure_from.append(vertex)
    assert len(vertex_indices_mask) == len(
        points_on_groundtruth_scan_to_measure_from)

    # For each vertex on the ground truth mesh, find the closest point on the surface of the predicted mesh:
    predicted_mesh_pymesh = pymesh.meshio.form_mesh(
        np.array(predicted_mesh_vertices_aligned), predicted_mesh_faces)
    squared_distances, face_indices, closest_points = pymesh.distance_to_mesh(
        predicted_mesh_pymesh, points_on_groundtruth_scan_to_measure_from)
    distances = [sqrt(d2) for d2 in squared_distances]

    # Save the distances to a file, alongside with each vertex id of the ground truth scan that the distance has been computed for:
    with open(out_filename, 'w') as csv_file:
        wr = csv.writer(csv_file, delimiter=' ')
        vertex_indices_with_distances = [
            [v_idx, v] for v_idx, v in zip(vertex_indices_mask, distances)
        ]
        wr.writerows(vertex_indices_with_distances)