示例#1
0
def setup_match_blobs_roi(blobs, tol):
    """Set up tolerances for matching blobs in an ROI.
    
    Args:
        blobs (:obj:`np.ndarray`): Sequence of blobs to resize if the
            first ROI profile (:attr:`config.roi_profiles`) ``resize_blobs``
            value is given.
        tol (List[int, float]): Sequence of tolerances.

    Returns:
        float, List[float], List[float], List[float], :obj:`np.ndarray`:
        Distance map threshold, scaling normalized by ``tol``, ROI padding
        shape, resize sequence retrieved from ROI profile, and ``blobs``
        after any resizing.

    """
    # convert tolerance seq to scaling and single number distance
    # threshold for point distance map, which assumes isotropy; use
    # custom tol rather than calculating isotropy since may need to give
    # greater latitude along a given axis, such as poorer res in z
    thresh = np.amax(tol)  # similar to longest radius from the tol bounding box
    scaling = thresh / tol
    # casting to int causes improper offset import into db
    inner_padding = np.floor(tol[::-1])
    libmag.printv(
        "verifying blobs with tol {} leading to thresh {}, scaling {}, "
        "inner_padding {}".format(tol, thresh, scaling, inner_padding))
    
    # resize blobs based only on first profile
    resize = config.get_roi_profile(0)["resize_blobs"]
    if resize:
        blobs = detector.multiply_blob_rel_coords(blobs, resize)
        libmag.printv("resized blobs by {}:\n{}".format(resize, blobs))
    
    return thresh, scaling, inner_padding, resize, blobs
示例#2
0
def remap_intensity(roi, channel=None):
    """Remap intensities, currently using adaptive histogram equalization
    but potentially plugging in alternative methods in the future.

    Args:
        roi (:obj:`np.ndarray`): Region of interest as a 3D or 3D+channel array.
        channel (int): Channel index of ``roi`` to saturate. Defaults to None
            to use all channels. If a specific channel is given, all other
            channels remain unchanged.

    Returns:
        :obj:`np.ndarray`: Remapped region of interest as a new array.

    """
    multichannel, channels = setup_channels(roi, channel, 3)
    roi_out = np.copy(roi)
    for chl in channels:
        roi_show = roi[..., chl] if multichannel else roi
        settings = config.get_roi_profile(chl)
        lim = settings["adapt_hist_lim"]
        print("Performing adaptive histogram equalization on channel {}, "
              "clip limit {}".format(chl, lim))
        equalized = []
        for plane in roi_show:
            # workaround for lack of current nD support in scikit-image CLAHE
            # implementation (but this PR looks promising:
            # https://github.com/scikit-image/scikit-image/pull/2761 )
            equalized.append(exposure.equalize_adapthist(plane,
                                                         clip_limit=lim))
        equalized = np.stack(equalized)
        if multichannel:
            roi_out[..., chl] = equalized
        else:
            roi_out = equalized
    return roi_out
示例#3
0
def get_mp_pool(
        initializer: Optional[Callable] = None,
        initargs: Optional[Tuple] = None) -> mp.Pool:
    """Get a multiprocessing ``Pool`` object, configured based on ``config``
    settings.
    
    Args:
        initializer: Function to be called on initialization for each process;
            defaults to None.
        initargs: Arguments to ``initializer``; defaults to None.
        
    
    Returns:
        Pool object with number of processes and max tasks per process
        determined by command-line and the main (first) ROI profile settings.

    """
    prof = config.get_roi_profile(0)
    max_tasks = None if not prof else prof["mp_max_tasks"]
    print("Setting up multiprocessing pool with {} processes (None uses all "
          "available)\nand max tasks of {} before replacing processes (None "
          "does not replace processes)".format(config.cpus, max_tasks))
    return mp.Pool(
        processes=config.cpus, maxtasksperchild=max_tasks,
        initializer=initializer, initargs=initargs)
示例#4
0
def denoise_roi(roi: np.ndarray,
                channel: Optional[Sequence[int]] = None) -> np.ndarray:
    """Denoise and further preprocess an image.
    
    Applies saturation, denoising, unsharp filtering, and erosion as image
    preprocessing for blob detection.

    Each step can be configured including turned off by
    :attr:`magmap.settings.config.roi_profiles`.
    
    Args:
        roi: Region of interest as a 3D (z, y, x) array. Note that 4D arrays 
            with channels are not allowed as the Scikit-Image gaussian filter 
            only accepts specifically 3 channels, presumably for RGB.
        channel: Sequence of channel indices in ``roi`` to
            saturate. Defaults to None to use all channels.
    
    Returns:
        Denoised region of interest.
    
    """
    multichannel, channels = setup_channels(roi, channel, 3)
    roi_out = None
    for chl in channels:
        # get single channel
        roi_show = roi[..., chl] if multichannel else roi
        settings = config.get_roi_profile(chl)
        # find gross density
        saturated_mean = np.mean(roi_show)

        # further saturation
        denoised = np.clip(roi_show, settings["clip_min"],
                           settings["clip_max"])

        tot_var_denoise = settings["tot_var_denoise"]
        if tot_var_denoise:
            # total variation denoising
            denoised = restoration.denoise_tv_chambolle(denoised,
                                                        weight=tot_var_denoise)

        # sharpening
        unsharp_strength = settings["unsharp_strength"]
        if unsharp_strength:
            blur_size = 8
            blurred = filters.gaussian(denoised, blur_size)
            high_pass = denoised - unsharp_strength * blurred
            denoised = denoised + high_pass

        # further erode denser regions to decrease overlap among blobs
        thresh_eros = settings["erosion_threshold"]
        if thresh_eros and saturated_mean > thresh_eros:
            #print("denoising for saturated mean of {}".format(saturated_mean))
            denoised = morphology.erosion(denoised, morphology.octahedron(1))
        if multichannel:
            if roi_out is None:
                roi_out = np.zeros(roi.shape, dtype=denoised.dtype)
            roi_out[..., chl] = denoised
        else:
            roi_out = denoised
    return roi_out
示例#5
0
def saturate_roi(roi: np.ndarray,
                 clip_vmin: float = -1,
                 clip_vmax: float = -1,
                 max_thresh_factor: float = -1,
                 channel: Optional[Sequence[int]] = None) -> np.ndarray:
    """Saturates an image, clipping extreme values and stretching remaining
    values to fit the full range.
    
    Args:
        roi: Region of interest.
        clip_vmin: Percent for lower clipping. Defaults to -1
            to use each channel's profile setting.
        clip_vmax: Percent for upper clipping. Defaults to -1
            to use each channel's profile setting.
        max_thresh_factor: Multiplier of :attr:`config.near_max`
            for ROI's scaled maximum value. If the max data range value
            adjusted through``clip_vmax``is below this product, this max
            value will be set to this product. Defaults to -1 to use each
            channel's profile setting.
        channel: Sequence of channel indices in ``roi`` to
            saturate. Defaults to None to use all channels.
    
    Returns:
        Saturated region of interest.
    """
    multichannel, channels = setup_channels(roi, channel, 3)
    roi_out = None
    for chl in channels:
        roi_show = roi[..., chl] if multichannel else roi

        # get settings from channel's profile
        settings = config.get_roi_profile(chl)
        clip_vmin_prof = settings["clip_vmin"] if clip_vmin == -1 else clip_vmin
        clip_vmax_prof = settings["clip_vmax"] if clip_vmax == -1 else clip_vmax
        max_thresh_factor_prof = (settings["max_thresh_factor"]
                                  if max_thresh_factor == -1 else
                                  max_thresh_factor)

        # enhance contrast and normalize to 0-1 scale, adjusting the near max
        # value derived globally from image5d for the chl
        vmin, vmax = np.percentile(roi_show, (clip_vmin_prof, clip_vmax_prof))
        if vmin == vmax:
            saturated = roi_show
        else:
            max_thresh = config.near_max[chl] * max_thresh_factor_prof
            if vmax < max_thresh:
                vmax = max_thresh
            saturated = np.clip(roi_show, vmin, vmax)
            saturated = (saturated - vmin) / (vmax - vmin)

        # insert into output array
        if multichannel:
            if roi_out is None:
                roi_out = np.zeros(roi.shape, dtype=saturated.dtype)
            roi_out[..., chl] = saturated
        else:
            roi_out = saturated
    return roi_out
示例#6
0
def saturate_roi(roi,
                 clip_vmin=-1,
                 clip_vmax=-1,
                 max_thresh_factor=-1,
                 channel=None):
    """Saturates an image, clipping extreme values and stretching remaining
    values to fit the full range.
    
    Args:
        roi (:obj:`np.ndarray`): Region of interest.
        clip_vmin (float): Percent for lower clipping. Defaults to -1
            to use the profile setting.
        clip_vmax (float): Percent for upper clipping. Defaults to -1
            to use the profile setting.
        max_thresh_factor (float): Multiplier of :attr:`config.near_max`
            for ROI's scaled maximum value. If the max data range value
            adjusted through``clip_vmax``is below this product, this max
            value will be set to this product. Defaults to -1 to use the
            profile setting.
        channel (List[int]): Sequence of channel indices in ``roi`` to
            saturate. Defaults to None to use all channels.
    
    Returns:
        Saturated region of interest.
    """
    multichannel, channels = setup_channels(roi, channel, 3)
    roi_out = None
    for chl in channels:
        roi_show = roi[..., chl] if multichannel else roi
        settings = config.get_roi_profile(chl)
        if clip_vmin == -1:
            clip_vmin = settings["clip_vmin"]
        if clip_vmax == -1:
            clip_vmax = settings["clip_vmax"]
        if max_thresh_factor == -1:
            max_thresh_factor = settings["max_thresh_factor"]
        # enhance contrast and normalize to 0-1 scale
        vmin, vmax = np.percentile(roi_show, (clip_vmin, clip_vmax))
        libmag.printv("vmin:", vmin, "vmax:", vmax, "near max:",
                      config.near_max[chl])
        # adjust the near max value derived globally from image5d for the chl
        max_thresh = config.near_max[chl] * max_thresh_factor
        if vmax < max_thresh:
            vmax = max_thresh
            libmag.printv("adjusted vmax to {}".format(vmax))
        saturated = np.clip(roi_show, vmin, vmax)
        saturated = (saturated - vmin) / (vmax - vmin)
        if multichannel:
            if roi_out is None:
                roi_out = np.zeros(roi.shape, dtype=saturated.dtype)
            roi_out[..., chl] = saturated
        else:
            roi_out = saturated
    return roi_out
示例#7
0
def get_mp_pool():
    """Get a multiprocessing ``Pool`` object, configured based on ``config``
    settings.
    
    Returns:
        :obj:`multiprocessing.Pool`: Pool object with number of processes
        and max tasks per process determined by command-line and the main
        (first) ROI profile settings.

    """
    prof = config.get_roi_profile(0)
    max_tasks = None if not prof else prof["mp_max_tasks"]
    print("Setting up multiprocessing pool with {} processes (None uses all "
          "available)\nand max tasks of {} before replacing processes (None "
          "does not replace processes)".format(config.cpus, max_tasks))
    return mp.Pool(processes=config.cpus, maxtasksperchild=max_tasks)
示例#8
0
def setup_match_blobs_roi(
    tol: Sequence[float],
    blobs: Optional[np.ndarray] = None
) -> Tuple[float, Sequence[float], np.ndarray, Sequence[float], np.ndarray]:
    """Set up tolerances for matching blobs in an ROI.
    
    Args:
        tol: Sequence of tolerances.
        blobs: Sequence of blobs to resize if the first ROI profile
            (:attr:`magmap.config.roi_profiles`) ``resize_blobs``
            value is given.

    Returns:
        Distance map threshold, scaling normalized by ``tol``, ROI padding
        shape, resize sequence retrieved from ROI profile, and ``blobs``
        after any resizing.

    """
    # convert tolerance seq to scaling and single number distance
    # threshold for point distance map, which assumes isotropy; use
    # custom tol rather than calculating isotropy since may need to give
    # greater latitude along a given axis, such as poorer res in z
    thresh = np.amax(
        tol)  # similar to longest radius from the tol bounding box
    scaling = thresh / tol
    # casting to int causes improper offset import into db
    inner_padding = np.floor(tol[::-1])
    libmag.log_once(
        _logger.debug,
        f"verifying blobs with tol {tol} leading to thresh {thresh}, "
        f"scaling {scaling}, inner_padding {inner_padding}")

    # resize blobs based only on first profile
    resize = config.get_roi_profile(0)["resize_blobs"]
    if resize and blobs is not None:
        blobs = detector.Blobs.multiply_blob_rel_coords(blobs, resize)
        libmag.log_once(_logger.debug, f"resized blobs by {resize}:\n{blobs}")

    return thresh, scaling, inner_padding, resize, blobs
示例#9
0
def detect_blobs(roi, channel, exclude_border=None):
    """Detects objects using 3D blob detection technique.
    
    Args:
        roi: Region of interest to segment.
        channel (Sequence[int]): Sequence of channels to select, which can
            be None to indicate all channels.
        exclude_border: Sequence of border pixels in x,y,z to exclude;
            defaults to None.
    
    Returns:
        Array of detected blobs, each given as 
            (z, row, column, radius, confirmation).
    """
    time_start = time()
    shape = roi.shape
    multichannel, channels = plot_3d.setup_channels(roi, channel, 3)
    isotropic = config.get_roi_profile(channels[0])["isotropic"]
    if isotropic is not None:
        # interpolate for (near) isotropy during detection, using only the 
        # first process settings since applies to entire ROI
        roi = cv_nd.make_isotropic(roi, isotropic)
    
    blobs_all = []
    for chl in channels:
        roi_detect = roi[..., chl] if multichannel else roi
        settings = config.get_roi_profile(chl)
        # scaling as a factor in pixel/um, where scaling of 1um/pixel  
        # corresponds to factor of 1, and 0.25um/pixel corresponds to
        # 1 / 0.25 = 4 pixels/um; currently simplified to be based on 
        # x scaling alone
        scale = calc_scaling_factor()
        scaling_factor = scale[2]
        
        # find blobs; sigma factors can be sequences by axes for anisotropic 
        # detection in skimage >= 0.15, or images can be interpolated to 
        # isotropy using the "isotropic" MagellanMapper setting
        min_sigma = settings["min_sigma_factor"] * scaling_factor
        max_sigma = settings["max_sigma_factor"] * scaling_factor
        num_sigma = settings["num_sigma"]
        threshold = settings["detection_threshold"]
        overlap = settings["overlap"]
        blobs_log = blob_log(
            roi_detect, min_sigma=min_sigma, max_sigma=max_sigma,
            num_sigma=num_sigma, threshold=threshold, overlap=overlap)
        if config.verbose:
            print("detecting blobs with min size {}, max {}, num std {}, "
                  "threshold {}, overlap {}"
                  .format(min_sigma, max_sigma, num_sigma, threshold, overlap))
            print("time for 3D blob detection: {}".format(time() - time_start))
        if blobs_log.size < 1:
            libmag.printv("no blobs detected")
            continue
        blobs_log[:, 3] = blobs_log[:, 3] * math.sqrt(3)
        blobs = format_blobs(blobs_log, chl)
        #print(blobs)
        blobs_all.append(blobs)
    if not blobs_all:
        return None
    blobs_all = np.vstack(blobs_all)
    if isotropic is not None:
        # if detected on isotropic ROI, need to reposition blob coordinates 
        # for original, non-isotropic ROI
        isotropic_factor = cv_nd.calc_isotropic_factor(isotropic)
        blobs_all = multiply_blob_rel_coords(blobs_all, 1 / isotropic_factor)
        blobs_all = multiply_blob_abs_coords(blobs_all, 1 / isotropic_factor)
    
    if exclude_border is not None:
        # exclude blobs from the border in x,y,z
        blobs_all = get_blobs_interior(blobs_all, shape, *exclude_border)
    
    return blobs_all
示例#10
0
    def plot_3d_points(self, roi, channel, flipz=False, offset=None):
        """Plots all pixels as points in 3D space.

        Points falling below a given threshold will be removed, allowing
        the viewer to see through the presumed background to masses within
        the region of interest.

        Args:
            roi (:class:`numpy.ndarray`): Region of interest either as a 3D
                ``z,y,x`` or 4D ``z,y,x,c`` array.
            channel (int): Channel to select, which can be None to indicate all
                channels.
            flipz (bool): True to invert the ROI along the z-axis to match
                the handedness of Matplotlib with z progressing upward;
                defaults to False.
            offset (Sequence[int]): Origin coordinates in ``z,y,x``; defaults
                to None.

        Returns:
            bool: True if points were rendered, False if no points to render.
        
        """
        print("Plotting ROI as 3D points")

        # streamline the image
        if roi is None or roi.size < 1: return False
        roi = plot_3d.saturate_roi(roi, clip_vmax=98.5, channel=channel)
        roi = np.clip(roi, 0.2, 0.8)
        roi = restoration.denoise_tv_chambolle(roi, weight=0.1)

        # separate parallel arrays for each dimension of all coordinates for
        # Mayavi input format, with the ROI itself given as a 1D scalar array ;
        # TODO: consider using np.mgrid to construct the x,y,z arrays
        time_start = time()
        shape = roi.shape
        isotropic = plot_3d.get_isotropic_vis(config.roi_profile)
        z = np.ones((shape[0], shape[1] * shape[2]))
        for i in range(shape[0]):
            z[i] = z[i] * i
        if flipz:
            # invert along z-axis to match handedness of Matplotlib with z up
            z *= -1
            if offset is not None:
                offset = np.copy(offset)
                offset[0] *= -1
        y = np.ones((shape[0] * shape[1], shape[2]))
        for i in range(shape[0]):
            for j in range(shape[1]):
                y[i * shape[1] + j] = y[i * shape[1] + j] * j
        x = np.ones((shape[0] * shape[1], shape[2]))
        for i in range(shape[0] * shape[1]):
            x[i] = np.arange(shape[2])

        if offset is not None:
            offset = np.multiply(offset, isotropic)
        coords = [z, y, x]
        for i, _ in enumerate(coords):
            # scale coordinates for isotropy
            coords[i] *= isotropic[i]
            if offset is not None:
                # translate by offset
                coords[i] += offset[i]

        multichannel, channels = plot_3d.setup_channels(roi, channel, 3)
        for chl in channels:
            roi_show = roi[..., chl] if multichannel else roi
            roi_show_1d = roi_show.reshape(roi_show.size)
            if chl == 0:
                x = np.reshape(x, roi_show.size)
                y = np.reshape(y, roi_show.size)
                z = np.reshape(z, roi_show.size)
            settings = config.get_roi_profile(chl)

            # clear background points to see remaining structures
            thresh = 0
            if len(np.unique(roi_show)) > 1:
                # need > 1 val to threshold
                try:
                    thresh = filters.threshold_otsu(roi_show, 64)
                except ValueError as e:
                    thresh = np.median(roi_show)
                    print("could not determine Otsu threshold, taking median "
                          "({}) instead".format(thresh))
                thresh *= settings["points_3d_thresh"]
            print("removing 3D points below threshold of {}".format(thresh))
            remove = np.where(roi_show_1d < thresh)
            roi_show_1d = np.delete(roi_show_1d, remove)

            # adjust range from 0-1 to region of colormap to use
            roi_show_1d = libmag.normalize(roi_show_1d, 0.6, 1.0)
            points_len = roi_show_1d.size
            if points_len == 0:
                print("no 3D points to display")
                return False
            mask = math.ceil(points_len / self._MASK_DIVIDEND)
            print("points: {}, mask: {}".format(points_len, mask))
            if any(np.isnan(roi_show_1d)):
                # TODO: see if some NaNs are permissible
                print(
                    "NaN values for 3D points, will not show 3D visualization")
                return False
            pts = self.scene.mlab.points3d(np.delete(x, remove),
                                           np.delete(y, remove),
                                           np.delete(z, remove),
                                           roi_show_1d,
                                           mode="sphere",
                                           scale_mode="scalar",
                                           mask_points=mask,
                                           line_width=1.0,
                                           vmax=1.0,
                                           vmin=0.0,
                                           transparent=True)
            cmap = colormaps.get_cmap(config.cmaps, chl)
            if cmap is not None:
                pts.module_manager.scalar_lut_manager.lut.table = cmap(
                    range(0, 256)) * 255

            # scale glyphs to partially fill in gaps from isotropic scaling;
            # do not use actor scaling as it also translates the points when
            # not positioned at the origin
            pts.glyph.glyph.scale_factor = 2 * max(isotropic)

        # keep visual ordering of surfaces when opacity is reduced
        self.scene.renderer.use_depth_peeling = True
        print("time for 3D points display: {}".format(time() - time_start))
        return True
示例#11
0
def detect_blobs_stack(filename_base, subimg_offset, subimg_size, coloc=False):
    """Detect blobs in a full stack, such as a whole large image.
    
    Process channels in separate sets of blocks if their profiles specify
    different block sizes.
    
    Args:
        filename_base (str): 
        subimg_offset (Sequence[int]): Sub-image offset as ``z,y,x`` to load
            from :attr:`config.image5d`; defaults to None.
        subimg_size (Sequence[int]): Sub-image size as ``z,y,x`` to load
            from :attr:`config.image5d`; defaults to None.
        coloc (bool): True to also detect blob-colocalizations based on image
            intensity; defaults to False. For match-based colocalizations,
            use the ``coloc_match`` task
            (:meth:`magmap.colocalizer.StackColocalizer.colocalize_stack`)
            instead.

    Returns:
        tuple[int, int, int], str, :class:`magmap.cv.detector.Blobs`:
        Combined ccuracy metrics from :class:`magmap.cv.detector.verify_rois`,
        feedback message from this same function, and detected blobs across
        all channels in :attr:`magmap.settings.config.channel`.

    """
    channels = plot_3d.setup_channels(config.image5d, config.channel, 4)[1]
    if roi_prof.ROIProfile.is_identical_settings(
            [config.get_roi_profile(c) for c in channels],
            roi_prof.ROIProfile.BLOCK_SIZES):
        print("Will process channels together in the same blocks")
        channels = [channels]
    else:
        print("Will process channels in separate blocks defined by their "
              "profiles")
    
    cols = ("stats", "fdbk", "blobs")
    detection_out = {}
    for chl in channels:
        # detect blobs in each channel separately unless all channels
        # are combined in a single list
        if not libmag.is_seq(chl):
            chl = [chl]
        blobs_out = detect_blobs_blocks(
            filename_base, config.image5d, subimg_offset, subimg_size,
            chl, config.truth_db_mode is config.TruthDBModes.VERIFY, 
            not config.grid_search_profile, config.image5d_is_roi, coloc)
        for col, val in zip(cols, blobs_out):
            detection_out.setdefault(col, []).append(val)
        print("{}\n".format("-" * 80))
    
    stats = None
    fdbk = None
    blobs_all = None
    if "blobs" in detection_out and detection_out["blobs"]:
        # join blobs and colocalizations from all channels and save archive
        blobs_all = detection_out["blobs"][0]
        blobs_all.blobs = libmag.combine_arrs(
            [b.blobs for b in detection_out["blobs"]
             if b.blobs is not None])
        print("\nTotal blobs found across channels:", len(blobs_all.blobs))
        detector.show_blobs_per_channel(blobs_all.blobs)
        blobs_all.colocalizations = libmag.combine_arrs(
            [b.colocalizations for b in detection_out["blobs"]
             if b.colocalizations is not None])
        blobs_all.save_archive()
        print()
        
        # combine verification stats and feedback messages
        stats = libmag.combine_arrs(
            detection_out["stats"], fn=np.sum)
        fdbk = "\n".join(
            [f for f in detection_out["fdbk"] if f is not None])
    return stats, fdbk, blobs_all
示例#12
0
def detect_blobs_blocks(filename_base, image5d, offset, size, channels,
                        verify=False, save_dfs=True, full_roi=False,
                        coloc=False):
    """Detect blobs by block processing of a large image.
    
    All channels are processed in the same blocks.
    
    Args:
        filename_base: Base path to use file output.
        image5d: Large image to process as a Numpy array of t,z,y,x,[c]
        offset: Sub-image offset given as coordinates in z,y,x.
        size: Sub-image shape given in z,y,x.
        channels (Sequence[int]): Sequence of channels, where None detects
            in all channels.
        verify: True to verify detections against truth database; defaults 
            to False.
        save_dfs: True to save data frames to file; defaults to True.
        full_roi (bool): True to treat ``image5d`` as the full ROI; defaults
            to False.
        coloc (bool): True to perform blob co-localizations; defaults to False.
    
    Returns:
        tuple[int, int, int], str, :class:`magmap.cv.detector.Blobs`:
        Accuracy metrics from :class:`magmap.cv.detector.verify_rois`,
        feedback message from this same function, and detected blobs.
    
    """
    time_start = time()
    subimg_path_base = filename_base
    if size is None or offset is None:
        # uses the entire stack if no size or offset specified
        size = image5d.shape[1:4]
        offset = (0, 0, 0)
    else:
        # get base path for sub-image
        subimg_path_base = naming.make_subimage_name(
            filename_base, offset, size)
    filename_blobs = libmag.combine_paths(subimg_path_base, config.SUFFIX_BLOBS)
    
    # get ROI for given region, including all channels
    if full_roi:
        # treat the full image as the ROI
        roi = image5d[0]
    else:
        roi = plot_3d.prepare_subimg(image5d, offset, size)
    num_chls_roi = 1 if len(roi.shape) < 4 else roi.shape[3]
    if num_chls_roi < 2:
        coloc = False
        print("Unable to co-localize as image has only 1 channel")
    
    # prep chunking ROI into sub-ROIs with size based on segment_size, scaling
    # by physical units to make more independent of resolution; use profile
    # from first channel to be processed for block settings
    time_detection_start = time()
    settings = config.get_roi_profile(channels[0])
    print("Profile for block settings:", settings[settings.NAME_KEY])
    sub_roi_slices, sub_rois_offsets, denoise_max_shape, exclude_border, \
        tol, overlap_base, overlap, overlap_padding = setup_blocks(
            settings, roi.shape)
    
    # TODO: option to distribute groups of sub-ROIs to different servers 
    # for blob detection
    seg_rois = StackDetector.detect_blobs_sub_rois(
        roi, sub_roi_slices, sub_rois_offsets, denoise_max_shape,
        exclude_border, coloc, channels)
    detection_time = time() - time_detection_start
    print("blob detection time (s):", detection_time)
    
    # prune blobs in overlapping portions of sub-ROIs
    time_pruning_start = time()
    segments_all, df_pruning = StackPruner.prune_blobs_mp(
        roi, seg_rois, overlap, tol, sub_roi_slices, sub_rois_offsets, channels,
        overlap_padding)
    pruning_time = time() - time_pruning_start
    print("blob pruning time (s):", pruning_time)
    #print("maxes:", np.amax(segments_all, axis=0))
    
    # get weighted mean of ratios
    if df_pruning is not None:
        print("\nBlob pruning ratios:")
        path_pruning = "blob_ratios.csv" if save_dfs else None
        df_pruning_all = df_io.data_frames_to_csv(
            df_pruning, path_pruning, show=" ")
        cols = df_pruning_all.columns.tolist()
        blob_pruning_means = {}
        if "blobs" in cols:
            blobs_unpruned = df_pruning_all["blobs"]
            num_blobs_unpruned = np.sum(blobs_unpruned)
            for col in cols[1:]:
                blob_pruning_means["mean_{}".format(col)] = [
                    np.sum(np.multiply(df_pruning_all[col], blobs_unpruned)) 
                    / num_blobs_unpruned]
            path_pruning_means = "blob_ratios_means.csv" if save_dfs else None
            df_pruning_means = df_io.dict_to_data_frame(
                blob_pruning_means, path_pruning_means, show=" ")
        else:
            print("no blob ratios found")
    
    '''# report any remaining duplicates
    np.set_printoptions(linewidth=500, threshold=10000000)
    print("all blobs (len {}):".format(len(segments_all)))
    sort = np.lexsort(
        (segments_all[:, 2], segments_all[:, 1], segments_all[:, 0]))
    blobs = segments_all[sort]
    print(blobs)
    print("checking for duplicates in all:")
    print(detector.remove_duplicate_blobs(blobs, slice(0, 3)))
    '''
    
    stats_detection = None
    fdbk = None
    colocs = None
    if segments_all is not None:
        # remove the duplicated elements that were used for pruning
        detector.replace_rel_with_abs_blob_coords(segments_all)
        if coloc:
            colocs = segments_all[:, 10:10+num_chls_roi].astype(np.uint8)
        # remove absolute coordinate and any co-localization columns
        segments_all = detector.remove_abs_blob_coords(segments_all)
        
        # compare detected blobs with truth blobs
        # TODO: assumes ground truth is relative to any ROI offset,
        # but should make customizable
        if verify:
            stats_detection, fdbk = verifier.verify_stack(
                filename_base, subimg_path_base, settings, segments_all,
                channels, overlap_base)
    
    if config.save_subimg:
        subimg_base_path = libmag.combine_paths(
            subimg_path_base, config.SUFFIX_SUBIMG)
        if (isinstance(config.image5d, np.memmap) and 
                config.image5d.filename == os.path.abspath(subimg_base_path)):
            # file at sub-image save path may have been opened as a memmap
            # file, in which case saving would fail
            libmag.warn("{} is currently open, cannot save sub-image"
                        .format(subimg_base_path))
        else:
            # write sub-image, which is in ROI (3D) format
            with open(subimg_base_path, "wb") as f:
                np.save(f, roi)

    # store blobs in Blobs instance
    # TODO: consider separating into blobs and blobs metadata archives
    blobs = detector.Blobs(
        segments_all, colocalizations=colocs, path=filename_blobs)
    blobs.resolutions = config.resolutions
    blobs.basename = os.path.basename(config.filename)
    blobs.roi_offset = offset
    blobs.roi_size = size
    
    # whole image benchmarking time
    times = (
        [detection_time], 
        [pruning_time], 
        time() - time_start)
    times_dict = {}
    for key, val in zip(StackTimes, times):
        times_dict[key] = val
    if segments_all is None:
        print("\nNo blobs detected")
    else:
        print("\nTotal blobs found:", len(segments_all))
        detector.show_blobs_per_channel(segments_all)
    print("\nTotal detection processing times (s):")
    path_times = "stack_detection_times.csv" if save_dfs else None
    df_io.dict_to_data_frame(times_dict, path_times, show=" ")
    
    return stats_detection, fdbk, blobs
示例#13
0
def plot_3d_points(roi, scene_mlab, channel, flipz=False):
    """Plots all pixels as points in 3D space.
    
    Points falling below a given threshold will be
    removed, allowing the viewer to see through the presumed
    background to masses within the region of interest.
    
    Args:
        roi (:obj:`np.ndarray`): Region of interest either as a 3D (z, y, x) or
            4D (z, y, x, channel) ndarray.
        scene_mlab (:mod:``mayavi.mlab``): Mayavi mlab module. Any
            current image will be cleared first.
        channel (int): Channel to select, which can be None to indicate all
            channels.
        flipz (bool): True to invert blobs along z-axis to match handedness
            of Matplotlib with z progressing upward; defaults to False.
    
    Returns:
        bool: True if points were rendered, False if no points to render.
    """
    print("plotting as 3D points")
    scene_mlab.clf()

    # streamline the image
    if roi is None or roi.size < 1: return False
    roi = saturate_roi(roi, clip_vmax=98.5, channel=channel)
    roi = np.clip(roi, 0.2, 0.8)
    roi = restoration.denoise_tv_chambolle(roi, weight=0.1)

    # separate parallel arrays for each dimension of all coordinates for
    # Mayavi input format, with the ROI itself given as a 1D scalar array ;
    # TODO: consider using np.mgrid to construct the x,y,z arrays
    time_start = time()
    shape = roi.shape
    z = np.ones((shape[0], shape[1] * shape[2]))
    for i in range(shape[0]):
        z[i] = z[i] * i
    if flipz:
        # invert along z-axis to match handedness of Matplotlib with z up
        z *= -1
        z += shape[0]
    y = np.ones((shape[0] * shape[1], shape[2]))
    for i in range(shape[0]):
        for j in range(shape[1]):
            y[i * shape[1] + j] = y[i * shape[1] + j] * j
    x = np.ones((shape[0] * shape[1], shape[2]))
    for i in range(shape[0] * shape[1]):
        x[i] = np.arange(shape[2])
    multichannel, channels = setup_channels(roi, channel, 3)
    for chl in channels:
        roi_show = roi[..., chl] if multichannel else roi
        roi_show_1d = roi_show.reshape(roi_show.size)
        if chl == 0:
            x = np.reshape(x, roi_show.size)
            y = np.reshape(y, roi_show.size)
            z = np.reshape(z, roi_show.size)
        settings = config.get_roi_profile(chl)

        # clear background points to see remaining structures
        thresh = 0
        if len(np.unique(roi_show)) > 1:
            # need > 1 val to threshold
            try:
                thresh = filters.threshold_otsu(roi_show, 64)
            except ValueError as e:
                thresh = np.median(roi_show)
                print("could not determine Otsu threshold, taking median "
                      "({}) instead".format(thresh))
            thresh *= settings["points_3d_thresh"]
        print("removing 3D points below threshold of {}".format(thresh))
        remove = np.where(roi_show_1d < thresh)
        roi_show_1d = np.delete(roi_show_1d, remove)

        # adjust range from 0-1 to region of colormap to use
        roi_show_1d = libmag.normalize(roi_show_1d, 0.6, 1.0)
        points_len = roi_show_1d.size
        if points_len == 0:
            print("no 3D points to display")
            return False
        mask = math.ceil(points_len / _MASK_DIVIDEND)
        print("points: {}, mask: {}".format(points_len, mask))
        if any(np.isnan(roi_show_1d)):
            # TODO: see if some NaNs are permissible
            print("NaN values for 3D points, will not show 3D visualization")
            return False
        pts = scene_mlab.points3d(np.delete(x, remove),
                                  np.delete(y, remove),
                                  np.delete(z, remove),
                                  roi_show_1d,
                                  mode="sphere",
                                  scale_mode="scalar",
                                  mask_points=mask,
                                  line_width=1.0,
                                  vmax=1.0,
                                  vmin=0.0,
                                  transparent=True)
        cmap = colormaps.get_cmap(config.cmaps, chl)
        if cmap is not None:
            pts.module_manager.scalar_lut_manager.lut.table = cmap(
                range(0, 256)) * 255
        _resize_glyphs_isotropic(settings, pts)

    print("time for 3D points display: {}".format(time() - time_start))
    return True
示例#14
0
def process_file(path,
                 proc_mode,
                 series=None,
                 subimg_offset=None,
                 subimg_size=None,
                 roi_offset=None,
                 roi_size=None):
    """Processes a single image file non-interactively.

    Assumes that the image has already been set up.
    
    Args:
        path (str): Path to image from which MagellanMapper-style paths will 
            be generated.
        proc_mode (str): Processing mode, which should be a key in
            :class:`config.ProcessTypes`, case-insensitive.
        series (int): Image series number; defaults to None.
        subimg_offset (List[int]): Sub-image offset as (z,y,x) to load;
            defaults to None.
        subimg_size (List[int]): Sub-image size as (z,y,x) to load;
            defaults to None.
        roi_offset (List[int]): Region of interest offset as (x, y, z) to
            process; defaults to None.
        roi_size (List[int]): Region of interest size of region to process,
            given as (x, y, z); defaults to None.
    
    Returns:
        Tuple of stats from processing, or None if no stats, and 
        text feedback from the processing, or None if no feedback.
    """
    # PROCESS BY TYPE
    stats = None
    fdbk = None
    filename_base = importer.filename_to_base(path, series)
    proc_type = libmag.get_enum(proc_mode, config.ProcessTypes)

    print("{}\n".format("-" * 80))
    if proc_type is config.ProcessTypes.LOAD:
        # loading completed
        return None, None

    elif proc_type is config.ProcessTypes.LOAD:
        # already imported so does nothing
        print("imported {}, will exit".format(path))

    elif proc_type is config.ProcessTypes.EXPORT_ROIS:
        # export ROIs; assumes that info_proc was already loaded to
        # give smaller region from which smaller ROIs from the truth DB
        # will be extracted
        from magmap.io import export_rois
        db = config.db if config.truth_db is None else config.truth_db
        export_rois.export_rois(db, config.image5d, config.channel,
                                filename_base,
                                config.plot_labels[config.PlotLabels.PADDING],
                                config.unit_factor, config.truth_db_mode,
                                os.path.basename(config.filename))

    elif proc_type is config.ProcessTypes.TRANSFORM:
        # transpose, rescale, and/or resize whole large image
        transformer.transpose_img(
            path,
            series,
            plane=config.plane,
            rescale=config.transform[config.Transforms.RESCALE],
            target_size=config.roi_size)

    elif proc_type in (config.ProcessTypes.EXTRACT,
                       config.ProcessTypes.ANIMATED):
        # generate animated GIF or extract single plane
        export_stack.stack_to_img(config.filenames, roi_offset, roi_size,
                                  series, subimg_offset, subimg_size,
                                  proc_type is config.ProcessTypes.ANIMATED,
                                  config.suffix)

    elif proc_type is config.ProcessTypes.EXPORT_BLOBS:
        # export blobs to CSV file
        from magmap.io import export_rois
        export_rois.blobs_to_csv(config.blobs.blobs, filename_base)

    elif proc_type in (config.ProcessTypes.DETECT,
                       config.ProcessTypes.DETECT_COLOC):
        # detect blobs in the full image, +/- co-localization
        coloc = proc_type is config.ProcessTypes.DETECT_COLOC
        stats, fdbk, _ = stack_detect.detect_blobs_stack(
            filename_base, subimg_offset, subimg_size, coloc)

    elif proc_type is config.ProcessTypes.COLOC_MATCH:
        if config.blobs is not None and config.blobs.blobs is not None:
            # colocalize blobs in separate channels by matching blobs
            shape = (config.image5d.shape[1:]
                     if subimg_size is None else subimg_size)
            matches = colocalizer.StackColocalizer.colocalize_stack(
                shape, config.blobs.blobs)
            # insert matches into database
            colocalizer.insert_matches(config.db, matches)
        else:
            print("No blobs loaded to colocalize, skipping")

    elif proc_type in (config.ProcessTypes.EXPORT_PLANES,
                       config.ProcessTypes.EXPORT_PLANES_CHANNELS):
        # export each plane as a separate image file
        export_stack.export_planes(
            config.image5d, config.savefig, config.channel,
            proc_type is config.ProcessTypes.EXPORT_PLANES_CHANNELS)

    elif proc_type is config.ProcessTypes.EXPORT_RAW:
        # export the main image as a raw data file
        out_path = libmag.combine_paths(config.filename, ".raw", sep="")
        libmag.backup_file(out_path)
        np_io.write_raw_file(config.image5d, out_path)

    elif proc_type is config.ProcessTypes.PREPROCESS:
        # pre-process a whole image and save to file
        # TODO: consider chunking option for larger images
        profile = config.get_roi_profile(0)
        out_path = config.prefix
        if not out_path:
            out_path = libmag.insert_before_ext(config.filename, "_preproc")
        transformer.preprocess_img(config.image5d, profile["preprocess"],
                                   config.channel, out_path)

    return stats, fdbk