def load_VTK(filename, name=None): """Loads VTK file format in the legacy format (vtk file extension). It relies on the reader from the VTK library. Parameters ---------- filename: str name of the meh file on disk Returns ------- Mesh the loaded mesh Note ---- VTU files have a 0-indexing """ _check_file(filename) vtk = import_optional_dependency("vtk") reader = vtk.vtkPolyDataReader() reader.SetFileName(filename) reader.Update() vtk_mesh = reader.GetOutput() vertices, faces = _dump_vtk(vtk_mesh) return Mesh(vertices, faces, name)
def _build_vtkPolyData(vertices, faces): """Builds a vtkPolyData object from vertices and faces""" vtk = import_optional_dependency("vtk") # Create a vtkPoints object and store the points in it points = vtk.vtkPoints() for point in vertices: points.InsertNextPoint(point) # Create a vtkCellArray to store faces cell_array = vtk.vtkCellArray() for face_ids in faces: if face_ids[0] == face_ids[-1]: # Triangle curface = face_ids[:3] vtk_face = vtk.vtkTriangle() else: # Quadrangle curface = face_ids[:4] vtk_face = vtk.vtkQuad() for idx, id in enumerate(curface): vtk_face.GetPointIds().SetId(idx, id) cell_array.InsertNextCell(vtk_face) polydata_mesh = vtk.vtkPolyData() polydata_mesh.SetPoints(points) polydata_mesh.SetPolys(cell_array) return polydata_mesh
def write_VTP(filename, vertices, faces): """Writes .vtp file format for the Paraview (Kitware (c)) visualisation software. It relies on the VTK library for its writer. VTP files use the last XML file format of the VTK library and correspond to polydata. Parameters ---------- filename: str name of the mesh file to be written on disk vertices: ndarray numpy array of the coordinates of the mesh's nodes faces: ndarray numpy array of the faces' nodes connectivities """ vtk = import_optional_dependency("vtk") writer = vtk.vtkXMLPolyDataWriter() writer.SetDataModeToAscii() writer.SetFileName(filename) polydata = _build_vtkPolyData(vertices, faces) writer.SetInputData(polydata) writer.Write()
def load_WRL(filename, name=None): """Loads VRML 2.0 mesh files. Parameters ---------- filename: str name of the mesh file on disk Returns ------- Mesh the loaded mesh """ import re vtk = import_optional_dependency("vtk") _check_file(filename) # Checking version with open(filename, 'r') as f: line = f.readline() ver = re.search(r'#VRML\s+V(\d.\d)', line).group(1) if not ver == '2.0': raise NotImplementedError('VRML loader only supports VRML 2.0 format (version %s given)' % ver) importer = vtk.vtkVRMLImporter() importer.SetFileName(str(filename)) importer.Update() actors = importer.GetRenderer().GetActors() actors.InitTraversal() dataset = actors.GetNextActor().GetMapper().GetInput() return _dump_vtk(dataset)
def plot_shape(self): """Plot the structure of the matrix using matplotlib.""" matplotlib = import_optional_dependency("matplotlib") plt = matplotlib.pyplot plt.figure() for patch in self._patches((0, 0)): plt.gca().add_patch(patch) plt.axis('equal') plt.xlim(0, self.shape[1]) plt.ylim(0, self.shape[0]) plt.gca().invert_yaxis()
def load_STL(filename, name=None): """Loads STL file format. It relies on the reader from the VTK library. As STL file format maintains a redundant set of vertices for each faces of the mesh, it returns a merged list of nodes and connectivity array by using the merge_duplicates function. Parameters ---------- filename: str name of the meh file on disk Returns ------- Mesh the loaded mesh Note ---- STL files have a 0-indexing """ vtk = import_optional_dependency("vtk") from capytaine.meshes.quality import merge_duplicate_rows _check_file(filename) reader = vtk.vtkSTLReader() reader.SetFileName(filename) reader.Update() data = reader.GetOutputDataObject(0) nv = data.GetNumberOfPoints() vertices = np.zeros((nv, 3), dtype=float) for k in range(nv): vertices[k] = np.array(data.GetPoint(k)) nf = data.GetNumberOfCells() faces = np.zeros((nf, 4), dtype=int) for k in range(nf): cell = data.GetCell(k) if cell is not None: for l in range(3): faces[k][l] = cell.GetPointId(l) faces[k][3] = faces[k][ 0] # always repeating the first node as stl is triangle only # Merging duplicates nodes vertices, new_id = merge_duplicate_rows(vertices) faces = new_id[faces] return Mesh(vertices, faces, name)
def _build_vtkUnstructuredGrid(vertices, faces): """Internal function that builds a VTK object for manipulation by the VTK library. Parameters ---------- vertices: ndarray numpy array of the coordinates of the mesh's nodes faces: ndarray numpy array of the faces' nodes connectivities Returns ------- vtkObject """ vtk = import_optional_dependency("vtk") nv = max(np.shape(vertices)) nf = max(np.shape(faces)) vtk_mesh = vtk.vtkUnstructuredGrid() vtk_mesh.Allocate(nf, nf) # Building the vtkPoints data structure vtk_points = vtk.vtkPoints() vtk_points.SetNumberOfPoints(nv) for idx, vertex in enumerate(vertices): vtk_points.SetPoint(idx, vertex) vtk_mesh.SetPoints(vtk_points) # Storing the points into vtk_mesh # Building the vtkCell data structure for cell in faces: if cell[-1] in cell[:-1]: vtk_cell = vtk.vtkTriangle() nc = 3 else: # #print 'quadrangle' vtk_cell = vtk.vtkQuad() nc = 4 for k in range(nc): vtk_cell.GetPointIds().SetId(k, cell[k]) vtk_mesh.InsertNextCell(vtk_cell.GetCellType(), vtk_cell.GetPointIds()) return vtk_mesh
def compute_quadrature(self, method): quadpy = import_optional_dependency("quadpy") transform = quadpy.ncube._helpers.transform get_detJ = quadpy.ncube._helpers.get_detJ if method is None: if 'quadrature' in self.__internals__: del self.__internals__['quadrature'] del self.__internals__['quadrature_method'] else: pass elif isinstance(method, quadpy.quadrilateral._helpers.QuadrilateralScheme): points = np.empty((self.nb_faces, len(method.points), 3)) weights = np.empty((self.nb_faces, len(method.points))) self.heal_triangles() for i_face in range(self.nb_faces): ref = self.vertices[self.faces[i_face, 0], :] A, B, C, D = self.vertices[self.faces[i_face, :], :] - ref n = self.faces_normals[i_face, :] ex = (B - A) / norm(B - A) ez = n / norm(n) ey = np.cross(ex, ez) R = np.array([ex, ey, ez]) quadrilateral = np.array([[R @ A, R @ D], [R @ B, R @ C]])[:, :, :2] quadpoints = transform(method.points.T, quadrilateral) quadpoints = np.concatenate( [quadpoints, np.zeros((quadpoints.shape[0], 1))], axis=1) quadpoints = np.array([R.T @ p for p in quadpoints]) + ref points[i_face, :, :] = quadpoints weights[i_face, :] = method.weights * abs( get_detJ(method.points.T, quadrilateral)) self.__internals__['quadrature'] = (points, weights) self.__internals__['quadrature_method'] = method else: raise NotImplementedError
def write_VTU(filename, vertices, faces): """Writes .vtu file format for the paraview (Kitware (c)) visualisation software. It relies on the VTK library for its writer. VTU files use the last XML file format of the VTK library. Parameters ---------- filename: str name of the mesh file to be written on disk vertices: ndarray numpy array of the coordinates of the mesh's nodes faces: ndarray numpy array of the faces' nodes connectivities """ vtk = import_optional_dependency("vtk") writer = vtk.vtkXMLUnstructuredGridWriter() writer.SetDataModeToAscii() writer.SetFileName(filename) unstructured_grid = _build_vtkUnstructuredGrid(vertices, faces) writer.SetInputData(unstructured_grid) writer.Write()
#!/usr/bin/env python # coding: utf-8 """Tools for 3D displays with VTK.""" # Copyright (C) 2017-2019 Matthieu Ancellin # See LICENSE file at <https://github.com/mancellin/capytaine> from typing import Union from capytaine.meshes.meshes import Mesh from capytaine.meshes.collections import CollectionOfMeshes from capytaine.tools.optional_imports import import_optional_dependency vtk = import_optional_dependency("vtk") def compute_vtk_polydata(mesh: Union[Mesh, CollectionOfMeshes]): """Transform a mesh into vtkPolydata.""" # Create a vtkPoints object and store the points in it points = vtk.vtkPoints() for point in mesh.vertices: points.InsertNextPoint(point) # Create a vtkCellArray to store faces faces = vtk.vtkCellArray() for face_ids in mesh.faces: if face_ids[0] == face_ids[-1]: # Triangle curface = face_ids[:3] vtk_face = vtk.vtkTriangle() else: # Quadrangle
def show_matplotlib(self, ax=None, normal_vectors=False, scale_normal_vector=None, saveas=None, **kwargs): """Poor man's viewer with matplotlib. Parameters ---------- ax: matplotlib axis The 3d axis in which to plot the mesh. If not provided, create a new one. normal_vector: bool If True, print normal vector. scale_normal_vector: array of shape (nb_faces, ) Scale separately each of the normal vectors. saveas: str file path where to save the image Other parameters are passed to Poly3DCollection. """ matplotlib = import_optional_dependency("matplotlib") plt = matplotlib.pyplot mpl_toolkits = import_optional_dependency("mpl_toolkits", package_name="matplotlib") Poly3DCollection = mpl_toolkits.mplot3d.art3d.Poly3DCollection default_axis = ax is None if default_axis: fig = plt.figure() ax = fig.add_subplot(111, projection="3d") faces = [] for face in self.faces: vertices = [] for index_vertex in face: vertices.append(self.vertices[int(index_vertex), :]) faces.append(vertices) if 'facecolors' not in kwargs: kwargs['facecolors'] = (0.3, 0.3, 0.3, 0.3) if 'edgecolor' not in kwargs: kwargs['edgecolor'] = 'k' ax.add_collection3d(Poly3DCollection(faces, **kwargs)) # Plot normal vectors. if normal_vectors: if scale_normal_vector is not None: vectors = (scale_normal_vector * self.faces_normals.T).T else: vectors = self.faces_normals ax.quiver(*zip(*self.faces_centers), *zip(*vectors), length=0.2) if default_axis: ax.set_xlabel("x") ax.set_ylabel("y") xmin, xmax, ymin, ymax, zmin, zmax = self.squared_axis_aligned_bbox ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) ax.set_zlim(zmin, zmax) if saveas is not None: plt.tight_layout() plt.savefig(saveas) else: plt.show()
def _patches(self, global_frame: Union[Tuple[int, int], np.ndarray] ): """Helper function for displaying the shape of the matrix. Recursively returns a list of rectangles representing the sub-blocks of the matrix. Uses BlockMatrix.display_color to assign color to the blocks. By default, it cycles through matplotlib default colors. But if display_color is redefined as a callable, it is called with the block as argument. Parameters ---------- global_frame: tuple of ints coordinates of the origin in the top left corner. Returns ------- list of matplotlib.patches.Rectangle """ matplotlib_patches = import_optional_dependency("matplotlib.patches", "matplotlib") Rectangle = matplotlib_patches.Rectangle all_blocks_in_flat_iterator = (block for line in self._stored_blocks for block in line) positions_of_all_blocks = self._stored_block_positions(global_frame=global_frame) patches = [] for block, positions_of_the_block in zip(all_blocks_in_flat_iterator, positions_of_all_blocks): position_of_first_appearance = positions_of_the_block[0] # Exchange coordinates: row index i -> y, column index j -> x position_of_first_appearance = np.array((position_of_first_appearance[1], position_of_first_appearance[0])) if isinstance(block, BlockMatrix): patches_of_this_block = block._patches(np.array((position_of_first_appearance[1], position_of_first_appearance[0]))) elif isinstance(block, np.ndarray): if isinstance(self.display_color, Iterator): color = next(self.display_color) elif callable(self.display_color): color = self.display_color(block) else: color = np.random.rand(3) patches_of_this_block = [Rectangle(position_of_first_appearance, block.shape[1], block.shape[0], edgecolor='k', facecolor=color)] elif isinstance(block, LowRankMatrix): if isinstance(self.display_color, Iterator): color = next(self.display_color) elif callable(self.display_color): color = self.display_color(block) else: color = np.random.rand(3) patches_of_this_block = [ # Left block Rectangle(position_of_first_appearance, block.left_matrix.shape[1], block.left_matrix.shape[0], edgecolor='k', facecolor=color), # Top block Rectangle(position_of_first_appearance, block.right_matrix.shape[1], block.right_matrix.shape[0], edgecolor='k', facecolor=color), # Rest of the matrix Rectangle(position_of_first_appearance, block.right_matrix.shape[1], block.left_matrix.shape[0], facecolor=color, alpha=0.2), ] else: raise NotImplementedError() patches.extend(patches_of_this_block) # For the other appearances, copy the patches of the first appearance for block_position in positions_of_the_block[1:]: block_position = np.array((block_position[1], block_position[0])) for patch in patches_of_this_block: # A block can be made of several patches. shift = block_position - position_of_first_appearance patch_position = np.array(patch.get_xy()) + shift patches.append(Rectangle(patch_position, patch.get_width(), patch.get_height(), facecolor=patch.get_facecolor(), alpha=0.2)) return patches
def show_matplotlib(self, ax=None, normal_vectors=False, scale_normal_vector=None, saveas=None, color_field=None, cmap=None, cbar_label=None, **kwargs): """Poor man's viewer with matplotlib. Parameters ---------- ax: matplotlib axis The 3d axis in which to plot the mesh. If not provided, create a new one. normal_vectors: bool If True, print normal vector. scale_normal_vector: array of shape (nb_faces, ) Scale separately each of the normal vectors. saveas: str File path where to save the image. color_field: array of shape (nb_faces, ) Scalar field to be plot on the mesh (optional). cmap: matplotlib colormap Colormap to use for field plotting. cbar_label: string Label for colormap Other parameters are passed to Poly3DCollection. """ matplotlib = import_optional_dependency("matplotlib") plt = matplotlib.pyplot cm = matplotlib.cm mpl_toolkits = import_optional_dependency("mpl_toolkits", package_name="matplotlib") Poly3DCollection = mpl_toolkits.mplot3d.art3d.Poly3DCollection default_axis = ax is None if default_axis: fig = plt.figure() ax = fig.add_subplot(111, projection="3d") faces = [] for face in self.faces: vertices = [] for index_vertex in face: vertices.append(self.vertices[int(index_vertex), :]) faces.append(vertices) if color_field is None: if 'facecolors' not in kwargs: kwargs['facecolors'] = "yellow" else: if cmap is None: cmap = cm.get_cmap('coolwarm') m = cm.ScalarMappable(cmap=cmap) m.set_array([min(color_field), max(color_field)]) m.set_clim(vmin=min(color_field), vmax=max(color_field)) colors = m.to_rgba(color_field) kwargs['facecolors'] = colors if 'edgecolor' not in kwargs: kwargs['edgecolor'] = 'k' ax.add_collection3d(Poly3DCollection(faces, **kwargs)) if color_field is not None: cbar = plt.colorbar(m) if cbar_label is not None: cbar.set_label(cbar_label) # Plot normal vectors. if normal_vectors: if scale_normal_vector is not None: vectors = (scale_normal_vector * self.faces_normals.T).T else: vectors = self.faces_normals ax.quiver(*zip(*self.faces_centers), *zip(*vectors), length=0.2) ax.set_xlabel("x") ax.set_ylabel("y") ax.set_zlabel("z") xmin, xmax, ymin, ymax, zmin, zmax = self.squared_axis_aligned_bbox ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) ax.set_zlim(zmin, zmax) if default_axis: if saveas is not None: plt.tight_layout() plt.savefig(saveas) else: plt.show()
def compute_quadrature(self, method): quadpy = import_optional_dependency("quadpy") transform = quadpy.c2.transform get_detJ = quadpy.cn._helpers.get_detJ if method is None: # No quadrature (i.e. default first order quadrature) if 'quadrature' in self.__internals__: del self.__internals__['quadrature'] del self.__internals__['quadrature_method'] else: pass elif isinstance(method, quadpy.c2._helpers.C2Scheme): assert method.points.shape[0] == method.dim == 2 nb_points = method.points.shape[1] points = np.empty((self.nb_faces, nb_points, 3)) weights = np.empty((self.nb_faces, nb_points)) self.heal_triangles() for i_face in range(self.nb_faces): # Define a local frame (Oxyz) such that # * the corner A of the quadrilateral panel is the origin of the local frame # * the edge AB of the quadrilateral panel is along the local x-axis, # * the quadrilateral panel is within the local xy-plane (that is, its normal is along the local z-axis). # Hence, the corners of the panels all have 0 as z-coordinate in the local frame. # Coordinates in global frame global_A, global_B, global_C, global_D = self.vertices[ self.faces[i_face, :], :] n = self.faces_normals[i_face, :] ex = (global_B - global_A) / norm( global_B - global_A) # unit vector of the local x-axis ez = n / norm(n) # unit vector of the local z-axis ey = np.cross( ex, ez ) # unit vector of the local y-axis, such that the basis is orthonormal R = np.array([ex, ey, ez]) local_A = np.zeros( (3, ) ) # coordinates of A in local frame, should be zero by construction local_B = R @ (global_B - global_A ) # coordinates of B in local frame local_C = R @ (global_C - global_A ) # coordinates of C in local frame local_D = R @ (global_D - global_A ) # coordinates of D in local frame local_quadrilateral = np.array([[local_A, local_D], [local_B, local_C]])[:, :, :-1] # Removing last index in last dimension because not interested in z-coordinate which is 0. local_quadpoints = transform(method.points, local_quadrilateral) local_quadpoints_in_3d = np.concatenate( [local_quadpoints, np.zeros((nb_points, 1))], axis=1) global_quadpoints = np.array( [R.T @ p for p in local_quadpoints_in_3d]) + global_A points[i_face, :, :] = global_quadpoints weights[i_face, :] = method.weights * 4 * np.abs( get_detJ(method.points, local_quadrilateral)) self.__internals__['quadrature'] = (points, weights) self.__internals__['quadrature_method'] = method else: raise NotImplementedError