def build_stack(self, axs: List, scale_bar: bool = True, fit: bool = False) -> Optional[List]: """Builds a stack of Matploblit 2D images. Uses multiprocessing to load or resize each image. Args: axs: Sub-plot axes. scale_bar: True to include scale bar; defaults to True. fit: True to fit the figure frame to the resulting image. 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. """ 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) # number of image types (eg atlas, labels) and corresponding planes num_image_types = len(self.images) if num_image_types < 1: return None num_images = len(self.images[0]) if num_images < 1: return None # import the images as Matplotlib artists via multiprocessing plotted_imgs: List = [None] * num_images img_shape = self.images[0][0].shape target_size = np.multiply(img_shape, self.rescale).astype(int) multichannel = self.images[0][0].ndim >= 3 if multichannel: print("building stack for channel: {}".format(config.channel)) target_size = target_size[:-1] # setup imshow parameters colorbar = config.roi_profile["colorbar"] cmaps_all = [config.cmaps, *self.cmaps_labels] text_pos = config.plot_labels[config.PlotLabels.TEXT_POS] StackPlaneIO.set_data(self.images) pool_results = None pool = None multiprocess = self.rescale != 1 if multiprocess: # set up multiprocessing initializer = None initargs = None if not chunking.is_fork(): # set up labels image as a shared array for spawned mode initializer, initargs = StackPlaneIO.build_pool_init( OrderedDict([(i, img) for i, img in enumerate(self.images)])) pool = chunking.get_mp_pool(initializer, initargs) pool_results = [] for i in range(num_images): # add rotation argument if necessary args = (i, target_size) if pool is None: # extract and handle without multiprocessing imgi, imgs = self.fn_process(*args) handle_extracted_plane() else: # extract plane in multiprocessing pool_results.append( pool.apply_async(self.fn_process, args=args)) if multiprocess: # handle multiprocessing output for result in pool_results: imgi, imgs = result.get() handle_extracted_plane() pool.close() pool.join() if fit and plotted_imgs: # fit each figure to its first available image for ax_img in plotted_imgs: # images may be flattened AxesImage, array of AxesImage and # Text, or None if alpha set to 0 if libmag.is_seq(ax_img): ax_img = ax_img[0] if isinstance(ax_img, AxesImage): plot_support.fit_frame_to_image(ax_img.figure, ax_img.get_array().shape, self.aspect) return plotted_imgs
def colocalize_stack(cls, shape, blobs): """Entry point to colocalizing blobs within a stack. Args: shape (List[int]): Image shape in z,y,x. blobs (:obj:`np.ndarray`): 2D Numpy array of blobs. Returns: dict[tuple[int, int], :class:`BlobMatch`]: The dictionary of matches, where keys are tuples of the channel pairs, and values are blob match objects. """ print("Colocalizing blobs based on matching blobs in each pair of " "channels") # set up ROI blocks from which to select blobs in each block sub_roi_slices, sub_rois_offsets, _, _, _, overlap_base, _, _ \ = stack_detect.setup_blocks(config.roi_profile, shape) match_tol = np.multiply( overlap_base, config.roi_profile["verify_tol_factor"]) is_fork = chunking.is_fork() if is_fork: # set shared data in forked multiprocessing cls.blobs = blobs cls.match_tol = match_tol pool = mp.Pool(processes=config.cpus) pool_results = [] for z in range(sub_roi_slices.shape[0]): for y in range(sub_roi_slices.shape[1]): for x in range(sub_roi_slices.shape[2]): coord = (z, y, x) offset = sub_rois_offsets[coord] slices = sub_roi_slices[coord] shape = [s.stop - s.start for s in slices] if is_fork: # use variables stored as class attributes pool_results.append(pool.apply_async( StackColocalizer.colocalize_block, args=(coord, offset, shape))) else: # pickle full set of variables pool_results.append(pool.apply_async( StackColocalizer.colocalize_block, args=(coord, offset, shape, detector.get_blobs_in_roi( blobs, offset, shape)[0], match_tol, True))) # dict of channel combos to blob matches data frame matches_all = {} for result in pool_results: coord, matches = result.get() count = 0 for key, val in matches.items(): matches_all.setdefault(key, []).append(val.df) count += len(val.df) print("adding {} matches from block at {} of {}" .format(count, coord, np.add(sub_roi_slices.shape, -1))) pool.close() pool.join() # prune duplicates by taking matches with shortest distance for key in matches_all.keys(): matches_all[key] = pd.concat(matches_all[key]) for blobi in (BlobMatch.Cols.BLOB1, BlobMatch.Cols.BLOB2): # convert blob column to ndarray to extract coords by column matches = matches_all[key] matches_uniq, matches_i, matches_inv, matches_cts = np.unique( np.vstack(matches[blobi.value])[:, :3], axis=0, return_index=True, return_inverse=True, return_counts=True) if np.sum(matches_cts > 1) > 0: # prune if at least one blob has been matched to multiple # other blobs singles = matches.iloc[matches_i[matches_cts == 1]] dups = [] for i, ct in enumerate(matches_cts): # include non-duplicates to retain index if ct <= 1: continue # get indices in orig matches at given unique array # index and take match with lowest dist matches_mult = matches.loc[matches_inv == i] dists = matches_mult[BlobMatch.Cols.DIST.value] min_dist = np.amin(dists) num_matches = len(matches_mult) if config.verbose and num_matches > 1: print("pruning from", num_matches, "matches of dist:", dists) matches_mult = matches_mult.loc[dists == min_dist] # take first in case of any ties dups.append(matches_mult.iloc[[0]]) matches_all[key] = pd.concat((singles, pd.concat(dups))) print("Colocalization matches for channels {}: {}" .format(key, len(matches_all[key]))) libmag.printv(print(matches_all[key])) # store data frame in BlobMatch object matches_all[key] = BlobMatch(df=matches_all[key]) return matches_all
def prune_blobs_mp(cls, img, seg_rois, overlap, tol, sub_roi_slices, sub_rois_offsets, channels, overlap_padding=None): """Prune close blobs within overlapping regions by checking within entire planes across the ROI in parallel with multiprocessing. Args: img (:obj:`np.ndarray`): Array in which to detect blobs. seg_rois (:obj:`np.ndarray`): Blobs from each sub-region. overlap: 1D array of size 3 with the number of overlapping pixels for each image axis. tol: Tolerance as (z, y, x), within which a segment will be considered a duplicate of a segment in the master array and removed. sub_roi_slices (:obj:`np.ndarray`): Object array of ub-regions, used to check size. sub_rois_offsets: Offsets of each sub-region. channels (Sequence[int]): Sequence of channels; defaults to None to detect in all channels. overlap_padding: Sequence of z,y,x for additional padding beyond ``overlap``. Defaults to None to use ``tol`` as padding. Returns: :obj:`np.ndarray`, :obj:`pd.DataFrame`: All blobs as a Numpy array and a data frame of pruning stats, or None for both if no blobs are in the ``seg_rois``. """ # collect all blobs in master array to group all overlapping regions, # with sub-ROI coordinates as last 3 columns blobs_merged = chunking.merge_blobs(seg_rois) if blobs_merged is None: return None, None print("total blobs before pruning:", len(blobs_merged)) print("pruning with overlap: {}, overlap tol: {}, pruning tol: {}" .format(overlap, overlap_padding, tol)) blobs_all = [] blob_ratios = {} cols = ("blobs", "ratio_pruning", "ratio_adjacent") if overlap_padding is None: overlap_padding = tol for i in channels: # prune blobs from each channel separately to avoid pruning based on # co-localized channel signals blobs = detector.blobs_in_channel(blobs_merged, i) for axis in range(3): # prune planes with all the overlapping regions within a given axis, # skipping if this axis has no overlapping sub-regions num_sections = sub_rois_offsets.shape[axis] if num_sections <= 1: continue # multiprocess pruning by overlapping planes blobs_all_non_ol = None # all blobs from non-overlapping regions blobs_to_prune = [] coord_last = tuple(np.subtract(sub_roi_slices.shape, 1)) for j in range(num_sections): # build overlapping region dimensions based on size of # sub-region in the given axis coord = np.zeros(3, dtype=np.int) coord[axis] = j print("** setting up blob pruning in axis {}, section {} " "of {}".format(axis, j, num_sections - 1)) offset = sub_rois_offsets[tuple(coord)] sub_roi = img[sub_roi_slices[tuple(coord)]] size = sub_roi.shape _logger.debug(f"offset: {offset}, size: {size}") # overlapping region: each region but the last extends # into the next region, with the overlapping volume from # the end of the region, minus the overlap and a tolerance # space, to the region's end plus this tolerance space; # non-overlapping region: the region before the overlap, # after any overlap with the prior region (n > 1) # to the start of the overlap (n < last region) blobs_ol = None blobs_ol_next = None blobs_in_non_ol = [] shift = overlap[axis] + overlap_padding[axis] offset_axis = offset[axis] if j < num_sections - 1: bounds = [offset_axis + size[axis] - shift, offset_axis + size[axis] + overlap_padding[axis]] libmag.printv( "axis {}, boundaries: {}".format(axis, bounds)) blobs_ol = blobs[np.all([ blobs[:, axis] >= bounds[0], blobs[:, axis] < bounds[1]], axis=0)] # get blobs from immediatley adjacent region of the same # size as that of the overlapping region; keep same # starting point with or without overlap_tol start = offset_axis + size[axis] + tol[axis] bounds_next = [ start, start + overlap[axis] + 2 * overlap_padding[axis]] shape = np.add( sub_rois_offsets[coord_last], sub_roi.shape[:3]) libmag.printv( "axis {}, boundaries (next): {}, max bounds: {}" .format(axis, bounds_next, shape[axis])) if np.all(np.less(bounds_next, shape[axis])): # ensure that next overlapping region is within ROI blobs_ol_next = blobs[np.all([ blobs[:, axis] >= bounds_next[0], blobs[:, axis] < bounds_next[1]], axis=0)] # non-overlapping region extends up this overlap blobs_in_non_ol.append(blobs[:, axis] < bounds[0]) else: # last non-overlapping region extends to end of region blobs_in_non_ol.append( blobs[:, axis] < offset_axis + size[axis]) # get non-overlapping area start = offset_axis if j > 0: # shift past overlapping part at start of region start += shift blobs_in_non_ol.append(blobs[:, axis] >= start) blobs_non_ol = blobs[np.all(blobs_in_non_ol, axis=0)] # collect all non-overlapping region blobs if blobs_all_non_ol is None: blobs_all_non_ol = blobs_non_ol elif blobs_non_ol is not None: blobs_all_non_ol = np.concatenate( (blobs_all_non_ol, blobs_non_ol)) blobs_to_prune.append((blobs_ol, axis, tol, blobs_ol_next)) is_fork = chunking.is_fork() if is_fork: # set data as class variables to share across forks cls.blobs_to_prune = blobs_to_prune pool = chunking.get_mp_pool() pool_results = [] for j in range(len(blobs_to_prune)): if is_fork: # prune blobs in overlapping regions by multiprocessing, # using a separate class to avoid pickling input blobs pool_results.append(pool.apply_async( StackPruner.prune_overlap_by_index, args=(j, ))) else: # for spawned methods, need to pickle the blobs pool_results.append(pool.apply_async( StackPruner.prune_overlap, args=( j, blobs_to_prune[j]))) # collect all the pruned blob lists blobs_all_ol = None for result in pool_results: blobs_ol_pruned, ratios = result.get() if blobs_all_ol is None: blobs_all_ol = blobs_ol_pruned elif blobs_ol_pruned is not None: blobs_all_ol = np.concatenate( (blobs_all_ol, blobs_ol_pruned)) if ratios: for col, val in zip(cols, ratios): blob_ratios.setdefault(col, []).append(val) # recombine blobs from the non-overlapping with the pruned # overlapping regions from the entire stack to re-prune along # any remaining axes pool.close() pool.join() if blobs_all_ol is None: blobs = blobs_all_non_ol elif blobs_all_non_ol is None: blobs = blobs_all_ol else: blobs = np.concatenate((blobs_all_non_ol, blobs_all_ol)) # build up list from each channel blobs_all.append(blobs) # merge blobs into Numpy array and remove sub-ROI coordinate columns blobs_all = np.vstack(blobs_all)[:, :-3] print("total blobs after pruning:", len(blobs_all)) # export blob ratios as data frame df = pd.DataFrame(blob_ratios) return blobs_all, df
def detect_blobs_sub_rois(cls, img, sub_roi_slices, sub_rois_offsets, denoise_max_shape, exclude_border, coloc, channel): """Process blobs in chunked sub-ROIs via multiprocessing. Args: img (:obj:`np.ndarray`): Array in which to detect blobs. sub_roi_slices (:obj:`np.ndarray`): Numpy object array containing chunked sub-ROIs within a stack. sub_rois_offsets (:obj:`np.ndarray`): Numpy object array of the same shape as ``sub_rois`` with offsets in z,y,x corresponding to each sub-ROI. Offets are used to transpose blobs into absolute coordinates. denoise_max_shape (Tuple[int]): Maximum shape of each unit within each sub-ROI for denoising. exclude_border (Tuple[int]): Sequence of border pixels in x,y,z to exclude; defaults to None. coloc (bool): True to perform blob co-localizations; defaults to False. channel (Sequence[int]): Sequence of channels, where None detects in all channels. Returns: :obj:`np.ndarray`: Numpy object array of blobs corresponding to ``sub_rois``, with each set of blobs given as a Numpy array in the format, ``[n, [z, row, column, radius, ...]]``, including additional elements as given in :meth:``StackDetect.detect_sub_roi``. """ # detect nuclei in each sub-ROI, passing an index to access each # sub-ROI to minimize pickling is_fork = chunking.is_fork() last_coord = np.subtract(sub_roi_slices.shape, 1) if is_fork: # set data as class attributes for direct access during forked # multiprocessing cls.img = img cls.last_coord = last_coord cls.denoise_max_shape = denoise_max_shape cls.exclude_border = exclude_border cls.coloc = coloc cls.channel = channel pool = chunking.get_mp_pool() pool_results = [] for z in range(sub_roi_slices.shape[0]): for y in range(sub_roi_slices.shape[1]): for x in range(sub_roi_slices.shape[2]): coord = (z, y, x) if is_fork: # use variables stored in class pool_results.append(pool.apply_async( StackDetector.detect_sub_roi_from_data, args=(coord, sub_roi_slices[coord], sub_rois_offsets[coord]))) else: # pickle full set of variables including sub-ROI and # filename from which to load image parameters pool_results.append(pool.apply_async( StackDetector.detect_sub_roi, args=(coord, sub_rois_offsets[coord], last_coord, denoise_max_shape, exclude_border, img[sub_roi_slices[coord]], channel, config.filename, coloc))) # retrieve blobs and assign to object array corresponding to sub_rois seg_rois = np.zeros(sub_roi_slices.shape, dtype=object) for result in pool_results: coord, segments = result.get() num_blobs = 0 if segments is None else len(segments) print("adding {} blobs from sub_roi at {} of {}" .format(num_blobs, coord, np.add(sub_roi_slices.shape, -1))) seg_rois[coord] = segments pool.close() pool.join() return seg_rois
def labels_to_markers_erosion( labels_img: np.ndarray, filter_size: int = 8, target_frac: Optional[float] = None, min_filter_size: Optional[int] = None, use_min_filter: bool = False, skel_eros_filt_size: Optional[int] = None, wt_dists: Optional[np.ndarray] = None, multiprocess: bool = True) -> Tuple[np.ndarray, pd.DataFrame]: """Convert a labels image to markers as eroded labels via multiprocessing. These markers can be used in segmentation algorithms such as watershed. Args: labels_img: Labels image as an integer Numpy array, where each unique int is a separate label. filter_size: Size of structing element for erosion, which should be > 0; defaults to 8. target_frac: Target fraction of original label to erode, passed to :func:`LabelToMarkerErosion.erode_label`. Defaults to None. min_filter_size: Minimum erosion filter size; defaults to None to use half of ``filter_size``, rounded down. use_min_filter: True to erode even if ``min_filter_size`` is reached; defaults to False to avoid any erosion if this size is reached. skel_eros_filt_size: Erosion filter size before skeletonization in :func:`LabelToMarkerErosion.erode_labels`. Defaults to None to use the minimum filter size, which is half of ``filter_size``. wt_dists: Array of distances by which to weight the filter size, such as a distance transform to the outer perimeter of ``labels_img`` to weight central labels more heavily. Defaults to None. multiprocess: True to use multiprocessing; defaults to True. Returns: Tuple of an image array of the same shape as ``img`` and the same number of labels as in ``labels_img``, with eroded labels, and a data frame of erosion metrics. """ def handle_eroded_label(): # mutate markers outside of mp for changes to persist and collect stats markers[tuple(slices)][filtered] = stats_eros[0] for col, stat in zip(cols, stats_eros): sizes_dict.setdefault(col, []).append(stat) # set up labels erosion start_time = time() _logger.info( "Eroding labels to markers with filter size %s, min filter size %s, " "and target fraction %s", filter_size, min_filter_size, target_frac) markers = np.zeros_like(labels_img) labels_unique = np.unique(labels_img) if min_filter_size is None: min_filter_size = filter_size // 2 if skel_eros_filt_size is None: skel_eros_filt_size = filter_size // 2 sizes_dict = {} cols = (config.AtlasMetrics.REGION.value, "SizeOrig", "SizeMarker", config.SmoothingMetrics.FILTER_SIZE.value) # share large images as class attributes for forked or non-multiprocessing LabelToMarkerErosion.set_labels_img(labels_img, wt_dists) is_fork = False pool_results = None pool = None if multiprocess: # set up multiprocessing is_fork = chunking.is_fork() initializer = None initargs = None if not is_fork: # set up labels image as a shared array for spawned mode initializer, initargs = LabelToMarkerErosion.build_pool_init( {config.RegNames.IMG_LABELS: labels_img}) pool = chunking.get_mp_pool(initializer, initargs) pool_results = [] for label_id in labels_unique: if label_id == 0: continue # erode labels to generate markers, excluding labels small enough # that they would require a filter smaller than half of original size args = [ label_id, filter_size, target_frac, min_filter_size, use_min_filter, skel_eros_filt_size ] if not is_fork: # pickle distance weight directly in spawned mode (not necessary # for non-multiprocessed but equivalent) if wt_dists is not None: args.append( LabelToMarkerErosion.meas_wt(labels_img, label_id, wt_dists)) if pool is None: # process labels without multiprocessing stats_eros, slices, filtered = LabelToMarkerErosion.erode_label( *args) handle_eroded_label() else: # process in multiprocessing pool_results.append( pool.apply_async(LabelToMarkerErosion.erode_label, args=args)) if multiprocess: # handle multiprocessing output for result in pool_results: stats_eros, slices, filtered = result.get() handle_eroded_label() pool.close() pool.join() # show erosion stats df = df_io.dict_to_data_frame(sizes_dict, show=True) _logger.info("Time elapsed to erode labels into markers: %s", time() - start_time) return markers, df
def transpose_img(filename, series, plane=None, rescale=None, target_size=None): """Transpose Numpy NPY saved arrays into new planar orientations and rescaling or resizing. Rescaling/resizing take place in multiprocessing. Files are saved through memmap-based arrays to minimize RAM usage. Output filenames are based on the ``make_modifer_[task]`` functions. Currently transposes all channels, ignoring :attr:``config.channel`` parameter. Args: filename: Full file path in :attribute:cli:`filename` format. series: Series within multi-series file. plane: Planar orientation (see :attribute:plot_2d:`PLANES`). Defaults to None, in which case no planar transformation will occur. rescale: Rescaling factor; defaults to None. Takes precedence over ``target_size``. target_size (List[int]): Target shape in x,y,z; defaults to None, in which case the target size will be extracted from the register profile if available if available. """ if target_size is None: target_size = config.atlas_profile["target_size"] if plane is None and rescale is None and target_size is None: print("No transposition to perform, skipping") return time_start = time() # even if loaded already, reread to get image metadata # TODO: consider saving metadata in config and retrieving from there img5d = importer.read_file(filename, series) info = img5d.meta image5d = img5d.img sizes = info["sizes"] # make filenames based on transpositions modifier = "" if plane is not None: modifier = make_modifier_plane(plane) # either rescaling or resizing if rescale is not None: modifier += make_modifier_scale(rescale) elif target_size: # target size may differ from final output size but allows a known # size to be used for finding the file later modifier += make_modifier_resized(target_size) filename_image5d_npz, filename_info_npz = importer.make_filenames( filename, series, modifier=modifier) # TODO: image5d should assume 4/5 dimensions offset = 0 if image5d.ndim <= 3 else 1 multichannel = image5d.ndim >= 5 image5d_swapped = image5d if plane is not None and plane != config.PLANE[0]: # swap z-y to get (y, z, x) order for xz orientation image5d_swapped = np.swapaxes(image5d_swapped, offset, offset + 1) config.resolutions[0] = libmag.swap_elements(config.resolutions[0], 0, 1) if plane == config.PLANE[2]: # swap new y-x to get (x, z, y) order for yz orientation image5d_swapped = np.swapaxes(image5d_swapped, offset, offset + 2) config.resolutions[0] = libmag.swap_elements( config.resolutions[0], 0, 2) scaling = None if rescale is not None or target_size is not None: # rescale based on scaling factor or target specific size rescaled = image5d_swapped # TODO: generalize for more than 1 preceding dimension? if offset > 0: rescaled = rescaled[0] max_pixels = [100, 500, 500] sub_roi_size = None if target_size: # to avoid artifacts from thin chunks, fit image into even # number of pixels per chunk by rounding up number of chunks # and resizing each chunk by ratio of total size to chunk num target_size = target_size[::-1] # change to z,y,x shape = rescaled.shape[:3] num_chunks = np.ceil(np.divide(shape, max_pixels)) max_pixels = np.ceil(np.divide(shape, num_chunks)).astype(np.int) sub_roi_size = np.floor(np.divide(target_size, num_chunks)).astype(np.int) print("Resizing image of shape {} to target_size: {}, using " "num_chunks: {}, max_pixels: {}, sub_roi_size: {}".format( rescaled.shape, target_size, num_chunks, max_pixels, sub_roi_size)) else: print("Rescaling image of shape {} by factor of {}".format( rescaled.shape, rescale)) # rescale in chunks with multiprocessing sub_roi_slices, _ = chunking.stack_splitter(rescaled.shape, max_pixels) is_fork = chunking.is_fork() if is_fork: Downsampler.set_data(rescaled) sub_rois = np.zeros_like(sub_roi_slices) pool = chunking.get_mp_pool() pool_results = [] for z in range(sub_roi_slices.shape[0]): for y in range(sub_roi_slices.shape[1]): for x in range(sub_roi_slices.shape[2]): coord = (z, y, x) slices = sub_roi_slices[coord] args = [coord, slices, rescale, sub_roi_size, multichannel] if not is_fork: # pickle chunk if img not directly available args.append(rescaled[slices]) pool_results.append( pool.apply_async(Downsampler.rescale_sub_roi, args=args)) for result in pool_results: coord, sub_roi = result.get() print("replacing sub_roi at {} of {}".format( coord, np.add(sub_roi_slices.shape, -1))) sub_rois[coord] = sub_roi pool.close() pool.join() rescaled_shape = chunking.get_split_stack_total_shape(sub_rois) if offset > 0: rescaled_shape = np.concatenate(([1], rescaled_shape)) print("rescaled_shape: {}".format(rescaled_shape)) # rescale chunks directly into memmap-backed array to minimize RAM usage image5d_transposed = np.lib.format.open_memmap( filename_image5d_npz, mode="w+", dtype=sub_rois[0, 0, 0].dtype, shape=tuple(rescaled_shape)) chunking.merge_split_stack2(sub_rois, None, offset, image5d_transposed) if rescale is not None: # scale resolutions based on single rescaling factor config.resolutions = np.multiply(config.resolutions, 1 / rescale) else: # scale resolutions based on size ratio for each dimension config.resolutions = np.multiply(config.resolutions, (image5d_swapped.shape / rescaled_shape)[1:4]) sizes[0] = rescaled_shape scaling = importer.calc_scaling(image5d_swapped, image5d_transposed) else: # transfer directly to memmap-backed array image5d_transposed = np.lib.format.open_memmap( filename_image5d_npz, mode="w+", dtype=image5d_swapped.dtype, shape=image5d_swapped.shape) if plane == config.PLANE[1] or plane == config.PLANE[2]: # flip upside-down if re-orienting planes if offset: image5d_transposed[0, :] = np.fliplr(image5d_swapped[0, :]) else: image5d_transposed[:] = np.fliplr(image5d_swapped[:]) else: image5d_transposed[:] = image5d_swapped[:] sizes[0] = image5d_swapped.shape # save image metadata print("detector.resolutions: {}".format(config.resolutions)) print("sizes: {}".format(sizes)) image5d.flush() importer.save_image_info( filename_info_npz, info["names"], sizes, config.resolutions, info["magnification"], info["zoom"], *importer.calc_intensity_bounds(image5d_transposed), scaling, plane) print("saved transposed file to {} with shape {}".format( filename_image5d_npz, image5d_transposed.shape)) print("time elapsed (s): {}".format(time() - time_start))