def calc_overlap(): """Calculate overlap based on scaling factor and :const:``OVERLAP_FACTOR``. Returns: Overlap as an array in the same shape and order as in :attr:``detector.resolutions``. """ return np.ceil(np.multiply(detector.calc_scaling_factor(), OVERLAP_FACTOR)).astype(int)
def setup_blocks(settings: "profiles.SettingsDict", shape: Sequence[int]) -> Blocks: """Set up blocks for block processing Each block is a chunk of a larger image processed sequentially or in parallel to optimize resource usage. Args: settings: Settings dictionary that defines that the blocks. shape: Shape of full image in ``z, y, x``. Returns: A named tuple of the block parameters. """ scaling_factor = detector.calc_scaling_factor() print("microsope scaling factor based on resolutions: {}".format( scaling_factor)) denoise_size = settings["denoise_size"] denoise_max_shape = None if denoise_size: # further subdivide each sub-ROI for local preprocessing denoise_max_shape = np.ceil(np.multiply(scaling_factor, denoise_size)).astype(int) # overlap sub-ROIs to minimize edge effects overlap_base = detector.calc_overlap() tol = np.multiply(overlap_base, settings["prune_tol_factor"]).astype(int) overlap_padding = np.copy(tol) overlap = np.copy(overlap_base) exclude_border = settings["exclude_border"] if exclude_border is not None: # exclude border to avoid blob detector edge effects, where blobs # often collect at the faces of the sub-ROI; # ensure that overlap is greater than twice the border exclusion per # axis so that no plane will be excluded from both overlapping sub-ROIs exclude_border_thresh = np.multiply(2, exclude_border) overlap_less = np.less(overlap, exclude_border_thresh) overlap[overlap_less] = exclude_border_thresh[overlap_less] excluded = np.greater(exclude_border, 0) overlap[excluded] += 1 # additional padding overlap_padding[excluded] = 0 # no need to prune past excluded border print("sub-ROI overlap: {}, pruning tolerance: {}, padding beyond " "overlap for pruning: {}, exclude borders: {}".format( overlap, tol, overlap_padding, exclude_border)) max_pixels = np.ceil(np.multiply(scaling_factor, settings["segment_size"])).astype(int) print("preprocessing max shape: {}, detection max pixels: {}".format( denoise_max_shape, max_pixels)) sub_roi_slices, sub_rois_offsets = chunking.stack_splitter( shape, max_pixels, overlap) return Blocks(sub_roi_slices, sub_rois_offsets, denoise_max_shape, exclude_border, tol, overlap_base, overlap, overlap_padding, max_pixels)
def labels_to_markers_blob(labels_img): """Convert a labels image to markers as blobs. 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. Returns: Image array of the same shape as ``img`` and the same number of labels as in ``labels_img``, with labels reduced to smaller markers. """ blobs = {} labels_unique = np.unique(labels_img) #labels_unique = np.concatenate((labels_unique[:5], labels_unique[-5:])) for label_id in labels_unique: if label_id == 0: continue print("finding centroid for label ID {}".format(label_id)) props = cv_nd.get_label_props(labels_img, label_id) if len(props) >= 1: # get centroid and convert to ellipsoid marker blob = [int(n) for n in props[0].centroid] blob.append(5) blobs[label_id] = np.array(blob) print("storing centroid as {}".format(blobs[label_id])) # build markers from centroids spacing = detector.calc_scaling_factor() spacing = spacing / np.amin(spacing) markers = plot_3d.build_ground_truth(np.zeros_like(labels_img), np.array(list(blobs.values())), ellipsoid=True, labels=list(blobs.keys()), spacing=spacing) return markers
def export_rois(db, image5d, channel, path, padding=None, unit_factor=None, truth_mode=None, exp_name=None): """Export all ROIs from database. If the current processing profile includes isotropic interpolation, the ROIs will be resized to make isotropic according to this factor. Args: db: Database from which to export. image5d: The image with the ROIs. channel (List[int]): Channels to export; currently only the first channel is used. path: Path with filename base from which to save the exported files. padding (List[int]): Padding in x,y,z to exclude from the ROI; defaults to None. unit_factor (float): Linear conversion factor for units (eg 1000.0 to convert um to mm). truth_mode (:obj:`config.TruthDBModes`): Truth mode enum; defaults to None. exp_name (str): Name of experiment to export; defaults to None to export all experiments in ``db``. Returns: :obj:`pd.DataFrame`: ROI metrics in a data frame. """ if padding is not None: padding = np.array(padding) # TODO: consider iterating through all channels channel = channel[0] if channel else 0 # convert volume base on scaling and unit factor phys_mult = np.prod(detector.calc_scaling_factor()) if unit_factor: phys_mult /= unit_factor**3 metrics_all = {} exps = sqlite.select_experiment(db.cur, None) for exp in exps: if exp_name and exp["name"] != exp_name: # DBs may contain many experiments, which may not correspond to # image5d, eg verified DBs from many truth sets continue rois = sqlite.select_rois(db.cur, exp["id"]) for roi in rois: # get ROI as a small image size = sqlite.get_roi_size(roi) offset = sqlite.get_roi_offset(roi) img3d = plot_3d.prepare_roi(image5d, size, offset) # get blobs and change confirmation flag to avoid confirmation # color in 2D plots roi_id = roi["id"] blobs = sqlite.select_blobs(db.cur, roi_id) blobs_detected = None if truth_mode is config.TruthDBModes.VERIFIED: # verified DBs use a truth value of -1 to indicate "detected", # non-truth blobs, including both correct and incorrect # detections, while the rest of blobs are "truth" blobs truth_vals = detector.get_blob_truth(blobs) blobs_detected = blobs[truth_vals == -1] blobs = blobs[truth_vals != -1] else: # default to include only confirmed blobs; truth sets # ironically do not use the truth flag but instead # assume all confirmed blobs are "truth" blobs = blobs[detector.get_blob_confirmed(blobs) == 1] blobs[:, 4] = -1 # adjust ROI size and offset if border set if padding is not None: size = np.subtract(img3d.shape[::-1], 2 * padding) img3d = plot_3d.prepare_roi(img3d, size, padding) blobs[:, 0:3] = np.subtract(blobs[:, 0:3], np.add(offset, padding)[::-1]) print("exporting ROI of shape {}".format(img3d.shape)) isotropic = config.roi_profile["isotropic"] blobs_orig = blobs if isotropic is not None: # interpolation for isotropy if set in first processing profile img3d = cv_nd.make_isotropic(img3d, isotropic) isotropic_factor = cv_nd.calc_isotropic_factor(isotropic) blobs_orig = np.copy(blobs) blobs = detector.multiply_blob_rel_coords( blobs, isotropic_factor) # export ROI and 2D plots path_base, path_dir_nifti, path_img, path_img_nifti, path_blobs, \ path_img_annot, path_img_annot_nifti = make_roi_paths( path, roi_id, channel, make_dirs=True) np.save(path_img, img3d) print("saved 3D image to {}".format(path_img)) # WORKAROUND: for some reason SimpleITK gives a conversion error # when converting from uint16 (>u2) Numpy array img3d = img3d.astype(np.float64) img3d_sitk = sitk.GetImageFromArray(img3d) ''' print(img3d_sitk) print("orig img:\n{}".format(img3d[0])) img3d_back = sitk.GetArrayFromImage(img3d_sitk) print(img3d.shape, img3d.dtype, img3d_back.shape, img3d_back.dtype) print("sitk img:\n{}".format(img3d_back[0])) ''' sitk.WriteImage(img3d_sitk, path_img_nifti, False) roi_ed = roi_editor.ROIEditor(img3d) roi_ed.plot_roi(blobs, channel, show=False, title=os.path.splitext(path_img)[0]) libmag.show_full_arrays() # export image and blobs, stripping blob flags and adjusting # user-added segments' radii; use original rather than blobs with # any interpolation since the ground truth will itself be # interpolated blobs = blobs_orig blobs = blobs[:, 0:4] # prior to v.0.5.0, user-added segments had a radius of 0.0 blobs[np.isclose(blobs[:, 3], 0), 3] = 5.0 # as of v.0.5.0, user-added segments have neg radii whose abs # value corresponds to the displayed radius blobs[:, 3] = np.abs(blobs[:, 3]) # make more rounded since near-integer values appear to give # edges of 5 straight pixels # https://github.com/scikit-image/scikit-image/issues/2112 #blobs[:, 3] += 1E-1 blobs[:, 3] -= 0.5 libmag.printv("blobs:\n{}".format(blobs)) np.save(path_blobs, blobs) # convert blobs to ground truth img3d_truth = plot_3d.build_ground_truth( np.zeros(size[::-1], dtype=np.uint8), blobs) if isotropic is not None: img3d_truth = cv_nd.make_isotropic(img3d_truth, isotropic) # remove fancy blending since truth set must be binary img3d_truth[img3d_truth >= 0.5] = 1 img3d_truth[img3d_truth < 0.5] = 0 print("exporting truth ROI of shape {}".format(img3d_truth.shape)) np.save(path_img_annot, img3d_truth) #print(img3d_truth) sitk.WriteImage(sitk.GetImageFromArray(img3d_truth), path_img_annot_nifti, False) # avoid smoothing interpolation, using "nearest" instead with plt.style.context(config.rc_params_mpl2_img_interp): roi_ed.plot_roi(img3d_truth, None, channel, show=False, title=os.path.splitext(path_img_annot)[0]) # measure ROI metrics and export to data frame; use AtlasMetrics # enum vals since will need LabelMetrics names instead metrics = { config.AtlasMetrics.SAMPLE.value: exp["name"], config.AtlasMetrics.CONDITION.value: "truth", config.AtlasMetrics.CHANNEL.value: channel, config.AtlasMetrics.OFFSET.value: offset, config.AtlasMetrics.SIZE.value: size, } # get basic counts for ROI and update volume for physical units vols.MeasureLabel.set_data(img3d, np.ones_like(img3d, dtype=np.int8)) _, metrics_counts = vols.MeasureLabel.measure_counts(1) metrics_counts[vols.LabelMetrics.Volume] *= phys_mult for key, val in metrics_counts.items(): # convert LabelMetrics to their name metrics[key.name] = val metrics[vols.LabelMetrics.Nuclei.name] = len(blobs) metrics_dicts = [metrics] if blobs_detected is not None: # add another row for detected blobs metrics_detected = dict(metrics) metrics_detected[ config.AtlasMetrics.CONDITION.value] = "detected" metrics_detected[vols.LabelMetrics.Nuclei.name] = len( blobs_detected) metrics_dicts.append(metrics_detected) for m in metrics_dicts: for key, val in m.items(): metrics_all.setdefault(key, []).append(val) print("exported {}".format(path_base)) #_test_loading_rois(db, channel, path) # convert to data frame and compute densities for nuclei and intensity df = df_io.dict_to_data_frame(metrics_all) vol = df[vols.LabelMetrics.Volume.name] df.loc[:, vols.LabelMetrics.DensityIntens.name] = ( df[vols.LabelMetrics.Intensity.name] / vol) df.loc[:, vols.LabelMetrics.Density.name] = ( df[vols.LabelMetrics.Nuclei.name] / vol) df = df_io.data_frames_to_csv(df, "{}_rois.csv".format(path)) return df
def setup_blocks(settings, shape): """Set up blocks for block processing, where each block is a chunk of a larger image processed sequentially or in parallel to optimize resource usage. Args: settings (:obj:`magmap.settings.profiles.SettingsDict`): Settings dictionary that defines that the blocks. shape (List[int]): Shape of full image in z,y,x. Returns: :obj:`np.ndarray`, :obj:`np.ndarray`, :obj:`np.ndarray`, List[int], :obj:`np.ndarray`, :obj:`np.ndarray`, :obj:`np.ndarray`, :obj:`np.ndarray`: Numpy object array of tuples containing slices of each block; similar Numpy array but with tuples of offsets; Numpy int array of max shape for each sub-block used for denoising; List of ints for border pixels to exclude in z,y,x; match tolerance as a Numpy float array in z,y,x; Numpy float array of overlapping pixels in z,y,x; similar overlap array but modified by the border exclusion; and similar overlap array but for padding beyond the overlap. """ scaling_factor = detector.calc_scaling_factor() print("microsope scaling factor based on resolutions: {}" .format(scaling_factor)) denoise_size = settings["denoise_size"] denoise_max_shape = None if denoise_size: # further subdivide each sub-ROI for local preprocessing denoise_max_shape = np.ceil( np.multiply(scaling_factor, denoise_size)).astype(int) # overlap sub-ROIs to minimize edge effects overlap_base = chunking.calc_overlap() tol = np.multiply(overlap_base, settings["prune_tol_factor"]).astype(int) overlap_padding = np.copy(tol) overlap = np.copy(overlap_base) exclude_border = settings["exclude_border"] if exclude_border is not None: # exclude border to avoid blob detector edge effects, where blobs # often collect at the faces of the sub-ROI; # ensure that overlap is greater than twice the border exclusion per # axis so that no plane will be excluded from both overlapping sub-ROIs exclude_border_thresh = np.multiply(2, exclude_border) overlap_less = np.less(overlap, exclude_border_thresh) overlap[overlap_less] = exclude_border_thresh[overlap_less] excluded = np.greater(exclude_border, 0) overlap[excluded] += 1 # additional padding overlap_padding[excluded] = 0 # no need to prune past excluded border print("sub-ROI overlap: {}, pruning tolerance: {}, padding beyond " "overlap for pruning: {}, exclude borders: {}" .format(overlap, tol, overlap_padding, exclude_border)) max_pixels = np.ceil(np.multiply( scaling_factor, settings["segment_size"])).astype(int) print("preprocessing max shape: {}, detection max pixels: {}" .format(denoise_max_shape, max_pixels)) sub_roi_slices, sub_rois_offsets = chunking.stack_splitter( shape, max_pixels, overlap) return sub_roi_slices, sub_rois_offsets, denoise_max_shape, \ exclude_border, tol, overlap_base, overlap, overlap_padding
def detect_blobs_large_image(filename_base, image5d, offset, size, verify=False, save_dfs=True, full_roi=False): """Detect blobs within a large image through parallel processing of smaller chunks. 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. 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. """ time_start = time() 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: # change base filename for ROI-based partial stack filename_base = make_subimage_name(filename_base, offset, size) filename_subimg = libmag.combine_paths(filename_base, config.SUFFIX_SUBIMG) filename_blobs = libmag.combine_paths(filename_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, size, offset) _, channels = plot_3d.setup_channels(roi, config.channel, 3) # prep chunking ROI into sub-ROIs with size based on segment_size, scaling # by physical units to make more independent of resolution time_detection_start = time() settings = config.roi_profile # use default settings scaling_factor = detector.calc_scaling_factor() print("microsope scaling factor based on resolutions: {}" .format(scaling_factor)) denoise_size = config.roi_profile["denoise_size"] denoise_max_shape = None if denoise_size: # further subdivide each sub-ROI for local preprocessing denoise_max_shape = np.ceil( np.multiply(scaling_factor, denoise_size)).astype(int) # overlap sub-ROIs to minimize edge effects overlap_base = chunking.calc_overlap() tol = np.multiply(overlap_base, settings["prune_tol_factor"]).astype(int) overlap_padding = np.copy(tol) overlap = np.copy(overlap_base) exclude_border = config.roi_profile["exclude_border"] if exclude_border is not None: # exclude border to avoid blob detector edge effects, where blobs # often collect at the faces of the sub-ROI; # ensure that overlap is greater than twice the border exclusion per # axis so that no plane will be excluded from both overlapping sub-ROIs exclude_border_thresh = np.multiply(2, exclude_border) overlap_less = np.less(overlap, exclude_border_thresh) overlap[overlap_less] = exclude_border_thresh[overlap_less] excluded = np.greater(exclude_border, 0) overlap[excluded] += 1 # additional padding overlap_padding[excluded] = 0 # no need to prune past excluded border print("sub-ROI overlap: {}, pruning tolerance: {}, padding beyond " "overlap for pruning: {}, exclude borders: {}" .format(overlap, tol, overlap_padding, exclude_border)) max_pixels = np.ceil(np.multiply( scaling_factor, config.roi_profile["segment_size"])).astype(int) print("preprocessing max shape: {}, detection max pixels: {}" .format(denoise_max_shape, max_pixels)) sub_roi_slices, sub_rois_offsets = chunking.stack_splitter( roi.shape, max_pixels, overlap) # TODO: option to distribute groups of sub-ROIs to different servers # for blob detection seg_rois = detect_blobs_sub_rois( roi, sub_roi_slices, sub_rois_offsets, denoise_max_shape, exclude_border) 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 = _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 if segments_all is not None: # remove the duplicated elements that were used for pruning detector.replace_rel_with_abs_blob_coords(segments_all) 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: db_path_base = None exp_name = os.path.splitext(os.path.basename(config.filename))[0] try: if config.truth_db is None: # find and load truth DB based on filename and subimage db_path_base = os.path.basename(filename_base) print("about to verify with truth db from {}" .format(db_path_base)) sqlite.load_truth_db(db_path_base) if config.truth_db is not None: # truth DB may contain multiple experiments for different # subimages; series not included in exp name since in ROI rois = config.truth_db.get_rois(exp_name) if rois is None: # exp may have been named by ROI print("{} experiment name not found, will try with" "ROI offset/size".format(exp_name)) exp_name = make_subimage_name(exp_name, offset, size) rois = config.truth_db.get_rois(exp_name) if rois is None: raise LookupError( "No truth set ROIs found for experiment {}, will " "skip detection verification".format(exp_name)) print("load ROIs from exp: {}".format(exp_name)) exp_id = sqlite.insert_experiment( config.verified_db.conn, config.verified_db.cur, exp_name, None) verify_tol = np.multiply( overlap_base, settings["verify_tol_factor"]) stats_detection, fdbk = detector.verify_rois( rois, segments_all, config.truth_db.blobs_truth, verify_tol, config.verified_db, exp_id, config.channel) except FileNotFoundError: libmag.warn("Could not load truth DB from {}; " "will not verify ROIs".format(db_path_base)) except LookupError as e: libmag.warn(str(e)) file_time_start = time() if config.save_subimg: if (isinstance(config.image5d, np.memmap) and config.image5d.filename == os.path.abspath(filename_subimg)): # 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(filename_subimg)) else: # write sub-image, which is in ROI (3D) format with open(filename_subimg, "wb") as f: np.save(f, roi) # save blobs # TODO: only segments used; consider removing the rest except ver outfile_blobs = open(filename_blobs, "wb") np.savez(outfile_blobs, ver=BLOBS_NP_VER, segments=segments_all, resolutions=config.resolutions, basename=os.path.basename(config.filename), # only save name offset=offset, roi_size=size) # None unless explicitly set outfile_blobs.close() file_save_time = time() - file_time_start # 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("file save time:", file_save_time) 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, segments_all