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 update_btn(self): """Update text and color of button to interpolate planes. """ if any(self.bounds): # show current values if any exist self.btn.label.set_text("Fill {} {}\nID {}".format( plot_support.get_plane_axis(self.plane), self.bounds, self.label_id)) self.btn.label.set_fontsize("xx-small") enable_btn(self.btn, all(self.bounds))
def handle_extracted_plane(): # get sub-plot and hide x/y axes ax = axs if libmag.is_seq(ax): ax = axs[imgi] plot_support.hide_axes(ax) # multiple artists can be shown at each frame by collecting # each group of artists in a list; overlay_images returns # a nested list containing a list for each image, which in turn # contains a list of artists for each channel ax_imgs = plot_support.overlay_images(ax, self.aspect, self.origin, imgs, None, cmaps_all, ignore_invis=True, check_single=True) if (colorbar is not None and len(ax_imgs) > 0 and len(ax_imgs[0]) > 0 and imgi == 0): # add colorbar with scientific notation if outside limits cbar = ax.figure.colorbar(ax_imgs[0][0], ax=ax, **colorbar) plot_support.set_scinot(cbar.ax, lbls=None, units=None) plotted_imgs[imgi] = np.array(ax_imgs).flatten() if libmag.is_seq(text_pos) and len(text_pos) > 1: # write plane index in axes rather than data coordinates text = ax.text(*text_pos[:2], "{}-plane: {}".format( plot_support.get_plane_axis(config.plane), self.start_planei + imgi), transform=ax.transAxes, color="w") plotted_imgs[imgi] = [*plotted_imgs[imgi], text] if scale_bar: plot_support.add_scale_bar(ax, 1 / self.rescale, config.plane)
def __str__(self): return "{}: {} (ID: {})".format( plot_support.get_plane_axis(self.plane), self.bounds, self.label_id)
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 setup_stack( image5d: np.ndarray, path: Optional[str] = None, offset: Optional[Sequence[int]] = None, roi_size: Optional[Sequence[int]] = None, slice_vals: Optional[Sequence[int]] = None, rescale: Optional[float] = None, labels_imgs: Optional[Sequence[np.ndarray]] = None) -> StackPlaneIO: """Set up a stack of images for export to file. Supports a stack of image files in a directory or a single volumetric image and associated labels images. Args: image5d: Images as a 4/5D Numpy array (t,z,y,x[c]). Can be None if ``path`` is set. path: Path to an image directory from which all files will be imported in Python sorted order, taking precedence over ``imaged5d``; defaults to None. offset: Tuple of offset given in user order (x, y, z); defaults to None. Requires ``roi_size`` to not be None. roi_size: Size of the region of interest in user order (x, y, z); defaults to None. Requires ``offset`` to not be None. slice_vals: List from which to construct a slice object to extract only a portion of the image. Defaults to None, which will give the whole image. If ``offset`` and ``roi_size`` are also given, ``slice_vals`` will only be used for its interval term. rescale: Rescaling factor for each image, performed on a plane-by-plane basis; defaults to None, in which case 1.0 will be used. labels_imgs: Sequence of labels-based images as a Numpy z,y,x arrays, typically including labels and borders images; defaults to None. Returns: Stack builder instance. """ print("Starting image stack setup") # build "z" slice, which will be applied to the transposed image interval = 1 # default to export each plane if offset is not None and roi_size is not None: # extract planes based on ROI settings # transpose coordinates to given plane _, arrs_1d = plot_support.transpose_images( config.plane, arrs_1d=[offset[::-1], roi_size[::-1]]) offset = arrs_1d[0][::-1] roi_size = arrs_1d[1][::-1] # ROI offset and size take precedence over slice vals except # for use of the interval term if slice_vals is not None and len(slice_vals) > 2: interval = slice_vals[2] size = roi_size[2] img_sl = slice(offset[2], offset[2] + size, interval) if interval is not None and interval < 0: # reverse start/stop order to iterate backward img_sl = slice(img_sl.stop, img_sl.start, interval) print("using ROI offset {}, size {}, {}".format(offset, size, img_sl)) elif slice_vals: # build directly from slice vals, replacing start and step if None sl = slice(*slice_vals) sl = [sl.start, sl.stop, sl.step] if sl[0] is None: # default to start at beginning of stack sl[0] = 0 if sl[2] is None: # default to interval/step of 1 sl[2] = 1 img_sl = slice(*sl) else: # default to take the whole image stack img_sl = slice(0, None, 1) if rescale is None: rescale = 1.0 aspect = None origin = None cmaps_labels = [] extracted_planes = [] start_planei = 0 if path and os.path.isdir(path): # builds animations from all files in a directory planes = sorted(glob.glob(os.path.join(path, "*")))[::interval] _logger.info("Importing images from %s: %s", path, planes) fnc = StackPlaneIO.import_img extracted_planes.append(planes) else: # load images from path and extract ROI based on slice parameters, # assuming 1st image is atlas, 2nd and beyond are labels-based imgs = [image5d] if labels_imgs is not None: for img in labels_imgs: if img is not None: imgs.append(img[None]) _setup_labels_cmaps(imgs, cmaps_labels) main_shape = None # z,y,x shape of 1st image for img in imgs: sl = img_sl img_shape = img.shape[1:4] if main_shape: if main_shape != img_shape: # scale slice bounds to the first image's shape scaling = np.divide(img_shape, main_shape) axis = plot_support.get_plane_axis(config.plane, True) sl = libmag.scale_slice(sl, scaling[axis], img_shape[axis]) else: main_shape = img_shape planes, aspect, origin = plot_support.extract_planes( img, sl, plane=config.plane) if offset is not None and roi_size is not None: # get ROI using transposed coordinates on transposed planes; # returns list planes = planes[:, offset[1]:offset[1] + roi_size[1], offset[0]:offset[0] + roi_size[0]] extracted_planes.append(planes) fnc = StackPlaneIO.process_plane if img_sl.start: start_planei = img_sl.start # store in stack worker stacker = StackPlaneIO() StackPlaneIO.interp_order = config.transform[ config.Transforms.INTERPOLATION] stacker.images = extracted_planes stacker.fn_process = fnc stacker.rescale = rescale stacker.start_planei = start_planei stacker.origin = origin stacker.aspect = aspect stacker.cmaps_labels = cmaps_labels stacker.img_slice = img_sl return stacker
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. """ size = config.plot_labels[config.PlotLabels.LAYOUT] ncols, nrows = size if size else (1, 1) fig, gs = plot_support.setup_fig( nrows, ncols, config.plot_labels[config.PlotLabels.SIZE]) plotted_imgs = None num_paths = len(paths) for i in range(nrows): for j in range(ncols): n = i * ncols + j if n >= num_paths: break ax = fig.add_subplot(gs[i, j]) path_sub = paths[n] # TODO: test directory of images # TODO: avoid reloading first image np_io.setup_images(path_sub, series, subimg_offset, subimg_size) plotted_imgs = stack_to_ax_imgs( ax, 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), multiplane=animated, fit=(size is None or ncols * nrows == 1)) path_base = paths[0] if animated: # generate animated image (eg animated GIF or movie file) animate_imgs(path_base, plotted_imgs, config.delay, config.savefig, suffix) else: # save image as single file if roi_offset: # get plane index from coordinate at the given axis in ROI offset planei = roi_offset[::-1][plot_support.get_plane_axis( config.plane, get_index=True)] else: # get plane index from slice start planei = config.slice_vals[0] if num_paths > 1: # 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") mod = "_plane_{}{}".format(plot_support.get_plane_axis(config.plane), planei) if suffix: path_base = libmag.insert_before_ext(path_base, suffix) plot_support.save_fig(path_base, config.savefig, mod)
def stack_to_ax_imgs(ax, image5d, path=None, offset=None, roi_size=None, slice_vals=None, rescale=None, labels_imgs=None, multiplane=False, fit=False): """Export a stack of images in a directory or a single volumetric image and associated labels images to :obj:`matplotlib.image.AxesImage` objects for export. Args: ax (:obj:`plt.Axes`): Matplotlib axes on which to plot images. image5d: Images as a 4/5D Numpy array (t,z,y,x[c]). Can be None if ``path`` is set. path: Path to an image directory from which all files will be imported in Python sorted order, taking precedence over ``imaged5d``; defaults to None. offset: Tuple of offset given in user order (x, y, z); defaults to None. Requires ``roi_size`` to not be None. roi_size: Size of the region of interest in user order (x, y, z); defaults to None. Requires ``offset`` to not be None. slice_vals: List from which to construct a slice object to extract only a portion of the image. Defaults to None, which will give the whole image. If ``offset`` and ``roi_size`` are also given, ``slice_vals`` will only be used for its interval term. rescale: Rescaling factor for each image, performed on a plane-by-plane basis; defaults to None, in which case 1.0 will be used. labels_imgs: Sequence of labels-based images as a Numpy z,y,x arrays, typically including labels and borders images; defaults to None. multiplane: True to extract the images as an animated GIF or movie file; False to extract a single plane only. Defaults to False. fit (bool): True to fit the figure frame to the resulting image. Returns: List[:obj:`matplotlib.image.AxesImage`]: List of image objects. """ print("Starting image stack export") # build "z" slice, which will be applied to the transposed image; # reduce image to 1 plane if in single mode interval = 1 if offset is not None and roi_size is not None: # transpose coordinates to given plane _, arrs_1d = plot_support.transpose_images( config.plane, arrs_1d=[offset[::-1], roi_size[::-1]]) offset = arrs_1d[0][::-1] roi_size = arrs_1d[1][::-1] # ROI offset and size take precedence over slice vals except # for use of the interval term interval = None if slice_vals is not None and len(slice_vals) > 2: interval = slice_vals[2] size = roi_size[2] if multiplane else 1 img_sl = slice(offset[2], offset[2] + size, interval) if interval is not None and interval < 0: # reverse start/stop order to iterate backward img_sl = slice(img_sl.stop, img_sl.start, interval) print("using ROI offset {}, size {}, {}".format(offset, size, img_sl)) elif slice_vals is not None: # build directly from slice vals unless not an animation if multiplane: img_sl = slice(*slice_vals) else: # single plane only for non-animation img_sl = slice(slice_vals[0], slice_vals[0] + 1) else: # default to take the whole image stack img_sl = slice(None, None) if rescale is None: rescale = 1.0 aspect = None origin = None cmaps_labels = [] extracted_planes = [] start_planei = 0 if path and os.path.isdir(path): # builds animations from all files in a directory planes = sorted(glob.glob(os.path.join(path, "*")))[::interval] print(planes) fnc = StackPlaneIO.import_img extracted_planes.append(planes) else: # load images from path and extract ROI based on slice parameters, # assuming 1st image is atlas, 2nd and beyond are labels-based imgs = [image5d] if labels_imgs is not None: for img in labels_imgs: if img is not None: imgs.append(img[None]) _setup_labels_cmaps(imgs, cmaps_labels) main_shape = None # z,y,x shape of 1st image for img in imgs: sl = img_sl img_shape = img.shape[1:4] if main_shape: if main_shape != img_shape: # scale slice bounds to the first image's shape scaling = np.divide(img_shape, main_shape) axis = plot_support.get_plane_axis(config.plane, True) sl = libmag.scale_slice(sl, scaling[axis], img_shape[axis]) else: main_shape = img_shape planes, aspect, origin = plot_support.extract_planes( img, sl, plane=config.plane) if offset is not None and roi_size is not None: # get ROI using transposed coordinates on transposed planes; # returns list planes = planes[:, offset[1]:offset[1] + roi_size[1], offset[0]:offset[0] + roi_size[0]] extracted_planes.append(planes) fnc = StackPlaneIO.process_plane if img_sl.start: start_planei = img_sl.start # export planes plotted_imgs = _build_stack( ax, extracted_planes, fnc, rescale, aspect=aspect, origin=origin, cmaps_labels=cmaps_labels, scale_bar=config.plot_labels[config.PlotLabels.SCALE_BAR], start_planei=start_planei) if fit and plotted_imgs: # fit frame to first plane's first available image ax_img = None for ax_img in plotted_imgs[0]: # images may be None if alpha set to 0 if ax_img is not None: break if ax_img is not None: plot_support.fit_frame_to_image(ax_img.figure, ax_img.get_array().shape, aspect) return plotted_imgs
def _build_stack(ax, images, process_fnc, rescale=1, aspect=None, origin=None, cmaps_labels=None, scale_bar=True, start_planei=0): """Builds a stack of Matploblit 2D images. Uses multiprocessing to load or resize each image. Args: images: Sequence of images. For import, each "image" is a path to and image file. For export, each "image" is a sequence of planes, with the first sequence assumed to an atlas, followed by labels-based images, each consisting of corresponding planes. process_fnc: Function to process each image through multiprocessing, where the function should take an index and image and return the index and processed plane. rescale (float): Rescale factor; defaults to 1. cmaps_labels: Sequence of colormaps for labels-based images; defaults to None. Length should be equal to that of ``images`` - 1. scale_bar: True to include scale bar; defaults to True. start_planei (int): Index of start plane, used for labeling the plane; defaults to 0. The plane is only annotated when :attr:`config.plot_labels[config.PlotLabels.TEXT_POS]` is given to specify the position of the text in ``x,y`` relative to the axes. Returns: :List[List[:obj:`matplotlib.image.AxesImage`]]: Nested list of axes image objects. The first list level contains planes, and the second level are channels within each plane. """ # number of image types (eg atlas, labels) and corresponding planes num_image_types = len(images) if num_image_types < 1: return None num_images = len(images[0]) if num_images < 1: return None # Matplotlib figure for building the animation plot_support.hide_axes(ax) # import the images as Matplotlib artists via multiprocessing plotted_imgs = [None] * num_images img_shape = images[0][0].shape target_size = np.multiply(img_shape, rescale).astype(int) multichannel = images[0][0].ndim >= 3 if multichannel: print("building stack for channel: {}".format(config.channel)) target_size = target_size[:-1] StackPlaneIO.set_data(images) pool = chunking.get_mp_pool() pool_results = [] for i in range(num_images): # add rotation argument if necessary pool_results.append( pool.apply_async(process_fnc, args=(i, target_size))) # setup imshow parameters colorbar = config.roi_profile["colorbar"] cmaps_all = [config.cmaps, *cmaps_labels] img_size = None text_pos = config.plot_labels[config.PlotLabels.TEXT_POS] for result in pool_results: i, imgs = result.get() if img_size is None: img_size = imgs[0].shape # multiple artists can be shown at each frame by collecting # each group of artists in a list; overlay_images returns # a nested list containing a list for each image, which in turn # contains a list of artists for each channel ax_imgs = plot_support.overlay_images(ax, aspect, origin, imgs, None, cmaps_all, ignore_invis=True, check_single=True) if colorbar and len(ax_imgs) > 0 and len(ax_imgs[0]) > 0: # add colorbar with scientific notation if outside limits cbar = ax.figure.colorbar(ax_imgs[0][0], ax=ax, shrink=0.7) plot_support.set_scinot(cbar.ax, lbls=None, units=None) plotted_imgs[i] = np.array(ax_imgs).flatten() if libmag.is_seq(text_pos) and len(text_pos) > 1: # write plane index in axes rather than data coordinates text = ax.text(*text_pos[:2], "{}-plane: {}".format( plot_support.get_plane_axis(config.plane), start_planei + i), transform=ax.transAxes, color="w") plotted_imgs[i] = [*plotted_imgs[i], text] pool.close() pool.join() if scale_bar: plot_support.add_scale_bar(ax, 1 / rescale, config.plane) return plotted_imgs