def make_labels_level_img(img_path, level, prefix=None, show=False): """Replace labels in an image with their parents at the given level. Labels that do not fall within a parent at that level will remain in place. Args: img_path: Path to the base image from which the corresponding registered image will be found. level: Ontological level at which to group child labels. prefix: Start of path for output image; defaults to None to use ``img_path`` instead. show: True to show the images after generating them; defaults to False. """ # load original labels image and setup ontology dictionary labels_sitk = sitk_io.load_registered_img(img_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_np = sitk.GetArrayFromImage(labels_sitk) ref = ontology.load_labels_ref(config.load_labels) labels_ref_lookup = ontology.create_aba_reverse_lookup(ref) ids = list(labels_ref_lookup.keys()) for key in ids: keys = [key, -1 * key] for region in keys: if region == 0: continue # get ontological label label = labels_ref_lookup[abs(region)] label_level = label[ontology.NODE][config.ABAKeys.LEVEL.value] if label_level == level: # get children (including parent first) at given level # and replace them with parent label_ids = ontology.get_children_from_id( labels_ref_lookup, region) labels_region = np.isin(labels_np, label_ids) print("replacing labels within", region) labels_np[labels_region] = region labels_level_sitk = sitk_io.replace_sitk_with_numpy(labels_sitk, labels_np) # generate an edge image at this level labels_edge = vols.make_labels_edge(labels_np) labels_edge_sikt = sitk_io.replace_sitk_with_numpy(labels_sitk, labels_edge) # write and optionally display labels level image imgs_write = { config.RegNames.IMG_LABELS_LEVEL.value.format(level): labels_level_sitk, config.RegNames.IMG_LABELS_EDGE_LEVEL.value.format(level): labels_edge_sikt, } out_path = prefix if prefix else img_path sitk_io.write_reg_images(imgs_write, out_path) if show: for img in imgs_write.values(): if img: sitk.Show(img)
def make_labels_level_img(img_path, level, prefix=None, show=False): """Replace labels in an image with their parents at the given level. Labels that do not fall within a parent at that level will remain in place. Args: img_path: Path to the base image from which the corresponding registered image will be found. level: Ontological level at which to group child labels. prefix: Start of path for output image; defaults to None to use ``img_path`` instead. show: True to show the images after generating them; defaults to False. """ # load original labels image and setup ontology dictionary labels_sitk = sitk_io.load_registered_img(img_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_np = sitk.GetArrayFromImage(labels_sitk) ref = ontology.LabelsRef(config.load_labels).load() # remap labels to given level labels_np = ontology.make_labels_level(labels_np, ref, level) labels_level_sitk = sitk_io.replace_sitk_with_numpy(labels_sitk, labels_np) # generate an edge image at this level labels_edge = vols.make_labels_edge(labels_np) labels_edge_sikt = sitk_io.replace_sitk_with_numpy(labels_sitk, labels_edge) # write and optionally display labels level image imgs_write = { config.RegNames.IMG_LABELS_LEVEL.value.format(level): labels_level_sitk, config.RegNames.IMG_LABELS_EDGE_LEVEL.value.format(level): labels_edge_sikt, } out_path = prefix if prefix else img_path sitk_io.write_reg_images(imgs_write, out_path) if show: for img in imgs_write.values(): if img: sitk.Show(img)
def make_labels_diff_img(img_path, df_path, meas, fn_avg, prefix=None, show=False, level=None, meas_path_name=None, col_wt=None): """Replace labels in an image with the differences in metrics for each given region between two conditions. Args: img_path: Path to the base image from which the corresponding registered image will be found. df_path: Path to data frame with metrics for the labels. meas: Name of colum in data frame with the chosen measurement. fn_avg: Function to apply to the set of measurements, such as a mean. Can be None if ``df_path`` points to a stats file from which to extract metrics directly in :meth:``vols.map_meas_to_labels``. prefix: Start of path for output image; defaults to None to use ``img_path`` instead. show: True to show the images after generating them; defaults to False. level: Ontological level at which to look up and show labels. Assume that labels level image corresponding to this value has already been generated by :meth:``make_labels_level_img``. Defaults to None to use only drawn labels. meas_path_name: Name to use in place of `meas` in output path; defaults to None. col_wt (str): Name of column to use for weighting; defaults to None. """ # load labels image and data frame before generating map for the # given metric of the chosen measurement print("Generating labels difference image for", meas, "from", df_path) reg_name = (config.RegNames.IMG_LABELS.value if level is None else config.RegNames.IMG_LABELS_LEVEL.value.format(level)) labels_sitk = sitk_io.load_registered_img(img_path, reg_name, get_sitk=True) labels_np = sitk.GetArrayFromImage(labels_sitk) df = pd.read_csv(df_path) labels_diff = vols.map_meas_to_labels( labels_np, df, meas, fn_avg, reverse=True, col_wt=col_wt) if labels_diff is None: return labels_diff_sitk = sitk_io.replace_sitk_with_numpy(labels_sitk, labels_diff) # save and show labels difference image using measurement name in # output path or overriding with custom name meas_path = meas if meas_path_name is None else meas_path_name reg_diff = libmag.insert_before_ext( config.RegNames.IMG_LABELS_DIFF.value, meas_path, "_") if fn_avg is not None: # add function name to output path if given reg_diff = libmag.insert_before_ext( reg_diff, fn_avg.__name__, "_") imgs_write = {reg_diff: labels_diff_sitk} out_path = prefix if prefix else img_path sitk_io.write_reg_images(imgs_write, out_path) if show: for img in imgs_write.values(): if img: sitk.Show(img)
def make_sub_segmented_labels(img_path, suffix=None): """Divide each label based on anatomical borders to create a sub-segmented image. The segmented labels image will be loaded, or if not available, the non-segmented labels will be loaded instead. Args: img_path: Path to main image from which registered images will be loaded. suffix: Modifier to append to end of ``img_path`` basename for registered image files that were output to a modified name; defaults to None. Returns: Sub-segmented image as a Numpy array of the same shape as the image at ``img_path``. """ # adjust image path with suffix mod_path = img_path if suffix is not None: mod_path = libmag.insert_before_ext(mod_path, suffix) # load labels labels_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) # atlas edge image is associated with original, not modified image atlas_edge = sitk_io.load_registered_img( img_path, config.RegNames.IMG_ATLAS_EDGE.value) # sub-divide the labels and save to file labels_img_np = sitk.GetArrayFromImage(labels_sitk) labels_subseg = segmenter.sub_segment_labels(labels_img_np, atlas_edge) labels_subseg_sitk = sitk_io.replace_sitk_with_numpy( labels_sitk, labels_subseg) sitk_io.write_reg_images( {config.RegNames.IMG_LABELS_SUBSEG.value: labels_subseg_sitk}, mod_path) return labels_subseg
def make_edge_images(path_img, show=True, atlas=True, suffix=None, path_atlas_dir=None): """Make edge-detected atlas and associated labels images. The atlas is assumed to be a sample (eg microscopy) image on which an edge-detection filter will be applied. The labels image is assumed to be an annotated image whose edges will be found by obtaining the borders of all separate labels. Args: path_img: Path to the image atlas. The labels image will be found as a corresponding, registered image, unless ``path_atlas_dir`` is given. show (bool): True if the output images should be displayed; defaults to True. atlas: True if the primary image is an atlas, which is assumed to be symmetrical. False if the image is an experimental/sample image, in which case erosion will be performed on the full images, and stats will not be performed. suffix: Modifier to append to end of ``path_img`` basename for registered image files that were output to a modified name; defaults to None. path_atlas_dir: Path to atlas directory to use labels from that directory rather than from labels image registered to ``path_img``, such as when the sample image is registered to an atlas rather than the other way around. Typically coupled with ``suffix`` to compare same sample against different labels. Defaults to None. """ # load intensity image from which to detect edges atlas_suffix = config.reg_suffixes[config.RegSuffixes.ATLAS] if not atlas_suffix: if atlas: # atlases default to using the atlas volume image print("generating edge images for atlas") atlas_suffix = config.RegNames.IMG_ATLAS.value else: # otherwise, use the experimental image print("generating edge images for experiment/sample image") atlas_suffix = config.RegNames.IMG_EXP.value # adjust image path with suffix mod_path = path_img if suffix is not None: mod_path = libmag.insert_before_ext(mod_path, suffix) labels_from_atlas_dir = path_atlas_dir and os.path.isdir(path_atlas_dir) if labels_from_atlas_dir: # load labels from atlas directory # TODO: consider applying suffix to labels dir path_atlas = path_img path_labels = os.path.join( path_atlas_dir, config.RegNames.IMG_LABELS.value) print("loading labels from", path_labels) labels_sitk = sitk.ReadImage(path_labels) else: # load labels registered to sample image path_atlas = mod_path labels_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_img_np = sitk.GetArrayFromImage(labels_sitk) # load atlas image, set resolution from it atlas_sitk = sitk_io.load_registered_img( path_atlas, atlas_suffix, get_sitk=True) config.resolutions = np.array([atlas_sitk.GetSpacing()[::-1]]) atlas_np = sitk.GetArrayFromImage(atlas_sitk) # output images atlas_sitk_log = None atlas_sitk_edge = None labels_sitk_interior = None log_sigma = config.atlas_profile["log_sigma"] if log_sigma is not None and suffix is None: # generate LoG and edge-detected images for original image print("generating LoG edge-detected images with sigma", log_sigma) thresh = (config.atlas_profile["atlas_threshold"] if config.atlas_profile["log_atlas_thresh"] else None) atlas_log = cv_nd.laplacian_of_gaussian_img( atlas_np, sigma=log_sigma, labels_img=labels_img_np, thresh=thresh) atlas_sitk_log = sitk_io.replace_sitk_with_numpy(atlas_sitk, atlas_log) atlas_edge = cv_nd.zero_crossing(atlas_log, 1).astype(np.uint8) atlas_sitk_edge = sitk_io.replace_sitk_with_numpy( atlas_sitk, atlas_edge) else: # if sigma not set or if using suffix to compare two images, # load from original image to compare against common image atlas_edge = sitk_io.load_registered_img( path_img, config.RegNames.IMG_ATLAS_EDGE.value) erode = config.atlas_profile["erode_labels"] if erode["interior"]: # make map of label interiors for interior/border comparisons print("Eroding labels to generate interior labels image") erosion = config.atlas_profile[ profiles.RegKeys.EDGE_AWARE_REANNOTATION] erosion_frac = config.atlas_profile["erosion_frac"] interior, _ = erode_labels( labels_img_np, erosion, erosion_frac, atlas and _is_profile_mirrored(), _get_mirror_mult()) labels_sitk_interior = sitk_io.replace_sitk_with_numpy( labels_sitk, interior) # make labels edge and edge distance images dist_to_orig, labels_edge = edge_distances( labels_img_np, atlas_edge, spacing=atlas_sitk.GetSpacing()[::-1]) dist_sitk = sitk_io.replace_sitk_with_numpy(atlas_sitk, dist_to_orig) labels_sitk_edge = sitk_io.replace_sitk_with_numpy(labels_sitk, labels_edge) # show all images imgs_write = { config.RegNames.IMG_ATLAS_LOG.value: atlas_sitk_log, config.RegNames.IMG_ATLAS_EDGE.value: atlas_sitk_edge, config.RegNames.IMG_LABELS_EDGE.value: labels_sitk_edge, config.RegNames.IMG_LABELS_INTERIOR.value: labels_sitk_interior, config.RegNames.IMG_LABELS_DIST.value: dist_sitk, } if show: for img in imgs_write.values(): if img: sitk.Show(img) # write images to same directory as atlas with appropriate suffix sitk_io.write_reg_images(imgs_write, mod_path)
def merge_atlas_segmentations(img_paths, show=True, atlas=True, suffix=None): """Merge atlas segmentations for a list of files as a multiprocessing wrapper for :func:``merge_atlas_segmentations``, after which edge image post-processing is performed separately since it contains tasks also performed in multiprocessing. Args: img_paths (List[str]): Sequence of image paths to load. show (bool): True if the output images should be displayed; defaults to True. atlas (bool): True if the image is an atlas; defaults to True. suffix (str): Modifier to append to end of ``img_path`` basename for registered image files that were output to a modified name; defaults to None. """ start_time = time() # erode all labels images into markers for watershed; not multiprocessed # since erosion is itself multiprocessed erode = config.atlas_profile["erode_labels"] erosion = config.atlas_profile[profiles.RegKeys.EDGE_AWARE_REANNOTATION] erosion_frac = config.atlas_profile["erosion_frac"] mirrored = atlas and _is_profile_mirrored() mirror_mult = _get_mirror_mult() dfs_eros = [] for img_path in img_paths: mod_path = img_path if suffix is not None: mod_path = libmag.insert_before_ext(mod_path, suffix) labels_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) print("Eroding labels to generate markers for atlas segmentation") df = None if erode["markers"]: # use default minimal post-erosion size (not setting erosion frac) markers, df = erode_labels( sitk.GetArrayFromImage(labels_sitk), erosion, mirrored=mirrored, mirror_mult=mirror_mult) labels_sitk_markers = sitk_io.replace_sitk_with_numpy( labels_sitk, markers) sitk_io.write_reg_images( {config.RegNames.IMG_LABELS_MARKERS.value: labels_sitk_markers}, mod_path) df_io.data_frames_to_csv( df, "{}_markers.csv".format(os.path.splitext(mod_path)[0])) dfs_eros.append(df) pool = chunking.get_mp_pool() pool_results = [] for img_path, df in zip(img_paths, dfs_eros): print("setting up atlas segmentation merge for", img_path) # convert labels image into markers exclude = df.loc[ np.isnan(df[config.SmoothingMetrics.FILTER_SIZE.value]), config.AtlasMetrics.REGION.value] print("excluding these labels from re-segmentation:\n", exclude) pool_results.append(pool.apply_async( edge_aware_segmentation, args=(img_path, show, atlas, suffix, exclude, mirror_mult))) for result in pool_results: # edge distance calculation and labels interior image generation # are multiprocessed, so run them as post-processing tasks to # avoid nested multiprocessing path = result.get() mod_path = path if suffix is not None: mod_path = libmag.insert_before_ext(path, suffix) # make edge distance images and stats labels_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_np = sitk.GetArrayFromImage(labels_sitk) dist_to_orig, labels_edge = edge_distances( labels_np, path=path, spacing=labels_sitk.GetSpacing()[::-1]) dist_sitk = sitk_io.replace_sitk_with_numpy(labels_sitk, dist_to_orig) labels_sitk_edge = sitk_io.replace_sitk_with_numpy( labels_sitk, labels_edge) labels_sitk_interior = None if erode["interior"]: # make interior images from labels using given targeted # post-erosion frac interior, _ = erode_labels( labels_np, erosion, erosion_frac=erosion_frac, mirrored=mirrored, mirror_mult=mirror_mult) labels_sitk_interior = sitk_io.replace_sitk_with_numpy( labels_sitk, interior) # write images to same directory as atlas imgs_write = { config.RegNames.IMG_LABELS_DIST.value: dist_sitk, config.RegNames.IMG_LABELS_EDGE.value: labels_sitk_edge, config.RegNames.IMG_LABELS_INTERIOR.value: labels_sitk_interior, } sitk_io.write_reg_images(imgs_write, mod_path) if show: for img in imgs_write.values(): if img: sitk.Show(img) print("finished {}".format(path)) pool.close() pool.join() print("time elapsed for merging atlas segmentations:", time() - start_time)
def edge_aware_segmentation(path_atlas, show=True, atlas=True, suffix=None, exclude_labels=None, mirror_mult=-1): """Segment an atlas using its previously generated edge map. Labels may not match their own underlying atlas image well, particularly in the orthogonal directions in which the labels were not constructed. To improve alignment between the labels and the atlas itself, register the labels to an automated, roughly segmented version of the atlas. The goal is to improve the labels' alignment so that the atlas/labels combination can be used for another form of automated segmentation by registering them to experimental brains via :func:``register``. Edge files are assumed to have been generated by :func:``make_edge_images``. Args: path_atlas (str): Path to the fixed file, typically the atlas file with stained sections. The corresponding edge and labels files will be loaded based on this path. show (bool): True if the output images should be displayed; defaults to True. atlas (bool): True if the primary image is an atlas, which is assumed to be symmetrical. False if the image is an experimental/sample image, in which case segmentation will be performed on the full images, and stats will not be performed. suffix (str): Modifier to append to end of ``path_atlas`` basename for registered image files that were output to a modified name; defaults to None. If ``atlas`` is True, ``suffix`` will only be applied to saved files, with files still loaded based on the original path. exclude_labels (List[int]): Sequence of labels to exclude from the segmentation; defaults to None. mirror_mult (int): Multiplier for mirrored labels; defaults to -1 to make mirrored labels the inverse of their source labels. """ # adjust image path with suffix load_path = path_atlas mod_path = path_atlas if suffix is not None: mod_path = libmag.insert_before_ext(mod_path, suffix) if atlas: load_path = mod_path # load corresponding files via SimpleITK atlas_sitk = sitk_io.load_registered_img( load_path, config.RegNames.IMG_ATLAS.value, get_sitk=True) atlas_sitk_edge = sitk_io.load_registered_img( load_path, config.RegNames.IMG_ATLAS_EDGE.value, get_sitk=True) labels_sitk = sitk_io.load_registered_img( load_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_sitk_markers = sitk_io.load_registered_img( load_path, config.RegNames.IMG_LABELS_MARKERS.value, get_sitk=True) # get Numpy arrays of images atlas_img_np = sitk.GetArrayFromImage(atlas_sitk) atlas_edge = sitk.GetArrayFromImage(atlas_sitk_edge) labels_img_np = sitk.GetArrayFromImage(labels_sitk) markers = sitk.GetArrayFromImage(labels_sitk_markers) # segment image from markers sym_axis = atlas_refiner.find_symmetric_axis(atlas_img_np) mirrorred = atlas and sym_axis >= 0 len_half = None seg_args = {"exclude_labels": exclude_labels} edge_prof = config.atlas_profile[profiles.RegKeys.EDGE_AWARE_REANNOTATION] if edge_prof: edge_filt = edge_prof[profiles.RegKeys.WATERSHED_MASK_FILTER] if edge_filt and len(edge_filt) > 1: # watershed mask filter settings from atlas profile seg_args["mask_filt"] = edge_filt[0] seg_args["mask_filt_size"] = edge_filt[1] if mirrorred: # segment only half of image, assuming symmetry len_half = atlas_img_np.shape[sym_axis] // 2 slices = [slice(None)] * labels_img_np.ndim slices[sym_axis] = slice(len_half) sl = tuple(slices) labels_seg = segmenter.segment_from_labels( atlas_edge[sl], markers[sl], labels_img_np[sl], **seg_args) else: # segment the full image, including excluded labels on the opposite side exclude_labels = exclude_labels.tolist().extend( (mirror_mult * exclude_labels).tolist()) seg_args["exclude_labels"] = exclude_labels labels_seg = segmenter.segment_from_labels( atlas_edge, markers, labels_img_np, **seg_args) smoothing = config.atlas_profile["smooth"] if smoothing is not None: # smoothing by opening operation based on profile setting atlas_refiner.smooth_labels( labels_seg, smoothing, config.SmoothingModes.opening) if mirrorred: # mirror back to other half labels_seg = _mirror_imported_labels( labels_seg, len_half, mirror_mult, sym_axis) # expand background to smoothed background of original labels to # roughly match background while still allowing holes to be filled crop = config.atlas_profile["crop_to_orig"] atlas_refiner.crop_to_orig( labels_img_np, labels_seg, crop) if labels_seg.dtype != labels_img_np.dtype: # watershed may give different output type, so cast back if so labels_seg = labels_seg.astype(labels_img_np.dtype) labels_sitk_seg = sitk_io.replace_sitk_with_numpy(labels_sitk, labels_seg) # show DSCs for labels print("\nMeasuring overlap of atlas and combined watershed labels:") atlas_refiner.measure_overlap_combined_labels(atlas_sitk, labels_sitk_seg) print("Measuring overlap of individual original and watershed labels:") atlas_refiner.measure_overlap_labels(labels_sitk, labels_sitk_seg) print("\nMeasuring overlap of combined original and watershed labels:") atlas_refiner.measure_overlap_labels( atlas_refiner.make_labels_fg(labels_sitk), atlas_refiner.make_labels_fg(labels_sitk_seg)) print() # show and write image to same directory as atlas with appropriate suffix sitk_io.write_reg_images( {config.RegNames.IMG_LABELS.value: labels_sitk_seg}, mod_path) if show: sitk.Show(labels_sitk_seg) return path_atlas
def make_density_image( img_path: str, scale: Optional[float] = None, shape: Optional[Sequence[int]] = None, suffix: Optional[str] = None, labels_img_sitk: Optional[sitk.Image] = None, channel: Optional[Sequence[int]] = None, matches: Dict[Tuple[int, int], "colocalizer.BlobMatch"] = None, atlas_profile: Optional["atlas_prof.AtlasProfile"] = None ) -> Tuple[np.ndarray, str]: """Make a density image based on associated blobs. Uses the size and resolutions of the original image stores in the blobs if available to determine scaling between the blobs and the output image. Otherwise, uses the shape of the registered labels image to set the voxel sizes for the blobs. If ``matches`` is given, a heat map will be generated for each set of channels given in the dictionary. Otherwise, if the loaded blobs file has intensity-based colocalizations, a heat map will be generated for each combination of channels. Args: img_path: Path to image, which will be used to indentify the blobs file. scale: Scaling factor between the blobs' space and the output space; defaults to None to use the register. Scaling is found by :meth:`magmap.np_io.find_scaling`. shape: Output shape, used for scaling; defaults to None. suffix: Modifier to append to end of ``img_path`` basename for registered image files that were output to a modified name; defaults to None. labels_img_sitk: Labels image; defaults to None to load from a registered labels image. channel: Sequence of channels to include in density image. For multiple channels, blobs from all these channels are combined into one heatmap. Defaults to None to use all channels. matches: Dictionary of channel combinations to blob matches; defaults to None. atlas_profile: Atlas profile, used for scaling; defaults to None. Returns: Tuple of the density image as a Numpy array in the same shape as the opened image and the original and ``img_path`` to track such as for multiprocessing. """ def make_heat_map(): # build heat map to store densities per label px and save to file coord_scaled = ontology.scale_coords(blobs_chl[:, :3], scaling, labels_img.shape) _logger.debug("Scaled coords:\n%s", coord_scaled) return cv_nd.build_heat_map(labels_img.shape, coord_scaled) # set up paths and get labels image _logger.info("\n\nGenerating heat map from blobs") mod_path = img_path if suffix is not None: mod_path = libmag.insert_before_ext(img_path, suffix) # load blobs blobs = detector.Blobs().load_blobs(np_io.img_to_blobs_path(img_path)) is_2d = False if (shape is not None and blobs.roi_size is not None and blobs.resolutions is not None): # prepare output image and scaling factor from it to the blobs scaling = np.divide(shape, blobs.roi_size) labels_spacing = np.divide(blobs.resolutions[0], scaling) labels_img = np.zeros(shape, dtype=np.uint8) labels_img_sitk = sitk.GetImageFromArray(labels_img) labels_img_sitk.SetSpacing(labels_spacing[::-1]) else: # default to use labels image as the size of the output image if labels_img_sitk is None: labels_img_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_img = sitk.GetArrayFromImage(labels_img_sitk) is_2d = labels_img.ndim == 2 if is_2d: # temporarily convert 2D images to 3D labels_img = labels_img[None] # find the scaling between the blobs and the labels image target_size = (None if atlas_profile is None else atlas_profile["target_size"]) scaling = np_io.find_scaling(img_path, labels_img.shape, scale, target_size)[0] if shape is not None: # scale blob coordinates and heat map to an alternative final shape scaling = np.divide(shape, np.divide(labels_img.shape, scaling)) labels_spacing = np.multiply(labels_img_sitk.GetSpacing()[::-1], np.divide(labels_img.shape, shape)) labels_img = np.zeros(shape, dtype=labels_img.dtype) labels_img_sitk.SetSpacing(labels_spacing[::-1]) _logger.debug("Using image scaling: {}".format(scaling)) # annotate blobs based on position blobs_chl = blobs.blobs if channel is not None: _logger.info( "Using blobs from channel(s), combining if multiple channels: %s", channel) blobs_chl = blobs_chl[np.isin( detector.Blobs.get_blobs_channel(blobs_chl), channel)] heat_map = make_heat_map() if is_2d: # convert back to 3D heat_map = heat_map[0] imgs_write = { config.RegNames.IMG_HEAT_MAP.value: sitk_io.replace_sitk_with_numpy(labels_img_sitk, heat_map) } heat_colocs = None if matches: # create heat maps for match-based colocalization combos heat_colocs = [] for chl_combo, chl_matches in matches.items(): _logger.info( "Generating match-based colocalization heat map " "for channel combo: %s", chl_combo) # use blobs in first channel of each channel pair for simplicity blobs_chl = chl_matches.get_blobs(1) heat_colocs.append(make_heat_map()) elif blobs.colocalizations is not None: # create heat map for each intensity-based colocalization combo # as a separate channel in output image blob_chls = range(blobs.colocalizations.shape[1]) blob_chls_len = len(blob_chls) if blob_chls_len > 1: # get all channel combos that include given channels combos = [] chls = blob_chls if channel is None else channel for r in range(2, blob_chls_len + 1): combos.extend([ tuple(c) for c in itertools.combinations(blob_chls, r) if all([h in c for h in chls]) ]) heat_colocs = [] for combo in combos: _logger.info( "Generating intensity-based colocalization heat map " "for channel combo: %s", combo) blobs_chl = blobs.blobs[np.all(np.equal( blobs.colocalizations[:, combo], 1), axis=1)] heat_colocs.append(make_heat_map()) if heat_colocs is not None: # combine heat maps into single image heat_colocs = np.stack(heat_colocs, axis=3) imgs_write[config.RegNames.IMG_HEAT_COLOC.value] = \ sitk_io.replace_sitk_with_numpy( labels_img_sitk, heat_colocs) # write images to file sitk_io.write_reg_images(imgs_write, mod_path) return heat_map, img_path
def make_density_image(img_path, scale=None, shape=None, suffix=None, labels_img_sitk=None): """Make a density image based on associated blobs. Uses the shape of the registered labels image by default to set the voxel sizes for the blobs. Args: img_path: Path to image, which will be used to indentify the blobs file. scale: Rescaling factor as a scalar value to find the corresponding full-sized image. Defaults to None to use the register setting ``target_size`` instead if available, falling back to load the full size image to find its shape if necessary. shape: Final shape size; defaults to None to use the shape of the labels image. suffix: Modifier to append to end of ``img_path`` basename for registered image files that were output to a modified name; defaults to None. labels_img_sitk: Labels image as a SimpleITK ``Image`` object; defaults to None, in which case the registered labels image file corresponding to ``img_path`` with any ``suffix`` modifier will be opened. Returns: Tuple of the density image as a Numpy array in the same shape as the opened image; Numpy array of blob IDs; and the original ``img_path`` to track such as for multiprocessing. """ mod_path = img_path if suffix is not None: mod_path = libmag.insert_before_ext(img_path, suffix) if labels_img_sitk is None: labels_img_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_img = sitk.GetArrayFromImage(labels_img_sitk) # load blobs blobs, scaling, _ = np_io.load_blobs(img_path, True, labels_img.shape, scale) if shape is not None: # scale blob coordinates and heat map to an alternative final shape scaling = np.divide(shape, np.divide(labels_img.shape, scaling)) labels_spacing = np.multiply(labels_img_sitk.GetSpacing()[::-1], np.divide(labels_img.shape, shape)) labels_img = np.zeros(shape, dtype=labels_img.dtype) labels_img_sitk.SetSpacing(labels_spacing[::-1]) print("using scaling: {}".format(scaling)) # annotate blobs based on position blobs_ids, coord_scaled = ontology.get_label_ids_from_position( blobs[:, :3], labels_img, scaling, return_coord_scaled=True) print("blobs_ids: {}".format(blobs_ids)) # build heat map to store densities per label px and save to file heat_map = cv_nd.build_heat_map(labels_img.shape, coord_scaled) out_path = sitk_io.reg_out_path(mod_path, config.RegNames.IMG_HEAT_MAP.value) print("writing {}".format(out_path)) heat_map_sitk = sitk_io.replace_sitk_with_numpy(labels_img_sitk, heat_map) sitk.WriteImage(heat_map_sitk, out_path, False) return heat_map, blobs_ids, img_path
def edge_aware_segmentation( path_atlas: str, atlas_profile: atlas_prof.AtlasProfile, show: bool = True, atlas: bool = True, suffix: Optional[str] = None, exclude_labels: Optional[pd.DataFrame] = None, mirror_mult: int = -1): """Segment an atlas using its previously generated edge map. Labels may not match their own underlying atlas image well, particularly in the orthogonal directions in which the labels were not constructed. To improve alignment between the labels and the atlas itself, register the labels to an automated, roughly segmented version of the atlas. The goal is to improve the labels' alignment so that the atlas/labels combination can be used for another form of automated segmentation by registering them to experimental brains via :func:``register``. Edge files are assumed to have been generated by :func:``make_edge_images``. Args: path_atlas: Path to the fixed file, typically the atlas file with stained sections. The corresponding edge and labels files will be loaded based on this path. atlas_profile: Atlas profile. show: True if the output images should be displayed; defaults to True. atlas: True if the primary image is an atlas, which is assumed to be symmetrical. False if the image is an experimental/sample image, in which case segmentation will be performed on the full images, and stats will not be performed. suffix: Modifier to append to end of ``path_atlas`` basename for registered image files that were output to a modified name; defaults to None. If ``atlas`` is True, ``suffix`` will only be applied to saved files, with files still loaded based on the original path. exclude_labels: Sequence of labels to exclude from the segmentation; defaults to None. mirror_mult: Multiplier for mirrored labels; defaults to -1 to make mirrored labels the inverse of their source labels. """ # adjust image path with suffix load_path = path_atlas mod_path = path_atlas if suffix is not None: mod_path = libmag.insert_before_ext(mod_path, suffix) if atlas: load_path = mod_path # load corresponding files via SimpleITK atlas_sitk = sitk_io.load_registered_img( load_path, config.RegNames.IMG_ATLAS.value, get_sitk=True) atlas_sitk_edge = sitk_io.load_registered_img( load_path, config.RegNames.IMG_ATLAS_EDGE.value, get_sitk=True) labels_sitk = sitk_io.load_registered_img( load_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_sitk_markers = sitk_io.load_registered_img( load_path, config.RegNames.IMG_LABELS_MARKERS.value, get_sitk=True) # get Numpy arrays of images atlas_img_np = sitk.GetArrayFromImage(atlas_sitk) atlas_edge = sitk.GetArrayFromImage(atlas_sitk_edge) labels_img_np = sitk.GetArrayFromImage(labels_sitk) markers = sitk.GetArrayFromImage(labels_sitk_markers) # segment image from markers sym_axis = atlas_refiner.find_symmetric_axis(atlas_img_np) mirrorred = atlas and sym_axis >= 0 len_half = None seg_args = {"exclude_labels": exclude_labels} edge_prof = atlas_profile[profiles.RegKeys.EDGE_AWARE_REANNOTATION] if edge_prof: edge_filt = edge_prof[profiles.RegKeys.WATERSHED_MASK_FILTER] if edge_filt and len(edge_filt) > 1: # watershed mask filter settings from atlas profile seg_args["mask_filt"] = edge_filt[0] seg_args["mask_filt_size"] = edge_filt[1] if mirrorred: # segment only half of image, assuming symmetry len_half = atlas_img_np.shape[sym_axis] // 2 slices = [slice(None)] * labels_img_np.ndim slices[sym_axis] = slice(len_half) sl = tuple(slices) labels_seg = segmenter.segment_from_labels( atlas_edge[sl], markers[sl], labels_img_np[sl], **seg_args) else: # segment the full image, including excluded labels on the opposite side exclude_labels = exclude_labels.tolist().extend( (mirror_mult * exclude_labels).tolist()) seg_args["exclude_labels"] = exclude_labels labels_seg = segmenter.segment_from_labels( atlas_edge, markers, labels_img_np, **seg_args) smoothing = atlas_profile["smooth"] smoothing_mode = atlas_profile["smoothing_mode"] cond = ["edge-aware_seg"] if smoothing is not None: # smoothing by opening operation based on profile setting meas_smoothing = atlas_profile["meas_smoothing"] cond.append("smoothing") df_aggr, df_raw = atlas_refiner.smooth_labels( labels_seg, smoothing, smoothing_mode, meas_smoothing, labels_sitk.GetSpacing()[::-1]) df_base_path = os.path.splitext(mod_path)[0] if df_raw is not None: # write raw smoothing metrics df_io.data_frames_to_csv( df_raw, f"{df_base_path}_{config.PATH_SMOOTHING_RAW_METRICS}") if df_aggr is not None: # write aggregated smoothing metrics df_io.data_frames_to_csv( df_aggr, f"{df_base_path}_{config.PATH_SMOOTHING_METRICS}") if mirrorred: # mirror back to other half labels_seg = _mirror_imported_labels( labels_seg, len_half, mirror_mult, sym_axis) # expand background to smoothed background of original labels to # roughly match background while still allowing holes to be filled crop = atlas_profile["crop_to_orig"] atlas_refiner.crop_to_orig( labels_img_np, labels_seg, crop) if labels_seg.dtype != labels_img_np.dtype: # watershed may give different output type, so cast back if so labels_seg = labels_seg.astype(labels_img_np.dtype) labels_sitk_seg = sitk_io.replace_sitk_with_numpy(labels_sitk, labels_seg) # show DSCs for labels _logger.info( "\nMeasuring overlap of individual original and watershed labels:") dsc_lbls_comb = atlas_refiner.measure_overlap_labels( labels_sitk, labels_sitk_seg) _logger.info( "\nMeasuring overlap of combined original and watershed labels:") dsc_lbls_indiv = atlas_refiner.measure_overlap_labels( atlas_refiner.make_labels_fg(labels_sitk), atlas_refiner.make_labels_fg(labels_sitk_seg)) _logger.info("") # measure and save whole atlas metrics metrics = { config.AtlasMetrics.SAMPLE: [os.path.basename(mod_path)], config.AtlasMetrics.REGION: config.REGION_ALL, config.AtlasMetrics.CONDITION: "|".join(cond), config.AtlasMetrics.DSC_LABELS_ORIG_NEW_COMBINED: dsc_lbls_comb, config.AtlasMetrics.DSC_LABELS_ORIG_NEW_INDIV: dsc_lbls_indiv, } df_metrics_path = libmag.combine_paths( mod_path, config.PATH_ATLAS_IMPORT_METRICS) atlas_refiner.measure_atlas_refinement( metrics, atlas_sitk, labels_sitk_seg, atlas_profile, df_metrics_path) # show and write image to same directory as atlas with appropriate suffix sitk_io.write_reg_images( {config.RegNames.IMG_LABELS.value: labels_sitk_seg}, mod_path) if show: sitk.Show(labels_sitk_seg) return path_atlas
def make_density_image(img_path, scale=None, shape=None, suffix=None, labels_img_sitk=None, channel=None, matches=None): """Make a density image based on associated blobs. Uses the shape of the registered labels image by default to set the voxel sizes for the blobs. If ``matches`` is given, a heat map will be generated for each set of channels given in the dictionary. Otherwise, if the loaded blobs file has intensity-based colocalizations, a heat map will be generated for each combination of channels. Args: img_path: Path to image, which will be used to indentify the blobs file. scale: Rescaling factor as a scalar value to find the corresponding full-sized image. Defaults to None to use the register setting ``target_size`` instead if available, falling back to load the full size image to find its shape if necessary. shape: Final shape size; defaults to None to use the shape of the labels image. suffix: Modifier to append to end of ``img_path`` basename for registered image files that were output to a modified name; defaults to None. labels_img_sitk: Labels image as a SimpleITK ``Image`` object; defaults to None, in which case the registered labels image file corresponding to ``img_path`` with any ``suffix`` modifier will be opened. channel (List[int]): Sequence of channels to include in density image; defaults to None to combine blobs from all channels. matches (dict[tuple[int, int], :class:`magmap.cv.colocalizer`): Dictionary of channel combinations to blob matches; defaults to None. Returns: :obj:`np.ndarray`, str: The density image as a Numpy array in the same shape as the opened image and the original and ``img_path`` to track such as for multiprocessing. """ def make_heat_map(): # build heat map to store densities per label px and save to file coord_scaled = ontology.scale_coords( blobs_chl[:, :3], scaling, labels_img.shape) print("coords", coord_scaled) return cv_nd.build_heat_map(labels_img.shape, coord_scaled) # set up paths and get labels image mod_path = img_path if suffix is not None: mod_path = libmag.insert_before_ext(img_path, suffix) if labels_img_sitk is None: labels_img_sitk = sitk_io.load_registered_img( mod_path, config.RegNames.IMG_LABELS.value, get_sitk=True) labels_img = sitk.GetArrayFromImage(labels_img_sitk) # load blobs blobs = detector.Blobs().load_blobs(np_io.img_to_blobs_path(img_path)) scaling = np_io.find_scaling(img_path, labels_img.shape, scale)[0] if shape is not None: # scale blob coordinates and heat map to an alternative final shape scaling = np.divide(shape, np.divide(labels_img.shape, scaling)) labels_spacing = np.multiply( labels_img_sitk.GetSpacing()[::-1], np.divide(labels_img.shape, shape)) labels_img = np.zeros(shape, dtype=labels_img.dtype) labels_img_sitk.SetSpacing(labels_spacing[::-1]) print("using scaling: {}".format(scaling)) # annotate blobs based on position blobs_chl = blobs.blobs if channel is not None: blobs_chl = blobs_chl[np.isin(detector.get_blobs_channel( blobs_chl), channel)] heat_map = make_heat_map() print("heat map", heat_map.shape, heat_map.dtype, labels_img.shape) imgs_write = { config.RegNames.IMG_HEAT_MAP.value: sitk_io.replace_sitk_with_numpy(labels_img_sitk, heat_map)} heat_colocs = None if matches: # create heat maps for match-based colocalization combos heat_colocs = [] for chl_combo, chl_matches in matches.items(): print("Generating match-based colocalization heat map " "for channel combo:", chl_combo) # use blobs in first channel of each channel pair for simplicity blobs_chl = chl_matches.get_blobs(1) heat_colocs.append(make_heat_map()) elif blobs.colocalizations is not None: # create heat map for each intensity-based colocalization combo # as a separate channel in output image blob_chls = range(blobs.colocalizations.shape[1]) blob_chls_len = len(blob_chls) if blob_chls_len > 1: # get all channel combos that include given channels combos = [] chls = blob_chls if channel is None else channel for r in range(2, blob_chls_len + 1): combos.extend( [tuple(c) for c in itertools.combinations(blob_chls, r) if all([h in c for h in chls])]) heat_colocs = [] for combo in combos: print("Generating intensity-based colocalization heat map " "for channel combo:", combo) blobs_chl = blobs.blobs[np.all(np.equal( blobs.colocalizations[:, combo], 1), axis=1)] heat_colocs.append(make_heat_map()) if heat_colocs is not None: # combine heat maps into single image heat_colocs = np.stack(heat_colocs, axis=3) imgs_write[config.RegNames.IMG_HEAT_COLOC.value] = \ sitk_io.replace_sitk_with_numpy( labels_img_sitk, heat_colocs) # write images to file sitk_io.write_reg_images(imgs_write, mod_path) return heat_map, img_path