def make_density_images_mp(img_paths, scale=None, shape=None, suffix=None): """Make density images for a list of files as a multiprocessing wrapper for :func:``make_density_image`` Args: img_paths (List[str]): Sequence of image paths, which will be used to indentify the blob files. scale (int, float): Rescaling factor as a scalar value. If set, the corresponding image for this factor will be opened. If None, the full size image will be used. Defaults to None. shape (List[int]): Sequence of target shape defining the voxels for the density map; defaults to None. suffix (str): Modifier to append to end of ``img_path`` basename for registered image files that were output to a modified name; defaults to None. """ start_time = time() pool = chunking.get_mp_pool() pool_results = [] for img_path in img_paths: print("making image", img_path) pool_results.append( pool.apply_async(make_density_image, args=(img_path, scale, shape, suffix))) for result in pool_results: _, _, path = result.get() print("finished {}".format(path)) pool.close() pool.join() print("time elapsed for making density images:", time() - start_time)
def cluster_by_label(cls, blobs, labels_img_np, blobs_lbl_scaling, blobs_iso_scaling, all_labels=False): coord_scaled = ontology.scale_coords(blobs, blobs_lbl_scaling) blobs_lbls = ontology.get_label_ids_from_position( coord_scaled, labels_img_np) blobs = np.multiply(blobs[:, :3], blobs_iso_scaling) blobs_clus = np.zeros((len(blobs), 5), dtype=int) blobs_clus[:, :3] = blobs blobs_clus[:, 3] = blobs_lbls cls.blobs = blobs_clus print(np.unique(blobs_clus[:, 3])) print(cls.blobs) # TODO: shift to separate func once load blobs without req labels img label_ids = np.unique(labels_img_np) cluster_settings = config.atlas_profile[ profiles.RegKeys.METRICS_CLUSTER] eps = cluster_settings[profiles.RegKeys.DBSCAN_EPS] minpts = cluster_settings[profiles.RegKeys.DBSCAN_MINPTS] if all_labels: # cluster all labels together # TODO: n_jobs appears to be ignored despite reported fixes _, labels = cls.cluster_within_label(None, eps, minpts, -1) cls.blobs[:, 4] = labels else: # cluster by individual label pool = chunking.get_mp_pool() pool_results = [] for label_id in label_ids: # add rotation argument if necessary pool_results.append( pool.apply_async(cls.cluster_within_label, args=(label_id, eps, minpts, None))) for result in pool_results: label_id, labels = result.get() if labels is not None: cls.blobs[cls.blobs[:, 3] == label_id, 4] = labels pool.close() pool.join() cls.blobs[:, :3] = np.divide(blobs[:, :3], blobs_iso_scaling) return cls.blobs
def make_density_images_mp(img_paths, scale=None, shape=None, suffix=None, channel=None): """Make density images for a list of files as a multiprocessing wrapper for :func:``make_density_image`` Args: img_paths (List[str]): Sequence of image paths, which will be used to indentify the blob files. scale (int, float): Rescaling factor as a scalar value. If set, the corresponding image for this factor will be opened. If None, the full size image will be used. Defaults to None. shape (List[int]): Sequence of target shape defining the voxels for the density map; defaults to None. suffix (str): Modifier to append to end of ``img_path`` basename for registered image files that were output to a modified name; defaults to None. channel (List[int]): Sequence of channels to include in density image; defaults to None to use all channels. """ start_time = time() pool = chunking.get_mp_pool() pool_results = [] for img_path in img_paths: print("Making density image from blobs related to:", img_path) if config.channel: # get blob matches for the given channels if available; must load # from db outside of multiprocessing to avoid MemoryError matches = colocalizer.select_matches( config.db, config.channel, exp_name=sqlite.get_exp_name(img_path)) else: matches = None pool_results.append( pool.apply_async(make_density_image, args=(img_path, scale, shape, suffix, None, channel, matches, config.atlas_profile))) for result in pool_results: _, path = result.get() print("finished {}".format(path)) pool.close() pool.join() print("time elapsed for making density images:", time() - start_time)
def sub_segment_labels(labels_img_np, atlas_edge): """Sub-segment a labels image into sub-labels based on anatomical boundaries. Args: labels_img_np: Integer labels image as a Numpy array. atlas_edge: Numpy array of atlas reduced to binary image of its edges. Returns: Image as a Numpy array of same shape as ``labels_img_np`` with each label sub-segmented based on anatomical boundaries. Labels in this image will correspond to the original labels multiplied by :const:``config.SUB_SEG_MULT`` to make room for sub-labels, which will each be incremented by 1. """ start_time = time() # use a class to set and process the label without having to # reference the labels image as a global variable SubSegmenter.set_images(labels_img_np, atlas_edge) pool = chunking.get_mp_pool() pool_results = [] label_ids = np.unique(labels_img_np) max_val = np.amax(labels_img_np) * (config.SUB_SEG_MULT + 1) dtype = libmag.dtype_within_range(-max_val, max_val, True) subseg = np.zeros_like(labels_img_np, dtype=dtype) for label_id in label_ids: # skip background if label_id == 0: continue pool_results.append( pool.apply_async( SubSegmenter.sub_segment, args=(label_id, dtype))) for result in pool_results: label_id, slices, labels_seg = result.get() # can only mutate markers outside of mp for changes to persist labels_seg_mask = labels_seg != 0 subseg[tuple(slices)][labels_seg_mask] = labels_seg[labels_seg_mask] print("finished sub-segmenting label ID {}".format(label_id)) pool.close() pool.join() print("time elapsed to sub-segment labels image:", time() - start_time) return subseg
def show_instances(instances, get_ip=False): """Show settings for instances. Args: instances: List of instance objects to query. get_ip: True to get instance IP; defaults to False. Returns: Dictionary of ``instance_id: instance_ip`` entries. """ # show instance info in multiprocessing to allow waiting for # each instance to start running pool = chunking.get_mp_pool() pool_results = [] for instance in instances: pool_results.append( pool.apply_async(instance_info, args=(instance.id, get_ip))) info = {} for result in pool_results: inst_id, inst_ip = result.get() info[inst_id] = inst_ip pool.close() pool.join() return info
def labels_to_markers_erosion(labels_img, filter_size=8, target_frac=None, min_filter_size=None, use_min_filter=False, skel_eros_filt_size=None, wt_dists=None): """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 (:obj:`np.ndarray`): Labels image as an integer Numpy array, where each unique int is a separate label. filter_size (int): Size of structing element for erosion, which should be > 0; defaults to 8. target_frac (float): Target fraction of original label to erode, passed to :func:`LabelToMarkerErosion.erode_label`. Defaults to None. min_filter_size (int): Minimum erosion filter size; defaults to None to use half of ``filter_size``, rounded down. use_min_filter (bool): 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 (int): 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 (:obj:`np.ndarray`): 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. Returns: :obj:`np.ndarray`: Image array of the same shape as ``img`` and the same number of labels as in ``labels_img``, with eroded labels. """ start_time = time() 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 #labels_unique = np.concatenate((labels_unique[:5], labels_unique[-5:])) sizes_dict = {} cols = (config.AtlasMetrics.REGION.value, "SizeOrig", "SizeMarker", config.SmoothingMetrics.FILTER_SIZE.value) # erode labels via multiprocessing print("Eroding labels to markers with filter size {}, min filter size {}, " "and target fraction {}".format(filter_size, min_filter_size, target_frac)) LabelToMarkerErosion.set_labels_img(labels_img, wt_dists) pool = chunking.get_mp_pool() 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 pool_results.append( pool.apply_async(LabelToMarkerErosion.erode_label, args=(label_id, filter_size, target_frac, min_filter_size, use_min_filter, skel_eros_filt_size))) for result in pool_results: stats_eros, slices, filtered = result.get() # can only mutate markers outside of mp for changes to persist markers[tuple(slices)][filtered] = stats_eros[0] for col, stat in zip(cols, stats_eros): sizes_dict.setdefault(col, []).append(stat) pool.close() pool.join() # show erosion stats df = df_io.dict_to_data_frame(sizes_dict, show=True) print("time elapsed to erode labels into markers:", time() - start_time) return markers, df
def merge_atlas_segmentations(img_paths, show=True, atlas=True, suffix=None): """Merge atlas segmentations for a list of files as a multiprocessing wrapper for :func:``merge_atlas_segmentations``, after which edge image post-processing is performed separately since it contains tasks also performed in multiprocessing. Args: img_paths (List[str]): Sequence of image paths to load. show (bool): True if the output images should be displayed; defaults to True. atlas (bool): True if the image is an atlas; defaults to True. suffix (str): Modifier to append to end of ``img_path`` basename for registered image files that were output to a modified name; defaults to None. """ start_time = time() # erode all labels images into markers for watershed; not multiprocessed # since erosion is itself multiprocessed erode = config.atlas_profile["erode_labels"] erosion = config.atlas_profile[profiles.RegKeys.EDGE_AWARE_REANNOTATION] erosion_frac = config.atlas_profile["erosion_frac"] mirrored = atlas and _is_profile_mirrored() mirror_mult = _get_mirror_mult() dfs_eros = [] for img_path in img_paths: mod_path = img_path if suffix is not None: mod_path = libmag.insert_before_ext(mod_path, suffix) labels_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) print("Eroding labels to generate markers for atlas segmentation") df = None if erode["markers"]: # use default minimal post-erosion size (not setting erosion frac) markers, df = erode_labels( sitk.GetArrayFromImage(labels_sitk), erosion, mirrored=mirrored, mirror_mult=mirror_mult) labels_sitk_markers = sitk_io.replace_sitk_with_numpy( labels_sitk, markers) sitk_io.write_reg_images( {config.RegNames.IMG_LABELS_MARKERS.value: labels_sitk_markers}, mod_path) df_io.data_frames_to_csv( df, "{}_markers.csv".format(os.path.splitext(mod_path)[0])) dfs_eros.append(df) pool = chunking.get_mp_pool() pool_results = [] for img_path, df in zip(img_paths, dfs_eros): print("setting up atlas segmentation merge for", img_path) # convert labels image into markers exclude = df.loc[ np.isnan(df[config.SmoothingMetrics.FILTER_SIZE.value]), config.AtlasMetrics.REGION.value] print("excluding these labels from re-segmentation:\n", exclude) pool_results.append(pool.apply_async( edge_aware_segmentation, args=(img_path, show, atlas, suffix, exclude, mirror_mult))) for result in pool_results: # edge distance calculation and labels interior image generation # are multiprocessed, so run them as post-processing tasks to # avoid nested multiprocessing path = result.get() mod_path = path if suffix is not None: mod_path = libmag.insert_before_ext(path, suffix) # make edge distance images and stats labels_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_np = sitk.GetArrayFromImage(labels_sitk) dist_to_orig, labels_edge = edge_distances( labels_np, path=path, spacing=labels_sitk.GetSpacing()[::-1]) dist_sitk = sitk_io.replace_sitk_with_numpy(labels_sitk, dist_to_orig) labels_sitk_edge = sitk_io.replace_sitk_with_numpy( labels_sitk, labels_edge) labels_sitk_interior = None if erode["interior"]: # make interior images from labels using given targeted # post-erosion frac interior, _ = erode_labels( labels_np, erosion, erosion_frac=erosion_frac, mirrored=mirrored, mirror_mult=mirror_mult) labels_sitk_interior = sitk_io.replace_sitk_with_numpy( labels_sitk, interior) # write images to same directory as atlas imgs_write = { config.RegNames.IMG_LABELS_DIST.value: dist_sitk, config.RegNames.IMG_LABELS_EDGE.value: labels_sitk_edge, config.RegNames.IMG_LABELS_INTERIOR.value: labels_sitk_interior, } sitk_io.write_reg_images(imgs_write, mod_path) if show: for img in imgs_write.values(): if img: sitk.Show(img) print("finished {}".format(path)) pool.close() pool.join() print("time elapsed for merging atlas segmentations:", time() - start_time)
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 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 _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
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))