def gt_to_vtk(din): """ From the graph-tool property value type creates an equivalent vtkDataArray object. Args: din (str): graph-tool property value type Returns: vtkDataArray object """ # Check that a string object is passed if not isinstance(din, str): error_msg = 'str object required as input.' raise pexceptions.PySegInputError(expr='gt_to_vtk (TypesConverter)', msg=error_msg) if (din == 'bool') or (din == 'vector<bool>'): return vtk.vtkIntArray() # was vtk.vtkBitArray() elif (din == 'int16_t') or (din == 'vector<int16_t>'): return vtk.vtkTypeInt16Array() elif (din == 'int32_t') or (din == 'vector<int32_t>'): return vtk.vtkIntArray() elif (din == 'int64_t') or (din == 'vector<int64_t>'): return vtk.vtkTypeInt64Array() elif (din == 'double') or (din == 'vector<double>'): return vtk.vtkFloatArray() else: error_msg = 'Graph-tool alias not identified.' raise pexceptions.PySegInputError(expr='gt_to_vtk (TypesConverter)', msg=error_msg)
def gt_to_numpy(din): """ From the graph-tool property value type return an equivalent numpy data type. Args: din (str): graph-tool property value type Returns: numpy type """ # Check that a string object is passed if not isinstance(din, str): error_msg = 'str object required as input.' raise pexceptions.PySegInputError( expr='gt_to_numpy (TypesConverter)', msg=error_msg ) if (din == 'bool') or (din == 'vector<bool>'): return np.bool elif (din == 'int16_t') or (din == 'vector<int16_t>'): return np.int16 elif (din == 'int32_t') or (din == 'vector<int32_t>'): return np.int32 elif (din == 'int64_t') or (din == 'vector<int64_t>'): return np.int64 elif (din == 'double') or (din == 'vector<double>'): return np.float else: error_msg = 'Graph-tool alias not identified.' raise pexceptions.PySegInputError( expr='gt_to_numpy (TypesConverter)', msg=error_msg )
def get_target_voxels_in_membrane_mask(ribo_mask, mem_mask, verbose=False): """ Gets target voxels from a ribosome mask and pre-filters them to those that are inside a membrane mask (value 1). Prints out the target voxel numbers before and after filtering and warns of the voxels that are not inside the membrane mask. Args: ribo_mask (numpy.ndarray): a ribosome mask mem_mask (numpy.ndarray): a membrane mask verbose (boolean, optional): it True (default False), additionally prints out the target voxels before and after filtering Returns: a list of the target voxels that are inside the membrane mask as tuples in form (x, y, z) """ if (isinstance(ribo_mask, np.ndarray) and (len(ribo_mask.shape) == 3) and isinstance(mem_mask, np.ndarray) and (len(mem_mask.shape) == 3)): if ribo_mask.shape != mem_mask.shape: error_msg = ( 'Both input 3D numpy ndarray objects have to have the ' 'same scales.') raise pexceptions.PySegInputError( expr='get_target_voxels_in_membrane_mask', msg=error_msg) # Find the set of voxels of ribosome centers mapped on the membrane, # called 'target voxels' from now on: target_voxels = get_foreground_voxels_from_mask(ribo_mask) print '%s target voxels' % len(target_voxels) if verbose: print target_voxels target_voxels_in_membrane_mask = [] for target_voxel in target_voxels: if mem_mask[target_voxel[0], target_voxel[1], target_voxel[2]] == 1: target_voxels_in_membrane_mask.append(target_voxel) else: error_msg = ( 'Target voxel (%s, %s, %s) not inside the ' 'membrane!' % (target_voxel[0], target_voxel[1], target_voxel[2])) raise pexceptions.PySegInputWarning( expr='get_target_voxels_in_membrane_mask', msg=error_msg) print('%s target voxels in membrane' % len(target_voxels_in_membrane_mask)) if verbose: print target_voxels_in_membrane_mask return target_voxels_in_membrane_mask else: error_msg = ('3D numpy ndarray objects required as first and second ' 'input') raise pexceptions.PySegInputError( expr='get_target_voxels_in_membrane_mask', msg=error_msg)
def load_tomo(fname, mmap=False): """ Loads a tomogram in MRC, EM or VTI format and converts it into a numpy format. Args: fname (str): full path to the tomogram, has to end with '.mrc', '.em' or '.vti' mmap (boolean, optional): if True (default False) a numpy.memmap object is loaded instead of numpy.ndarray, which means that data are not loaded completely to memory, this is useful only for very large tomograms. Only valid with formats MRC and EM. VERY IMPORTANT: This subclass of ndarray has some unpleasant interaction with some operations, because it does not quite fit properly as a subclass of numpy.ndarray Returns: numpy.ndarray or numpy.memmap object """ # Input parsing stem, ext = os.path.splitext(fname) if mmap and (not (ext == '.mrc' or (ext == '.em'))): error_msg = ('mmap option is only valid for MRC or EM formats, current ' + ext) raise pexceptions.PySegInputError(expr='load_tomo', msg=error_msg) # if ext == '.fits': # im_data = pyfits.getdata(fname).transpose() elif ext == '.mrc': image = ImageIO() if mmap: image.readMRC(fname, memmap=mmap) else: image.readMRC(fname) im_data = image.data elif ext == '.em': image = ImageIO() if mmap: image.readEM(fname, memmap=mmap) else: image.readEM(fname) im_data = image.data elif ext == '.vti': reader = vtk.vtkXMLImageDataReader() reader.SetFileName(fname) reader.Update() im_data = vti_to_numpy(reader.GetOutput()) else: error_msg = '%s is non valid format.' % ext raise pexceptions.PySegInputError(expr='load_tomo', msg=error_msg) # For avoiding 2D arrays if len(im_data.shape) == 2: im_data = np.reshape(im_data, (im_data.shape[0], im_data.shape[1], 1)) return im_data
def foreground_neighbors_of_voxel(mask, voxel): """ Returns neighbor voxels with value 1 (foreground) of a given voxel inside a binary mask of a membrane segmentation. Args: mask (numpy.ndarray): a binary 3D mask voxel (tuple): voxel coordinates in the mask as a tuple of integers of length 3: (x, y, z) Returns: a list of tuples with neighbor voxels coordinates in format [(x1, y1, z1), (x2, y2, z2), ...] """ neighbor_voxels = [] if isinstance(mask, np.ndarray) and (len(mask.shape) == 3): if isinstance(voxel, tuple) and (len(voxel) == 3): x = voxel[0] y = voxel[1] z = voxel[2] x_size = mask.shape[0] y_size = mask.shape[1] z_size = mask.shape[2] # iterate over all possible (max 26) neighbor voxels # combinations for i in (x - 1, x, x + 1): for j in (y - 1, y, y + 1): for k in (z - 1, z, z + 1): # do not add the voxel itself and voxels outside the # border if ((i, j, k) != (x, y, z) and i in xrange(0, x_size) and j in xrange(0, y_size) and k in xrange(0, z_size)): # add only foreground voxels to the neighbors # list if mask[i, j, k] == 1: neighbor_voxels.append((i, j, k)) else: error_msg = ('A tuple of integers of length 3 required as the' 'second input.') raise pexceptions.PySegInputError( expr='foreground_neighbors_of_voxel (VoxelGraph)', msg=error_msg) else: error_msg = 'A 3D numpy ndarray required as the first input.' raise pexceptions.PySegInputError( expr='foreground_neighbors_of_voxel (VoxelGraph)', msg=error_msg) return neighbor_voxels
def vtk_to_numpy(din): """ From a vtkDataArray object returns an equivalent numpy data type. Args: din (vtk.vtkDataArray): input vtkDataArray object Returns: numpy type """ # Check that a type object is passed if not isinstance(din, vtk.vtkDataArray): error_msg = 'vtkDataArray object required as input.' raise pexceptions.PySegInputError( expr='vtk_to_numpy (TypesConverter)', msg=error_msg ) if isinstance(din, vtk.vtkBitArray): return np.bool elif (isinstance(din, vtk.vtkIntArray) or isinstance(din, vtk.vtkTypeInt32Array)): return np.int elif isinstance(din, vtk.vtkTypeInt8Array): return np.int8 elif isinstance(din, vtk.vtkTypeInt16Array): return np.int16 elif isinstance(din, vtk.vtkTypeInt64Array): return np.int64 elif isinstance(din, vtk.vtkTypeUInt8Array): return np.uint8 elif isinstance(din, vtk.vtkTypeUInt16Array): return np.uint16 elif isinstance(din, vtk.vtkTypeUInt32Array): return np.uint32 elif isinstance(din, vtk.vtkTypeUInt64Array): return np.uint64 elif (isinstance(din, vtk.vtkFloatArray) or isinstance(din, vtk.vtkTypeFloat32Array)): return np.float32 elif (isinstance(din, vtk.vtkDoubleArray) or isinstance(din, vtk.vtkTypeFloat64Array)): return np.float64 else: error_msg = 'VTK type not identified.' raise pexceptions.PySegInputError( expr='numpy_to_vtk_array (TypesConverter)', msg=error_msg )
def get_foreground_voxels_from_mask(mask): """ Gets foreground (non-zero) voxel coordinates from a mask (binary tomographic data). Args: mask (numpy ndarray): a 3D array holding the mask, which should be binary Returns: a list of foreground (nonzero) voxel coordinates as tuples in form (x, y, z) """ voxels = [] # check that the mask is a 3D numpy array: if isinstance(mask, np.ndarray) and (len(mask.shape) == 3): indices = mask.nonzero() voxels_num = indices[0].size for i in xrange(voxels_num): voxel_i = (indices[0][i], indices[1][i], indices[2][i]) voxels.append(voxel_i) else: error_msg = 'A 3D numpy ndarray object required as input.' raise pexceptions.PySegInputError( expr='get_foreground_voxels_from_mask', msg=error_msg) return voxels
def ndarray_voxels_to_tupel_list(voxels_ndarray): """ Turns voxel coordinates from a 2D numpy ndarray in format [[x1, y1, z1], [x2, y2, z2], ...] into a list of tuples in format [(x1, y1, z1), (x2, y2, z2), ...]. Args: voxels_ndarray (numpy.ndarray): a 2D array containing voxel coordinates in format [[x1, y1, z1], [x2, y2, z2], ...] Returns: a list of tuples containing the voxel coordinates in format [(x1, y1, z1), (x2, y2, z2), ...] """ tupel_list = [] # check that voxels_ndarray is a 2D numpy array with 3 columns: if (isinstance(voxels_ndarray, np.ndarray) and (len(voxels_ndarray.shape) == 2) and (voxels_ndarray.shape[1] == 3)): voxels_list = voxels_ndarray.tolist() for voxel in voxels_list: x = voxel[0] y = voxel[1] z = voxel[2] tupel_list.append((x, y, z)) else: error_msg = 'A 2D numpy ndarray with 3 columns required as input.' raise pexceptions.PySegInputError(expr='ndarray_voxels_to_tupel_list', msg=error_msg) return tupel_list
def distance_between_voxels(voxel1, voxel2): """ Calculates and returns the Euclidean distance between two voxels. Args: voxel1 (tuple): first voxel coordinates in form of a tuple of integers of length 3 (x1, y1, z1) voxel2 (tuple): second voxel coordinates in form of a tuple of integers of length 3 (x2, y2, z2) Returns: the Euclidean distance between two voxels (float) """ if (isinstance(voxel1, tuple) and (len(voxel1) == 3) and isinstance(voxel2, tuple) and (len(voxel2) == 3)): sum_of_squared_differences = 0 for i in range(3): # for each dimension sum_of_squared_differences += (voxel1[i] - voxel2[i]) ** 2 return math.sqrt(sum_of_squared_differences) else: error_msg = ('Tuples of integers of length 3 required as first and ' 'second input.') raise pexceptions.PySegInputError( expr='distance_between_voxels (SegmentationGraph)', msg=error_msg )
def __filter_null_triangles(surface, verbose=False): """ For a given VTK PolyData surface, filters out triangles with zero area, if any are present. Is used by the function run_gen_surface. Args: surface (vtk.PolyData): surface of triangles verbose (boolean, optional): if True (default False), some extra information will be printed out Returns: the filtered triangle surface (vtk.PolyData) """ if isinstance(surface, vtk.vtkPolyData): # Check numbers of cells (polygons or triangles) (and all points). print 'The surface has %s cells' % surface.GetNumberOfCells() if verbose: print '%s points' % surface.GetNumberOfPoints() null_area_triangles = 0 for i in range(surface.GetNumberOfCells()): # Get the cell i and check if it's a triangle: cell = surface.GetCell(i) if isinstance(cell, vtk.vtkTriangle): # Get the 3 points which made up the triangular cell i: points_cell = cell.GetPoints() # Calculate the area of the triangle i; area = cell.TriangleArea(points_cell.GetPoint(0), points_cell.GetPoint(1), points_cell.GetPoint(2)) if area <= 0: if verbose: print( 'Triangle %s is marked for deletion, because its' 'area is not > 0' % i) surface.DeleteCell(i) null_area_triangles += 1 else: print 'Oops, the cell number %s is not a triangle!' % i if null_area_triangles: surface.RemoveDeletedCells() print('%s triangles with area = 0 were removed, resulting in:' % null_area_triangles) # Recheck numbers of cells (polygons or triangles): print '%s cells' % surface.GetNumberOfCells() else: error_msg = 'The first input must be a vtkPolyData object.' raise pexceptions.PySegInputError(expr='__filter_null_triangles', msg=error_msg) return surface
def build_graph_from_np_ndarray(self, mask, verbose=False): """ Builds a graph from a binary mask of a membrane segmentation, including only voxels with value 1 (foreground voxels). Each foreground voxel, its foreground neighbor voxels and edges with euclidean distances between the voxel and its neighbor voxels (all scaled in nm) are added to the graph. Args: mask (numpy.ndarray): a binary 3D mask verbose (boolean, optional): if True (default False), some extra information will be printed out Returns: None """ if isinstance(mask, np.ndarray) and (len(mask.shape) == 3): if mask.shape != (self.scale_x, self.scale_y, self.scale_z): error_msg = ( 'Scales of the input mask have to be equal to ' 'those set during the generation of the VoxelGraph' 'object.') raise pexceptions.PySegInputError( expr='build_graph_from_np_ndarray (VoxelGraph)', msg=error_msg) # Find the set of the membrane voxels, which become the vertices of # the graph: membrane_voxels = get_foreground_voxels_from_mask(mask) print '%s membrane voxels' % len(membrane_voxels) if verbose: print membrane_voxels self.__expand_voxels(mask, membrane_voxels, verbose) else: error_msg = 'A 3D numpy ndarray object required as first input.' raise pexceptions.PySegInputError( expr='build_graph_from_np_ndarray (VoxelGraph)', msg=error_msg)
def save_vtp(poly, fname): """ Saves a VTK PolyData object into a VTP file. Args: poly (vtk.vtkPolyData): input VTK PolyData object fname (str): output file name Returns: None """ writer = vtk.vtkXMLPolyDataWriter() writer.SetFileName(fname) writer.SetInputData(poly) if writer.Write() != 1: error_msg = 'Error writing the file %s.' % fname raise pexceptions.PySegInputError(expr='save_vtp', msg=error_msg)
def save_numpy(array, fname): """ Saves a numpy array to a file in MRC, EM or VTI format. Args: array (numpy.ndarray): input array fname (str): full path to the tomogram, has to end with '.mrc', '.em' or '.vti' Returns: None """ _, ext = os.path.splitext(fname) # Parse input array for fulfilling format requirements if (ext == '.mrc') or (ext == '.em'): if ((array.dtype != 'ubyte') and (array.dtype != 'int16') and (array.dtype != 'float32')): array = array.astype('float32') # if (len(array.shape) == 3) and (array.shape[2] == 1): # array = np.reshape(array, (array.shape[0], array.shape[1])) if ext == '.vti': pname, fnameh = os.path.split(fname) save_vti(numpy_to_vti(array), fnameh, pname) # elif ext == '.fits': # warnings.resetwarnings() # warnings.filterwarnings('ignore', category=UserWarning, append=True) # pyfits.writeto(fname, array, clobber=True, output_verify='silentfix') # warnings.resetwarnings() # warnings.filterwarnings('always', category=UserWarning, append=True) elif ext == '.mrc': img = ImageIO() # img.setData(np.transpose(array, (1, 0, 2))) img.setData(array) img.writeMRC(fname) elif ext == '.em': img = ImageIO() # img.setData(np.transpose(array, (1, 0, 2))) img.setData(array) img.writeEM(fname) else: error_msg = 'Format not valid %s.' % ext raise pexceptions.PySegInputError(expr='save_numpy', msg=error_msg)
def save_vti(image, fname, outputdir): """ Saves a VTK image data object into a VTI file. Args: image (vtk.vtkImageData): fname (str): output file name, should end with '.vti' outputdir (str): output directory Returns: None """ writer = vtk.vtkXMLImageDataWriter() outputfile = outputdir + '/' + fname writer.SetFileName(outputfile) writer.SetInputData(image) if writer.Write() != 1: error_msg = 'Error writing the %s file on %s.' % (fname, outputdir) raise pexceptions.PySegInputError(expr='save_vti', msg=error_msg)
def get_vertex_property_array(self, property_name): """ Gets a numpy array with all values of a vertex property of the graph, printing out the number of values, the minimal and the maximal value. Args: property_name (str): vertex property name Returns: an array (numpy.ndarray) with all values of the vertex property """ if (isinstance(property_name, str) and property_name in self.graph.vertex_properties): values = self.graph.vertex_properties[property_name].get_array() print '%s "%s" values' % (len(values), property_name) print 'min = %s, max = %s' % (min(values), max(values)) return values else: error_msg = ('The input "%s" is not a str object or is not found ' 'in vertex properties of the graph.' % property_name) raise pexceptions.PySegInputError( expr='get_vertex_property_array (SegmentationGraph)', msg=error_msg)
def calculate_density(self, mask=None, target_coordinates=None, verbose=False): """ Calculates ribosome density for each membrane graph vertex. Calculates shortest geodesic distances (d) for each vertex in the graph to each reachable ribosome center mapped on the membrane given by a binary mask with coordinates in pixels or an array of coordinates in nm. Then, calculates a density measure of ribosomes at each vertex or membrane voxel: D = sum {over all reachable ribosomes} (1 / (d + 1)). Adds the density as vertex PropertyMap to the graph. Returns an array with the same shape as the underlying segmentation with the densities plus 1, in order to distinguish membrane voxels with 0 density from the background. Args: mask (numpy.ndarray, optional): a binary mask of the ribosome centers as 3D array where indices are coordinates in pixels (default None) target_coordinates (numpy.ndarray, optional): the ribosome centers coordinates in nm as 2D array in format [[x1, y1, z1], [x2, y2, z2], ...] (default None) verbose (boolean, optional): if True (default False), some extra information will be printed out Returns: a 3D numpy ndarray with the densities + 1 Note: One of the first two parameters, mask or target_coordinates, has to be given. """ import ribosome_density as rd # If a mask is given, find the set of voxels of ribosome centers mapped # on the membrane, 'target_voxels', and rescale them to nm, # 'target_coordinates': if mask is not None: if mask.shape != (self.scale_x, self.scale_y, self.scale_z): error_msg = ("Scales of the input 'mask' have to be equal to " "those set during the generation of the graph.") raise pexceptions.PySegInputError( expr='calculate_density (SegmentationGraph)', msg=error_msg ) # output as a list of tuples [(x1,y1,z1), (x2,y2,z2), ...] in pixels target_voxels = rd.get_foreground_voxels_from_mask(mask) # for rescaling have to convert to an ndarray target_ndarray_voxels = rd.tupel_list_to_ndarray_voxels( target_voxels ) # rescale to nm, output an ndarray [[x1,y1,z1], [x2,y2,z2], ...] target_ndarray_coordinates = (target_ndarray_voxels * self.scale_factor_to_nm) # convert to a list of tuples, which are in nm now target_coordinates = rd.ndarray_voxels_to_tupel_list( target_ndarray_coordinates ) # If target_coordinates are given (in nm), convert them from a numpy # ndarray to a list of tuples: elif target_coordinates is not None: target_coordinates = rd.ndarray_voxels_to_tupel_list( target_coordinates ) # Exit if the target_voxels list is empty: if len(target_coordinates) == 0: error_msg = ("No target voxels were found! Check your input " "('mask' or 'target_coordinates').") raise pexceptions.PySegInputError( expr='calculate_density (SegmentationGraph)', msg=error_msg ) print '%s target voxels' % len(target_coordinates) if verbose: print target_coordinates # Pre-filter the target coordinates to those existing in the graph # (should already all be in the graph, but just in case): target_coordinates_in_graph = [] for target_xyz in target_coordinates: if target_xyz in self.coordinates_to_vertex_index: target_coordinates_in_graph.append(target_xyz) else: error_msg = ('Target (%s, %s, %s) not inside the membrane!' % (target_xyz[0], target_xyz[1], target_xyz[2])) raise pexceptions.PySegInputWarning( expr='calculate_density (SegmentationGraph)', msg=error_msg ) print '%s target coordinates in graph' % len( target_coordinates_in_graph) if verbose: print target_coordinates_in_graph # Get all indices of the target coordinates: target_vertices_indices = [] for target_xyz in target_coordinates_in_graph: v_target_index = self.coordinates_to_vertex_index[target_xyz] target_vertices_indices.append(v_target_index) # Density calculation # Add a new vertex property to the graph, density: self.graph.vp.density = self.graph.new_vertex_property("float") # Dictionary mapping voxel coordinates (for the volume returned later) # to a list of density values falling within that voxel: voxel_to_densities = {} # For each vertex in the graph: for v_membrane in self.graph.vertices(): # Get its coordinates: membrane_xyz = self.graph.vp.xyz[v_membrane] if verbose: print ('Membrane vertex (%s, %s, %s)' % (membrane_xyz[0], membrane_xyz[1], membrane_xyz[2])) # Get a distance map with all pairs of distances between current # graph vertex (membrane_xyz) and target vertices (ribosome # coordinates): dist_map = shortest_distance(self.graph, source=v_membrane, target=target_vertices_indices, weights=self.graph.ep.distance) # Iterate over all shortest distances from the membrane vertex to # the target vertices, while calculating the density: # Initializing: membrane coordinates with no reachable ribosomes # will have a value of 0, those with reachable ribosomes > 0. density = 0 # If there is only one target voxel, dist_map is a single value - # wrap it into a list. if len(target_coordinates_in_graph) == 1: dist_map = [dist_map] for d in dist_map: if verbose: print '\tTarget vertex ...' # if unreachable, the maximum float64 is stored if d == np.finfo(np.float64).max: if verbose: print '\t\tunreachable' else: if verbose: print '\t\td = %s' % d density += 1 / (d + 1) # Add the density of the membrane vertex as a property of the # current vertex in the graph: self.graph.vp.density[v_membrane] = density # Calculate the corresponding voxel of the vertex and add the # density to the list keyed by the voxel in the dictionary: # Scaling the coordinates back from nm to voxels. (Without round # float coordinates are truncated to the next lowest integer.) voxel_x = int(round(membrane_xyz[0] / self.scale_factor_to_nm)) voxel_y = int(round(membrane_xyz[1] / self.scale_factor_to_nm)) voxel_z = int(round(membrane_xyz[2] / self.scale_factor_to_nm)) voxel = (voxel_x, voxel_y, voxel_z) if voxel in voxel_to_densities: voxel_to_densities[voxel].append(density) else: voxel_to_densities[voxel] = [density] if verbose: print '\tdensity = %s' % density if (self.graph.vertex_index[v_membrane] + 1) % 1000 == 0: now = datetime.now() print ('%s membrane vertices processed on: %s-%s-%s %s:%s:%s' % (self.graph.vertex_index[v_membrane] + 1, now.year, now.month, now.day, now.hour, now.minute, now.second)) # Initialize an array scaled like the original segmentation, which will # hold in each membrane voxel the maximal density among the # corresponding vertex coordinates in the graph plus 1 and 0 in each # background (non-membrane) voxel: densities = np.zeros((self.scale_x, self.scale_y, self.scale_z), dtype=np.float16) # The densities array membrane voxels are initialized with 1 in order to # distinguish membrane voxels from the background. for voxel in voxel_to_densities: densities[voxel[0], voxel[1], voxel[2]] = 1 + max( voxel_to_densities[voxel]) if verbose: print 'densities:\n%s' % densities return densities
def run_gen_surface(tomo, outfile_base, lbl=1, mask=True, save_input_as_vti=False, verbose=False): """ Runs pysurf_io.gen_surface function, which generates a VTK PolyData triangle surface for objects in a segmented volume with a given label. Removes triangles with zero area, if any are present, from the resulting surface. Args: tomo (str or numpy.ndarray): segmentation input file in one of the formats: '.mrc', '.em' or '.vti', or 3D array containing the segmentation outfile_base (str): the path and filename without the ending for saving the surface (ending '.surface.vtp' will be added automatically) lbl (int, optional): the label to be considered, 0 will be ignored, default 1 mask (boolean, optional): if True (default), a mask of the binary objects is applied on the resulting surface to reduce artifacts save_input_as_vti (boolean, optional): if True (default False), the input is saved as a '.vti' file ('<outfile_base>.vti') verbose (boolean, optional): if True (default False), some extra information will be printed out Returns: the triangle surface (vtk.PolyData) """ t_begin = time.time() # Generating the surface (vtkPolyData object) surface = io.gen_surface(tomo, lbl=lbl, mask=mask, verbose=verbose) # Filter out triangles with area=0, if any are present surface = __filter_null_triangles(surface, verbose=verbose) t_end = time.time() duration = t_end - t_begin print 'Surface generation took: %s min %s s' % divmod(duration, 60) # Writing the vtkPolyData surface into a VTP file io.save_vtp(surface, outfile_base + '.surface.vtp') print 'Surface was written to the file %s.vtp' % outfile_base if save_input_as_vti is True: # If input is a file name, read in the segmentation array from the file: if isinstance(tomo, str): tomo = io.load_tomo(tomo) elif not isinstance(tomo, np.ndarray): error_msg = 'Input must be either a file name or a ndarray.' raise pexceptions.PySegInputError(expr='run_gen_surface', msg=error_msg) # Save the segmentation as VTI for opening it in ParaView: io.save_numpy(tomo, outfile_base + '.vti') print 'Input was saved as the file %s.vti' % outfile_base return surface
def gen_surface(tomo, lbl=1, mask=True, purge_ratio=1, field=False, mode_2d=False, verbose=False): """ Generates a VTK PolyData surface from a segmented tomogram. Args: tomo (numpy.ndarray or str): the input segmentation as numpy ndarray or the file name in MRC, EM or VTI format lbl (int, optional): label for the foreground, default 1 mask (boolean, optional): if True (default), the input segmentation is used as mask for the surface purge_ratio (int, optional): if greater than 1 (default), then 1 every purge_ratio points of the segmentation are randomly deleted field (boolean, optional): if True (default False), additionally returns the polarity distance scalar field mode_2d (boolean, optional): needed for polarity distance calculation (if field is True), if True (default False), ... verbose (boolean, optional): if True (default False), prints out messages for checking the progress Returns: - output surface (vtk.vtkPolyData) - polarity distance scalar field (np.ndarray), if field is True """ # Check input format if isinstance(tomo, str): fname, fext = os.path.splitext(tomo) # if fext == '.fits': # tomo = pyfits.getdata(tomo) if fext == '.mrc': hold = ImageIO() hold.readMRC(file=tomo) tomo = hold.data elif fext == '.em': hold = ImageIO() hold.readEM(file=tomo) tomo = hold.data elif fext == '.vti': reader = vtk.vtkXMLImageDataReader() reader.SetFileName(tomo) reader.Update() tomo = vti_to_numpy(reader.GetOutput()) else: error_msg = 'Format %s not readable.' % fext raise pexceptions.PySegInputError(expr='gen_surface', msg=error_msg) elif not isinstance(tomo, np.ndarray): error_msg = 'Input must be either a file name or a ndarray.' raise pexceptions.PySegInputError(expr='gen_surface', msg=error_msg) # Load file with the cloud of points nx, ny, nz = tomo.shape cloud = vtk.vtkPolyData() points = vtk.vtkPoints() cloud.SetPoints(points) if purge_ratio <= 1: for x in range(nx): for y in range(ny): for z in range(nz): if tomo[x, y, z] == lbl: points.InsertNextPoint(x, y, z) else: count = 0 mx_value = purge_ratio - 1 purge = np.random.randint(0, purge_ratio+1, nx*ny*nz) for x in range(nx): for y in range(ny): for z in range(nz): if purge[count] == mx_value: if tomo[x, y, z] == lbl: points.InsertNextPoint(x, y, z) count += 1 if verbose: print 'Cloud of points loaded...' # Creating the isosurface surf = vtk.vtkSurfaceReconstructionFilter() # surf.SetSampleSpacing(2) surf.SetSampleSpacing(purge_ratio) # surf.SetNeighborhoodSize(10) surf.SetInputData(cloud) contf = vtk.vtkContourFilter() contf.SetInputConnection(surf.GetOutputPort()) # if thick is None: contf.SetValue(0, 0) # else: # contf.SetValue(0, thick) # Sometimes the contouring algorithm can create a volume whose gradient # vector and ordering of polygon (using the right hand rule) are # inconsistent. vtkReverseSense cures this problem. reverse = vtk.vtkReverseSense() reverse.SetInputConnection(contf.GetOutputPort()) reverse.ReverseCellsOn() reverse.ReverseNormalsOn() reverse.Update() rsurf = reverse.GetOutput() if verbose: print 'Isosurfaces generated...' # Translate and scale to the proper positions cloud.ComputeBounds() rsurf.ComputeBounds() xmin, xmax, ymin, ymax, zmin, zmax = cloud.GetBounds() rxmin, rxmax, rymin, rymax, rzmin, rzmax = rsurf.GetBounds() scale_x = (xmax-xmin) / (rxmax-rxmin) scale_y = (ymax-ymin) / (rymax-rymin) denom = rzmax - rzmin num = zmax - xmin if (denom == 0) or (num == 0): scale_z = 1 else: scale_z = (zmax-zmin) / (rzmax-rzmin) transp = vtk.vtkTransform() transp.Translate(xmin, ymin, zmin) transp.Scale(scale_x, scale_y, scale_z) transp.Translate(-rxmin, -rymin, -rzmin) tpd = vtk.vtkTransformPolyDataFilter() tpd.SetInputData(rsurf) tpd.SetTransform(transp) tpd.Update() tsurf = tpd.GetOutput() if verbose: print 'Rescaled and translated...' # Masking according to distance to the original segmentation if mask: tomod = scipy.ndimage.morphology.distance_transform_edt( np.invert(tomo == lbl)) for i in range(tsurf.GetNumberOfCells()): # Check if all points which made up the polygon are in the mask points_cell = tsurf.GetCell(i).GetPoints() count = 0 for j in range(0, points_cell.GetNumberOfPoints()): x, y, z = points_cell.GetPoint(j) if (tomod[int(round(x)), int(round(y)), int(round(z))] > MAX_DIST_SURF): count += 1 if count > 0: tsurf.DeleteCell(i) # Release free memory tsurf.RemoveDeletedCells() if verbose: print 'Mask applied...' # Field distance if field: # Get normal attributes norm_flt = vtk.vtkPolyDataNormals() norm_flt.SetInputData(tsurf) norm_flt.ComputeCellNormalsOn() norm_flt.AutoOrientNormalsOn() norm_flt.ConsistencyOn() norm_flt.Update() tsurf = norm_flt.GetOutput() # for i in range(tsurf.GetPointData().GetNumberOfArrays()): # array = tsurf.GetPointData().GetArray(i) # if array.GetNumberOfComponents() == 3: # break array = tsurf.GetCellData().GetNormals() # Build membrane mask tomoh = np.ones(shape=tomo.shape, dtype=np.bool) tomon = np.ones(shape=(tomo.shape[0], tomo.shape[1], tomo.shape[2], 3), dtype=TypesConverter().vtk_to_numpy(array)) # for i in range(tsurf.GetNumberOfCells()): # points_cell = tsurf.GetCell(i).GetPoints() # for j in range(0, points_cell.GetNumberOfPoints()): # x, y, z = points_cell.GetPoint(j) # # print x, y, z, array.GetTuple(j) # x, y, z = int(round(x)), int(round(y)), int(round(z)) # tomo[x, y, z] = False # tomon[x, y, z, :] = array.GetTuple(j) for i in range(tsurf.GetNumberOfCells()): points_cell = tsurf.GetCell(i).GetPoints() for j in range(0, points_cell.GetNumberOfPoints()): x, y, z = points_cell.GetPoint(j) # print x, y, z, array.GetTuple(j) x, y, z = int(round(x)), int(round(y)), int(round(z)) if tomo[x, y, z] == lbl: tomoh[x, y, z] = False tomon[x, y, z, :] = array.GetTuple(i) # Distance transform tomod, ids = scipy.ndimage.morphology.distance_transform_edt( tomoh, return_indices=True) # Compute polarity if mode_2d: for x in range(nx): for y in range(ny): for z in range(nz): i_x, i_y, i_z = (ids[0, x, y, z], ids[1, x, y, z], ids[2, x, y, z]) norm = tomon[i_x, i_y, i_z] norm[2] = 0 pnorm = (i_x, i_y, 0) p = (x, y, 0) dprod = dot_norm(np.asarray(p, dtype=np.float), np.asarray(pnorm, dtype=np.float), np.asarray(norm, dtype=np.float)) tomod[x, y, z] = tomod[x, y, z] * np.sign(dprod) else: for x in range(nx): for y in range(ny): for z in range(nz): i_x, i_y, i_z = (ids[0, x, y, z], ids[1, x, y, z], ids[2, x, y, z]) hold_norm = tomon[i_x, i_y, i_z] norm = hold_norm # norm[0] = (-1) * hold_norm[1] # norm[1] = hold_norm[0] # norm[2] = hold_norm[2] pnorm = (i_x, i_y, i_z) p = (x, y, z) dprod = dot_norm(np.asarray(pnorm, dtype=np.float), np.asarray(p, dtype=np.float), np.asarray(norm, dtype=np.float)) tomod[x, y, z] = tomod[x, y, z] * np.sign(dprod) if verbose: print 'Distance field generated...' return tsurf, tomod if verbose: print 'Finished!' return tsurf