def setup_plot_ed(axis, gs_spec): # set up a PlotEditor for the given axis # subplot grid, with larger height preference for plot for # each increased row to make sliders of approx equal size and # align top borders of top images rows_cols = gs_spec.get_rows_columns() extra_rows = rows_cols[3] - rows_cols[2] gs_plot = gridspec.GridSpecFromSubplotSpec( 2, 1, subplot_spec=gs_spec, height_ratios=(1, 10 + 14 * extra_rows), hspace=0.1 / (extra_rows * 1.4 + 1)) # transform arrays to the given orthogonal direction ax = fig.add_subplot(gs_plot[1, 0]) plot_support.hide_axes(ax) plane = config.PLANE[axis] arrs_3d, aspect, origin, scaling = \ plot_support.setup_images_for_plane( plane, (self.image5d[0], self.labels_img, self.borders_img)) img3d_transposed = arrs_3d[0] labels_img_transposed = libmag.get_if_within(arrs_3d, 1) borders_img_transposed = libmag.get_if_within(arrs_3d, 2) # slider through image planes ax_scroll = fig.add_subplot(gs_plot[0, 0]) plane_slider = Slider(ax_scroll, plot_support.get_plane_axis(plane), 0, len(img3d_transposed) - 1, valfmt="%d", valinit=0, valstep=1) # plot editor max_size = max_sizes[axis] if max_sizes else None plot_ed = plot_editor.PlotEditor( ax, img3d_transposed, labels_img_transposed, cmap_labels, plane, aspect, origin, self.update_coords, self.refresh_images, scaling, plane_slider, img3d_borders=borders_img_transposed, cmap_borders=cmap_borders, fn_show_label_3d=self.fn_show_label_3d, interp_planes=self.interp_planes, fn_update_intensity=self.update_color_picker, max_size=max_size, fn_status_bar=self.fn_status_bar) return plot_ed
def fit_frame_to_image(fig, shape, aspect): """Compress figure to fit image only. Use :attr:`config.plot_labels[config.PlotLabels.PADDING]` to configure figure padding, which will turn off the constrained layout. Args: fig: Figure to compress. shape: Shape of image to which the figure will be fit. aspect: Aspect ratio of image. """ pad = config.plot_labels[config.PlotLabels.PADDING] if aspect is None: aspect = 1 img_size_inches = np.divide(shape, fig.dpi) # convert to inches print("image shape: {}, img_size_inches: {}, aspect: {}".format( shape, img_size_inches, aspect)) if aspect > 1: fig.set_size_inches(img_size_inches[1], img_size_inches[0] * aspect) else: # multiply both sides by 1 / aspect => number > 1 to enlarge fig.set_size_inches(img_size_inches[1] / aspect, img_size_inches[0]) if pad: # use neg padding to remove thin left border that sometimes appears; # NOTE: this setting will turn off constrained layout fig.tight_layout(pad=libmag.get_if_within(pad, 0, 0)) print("fig size: {}".format(fig.get_size_inches()))
def rotate90(roi: np.ndarray, rotate: int, axes: Optional[Sequence[int]] = None, multichannel: bool = False) -> np.ndarray: """Rotate an image by increments of 90 degrees. Serves as a wrapper for :meth:`numpy.rot90` with default rotation in the xy plane. Args: roi: Image as a 3D+/-channel array. Can be None to return as-is. rotate: Number of times to rotate 90 degrees. axes: Sequence of two axes defining the plane to rotate; defaults to None to use ``[-2, -1]``, the 2nd to last and last axes. multichannel: True if the image is multichannel; defaults to False. Only used if ``axes`` contains negative axis indices. Returns: Rotated image. """ if rotate is None: return roi if axes is None: # default to using the last 2 axes (xy plane) ax = [-2, -1] else: ax = list(axes) for i, a in enumerate(ax): if a < 0: # wrap neg axes to the end of the axes ax[i] += roi.ndim if multichannel: # skip the channel axis ax[i] -= 1 roi = np.rot90(roi, libmag.get_if_within(rotate, 0), ax) return roi
def stack_to_img(paths, roi_offset, roi_size, series=None, subimg_offset=None, subimg_size=None, animated=False, suffix=None): """Build an image file from a stack of images in a directory or an array, exporting as an animated GIF or movie for multiple planes or extracting a single plane to a standard image file format. Writes the file to the parent directory of path. Args: paths (List[str]): Image paths, which can each be either an image directory or a base path to a single image, including volumetric images. roi_offset (Sequence[int]): Tuple of offset given in user order ``x,y,z``; defaults to None. Requires ``roi_size`` to not be None. roi_size (Sequence[int]): Size of the region of interest in user order ``x,y,z``; defaults to None. Requires ``roi_offset`` to not be None. series (int): Image series number; defaults to None. subimg_offset (List[int]): Sub-image offset as (z,y,x) to load; defaults to None. subimg_size (List[int]): Sub-image size as (z,y,x) to load; defaults to None. animated (bool): True to export as an animated image; defaults to False. suffix (str): String to append to output path before extension; defaults to None to ignore. """ # set up figure layout for collages size = config.plot_labels[config.PlotLabels.LAYOUT] ncols, nrows = size if size else (1, 1) num_paths = len(paths) collage = num_paths > 1 figs = {} for i in range(nrows): for j in range(ncols): n = i * ncols + j if n >= num_paths: break # load an image and set up its image stacker path_sub = paths[n] axs = [] # TODO: test directory of images # TODO: consider not reloading first image np_io.setup_images(path_sub, series, subimg_offset, subimg_size) stacker = setup_stack( config.image5d, path_sub, offset=roi_offset, roi_size=roi_size, slice_vals=config.slice_vals, rescale=config.transform[config.Transforms.RESCALE], labels_imgs=(config.labels_img, config.borders_img)) # add sub-plot title unless groups given as empty string title = None if config.groups: title = libmag.get_if_within(config.groups, n) elif num_paths > 1: title = os.path.basename(path_sub) if not stacker.images: continue ax = None for k in range(len(stacker.images[0])): # create or retrieve fig; animation has only 1 fig planei = 0 if animated else (stacker.img_slice.start + k * stacker.img_slice.step) fig_dict = figs.get(planei) if not fig_dict: # set up new fig fig, gs = plot_support.setup_fig( nrows, ncols, config.plot_labels[config.PlotLabels.SIZE]) fig_dict = {"fig": fig, "gs": gs, "imgs": []} figs[planei] = fig_dict if ax is None: # generate new axes for the gridspec position ax = fig_dict["fig"].add_subplot(fig_dict["gs"][i, j]) if title: ax.title.set_text(title) axs.append(ax) # export planes plotted_imgs = stacker.build_stack( axs, config.plot_labels[config.PlotLabels.SCALE_BAR], size is None or ncols * nrows == 1) if animated: # store all plotted images in single fig fig_dict = figs.get(0) if fig_dict: fig_dict["imgs"] = plotted_imgs else: # store one plotted image per fig; not used currently for fig_dict, img in zip(figs.values(), plotted_imgs): fig_dict["imgs"].append(img) path_base = paths[0] for planei, fig_dict in figs.items(): if animated: # generate animated image (eg animated GIF or movie file) animate_imgs(path_base, fig_dict["imgs"], config.delay, config.savefig, suffix) else: # generate single figure with axis and plane index in filename if collage: # output filename as a collage of images if not os.path.isdir(path_base): path_base = os.path.dirname(path_base) path_base = os.path.join(path_base, "collage") # insert mod as suffix, then add any additional suffix; # can use config.prefix_out for make_out_path prefix mod = "_plane_{}{}".format( plot_support.get_plane_axis(config.plane), planei) out_path = libmag.make_out_path(path_base, suffix=mod) if suffix: out_path = libmag.insert_before_ext(out_path, suffix) plot_support.save_fig(out_path, config.savefig, fig=fig_dict["fig"])
def show_overview(self): """Show the main 2D plane, taken as a z-plane.""" # assume colorbar already shown if set and image previously displayed colorbar = (config.roi_profile["colorbar"] and len(self.axes.images) < 1) self.axes.clear() self.hline = None self.vline = None # prep 2D image from main image, assumed to be an intensity image imgs2d = [self._get_img2d(0, self.img3d, self.max_intens_proj)] self._channels = [config.channel] cmaps = [config.cmaps] alphas = [config.alphas[0]] shapes = [self._img3d_shapes[0][1:3]] vmaxs = [None] vmins = [None] if self._plot_ax_imgs: # use settings from previously displayed images if available vmaxs[0] = [a.ax_img.norm.vmax for a in self._plot_ax_imgs[0]] vmins[0] = [a.ax_img.norm.vmin for a in self._plot_ax_imgs[0]] if self.img3d_labels is not None: # prep labels with discrete colormap and prior alpha if available imgs2d.append(self._get_img2d(1, self.img3d_labels)) self._channels.append([0]) cmaps.append(self.cmap_labels) alphas.append(self._ax_img_labels.get_alpha() if self. _ax_img_labels else self.alpha) shapes.append(self._img3d_shapes[1][1:3]) vmaxs.append(None) vmins.append(None) if self.img3d_borders is not None: # prep borders image, which may have an extra channels # dimension for multiple sets of borders img2d = self._get_img2d(2, self.img3d_borders) channels = img2d.ndim if img2d.ndim >= 3 else 1 for i, channel in enumerate(range(channels - 1, -1, -1)): # show first (original) borders image last so that its # colormap values take precedence to highlight original bounds img_add = img2d[..., channel] if channels > 1 else img2d imgs2d.append(img_add) self._channels.append([0]) cmaps.append(self.cmap_borders[channel]) alphas.append(libmag.get_if_within(config.alphas, 2 + i, 1)) shapes.append(self._img3d_shapes[2][1:3]) vmaxs.append(None) vmins.append(None) if self.img3d_extras is not None: for i, img in enumerate(self.img3d_extras): # prep additional intensity image imgi = 3 + i imgs2d.append(self._get_img2d(imgi, img)) self._channels.append([0]) cmaps.append(("Greys", )) alphas.append(0.4) shapes.append(self._img3d_shapes[imgi][1:3]) vmaxs.append(None) vmins.append(None) # overlay all images and set labels for footer value on mouseover; # if first time showing image, need to check for images with single # value since they fail to update on subsequent updates for unclear # reasons ax_imgs = plot_support.overlay_images( self.axes, self.aspect, self.origin, imgs2d, self._channels, cmaps, alphas, vmins, vmaxs, check_single=(self._ax_img_labels is None)) if colorbar: self.axes.figure.colorbar(ax_imgs[0][0], ax=self.axes) self.axes.format_coord = pixel_display.PixelDisplay( imgs2d, ax_imgs, shapes, cmap_labels=self.cmap_labels) # trigger actual display through slider or call update directly if self.plane_slider: self.plane_slider.set_val(self.coord[0]) else: self._update_overview(self.coord[0]) self.show_roi() if self.scale_bar: plot_support.add_scale_bar(self.axes, self._downsample[0]) # store displayed images if len(ax_imgs) > 1: self._ax_img_labels = ax_imgs[1][0] self._plot_ax_imgs = [[PlotAxImg(img) for img in imgs] for imgs in ax_imgs] if self.xlim is not None and self.ylim is not None: # restore pan/zoom view self.axes.set_xlim(self.xlim) self.axes.set_ylim(self.ylim) if not self.connected: # connect once get AxesImage self.connect() # text label with color for visibility on axes plus fig background self.region_label = self.axes.text(0, 0, "", color="k", bbox=dict(facecolor="xkcd:silver", alpha=0.5)) self.circle = None
def imshow_multichannel(ax, img2d, channel, cmaps, aspect, alpha=None, vmin=None, vmax=None, origin=None, interpolation=None, norms=None, nan_color=None, ignore_invis=False): """Show multichannel 2D image with channels overlaid over one another. Applies :attr:`config.transform` with :obj:`config.Transforms.ROTATE` to rotate images. If not available, also checks the first element in :attr:``config.flip`` to rotate the image by 180 degrees. Applies :attr:`config.transform` with :obj:`config.Transforms.FLIP_HORIZ` and :obj:`config.Transforms.FLIP_VERT` to invert images. Args: ax: Axes plot. img2d: 2D image either as 2D (y, x) or 3D (y, x, channel) array. channel: Channel to display; if None, all channels will be shown. cmaps: List of colormaps corresponding to each channel. Colormaps can be the names of specific maps in :mod:``config``. aspect: Aspect ratio. alpha (float, List[float]): Transparency level for all channels or sequence of levels for each channel. If any value is 0, the corresponding image will not be output. Defaults to None to use 1. vmin (float, List[float]): Scalar or sequence of vmin levels for all channels; defaults to None. vmax (float, List[float]): Scalar or sequence of vmax levels for all channels; defaults to None. origin: Image origin; defaults to None. interpolation: Type of interpolation; defaults to None. norms: List of normalizations, which should correspond to ``cmaps``. nan_color (str): String of color to use for NaN values; defaults to None to leave these pixels empty. ignore_invis (bool): True to give None instead of an ``AxesImage`` object that would be invisible; defaults to False. Returns: List of ``AxesImage`` objects. """ # assume that 3D array has a channel dimension multichannel, channels = plot_3d.setup_channels(img2d, channel, 2) img = [] num_chls = len(channels) if alpha is None: alpha = 1 if num_chls > 1 and not libmag.is_seq(alpha): # if alphas not explicitly set per channel, make all channels more # translucent at a fixed value that is higher with more channels alpha /= np.sqrt(num_chls + 1) # transform image based on config parameters rotate = config.transform[config.Transforms.ROTATE] if rotate is not None: last_axis = img2d.ndim - 1 if multichannel: last_axis -= 1 # use first rotation value img2d = np.rot90(img2d, libmag.get_if_within(rotate, 0), (last_axis - 1, last_axis)) for chl in channels: img2d_show = img2d[..., chl] if multichannel else img2d cmap = None if cmaps is None else cmaps[chl] norm = None if norms is None else norms[chl] cmap = colormaps.get_cmap(cmap) if cmap is not None and nan_color: # given color for masked values such as NaNs to distinguish from 0 cmap.set_bad(color=nan_color) # get setting corresponding to the channel index, or use the value # directly if it is a scalar vmin_plane = libmag.get_if_within(vmin, chl) vmax_plane = libmag.get_if_within(vmax, chl) alpha_plane = libmag.get_if_within(alpha, chl) img_chl = None if not ignore_invis or alpha_plane > 0: # skip display if alpha is 0 to avoid outputting a hidden image # that may show up in other renderers (eg PDF viewers) img_chl = ax.imshow(img2d_show, cmap=cmap, norm=norm, aspect=aspect, alpha=alpha_plane, vmin=vmin_plane, vmax=vmax_plane, origin=origin, interpolation=interpolation) img.append(img_chl) # flip horizontally or vertically by inverting axes if config.transform[config.Transforms.FLIP_HORIZ]: if not ax.xaxis_inverted(): ax.invert_xaxis() if config.transform[config.Transforms.FLIP_VERT]: inverted = ax.yaxis_inverted() if (origin in (None, "lower") and inverted) or (origin == "upper" and not inverted): # invert only if inversion state is same as expected from origin # to avoid repeated inversions with repeated calls ax.invert_yaxis() return img
def show_overview(self): """Show the main 2D plane, taken as a z-plane.""" self.axes.clear() self.hline = None self.vline = None # prep 2D image from main image, assumed to be an intensity image, # with settings for each channel within this main image imgs2d = [self._get_img2d(0, self.img3d, self.max_intens_proj)] self._channels = [config.channel] cmaps = [config.cmaps] alphas = [config.alphas[0]] alpha_blends = [None] shapes = [self._img3d_shapes[0][1:3]] vmaxs = [None] vmins = [None] brightnesses = [None] contrasts = [None] if self._plot_ax_imgs: # use vmin/vmax from norm values in previously displayed images # if available; None specifies auto-scaling vmaxs[0] = [ p.vmax if p.vmax is None else p.ax_img.norm.vmax for p in self._plot_ax_imgs[0] ] vmins[0] = [ p.vmin if p.vmin is None else p.ax_img.norm.vmin for p in self._plot_ax_imgs[0] ] # use opacity, brightness, anc contrast from prior images alphas[0] = [p.alpha for p in self._plot_ax_imgs[0]] alpha_blends[0] = [p.alpha_blend for p in self._plot_ax_imgs[0]] brightnesses[0] = [p.brightness for p in self._plot_ax_imgs[0]] contrasts[0] = [p.contrast for p in self._plot_ax_imgs[0]] if self.img3d_labels is not None: # prep labels with discrete colormap and prior alpha if available imgs2d.append(self._get_img2d(1, self.img3d_labels)) self._channels.append([0]) cmaps.append(self.cmap_labels) alphas.append(self._ax_img_labels.get_alpha() if self. _ax_img_labels else self.alpha) alpha_blends.append(None) shapes.append(self._img3d_shapes[1][1:3]) vmaxs.append(None) vmins.append(None) if self.img3d_borders is not None: # prep borders image, which may have an extra channels # dimension for multiple sets of borders img2d = self._get_img2d(2, self.img3d_borders) channels = img2d.ndim if img2d.ndim >= 3 else 1 for i, channel in enumerate(range(channels - 1, -1, -1)): # show first (original) borders image last so that its # colormap values take precedence to highlight original bounds img_add = img2d[..., channel] if channels > 1 else img2d imgs2d.append(img_add) self._channels.append([0]) cmaps.append(self.cmap_borders[channel]) # get alpha for last corresponding borders plane if available ax_img = libmag.get_if_within(self._plot_ax_imgs, 2 + i, None) alpha = (ax_img[i].alpha if ax_img else libmag.get_if_within( config.alphas, 2 + i, 1)) alphas.append(alpha) alpha_blends.append(None) shapes.append(self._img3d_shapes[2][1:3]) vmaxs.append(None) vmins.append(None) if self.img3d_extras is not None: for i, img in enumerate(self.img3d_extras): # prep additional intensity image imgi = 3 + i imgs2d.append(self._get_img2d(imgi, img)) self._channels.append([0]) cmaps.append(("Greys", )) alphas.append(0.4) alpha_blends.append(None) shapes.append(self._img3d_shapes[imgi][1:3]) vmaxs.append(None) vmins.append(None) # overlay all images and set labels for footer value on mouseover; # if first time showing image, need to check for images with single # value since they fail to update on subsequent updates for unclear # reasons ax_imgs = plot_support.overlay_images( self.axes, self.aspect, self.origin, imgs2d, self._channels, cmaps, alphas, vmins, vmaxs, check_single=(self._ax_img_labels is None), alpha_blends=alpha_blends) # add or update colorbar colobar_prof = config.roi_profile["colorbar"] if self._colorbar: self._colorbar.update_normal(ax_imgs[0][0]) elif colobar_prof: # store colorbar since it's tied to the artist, which will be # replaced with the next display and cannot be further accessed self._colorbar = self.axes.figure.colorbar(ax_imgs[0][0], ax=self.axes, **colobar_prof) # display coordinates and label values for each image self.axes.format_coord = pixel_display.PixelDisplay( imgs2d, ax_imgs, shapes, cmap_labels=self.cmap_labels) # trigger actual display through slider or call update directly if self.plane_slider: self.plane_slider.set_val(self.coord[0]) else: self._update_overview(self.coord[0]) self.show_roi() if self.scale_bar: plot_support.add_scale_bar(self.axes, self._downsample[0]) # store displayed images in the PlotAxImg container class and update # displayed brightness/contrast if len(ax_imgs) > 1: self._ax_img_labels = ax_imgs[1][0] self._plot_ax_imgs = [] for i, imgs in enumerate(ax_imgs): plot_ax_imgs = [] for j, img in enumerate(imgs): plot_ax_img = PlotAxImg(img) if i == 0: # specified vmin/vmax, in contrast to the AxesImages's # norm, which holds the values used for the displayed image plot_ax_img.vmin = libmag.get_if_within(vmins[i], j) plot_ax_img.vmax = libmag.get_if_within(vmaxs[i], j) # set brightness/contrast self.change_brightness_contrast( plot_ax_img, libmag.get_if_within(brightnesses[i], j), libmag.get_if_within(contrasts[i], j)) plot_ax_img.alpha = libmag.get_if_within(alphas[i], j) plot_ax_img.alpha_blend = libmag.get_if_within( alpha_blends[i], j) plot_ax_imgs.append(plot_ax_img) self._plot_ax_imgs.append(plot_ax_imgs) if self.xlim is not None and self.ylim is not None: # restore pan/zoom view self.axes.set_xlim(self.xlim) self.axes.set_ylim(self.ylim) if not self.connected: # connect once get AxesImage self.connect() # text label with color for visibility on axes plus fig background self.region_label = self.axes.text(0, 0, "", color="k", bbox=dict(facecolor="xkcd:silver", alpha=0.5)) self.circle = None