Esempio n. 1
0
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)
Esempio n. 2
0
    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
Esempio n. 3
0
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)
Esempio n. 4
0
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
Esempio n. 5
0
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
Esempio n. 6
0
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
Esempio n. 7
0
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)
Esempio n. 8
0
    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
Esempio n. 9
0
 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
Esempio n. 10
0
    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
Esempio n. 11
0
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
Esempio n. 12
0
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
Esempio n. 13
0
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))