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