[docs]classOdfSlicerActor(Actor):"""VTK actor for visualizing slices of ODF field. Parameters ---------- odfs : ndarray SF or SH coefficients 2-dimensional array. vertices: ndarray The sphere vertices used for SH to SF projection. faces: ndarray Indices of sphere vertices forming triangles. Should be ordered clockwise (see fury.utils.fix_winding_order). indices: tuple Indices given in tuple(x_indices, y_indices, z_indices) format for mapping 2D ODF array to 3D voxel grid. scale : float Multiplicative factor to apply to ODF amplitudes. norm : bool Normalize SF amplitudes so that the maximum ODF amplitude per voxel along a direction is 1. radial_scale : bool Scale sphere points by ODF values. global_cm : bool If True the colormap will be applied in all ODFs. If False it will be applied individually at each voxel. colormap : None or str The name of the colormap to use. Matplotlib colormaps are supported (e.g., 'inferno'). If None then a RGB colormap is used. opacity : float Takes values from 0 (fully transparent) to 1 (opaque). affine : array optional 4x4 transformation array from native coordinates to world coordinates. B : ndarray (n_coeffs, n_vertices) Optional SH to SF matrix for projecting `odfs` given in SH coefficients on the `sphere`. If None, then the input is assumed to be expressed in SF coefficients. """@warn_on_args_to_kwargs()def__init__(self,odfs,vertices,faces,indices,scale,norm,radial_scale,shape,global_cm,colormap,opacity,*,affine=None,B=None,):self.vertices=verticesself.faces=facesself.odfs=odfsself.indices=indicesself.B=Bself.radial_scale=radial_scaleself.colormap=colormapself.grid_shape=shapeself.global_cm=global_cm# declare a mask to be instantiated in slice_along_axisself.mask=None# If a B matrix is given, odfs are expected to# be in SH basis coefficients.ifself.BisnotNone:# In that case, we need to save our normalisation and scale# to apply them after conversion from SH to SF.self.norm=normself.scale=scaleelse:# If our input is in SF coefficients, we can normalise and# scale it only once, here.ifnorm:self.odfs/=np.abs(self.odfs).max(axis=-1,keepdims=True)self.odfs*=scale# Compute world coordinates of an affine is suppliedself.affine=affineifself.affineisnotNone:self.w_verts=self.vertices.dot(affine[:3,:3])self.w_pos=apply_affine(affine,np.asarray(self.indices).T)# Initialize mapper and slice to the# middle of the volume along Z axisself.mapper=PolyDataMapper()self.SetMapper(self.mapper)self.slice_along_axis(self.grid_shape[-1]//2)self.set_opacity(opacity)
[docs]defset_opacity(self,opacity):"""Set opacity value of ODFs to display."""self.GetProperty().SetOpacity(opacity)
[docs]defdisplay_extent(self,x1,x2,y1,y2,z1,z2):"""Set visible volume from x1 (inclusive) to x2 (inclusive), y1 (inclusive) to y2 (inclusive), z1 (inclusive) to z2 (inclusive). """mask=np.zeros(self.grid_shape,dtype=bool)mask[x1:x2+1,y1:y2+1,z1:z2+1]=Trueself.mask=maskself._update_mapper()
[docs]@warn_on_args_to_kwargs()defslice_along_axis(self,slice_index,*,axis="zaxis"):"""Slice ODF field at given `slice_index` along axis in ['xaxis', 'yaxis', zaxis']. """ifaxis=="xaxis":self.display_extent(slice_index,slice_index,0,self.grid_shape[1]-1,0,self.grid_shape[2]-1,)elifaxis=="yaxis":self.display_extent(0,self.grid_shape[0]-1,slice_index,slice_index,0,self.grid_shape[2]-1,)elifaxis=="zaxis":self.display_extent(0,self.grid_shape[0]-1,0,self.grid_shape[1]-1,slice_index,slice_index,)else:raiseValueError("Invalid axis name {0}.".format(axis))
[docs]@warn_on_args_to_kwargs()defdisplay(self,*,x=None,y=None,z=None):"""Display a slice along x, y, or z axis."""ifxisNoneandyisNoneandzisNone:self.slice_along_axis(self.grid_shape[2]//2)elifxisnotNone:self.slice_along_axis(x,axis="xaxis")elifyisnotNone:self.slice_along_axis(y,axis="yaxis")elifzisnotNone:self.slice_along_axis(z,axis="zaxis")
[docs]defupdate_sphere(self,vertices,faces,B):"""Dynamically change the sphere used for SH to SF projection."""ifself.BisNone:raiseValueError("Can't update sphere when using ""SF coefficients.")self.vertices=verticesifself.affineisnotNone:self.w_verts=self.vertices.dot(self.affine[:3,:3])self.faces=facesself.B=B# draw ODFs with new sphereself._update_mapper()
def_update_mapper(self):"""Map vtkPolyData to the actor."""polydata=PolyData()offsets=self._get_odf_offsets(self.mask)iflen(offsets)==0:self.mapper.SetInputData(polydata)returnNonesph_dirs=self._get_sphere_directions()sf=self._get_sf(self.mask)all_vertices=self._get_all_vertices(offsets,sph_dirs,sf)all_faces=self._get_all_faces(len(offsets),len(sph_dirs))all_colors=self._generate_color_for_vertices(sf)# TODO: There is a lot of deep copy here.# Optimize (see viz_network.py example).set_polydata_triangles(polydata,all_faces)set_polydata_vertices(polydata,all_vertices)set_polydata_colors(polydata,all_colors)self.mapper.SetInputData(polydata)def_get_odf_offsets(self,mask):"""Get the position of non-zero voxels inside `mask`."""ifself.affineisnotNone:returnself.w_pos[mask[self.indices]]returnnp.asarray(self.indices).T[mask[self.indices]]def_get_sphere_directions(self):"""Get the sphere directions onto which is projected the signal."""ifself.affineisnotNone:returnself.w_vertsreturnself.verticesdef_get_sf(self,mask):"""Get SF coefficients inside `mask`."""# when odfs are expressed in SH coefficientsifself.BisnotNone:sf=self.odfs[mask[self.indices]].dot(self.B)# normalisation and scaling is done on SF coefficientsifself.norm:sf/=np.abs(sf).max(axis=-1,keepdims=True)returnsf*self.scale# when odfs are in SF coefficients, the normalisation and scaling# are done during initialisation. We simply return them:returnself.odfs[mask[self.indices]]def_get_all_vertices(self,offsets,sph_dirs,sf):"""Get array of all the vertices of the ODFs to display."""ifself.radial_scale:# apply SF amplitudes to all sphere# directions and offset each voxelreturnnp.tile(sph_dirs,(len(offsets),1))*sf.reshape(-1,1)+np.repeat(offsets,len(sph_dirs),axis=0)# return scaled spheres offsetted by `offsets`returnnp.tile(sph_dirs,(len(offsets),1))*self.scale+np.repeat(offsets,len(sph_dirs),axis=0)def_get_all_faces(self,nb_odfs,nb_dirs):"""Get array of all the faces of the ODFs to display."""returnnp.tile(self.faces,(nb_odfs,1))+np.repeat(np.arange(nb_odfs)*nb_dirs,len(self.faces)).reshape(-1,1)def_generate_color_for_vertices(self,sf):"""Get array of all vertices colors."""ifself.global_cm:ifself.colormapisNone:raiseIOError("if global_cm=True, colormap must be defined.")else:all_colors=create_colormap(sf.ravel(),name=self.colormap)*255elifself.colormapisnotNone:ifisinstance(self.colormap,str):# Map ODFs values [min, max] to [0, 1] for each ODFrange_sf=sf.max(axis=-1)-sf.min(axis=-1)rescaled=sf-sf.min(axis=-1,keepdims=True)rescaled[range_sf>0]/=range_sf[range_sf>0][...,None]all_colors=create_colormap(rescaled.ravel(),name=self.colormap)*255else:all_colors=np.tile(np.array(self.colormap).reshape(1,3),(sf.shape[0]*sf.shape[1],1),)else:all_colors=np.tile(np.abs(self.vertices)*255,(len(sf),1))returnall_colors.astype(np.uint8)