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
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
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)
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
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
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
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)
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
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
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
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
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
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
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