def _average_distance(points, transform1, transform2, translate=True): assert points.shape == (points.shape[0], 3) assert transform1.shape == (4, 4) assert transform2.shape == (4, 4) points1 = tf.transform_points(points, transform1, translate=translate) points2 = tf.transform_points(points, transform2, translate=translate) add = np.linalg.norm(points1 - points2, axis=1).mean() kdtree = sklearn.neighbors.KDTree(points2) indices = kdtree.query(points1, return_distance=False)[:, 0] add_s = np.linalg.norm(points1 - points2[indices], axis=1).mean() return add, add_s
def callback(dt): if window.rotate: for widget in widgets.values(): if isinstance(widget, trimesh.viewer.SceneWidget): scene = widget.scene camera = scene.camera axis = tf.transform_points([[0, 1, 0]], camera.transform, translate=False)[0] camera.transform = tf.rotation_matrix( np.deg2rad(window.rotate), axis, point=scene.centroid, ) @ camera.transform widget.view['ball']._n_pose = camera.transform return if window.scenes_group and (window.next or window.play): try: scenes = next(window.scenes_group) for key, widget in widgets.items(): if isinstance(widget, trimesh.viewer.SceneWidget): widget.scene.geometry.update(scenes[key].geometry) widget.scene.graph.load( scenes[key].graph.to_edgelist()) widget._draw() elif isinstance(widget, glooey.Image): widget.set_image(numpy_to_image(scenes[key])) except StopIteration: window.play = False window.next = False
def get_reciprocal_slice(self, plane_normal: Tuple[int, int, int], distance: float = 0) -> ReciprocalSlice: """ Get a reciprocal slice through the Brillouin zone, defined by the intersection of a plane with the lattice. Args: plane_normal: The plane normal in fractional indices. E.g., ``(1, 0, 0)``. distance: The distance from the center of the Brillouin zone (the Gamma point). Returns: The reciprocal slice. """ cart_normal = np.dot(plane_normal, self.reciprocal_lattice) cart_center = cart_normal * distance # get the intersections with the faces intersections, _ = plane_lines(cart_center, cart_normal, self.lines.transpose(1, 0, 2)) if len(intersections) == 0: raise ValueError("Plane does not intersect reciprocal cell") # transform the intersections from 3D space to 2D coordinates transformation = plane_transform(origin=cart_center, normal=cart_normal) points = transform_points(intersections, transformation)[:, :2] return ReciprocalSlice(self, points, transformation)
def world_to_camera(self, points): """Transform points from world to camera coordinates. Useful for understanding where the objects are, as seen by the camera. :param points: either n * 3 array, or a single 3-vector """ points = np.atleast_2d(points) return tt.transform_points(points, self._world_to_camera_4x4)
def update_octree(self, instance_id): T_cad2cam_pred = self._Ts_cad2cam_pred[instance_id] class_id = self._class_ids[instance_id] # points = self._models.get_pcd(class_id=class_id) points = self._models.get_solid_voxel_grid(class_id=class_id).points points = ttf.transform_points(points, T_cad2cam_pred) self._mapping.update(instance_id, points)
def main(): models = morefusion.datasets.YCBVideoModels() points = models.get_pcd(class_id=2) quaternion_true = tf.random_quaternion() quaternion_pred = quaternion_true + [0.1, 0, 0, 0] transform_true = tf.quaternion_matrix(quaternion_true) transform_pred = tf.quaternion_matrix(quaternion_pred) scenes = {} for use_translation in [False, True]: if use_translation: translation_true = np.random.uniform(-0.02, 0.02, (3,)) translation_pred = np.random.uniform(-0.02, 0.02, (3,)) transform_true[:3, 3] = translation_true transform_pred[:3, 3] = translation_pred add = morefusion.metrics.average_distance( [points], [transform_true], [transform_pred] )[0][0] # --------------------------------------------------------------------- scene = trimesh.Scene() points_true = tf.transform_points(points, transform_true) colors = np.full((points_true.shape[0], 3), [1.0, 0, 0]) geom = trimesh.PointCloud(vertices=points_true, color=colors) scene.add_geometry(geom) points_pred = tf.transform_points(points, transform_pred) colors = np.full((points_true.shape[0], 3), [0, 0, 1.0]) geom = trimesh.PointCloud(vertices=points_pred, color=colors) scene.add_geometry(geom) scenes[f"use_translation: {use_translation}, add: {add}"] = scene if scenes: camera_transform = list(scenes.values())[0].camera_transform scene.camera_transform = camera_transform morefusion.extra.trimesh.display_scenes(scenes)
def camera_to_world(self, points, translate=True): """Transform points from camera to world coordinates. Useful for understanding where objects bound to camera (e.g., image pixels) are in the world. :param points: either n * 3 array, or a single 3-vector :param translate: if True, also translate the points """ points = np.atleast_2d(points) return tt.transform_points(points, self._camera_to_world_4x4, translate=translate)
def callback(dt): if window.rotate: for widget in widgets.values(): if isinstance(widget, trimesh.viewer.SceneWidget): axis = tf.transform_points( [[0, 1, 0]], widget.scene.camera_transform, translate=False, )[0] widget.scene.camera_transform[...] = (tf.rotation_matrix( np.deg2rad(window.rotate * rotation_scaling), axis, point=widget.scene.centroid, ) @ widget.scene.camera_transform) widget.view["ball"]._n_pose = widget.scene.camera_transform return if window.scenes_group and (window.next or window.play): try: scenes = next(window.scenes_group) clear = scenes.get("__clear__", False) or window._clear window._clear = False for key, widget in widgets.items(): scene = scenes[key] if isinstance(widget, trimesh.viewer.SceneWidget): assert isinstance(scene, trimesh.Scene) if clear: widget.clear() widget.scene = scene else: widget.scene.geometry.update(scene.geometry) widget.scene.graph.load(scene.graph.to_edgelist()) widget.scene.camera_transform[ ...] = scene.camera_transform widget.view[ "ball"]._n_pose = widget.scene.camera_transform widget._draw() elif isinstance(widget, glooey.Image): widget.set_image(numpy_to_image(scene)) except StopIteration: print("Reached the end of the scenes") window.play = False window.next = False
def __getitem__(self, index): # find shape that contains the point with given global index shape_ind, patch_ind = self.shape_index(index) def get_patch_points(shape, query_point): from source.base import point_cloud # optionally always pick the same points for a given patch index (mainly for debugging) if self.identical_epochs: self.rng.seed((self.seed + index) % (2**32)) patch_pts_ids = point_cloud.get_patch_kdtree( kdtree=shape.kdtree, rng=self.rng, query_point=query_point, patch_radius=self.patch_radius, points_per_patch=self.points_per_patch, n_jobs=1) # find -1 ids for padding patch_pts_pad_ids = patch_pts_ids == -1 patch_pts_ids[patch_pts_pad_ids] = 0 pts_patch_ms = shape.pts[patch_pts_ids, :] # replace padding points with query point so that they appear in the patch origin pts_patch_ms[patch_pts_pad_ids, :] = query_point patch_radius_ms = utils.get_patch_radii(pts_patch_ms, query_point)\ if self.patch_radius <= 0.0 else self.patch_radius pts_patch_ps = utils.model_space_to_patch_space( pts_to_convert_ms=pts_patch_ms, pts_patch_center_ms=query_point, patch_radius_ms=patch_radius_ms) return patch_pts_ids, pts_patch_ps, pts_patch_ms, patch_radius_ms shape = self.shape_cache.get(shape_ind) imp_surf_query_point_ms = shape.imp_surf_query_point_ms[patch_ind] # get neighboring points patch_pts_ids, patch_pts_ps, pts_patch_ms, patch_radius_ms = \ get_patch_points(shape=shape, query_point=imp_surf_query_point_ms) imp_surf_query_point_ps = utils.model_space_to_patch_space_single_point( imp_surf_query_point_ms, imp_surf_query_point_ms, patch_radius_ms) # surf dist can be None because we have no ground truth for evaluation # need a number or Pytorch will complain when assembling the batch if self.reconstruction: imp_surf_dist_ms = np.array([np.inf]) imp_surf_dist_sign_ms = np.array([np.inf]) else: imp_surf_dist_ms = shape.imp_surf_dist_ms[patch_ind] imp_surf_dist_sign_ms = np.sign(imp_surf_dist_ms) imp_surf_dist_sign_ms = 0.0 if imp_surf_dist_sign_ms < 0.0 else 1.0 if self.sub_sample_size > 0: pts_sub_sample_ms = utils.get_point_cloud_sub_sample( sub_sample_size=self.sub_sample_size, pts_ms=shape.pts, query_point_ms=imp_surf_query_point_ms, uniform=self.uniform_subsample) else: pts_sub_sample_ms = np.array([], dtype=np.float32) if not self.reconstruction: import trimesh.transformations as trafo # random rotation of shape and patch as data augmentation rand_rot = trimesh.transformations.random_rotation_matrix(self.rng.rand(3)) # rand_rot = trimesh.transformations.identity_matrix() pts_sub_sample_ms = \ trafo.transform_points(pts_sub_sample_ms, rand_rot).astype(np.float32) patch_pts_ps = \ trafo.transform_points(patch_pts_ps, rand_rot).astype(np.float32) imp_surf_query_point_ms = \ trafo.transform_points(np.expand_dims(imp_surf_query_point_ms, 0), rand_rot)[0].astype(np.float32) imp_surf_query_point_ps = \ trafo.transform_points(np.expand_dims(imp_surf_query_point_ps, 0), rand_rot)[0].astype(np.float32) patch_data = dict() # create new arrays to close the memory mapped files patch_data['patch_pts_ps'] = patch_pts_ps patch_data['patch_radius_ms'] = np.array(patch_radius_ms, dtype=np.float32) patch_data['pts_sub_sample_ms'] = pts_sub_sample_ms patch_data['imp_surf_query_point_ms'] = imp_surf_query_point_ms patch_data['imp_surf_query_point_ps'] = np.array(imp_surf_query_point_ps) patch_data['imp_surf_ms'] = np.array([imp_surf_dist_ms], dtype=np.float32) patch_data['imp_surf_magnitude_ms'] = np.array([np.abs(imp_surf_dist_ms)], dtype=np.float32) patch_data['imp_surf_dist_sign_ms'] = np.array([imp_surf_dist_sign_ms], dtype=np.float32) # un-comment to get a debug output of a training sample # import evaluation # evaluation.visualize_patch( # patch_pts_ps=patch_data['patch_pts_ps'], patch_pts_ms=pts_patch_ms, # query_point_ps=patch_data['imp_surf_query_point_ps'], # pts_sub_sample_ms=patch_data['pts_sub_sample_ms'], query_point_ms=patch_data['imp_surf_query_point_ms'], # file_path='debug/patch_local_and_global.ply') # patch_sphere = trimesh.primitives.Sphere(radius=self.patch_radius, center=imp_surf_query_point_ms) # patch_sphere.export(file_obj='debug/patch_sphere.ply') # print('Debug patch outputs with radius {} in "debug" dir'.format(self.patch_radius)) # convert to tensors for key in patch_data.keys(): patch_data[key] = torch.from_numpy(patch_data[key]) return patch_data
def extrude_triangulation(vertices, faces, height, cap=True, base=True, transform=None): """ Based on Trimesh extrude_triangulation, but allows to exclude cap and base. """ vertices = np.asanyarray(vertices, dtype=np.float64) height = float(height) faces = np.asanyarray(faces, dtype=np.int64) if not util.is_shape(vertices, (-1, 2)): raise ValueError('Vertices must be (n,2)') if not util.is_shape(faces, (-1, 3)): raise ValueError('Faces must be (n,3)') if np.abs(height) < constants.tol.merge: raise ValueError('Height must be nonzero!') # Make sure triangulation winding is pointing up normal_test = triangles.normals([util.stack_3D(vertices[faces[0]])])[0] normal_dot = np.dot(normal_test, [0.0, 0.0, np.sign(height)])[0] # Make sure the triangulation is aligned with the sign of # the height we've been passed if normal_dot < 0.0: faces = np.fliplr(faces) # stack the (n,3) faces into (3*n, 2) edges edges = geometry.faces_to_edges(faces) edges_sorted = np.sort(edges, axis=1) # Edges which only occur once are on the boundary of the polygon # since the triangulation may have subdivided the boundary of the # shapely polygon, we need to find it again edges_unique = grouping.group_rows(edges_sorted, require_count=1) # (n, 2, 2) set of line segments (positions, not references) boundary = vertices[edges[edges_unique]] # We are creating two vertical triangles for every 2D line segment # on the boundary of the 2D triangulation vertical = np.tile(boundary.reshape((-1, 2)), 2).reshape((-1, 2)) vertical = np.column_stack( (vertical, np.tile([0, height, 0, height], len(boundary)))) vertical_faces = np.tile([3, 1, 2, 2, 1, 0], (len(boundary), 1)) vertical_faces += np.arange(len(boundary)).reshape((-1, 1)) * 4 vertical_faces = vertical_faces.reshape((-1, 3)) # Stack the (n,2) vertices with zeros to make them (n, 3) vertices_3D = util.stack_3D(vertices) # A sequence of zero- indexed faces, which will then be appended # with offsets to create the final mesh if not base and not cap: vertices_seq = [vertical] faces_seq = [vertical_faces] elif not base and cap: vertices_seq = [vertices_3D.copy() + [0.0, 0, height], vertical] faces_seq = [faces.copy(), vertical_faces] elif base and not cap: vertices_seq = [vertices_3D, vertical] faces_seq = [faces[:, ::-1], vertical_faces] else: vertices_seq = [ vertices_3D, vertices_3D.copy() + [0.0, 0, height], vertical ] faces_seq = [faces[:, ::-1], faces.copy(), vertical_faces] # Append sequences into flat nicely indexed arrays vertices, faces = util.append_faces(vertices_seq, faces_seq) # Apply transform here to avoid later bookkeeping if transform is not None: vertices = transformations.transform_points(vertices, transform) # If the transform flips the winding flip faces back so that the normals will be facing outwards if transformations.flips_winding(transform): faces = np.ascontiguousarray( np.fliplr(faces)) # fliplr makes arrays non-contiguous # create mesh object with passed keywords mesh = Trimesh(vertices=vertices, faces=faces) return mesh
def _pcd_files_to_pts(pcd_files, pts_file_npy, pts_file, obj_locations, obj_rotations, min_pts_size=0, debug=False): """ Convert pcd blensor results to xyz or directly to npy files. Merge front and back scans. Moving the object instead of the camera because the point cloud is in some very weird space that behaves crazy when the camera moves. A full day wasted on this shit! :param pcd_files: :param pts_file_npy: :param pts_file: :param trafos_inv: :param debug: :return: """ import gzip def revert_offset(pts_data: np.ndarray, inv_offset: np.ndarray): pts_reverted = pts_data # don't just check the header because missing rays may be added with NaNs if pts_reverted.shape[0] > 0: pts_offset_correction = np.broadcast_to(inv_offset, pts_reverted.shape) pts_reverted += pts_offset_correction return pts_reverted # https://www.blensor.org/numpy_import.html def extract_xyz_from_blensor_numpy(arr_raw): # timestamp # yaw, pitch # distance,distance_noise # x,y,z # x_noise,y_noise,z_noise # object_id # 255*color[0] # 255*color[1] # 255*color[2] # idx hits = arr_raw[arr_raw[:, 3] != 0.0] # distance != 0.0 --> hit noisy_xyz = hits[:, [8, 9, 10]] return noisy_xyz pts_data_to_cat = [] for fi, f in enumerate(pcd_files): try: if f.endswith('.numpy.gz'): pts_data_vs = extract_xyz_from_blensor_numpy( np.loadtxt(gzip.GzipFile(f, "r"))) elif f.endswith('.numpy'): pts_data_vs = extract_xyz_from_blensor_numpy(np.loadtxt(f)) elif f.endswith('.pcd'): pts_data_vs, header_info = point_cloud.load_pcd(file_in=f) else: raise ValueError( 'Input file {} has an unknown format!'.format(f)) except EOFError as er: print('Error processing {}: {}'.format(f, er)) continue # undo coordinate system changes pts_data_vs = utils.right_handed_to_left_handed(pts_data_vs) # move back from camera distance, always along x axis obj_location = np.array(obj_locations[fi]) revert_offset(pts_data_vs, -obj_location) # get and apply inverse rotation matrix of camera scanner_rotation_inv = trafo.quaternion_matrix( trafo.quaternion_conjugate(obj_rotations[fi])) pts_data_ws_test_inv = trafo.transform_points(pts_data_vs, scanner_rotation_inv, translate=False) pts_data_ws = pts_data_ws_test_inv if pts_data_ws.shape[0] > 0: pts_data_to_cat += [pts_data_ws.astype(np.float32)] # debug outputs to check the rotations... the pointcloud MUST align exactly with the mesh if debug: point_cloud.write_xyz(file_path=os.path.join( 'debug', 'test_{}.xyz'.format(str(fi))), points=pts_data_ws) if len(pts_data_to_cat) > 0: pts_data = np.concatenate(tuple(pts_data_to_cat), axis=0) if pts_data.shape[0] > min_pts_size: point_cloud.write_xyz(file_path=pts_file, points=pts_data) np.save(pts_file_npy, pts_data)
K = camera.K print("\n# # Camera intrinsic") print(K) scene.show() # render scene from io import BytesIO img = scene.save_image(resolution=(640, 480)) from PIL import Image rendered = Image.open(BytesIO(img)).convert("RGB") rendered.save("rendered.jpg") # numpy version vertices = colors transformed = transform_points(vertices, transform) # transformation projected = np.matmul(transformed, K.T) # homogeneous projection xy = projected[:, :2] / projected[:, 2:] # make non-homogeneous print("\n# # Projected 2D points\n", xy) def show(xy): import matplotlib.pyplot as plt fig, ax = plt.subplots(2, 1) x, y = xy.T # ax = plt.gca() ax[0].scatter(x, y, c=colors) ax[1].imshow(rendered) plt.show()
def on_mouse_double_click(self, x, y): res = self._scene.camera.resolution fov_y = np.radians(self._scene.camera.fov[1] / 2.0) fov_x = fov_y * (res[0] / float(res[1])) half_fov = np.stack([fov_x, fov_y]) right_top = np.tan(half_fov) right_top *= 1 - (1.0 / res) left_bottom = -right_top right, top = right_top left, bottom = left_bottom xy_vec = tu.grid_linspace(bounds=[[left, top], [right, bottom]], count=res).astype(np.float64) pixels = tu.grid_linspace(bounds=[[0, 0], [res[0] - 1, res[1] - 1]], count=res).astype(np.int64) assert xy_vec.shape == pixels.shape transform = self._scene.camera_transform vectors = tu.unitize(np.column_stack((xy_vec, -np.ones_like(xy_vec[:, :1])))) vectors = tf.transform_points(vectors, transform, translate=False) origins = (np.ones_like(vectors) * tf.translation_from_matrix(transform)) indices = np.where(np.all(pixels == np.array([x, y]), axis=1)) if len(indices) > 0 and len(indices[0]) > 0: pixel_id = indices[0][0] ray_origin = np.expand_dims(origins[pixel_id], 0) ray_direction = np.expand_dims(vectors[pixel_id], 0) # print(x, y, pixel_id, ray_origin, ray_direction) mesh = self._scene.geometry['geometry_0'] locations, index_ray, index_tri = mesh.ray.intersects_location( ray_origins=ray_origin, ray_directions=ray_direction) if locations.size == 0: return ray_origins = np.tile(ray_origin, [locations.shape[0], 1]) distances = np.linalg.norm(locations - ray_origins, axis=1) idx = np.argsort(distances) # sort by disctances # color closest hit tri_color = mesh.visual.face_colors[index_tri[idx[0]]] if not np.alltrue(tri_color == [255, 0, 0, 255]): tri_color = [255, 0, 0, 255] else: # unselect triangle tri_color = [200, 200, 200, 255] mesh.visual.face_colors[index_tri[idx[0]]] = tri_color # collect clicked triangle ids tri_ids = np.where(np.all(mesh.visual.face_colors == [255, 0, 0, 255], axis=-1))[0] if len(tri_ids) >= self._settings_loader.min_triangles: # get center of triangles barycentric = mesh.triangles_center[tri_ids] joint_x = np.mean(barycentric[:, 0]) joint_y = np.mean(barycentric[:, 1]) joint_z = np.mean(barycentric[:, 2]) joint = np.stack([joint_x, joint_y, joint_z]) if 'joint_0' in self._scene.geometry: self._scene.delete_geometry('joint_0') joint = np.expand_dims(joint, 0) joint = PointCloud(joint, process=False) self._scene.add_geometry(joint, geom_name='joint_0') if self.view['rays']: from trimesh import load_path ray_visualize = load_path(np.hstack((ray_origin, ray_origin + ray_direction)).reshape(-1, 2, 3)) self._scene.add_geometry(ray_visualize, geom_name='cam_rays') # draw path where camera ray hits with mesh (only take 2 closest hits) path = np.hstack(locations[:2]).reshape(-1, 2, 3) ray_visualize = load_path(path) self._scene.add_geometry(ray_visualize, geom_name='cam_rays_hits')
def SplitByPlaneOneSide(mesh, plane_normal, plane_origin, cap=False, cached_dots=None, reversed=False, **kwargs): """ Slice a mesh with a plane, returning a new mesh that is the portion of the original mesh to the positive normal side of the plane Parameters --------- mesh : Trimesh object Source mesh to slice plane_normal : (3,) float Normal vector of plane to intersect with mesh plane_origin : (3,) float Point on plane to intersect with mesh cap : bool If True, cap the result with a triangulated polygon cached_dots : (n, 3) float If an external function has stored dot products pass them here to avoid recomputing kwargs : dict Passed to the newly created sliced mesh Returns ---------- new_vertices : (n, 3) float Vertices of sliced mesh new_faces : (n, 3) int Faces of sliced mesh """ # check input for none if mesh is None: return None # check input plane plane_normal = np.asanyarray(plane_normal, dtype=np.float64) plane_origin = np.asanyarray(plane_origin, dtype=np.float64) # check to make sure origins and normals have acceptable shape shape_ok = ( (plane_origin.shape == (3, ) or util.is_shape(plane_origin, (-1, 3))) and (plane_normal.shape == (3, ) or util.is_shape(plane_normal, (-1, 3))) and plane_origin.shape == plane_normal.shape) if not shape_ok: raise ValueError('plane origins and normals must be (n, 3)!') # start with copy of original mesh, faces, and vertices sliced_mesh = mesh.copy() vertices = mesh.vertices.copy() faces = mesh.faces.copy() # slice away specified planes for origin, normal in zip(plane_origin.reshape((-1, 3)), plane_normal.reshape((-1, 3))): # calculate dots here if not passed in to save time # in case of cap if cached_dots is None: # dot product of each vertex with the plane normal indexed by face # so for each face the dot product of each vertex is a row # shape is the same as faces (n,3) dots = np.einsum('i,ij->j', normal, (vertices - origin).T)[faces] else: dots = cached_dots # save the new vertices and faces vertices, faces = intersections.slice_faces_plane(vertices=vertices, faces=faces, plane_normal=normal, plane_origin=origin, cached_dots=dots) # check if cap arg specified if cap: # check if mesh is watertight (can't cap if not) if not sliced_mesh.is_watertight: raise ValueError('Input mesh must be watertight to cap slice') path = sliced_mesh.section(plane_normal=normal, plane_origin=origin) if not path: if reversed: return sliced_mesh else: return None # transform Path3D onto XY plane for triangulation on_plane, to_3D = path.to_planar() # triangulate each closed region of 2D cap # without adding any new vertices v, f = [], [] for polygon in on_plane.polygons_full: t = triangulate_polygon(polygon, triangle_args='pY', engine='triangle') v.append(t[0]) f.append(t[1]) if tol.strict: # in unit tests make sure that our triangulation didn't # insert any new vertices which would break watertightness from scipy.spatial import cKDTree # get all interior and exterior points on tree check = [np.array(polygon.exterior.coords)] check.extend(np.array(i.coords) for i in polygon.interiors) tree = cKDTree(np.vstack(check)) # every new vertex should be on an old vertex assert np.allclose(tree.query(v[-1])[0], 0.0) # append regions and reindex vf, ff = util.append_faces(v, f) # make vertices 3D and transform back to mesh frame vf = tf.transform_points(np.column_stack((vf, np.zeros(len(vf)))), to_3D) # check to see if our new faces are aligned with our normal #check = windings_aligned(vf[ff], normal) # ## if 50% of our new faces are aligned with the normal flip #if check.astype(np.float64).mean() > 0.5: # ff = np.fliplr(ff) # add cap vertices and faces and reindex vertices, faces = util.append_faces([vertices, vf], [faces, ff]) # Update mesh with cap (processing needed to merge vertices) sliced_mesh = Trimesh(vertices=vertices, faces=faces) vertices, faces = sliced_mesh.vertices.copy( ), sliced_mesh.faces.copy() return sliced_mesh