def set_plotting_defaults(self): """Set default matplotlib plotting options for main image, dose field, and jacobian determinant.""" Image.set_plotting_defaults(self) self.dose_kwargs = {"cmap": "jet", "alpha": 0.5, "vmin": None, "vmax": None} self.jacobian_kwargs = { "cmap": "seismic", "alpha": 0.5, "vmin": 0.8, "vmax": 1.2, }
def __init__(self, nii, spacing=_default_spacing, plot_type="grid", **kwargs): """Load deformation field. Parameters ---------- nii : str/array/nifti Source of the image data to load. This can be either: (a) The path to a NIfTI file; (b) A nibabel.nifti1.Nifti1Image object; (c) The path to a file containing a NumPy array; (d) A NumPy array. """ Image.__init__(self, nii, **kwargs) if not self.valid: return if self.data.ndim != 5: raise RuntimeError( f"Deformation field in {nii} must contain a " "five-dimensional array!" ) self.data = self.data[:, :, :, 0, :] self.set_spacing(spacing)
def load_to(self, nii, attr, kwargs): """Load image data into a class attribute.""" # Load single image rescale = "dose" if attr == "dose" else True if not isinstance(nii, dict): data = Image(nii, rescale=rescale, **kwargs) data.match_size(self, 0) valid = data.valid else: data = {view: Image(nii[view], rescale=rescale, **kwargs) for view in nii} for view in _orient: if view not in data or not data[view].valid: data[view] = None else: data[view].match_size(self, 0) valid = any([d.valid for d in data.values() if d is not None]) setattr(self, attr, data) setattr(self, f"has_{attr}", valid) setattr(self, f"{attr}_dict", isinstance(nii, dict))
def plot( self, view, sl=None, pos=None, ax=None, gs=None, figsize=None, zoom=None, zoom_centre=None, mpl_kwargs=None, show=True, colorbar=False, colorbar_label="HU", struct_kwargs=None, struct_plot_type=None, major_ticks=None, minor_ticks=None, ticks_all_sides=False, **kwargs, ): """Plot MultiImage and orthogonal view of main image and structs.""" self.set_axes(view, ax, gs, figsize, zoom, colorbar) # Plot the MultiImage MultiImage.plot( self, view, sl=sl, pos=pos, ax=self.ax, zoom=zoom, zoom_centre=zoom_centre, colorbar=colorbar, show=False, colorbar_label=colorbar_label, mpl_kwargs=mpl_kwargs, struct_kwargs=struct_kwargs, struct_plot_type=struct_plot_type, major_ticks=major_ticks, minor_ticks=minor_ticks, ticks_all_sides=ticks_all_sides, **kwargs, ) # Plot orthogonal view orthog_view = _orthog[view] orthog_sl = self.orthog_slices[_slider_axes[orthog_view]] Image.plot( self, orthog_view, sl=orthog_sl, ax=self.orthog_ax, mpl_kwargs=mpl_kwargs, show=False, colorbar=False, no_ylabel=True, no_title=True, major_ticks=major_ticks, minor_ticks=minor_ticks, ticks_all_sides=ticks_all_sides, ) # Plot structures on orthogonal image for struct in self.structs: if not struct.visible: continue plot_type = struct_plot_type if plot_type == "centroid": plot_type = "contour" elif plot_type == "filled centroid": plot_type = "filled" struct.plot( orthog_view, sl=orthog_sl, ax=self.orthog_ax, mpl_kwargs=struct_kwargs, plot_type=plot_type, no_title=True, ) # Plot indicator line pos = sl if not self.scale_in_mm else self.slice_to_pos(sl, _slider_axes[view]) if view == "x-y": full_y = ( self.extent[orthog_view][2:] if self.scale_in_mm else [0.5, self.n_voxels[_plot_axes[orthog_view][1]] + 0.5] ) self.orthog_ax.plot([pos, pos], full_y, "r") else: full_x = ( self.extent[orthog_view][:2] if self.scale_in_mm else [0.5, self.n_voxels[_plot_axes[orthog_view][0]] + 0.5] ) self.orthog_ax.plot(full_x, [pos, pos], "r") if show: plt.tight_layout() plt.show()
def plot( self, view="x-y", sl=None, pos=None, ax=None, gs=None, figsize=None, zoom=None, zoom_centre=None, mpl_kwargs=None, n_date=1, show=True, colorbar=False, colorbar_label="HU", dose_kwargs=None, masked=False, invert_mask=False, mask_color="black", jacobian_kwargs=None, df_kwargs=None, df_plot_type="grid", df_spacing=30, struct_kwargs=None, struct_plot_type="contour", struct_legend=True, legend_loc="lower left", struct_plot_grouping=None, struct_to_plot=None, annotate_slice=None, major_ticks=None, minor_ticks=None, ticks_all_sides=False, ): """Plot a 2D slice of this image and all extra features. Parameters ---------- view : str Orientation in which to plot ("x-y"/"y-z"/"x-z"). sl : int, default=None Slice number. If <sl> and <pos> are both None, the middle slice will be plotted. pos : float, default=None Position in mm of the slice to plot (will be rounded to the nearest slice). If <sl> and <pos> are both None, the middle slice will be plotted. If <sl> and <pos> are both given, <sl> supercedes <pos>. ax : matplotlib.pyplot.Axes, default=None Axes on which to plot. If None, new axes will be created. gs : matplotlib.gridspec.GridSpec, default=None If not None and <ax> is None, new axes will be created on the current matplotlib figure with this gridspec. figsize : float, default=None Figure height in inches; only used if <ax> and <gs> are None. If None, the value in _default_figsize will be used. zoom : int/float/tuple, default=None Factor by which to zoom in. If a single int or float is given, the same zoom factor will be applied in all directions. If a tuple of three values is given, these will be used as the zoom factors in each direction in the order (x, y, z). If None, the image will not be zoomed in. mpl_kwargs : dict, default=None Dictionary of keyword arguments to pass to matplotlib.imshow() for the main image. show : bool, default=True If True, the plotted figure will be shown via matplotlib.pyplot.show(). colorbar : bool, default=True If True, a colorbar will be drawn alongside the plot. dose_kwargs : dict, default=None Dictionary of keyword arguments to pass to matplotlib.imshow() for the dose field. masked : bool, default=False If True and this object has attribute self.data_mask assigned, the image will be masked with the array in self.data_mask. invert_mask : bool, default=True If True and a mask is applied, the mask will be inverted. mask_color : matplotlib color, default="black" color in which to plot masked areas. mask_threshold : float, default=0.5 Threshold on mask array; voxels with values below this threshold will be masked (or values above, if <invert_mask> is True). jacobian_kwargs : dict, default=None Dictionary of keyword arguments to pass to matplotlib.imshow() for the jacobian determinant. df_kwargs : dict, default=None Dictionary of keyword arguments to pass to matplotlib.imshow() for the deformation field. df_plot_type : str, default="grid" Type of plot ("grid"/"quiver") to produce for the deformation field. df_spacing : int/float/tuple, default=30 Grid spacing for the deformation field plot. If self.scale_in_mm is true, the spacing will be in mm; otherwise in voxels. Can be either a single value for all directions, or a tuple of values for each direction in order (x, y, z). struct_kwargs : dict, default=None Dictionary of keyword arguments to pass to matplotlib for structure plotting. struct_plot_type : str, default="contour" Plot type for structures ("contour"/"mask"/"filled") struct_legend : bool, default=True If True, a legend will be drawn labelling any structrues visible on this slice. legend_loc : str, default='lower left' Position for the structure legend, if used. annotate_slice : str, default=None Color for annotation of slice number. If None, no annotation will be added. If True, the default color (white) will be used. """ # Set date if self.timeseries: self.set_date(n_date) # Plot image self.set_ax(view, ax, gs, figsize, zoom, colorbar) Image.plot( self, view, sl, pos, ax=self.ax, mpl_kwargs=mpl_kwargs, show=False, colorbar=colorbar, colorbar_label=colorbar_label, masked=masked, invert_mask=invert_mask, mask_color=mask_color, figsize=figsize, major_ticks=major_ticks, minor_ticks=minor_ticks, ticks_all_sides=ticks_all_sides, ) # Plot dose field self.dose.plot( view, self.sl, ax=self.ax, mpl_kwargs=self.get_kwargs(dose_kwargs, default=self.dose_kwargs), show=False, masked=masked, invert_mask=invert_mask, mask_color=mask_color, colorbar=colorbar, colorbar_label="Dose (Gy)", ) # Plot jacobian self.jacobian.plot( view, self.sl, ax=self.ax, mpl_kwargs=self.get_kwargs(jacobian_kwargs, default=self.jacobian_kwargs), show=False, colorbar=colorbar, colorbar_label="Jacobian determinant", ) # Plot standalone structures and comparisons for s in self.standalone_structs: s.plot( view, self.sl, ax=self.ax, mpl_kwargs=struct_kwargs, plot_type=struct_plot_type, ) for s in self.struct_comparisons: if struct_plot_grouping == "group others": if s.s1.name_unique != struct_to_plot: continue s.plot( view, self.sl, ax=self.ax, mpl_kwargs=struct_kwargs, plot_type=struct_plot_type, plot_grouping=struct_plot_grouping, ) # Plot deformation field self.df.plot( view, self.sl, ax=self.ax, mpl_kwargs=df_kwargs, plot_type=df_plot_type, spacing=df_spacing, ) # Draw structure legend if struct_legend and struct_plot_type != "none": handles = [] for s in self.structs: if struct_plot_grouping == "group others": if s.name_unique == struct_to_plot: handles.append(mpatches.Patch(color=s.color, label=s.name_nice)) handles.append(mpatches.Patch(color="white", label="Others")) elif s.visible and s.on_slice(view, self.sl): handles.append(mpatches.Patch(color=s.color, label=s.name_nice)) if len(handles): self.ax.legend( handles=handles, loc=legend_loc, facecolor="white", framealpha=1 ) self.adjust_ax(view, zoom, zoom_centre) self.label_ax(view, annotate_slice=annotate_slice) # Display image if show: plt.tight_layout() plt.show()
def get_relative_width(self, view, zoom=None, colorbar=False, figsize=None): """Get the relative width for this plot, including all colorbars.""" return Image.get_relative_width( self, view, zoom, self.get_n_colorbars(colorbar), figsize )
def __init__( self, nii=None, dose=None, mask=None, jacobian=None, df=None, structs=None, multi_structs=None, timeseries=None, struct_colors=None, structs_as_mask=False, struct_names=None, compare_structs=False, comp_type="auto", ignore_empty_structs=False, ignore_unpaired_structs=False, structs_to_keep=None, structs_to_ignore=None, autoload_structs=True, mask_threshold=0.5, **kwargs, ): """Load a MultiImage object. Parameters ---------- nii : str/nifti/array Path to a .nii/.npy file, or an nibabel nifti object/numpy array. title : str, default=None Title for this image when plotted. If None and <nii> is loaded from a file, the filename will be used. dose : str/nifti/array, default=None Path or object from which to load dose field. mask : str/nifti/array, default=None Path or object from which to load mask array. jacobian : str/nifti/array, default=None Path or object from which to load jacobian determinant field. df : str/nifti/array, default=None Path or object from which to load deformation field. structs : str/list, default=None A string containing a path, directory, or wildcard pointing to nifti file(s) containing structure(s). Can also be a list of paths/directories/wildcards. struct_colors : dict, default=None Custom colors to use for structures. Dictionary keys can be a structure name or a wildcard matching structure name(s). Values should be any valid matplotlib color. structs_as_mask : bool, default=False If True, structures will be used as masks. struct_names : list/dict, default=None For multi_structs, this parameter will be used to name the structures. Can either be a list (i.e. the first structure in the file will be given the first name in the list and so on), or a dict of numbers and names (e.g. {1: "first structure"} etc). compare_structs : bool, default=False If True, structures will be paired together into comparisons. mask_threshold : float, default=0.5 Threshold on mask array; voxels with values below this threshold will be masked (or values above, if <invert_mask> is True). """ # Flags for image type self.dose_as_im = False self.dose_comp = False self.timeseries = False # Load the scan image if nii is not None: Image.__init__(self, nii, **kwargs) self.timeseries = False # Load a dose field only elif dose is not None and timeseries is None: self.dose_as_im = True Image.__init__(self, dose, **kwargs) dose = None # Load a timeseries of images elif timeseries is not None: self.timeseries = True dates = self.get_date_dict(timeseries) self.dates = list(dates.keys()) if "title" in kwargs: kwargs.pop("title") self.ims = { date: Image(file, title=date, **kwargs) for date, file in dates.items() } Image.__init__(self, dates[self.dates[0]], title=self.dates[0], **kwargs) self.date = self.dates[0] else: raise TypeError("Must provide either <nii>, <dose>, or " "<timeseries!>") if not self.valid: return # Load extra overlays self.load_to(dose, "dose", kwargs) self.load_to(mask, "mask", kwargs) self.load_to(jacobian, "jacobian", kwargs) self.load_df(df) # Load structs self.comp_type = comp_type self.load_structs( structs, multi_structs, names=struct_names, colors=struct_colors, compare_structs=compare_structs, ignore_empty=ignore_empty_structs, ignore_unpaired=ignore_unpaired_structs, comp_type=comp_type, to_keep=structs_to_keep, to_ignore=structs_to_ignore, autoload=autoload_structs ) # Mask settings self.structs_as_mask = structs_as_mask if self.has_structs and structs_as_mask: self.has_mask = True self.mask_threshold = mask_threshold self.set_masks()
def test_nifti_image_zoom(): im = Image(arr) im.plot(zoom=(1, 2, 3), show=False) assert im.valid
def test_nifti_image_plot(): im = Image(arr) im.plot("x-y", 25, show=False, colorbar=True)
def test_nifti_image_downsample(): ds = 10 im = Image(arr, downsample=ds) assert im.valid assert im.n_voxels["z"] <= shape[2] / (ds - 1)
def test_nifti_image_from_file(): im = Image(arr_file) assert im.valid assert im.n_voxels["y"] == shape[1] assert im.title == "array.npy"
def test_nifti_image_from_array(): t = "test title" im = Image(arr, title=t) assert im.valid assert im.n_voxels["x"] == shape[0] assert im.title == t
def __init__( self, shape, filename=None, origin=(0, 0, 0), voxel_sizes=(1, 1, 1), intensity=-1024, noise_std=None, ): """Create data to write to a NIfTI file, initially containing a blank image array. Parameters ---------- shape : int/tuple Dimensions of the image array to create. If an int is given, the image will be created with dimensions (shape, shape, shape). filename : str, default=None Name of output NIfTI file. If given, the NIfTI file will automatically be written; otherwise, no file will be written until the "write" method is called. origin : tuple, default=(0, 0, 0) Origin in mm for the image. voxel_sizes : tuple, default=(1, 1, 1) Voxel sizes in mm for the image. intensity : float, default=-1000 Intensity in HU for the background of the image. noise_std : float, default=None Standard deviation of Gaussian noise to apply to the image. If None, no noise will be applied. """ # Create image properties self.shape = make_three(shape) voxel_sizes = [abs(v) for v in make_three(voxel_sizes)] self.affine = np.array( [ [ -voxel_sizes[0], 0, 0, origin[0] + (self.shape[0] - 1) * voxel_sizes[0], ], [0, voxel_sizes[1], 0, origin[1]], [0, 0, voxel_sizes[2], origin[2]], [0, 0, 0, 1], ] ) self.max_hu = 0 if noise_std is None else noise_std * 3 self.min_hu = -self.max_hu if self.max_hu != 0 else -20 self.noise_std = noise_std self.bg_intensity = intensity self.background = self.make_background() self.shapes = [] self.structs = [] self.groups = {} self.shape_count = {} self.translation = None self.rotation = None # Initialise as Image Image.__init__(self, self.background, affine=self.affine) # Write to file if a filename is given if filename is not None: self.filename = os.path.expanduser(filename) self.write()