def __init__(self, primary_images: ImageStack, nuclei: ImageStack) -> None: """Implements watershed segmentation of cells seeded from a nuclei image Algorithm is seeded by a nuclei image. Binary segmentation mask is computed from a maximum projection of spots across C and R, which is subsequently thresholded. Parameters ---------- primary_images : ImageStack primary hybridization images nuclei : ImageStack nuclei image """ # create a 'stain' for segmentation mp = primary_images.reduce({Axes.CH, Axes.ZPLANE}, func="max") self.stain = mp.reduce({Axes.ROUND}, func="mean", level_method=Levels.SCALE_BY_IMAGE) self.nuclei_mp_scaled = nuclei.reduce( {Axes.ROUND, Axes.CH, Axes.ZPLANE}, func="max", level_method=Levels.SCALE_BY_IMAGE, ) self.markers: Optional[BinaryMaskCollection] = None self.num_cells: Optional[int] = None self.mask: Optional[BinaryMaskCollection] = None self.segmented: Optional[BinaryMaskCollection] = None
def run( self, stack: ImageStack, in_place: bool = False, verbose=False, n_processes: Optional[int] = None, *args, ) -> Optional[ImageStack]: """Perform filtering of an image stack Parameters ---------- stack : ImageStack Stack to be filtered. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on the percentage completed during processing (default = False) n_processes : Optional[int]: None Not implemented. Number of processes to use when applying filter. Returns ------- ImageStack : If in-place is False, return the results of filter as a new stack. Otherwise return the original stack. """ # The default is False, so even if code requests True require config to be True as well verbose = verbose and StarfishConfig().verbose channels_per_round = stack.xarray.groupby(Axes.ROUND.value) channels_per_round = tqdm(channels_per_round) if verbose else channels_per_round if not in_place: new_stack = deepcopy(stack) self.run(new_stack, in_place=True) return new_stack # compute channel magnitude mask for r, dat in channels_per_round: # nervous about how xarray orders dimensions so i put this here explicitly .... dat = dat.transpose(Axes.CH.value, Axes.ZPLANE.value, Axes.Y.value, Axes.X.value ) # ... to account for this line taking the norm across axis 0, or the channel axis ch_magnitude = np.linalg.norm(dat, ord=2, axis=0) magnitude_mask = ch_magnitude >= self.thresh # apply mask and optionally, normalize by channel magnitude for c in stack.axis_labels(Axes.CH): ind = {Axes.ROUND.value: r, Axes.CH.value: c} stack._data[ind] = stack._data[ind] * magnitude_mask if self.normalize: stack._data[ind] = np.divide(stack._data[ind], ch_magnitude, where=magnitude_mask ) return None
def run(self, stack: ImageStack, transforms_list: TransformsList, in_place: bool = False, verbose: bool = False, *args, **kwargs) -> Optional[ImageStack]: if not in_place: # create a copy of the ImageStack, call apply on that stack with in_place=True image_stack = deepcopy(stack) self.run(image_stack, transforms_list, in_place=True, **kwargs) return image_stack if verbose and StarfishConfig().verbose: transforms_list.transforms = tqdm(transforms_list.transforms) all_axes = {Axes.ROUND, Axes.CH, Axes.ZPLANE} for selector, _, transformation_object in transforms_list.transforms: other_axes = all_axes - set(selector.keys()) # iterate through remaining axes for axes in stack._iter_axes(other_axes): # combine all axes data to select one tile selector.update(axes) # type: ignore selected_image, _ = stack.get_slice(selector) warped_image = warp(selected_image, transformation_object, **kwargs).astype(np.float32) stack.set_slice(selector, warped_image) return None
def measure_spot_intensities( data_image: ImageStack, spot_attributes: SpotAttributes, measurement_function: Callable[[Sequence], Number], radius_is_gyration: bool = False, ) -> IntensityTable: """given spots found from a reference image, find those spots across a data_image Parameters ---------- data_image : ImageStack ImageStack containing multiple volumes for which spots' intensities must be calculated spot_attributes : pd.Dataframe Locations and radii of spots measurement_function : Callable[[Sequence], Number]) Function to apply over the spot volumes to identify the intensity (e.g. max, mean, ...) radius_is_gyration : bool if True, indicates that the radius corresponds to radius of gyration, which is a function of spot intensity, but typically is a smaller unit than the sigma generated by blob_log. In this case, the spot's bounding box is rounded up instead of down when measuring intensity. (default False) Returns ------- IntensityTable : 3d tensor of (spot, channel, round) information for each coded spot """ # determine the shape of the intensity table ch_labels = data_image.axis_labels(Axes.CH) round_labels = data_image.axis_labels(Axes.ROUND) # construct the empty intensity table intensity_table = IntensityTable.zeros( spot_attributes=spot_attributes, ch_labels=ch_labels, round_labels=round_labels, ) # if no spots were detected, return the empty IntensityTable if intensity_table.sizes[Features.AXIS] == 0: return intensity_table # fill the intensity table indices = product(ch_labels, round_labels) for c, r in indices: image, _ = data_image.get_slice({Axes.CH: c, Axes.ROUND: r}) blob_intensities: pd.Series = measure_spot_intensity( image, spot_attributes, measurement_function, radius_is_gyration=radius_is_gyration) intensity_table.loc[dict(c=c, r=r)] = blob_intensities return intensity_table
def measure_intensities_at_spot_locations_across_imagestack( data_image: ImageStack, reference_spots: PerImageSliceSpotResults, measurement_function: Callable[[np.ndarray], Number], radius_is_gyration: bool = False) -> SpotFindingResults: """given spots found from a reference image, find those spots across a data_image Parameters ---------- data_image : ImageStack ImageStack containing multiple volumes for which spots' intensities must be calculated reference_spots : PerImageSliceSpotResults Spots found in a reference image measurement_function : Callable[[np.ndarray], Number]) Function to apply over the spot volumes to identify the intensity (e.g. max, mean, ...) radius_is_gyration : bool if True, indicates that the radius corresponds to radius of gyration, which is a function of spot intensity, but typically is a smaller unit than the sigma generated by blob_log. In this case, the spot's bounding box is rounded up instead of down when measuring intensity. (default False) Returns ------- SpotFindingResults : A Dict of tile indices and their corresponding measured SpotAttributes """ ch_labels = data_image.axis_labels(Axes.CH) round_labels = data_image.axis_labels(Axes.ROUND) spot_results = SpotFindingResults( imagestack_coords=data_image.xarray.coords, log=data_image.log) # measure spots in each tile indices = product(ch_labels, round_labels) for c, r in indices: tile_indices = {Axes.ROUND: r, Axes.CH: c} if reference_spots.spot_attrs.data.empty: # if no spots found don't measure spot_results[tile_indices] = reference_spots else: image, _ = data_image.get_slice({Axes.CH: c, Axes.ROUND: r}) blob_intensities: pd.Series = measure_intensities_at_spot_locations_in_image( image, reference_spots.spot_attrs, measurement_function, radius_is_gyration=radius_is_gyration) # copy reference spot positions and attributes tile_spots = SpotAttributes(reference_spots.spot_attrs.data.copy()) # fill in intensities tile_spots.data[Features.INTENSITY] = blob_intensities spot_results[tile_indices] = PerImageSliceSpotResults( spot_attrs=tile_spots, extras=None) return spot_results
def run(self, primary_images: ImageStack, nuclei: ImageStack, *args) -> BinaryMaskCollection: """Segments nuclei in 2-d using a nuclei ImageStack Primary images are used to expand the nuclear mask, but only in cases where there are densely detected points surrounding the nuclei. Parameters ---------- primary_images : ImageStack contains primary image data nuclei : ImageStack contains nuclei image data Returns ------- masks : BinaryMaskCollection binary masks segmenting each cell """ # create a 'stain' for segmentation mp = primary_images.reduce({Axes.CH, Axes.ZPLANE}, func="max") mp_numpy = mp._squeezed_numpy(Axes.CH, Axes.ZPLANE) stain = np.mean(mp_numpy, axis=0) stain = stain / stain.max() # TODO make these parameterizable or determine whether they are useful or not size_lim = (10, 10000) disk_size_markers = None disk_size_mask = None nuclei_mp = nuclei.reduce({Axes.ROUND, Axes.CH, Axes.ZPLANE}, func="max") nuclei__mp_numpy = nuclei_mp._squeezed_numpy(Axes.ROUND, Axes.CH, Axes.ZPLANE) self._segmentation_instance = _WatershedSegmenter( nuclei__mp_numpy, stain) label_image = self._segmentation_instance.segment( self.nuclei_threshold, self.input_threshold, size_lim, disk_size_markers, disk_size_mask, self.min_distance) # we max-projected and squeezed the Z-plane so label_image.ndim == 2 physical_ticks = { coord: nuclei.xarray.coords[coord.value].data for coord in (Coordinates.Y, Coordinates.X) } return BinaryMaskCollection.from_label_image(label_image, physical_ticks)
def imagestack_factory( fetched_tile_cls: Type[LocationAwareFetchedTile], round_labels: Sequence[int], ch_labels: Sequence[int], zplane_labels: Sequence[int], tile_height: int, tile_width: int, xrange: Tuple[Number, Number], yrange: Tuple[Number, Number], zrange: Tuple[Number, Number], crop_parameters: Optional[CropParameters] = None) -> ImageStack: """Given a type that implements the :py:class:`LocationAwareFetchedTile` contract, produce an imagestack with those tiles, and apply coordinates such that the 5D tensor has coordinates that range from `xrange[0]:xrange[1]`, `yrange[0]:yrange[1]`, `zrange[0]:zrange[1]`. Parameters ---------- fetched_tile_cls : Type[LocationAwareFetchedTile] The class of the FetchedTile. round_labels : Sequence[int] Labels for the rounds. ch_labels : Sequence[int] Labels for the channels. zplane_labels : Sequence[int] Labels for the zplanes. tile_height : int Height of each tile, in pixels. tile_width : int Width of each tile, in pixels. xrange : Tuple[Number, Number] The starting and ending x physical coordinates for the tile. yrange : Tuple[Number, Number] The starting and ending y physical coordinates for the tile. zrange : Tuple[Number, Number] The starting and ending z physical coordinates for the tile. crop_parameters : Optional[CropParameters] The crop parameters to apply during ImageStack construction. """ original_tile_fetcher = tile_fetcher_factory( fetched_tile_cls, True, round_labels, ch_labels, zplane_labels, tile_height, tile_width, ) modified_tile_fetcher = _apply_coords_range_fetcher( original_tile_fetcher, zplane_labels, xrange, yrange, zrange) collection = build_image( range(1), round_labels, ch_labels, zplane_labels, modified_tile_fetcher, ) tileset = list(collection.all_tilesets())[0][1] return ImageStack.from_tileset(tileset, crop_parameters)
def _find_spots( self, data_stack: ImageStack, verbose: bool = False, n_processes: Optional[int] = None ) -> Dict[Tuple[int, int], np.ndarray]: """Find spots in all (z, y, x) volumes of an ImageStack. Parameters ---------- data_stack : ImageStack Stack containing spots to find. Returns ------- Dict[Tuple[int, int], np.ndarray] Dictionary mapping (round, channel) pairs to a spot table generated by skimage blob_log or blob_dog. """ # find spots in each (r, c) volume transform_results = data_stack.transform( self._spot_finder, group_by=determine_axes_to_group_by(self.is_volume), n_processes=n_processes, ) # create output dictionary spot_results = {} for spot_calls, axes_dict in transform_results: r = axes_dict[Axes.ROUND] c = axes_dict[Axes.CH] spot_results[r, c] = spot_calls return spot_results
def run( self, stack: ImageStack, in_place: bool = False, verbose: bool = False, n_processes: Optional[int] = None, *args, ) -> ImageStack: """Perform filtering of an image stack Parameters ---------- stack : ImageStack Stack to be filtered. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on filtering progress (default = False) n_processes : Optional[int] Number of parallel processes to devote to calculating the filter Returns ------- ImageStack : If in-place is False, return the results of filter as a new stack. Otherwise return the original stack. """ return stack.max_proj(*tuple(Axes(dim) for dim in self.dims))
def run( self, stack: ImageStack, *args, ) -> Optional[ImageStack]: """Map from input to output by applying a specified function to the input. Parameters ---------- stack : ImageStack Stack to be filtered. Returns ------- Optional[ImageStack] : If in-place is False, return the results of filter as a new stack. Otherwise return None """ # Apply the reducing function return stack.apply(self.func, *self.func_args, group_by=self.group_by, in_place=self.in_place, clip_method=self.clip_method, **self.func_kwargs)
def import_ilastik_probabilities( cls, path_to_h5_file: Union[str, Path], dataset_name: str = "exported_data") -> ImageStack: """ Import cell probabilities provided by ilastik as an ImageStack. Parameters ---------- path_to_h5_file : Union[str, Path] Path to the .h5 file outputted by ilastik dataset_name : str Name of dataset in ilastik Export Image Settings Returns ------- ImageStack : A new ImageStack created from the cell probabilities provided by ilastik. """ h5 = h5py.File(path_to_h5_file) probability_images = h5[dataset_name][:] h5.close() cell_probabilities, _ = probability_images[:, :, 0], probability_images[:, :, 1] label_array = ndi.label(cell_probabilities)[0] label_array = label_array[np.newaxis, np.newaxis, np.newaxis, ...] return ImageStack.from_numpy(label_array)
def get_image(self, item: str, aligned_group: int = 0, x_slice: Optional[Union[int, slice]] = None, y_slice: Optional[Union[int, slice]] = None, ) -> ImageStack: """ Load into memory the Imagestack representation of an aligned image group. If crop parameters provided, first crop the TileSet. Parameters ---------- item: str The name of the tileset ex. 'primary' or 'nuclei' aligned_group: int The aligned subgroup, default 0 x_slice: int or slice The cropping parameters for the x axis y_slice: The cropping parameters for the y axis Returns ------- ImageStack The instantiated image stack """ crop_params = copy.copy((self.aligned_coordinate_groups[item][aligned_group])) crop_params._x_slice = x_slice crop_params._y_slice = y_slice return ImageStack.from_tileset(self._images[item], crop_parameters=crop_params)
def intensity_histogram(image_stack: ImageStack, sel: Optional[Mapping[Axes, Union[int, tuple]]] = None, ax=None, title: Optional[str] = None, **kwargs) -> None: """ Plot the 1-d intensity histogram of linearized image_stack. Parameters ---------- image_stack : ImageStack imagestack containing intensities sel : Optional[Mapping[Axes, Union[int, tuple]]] Optional, Selector to pass ImageStack.sel that will restrict the histogram construction to the specified subset of image_stack. ax : Axes to plot on. If not passed, defaults to the current axes. title : Optional[str] Title to assign the Axes being plotted on. kwargs : additional keyword arguments to pass to plt.hist """ if ax is None: ax = plt.gca() if sel is not None: image_stack = image_stack.sel(sel) if title is not None: ax.set_title(title) data: np.ndarray = np.ravel(image_stack.xarray) ax.hist(data, **kwargs)
def test_match_histograms(): linear_gradient = np.linspace(0, 0.5, 2000, dtype=np.float32) image = linear_gradient.reshape(2, 4, 5, 5, 10) # linear_gradient = np.linspace(0, 1, 10)[::-1] # grad = np.repeat(linear_gradient[np.newaxis, :], 10, axis=0) # image2 = np.tile(grad, (1, 2, 2, 10, 10)) # because of how the image was structured, every volume should be the same after # quantile normalization stack = ImageStack.from_numpy(image) mh = MatchHistograms({Axes.CH, Axes.ROUND}) results = mh.run(stack) assert len(np.unique(results.xarray.sum(("x", "y", "z")))) == 1 # because here we are allowing variation to persist across rounds, each # round within each channel should be different mh = MatchHistograms({Axes.CH}) results2 = mh.run(stack) assert len(np.unique(results2.xarray.sum(("x", "y", "z")))) == 2 # same as above, but verifying this functions for a different data shape (2 rounds, 4 channels) mh = MatchHistograms({Axes.ROUND}) results2 = mh.run(stack) assert len(np.unique(results2.xarray.sum(("x", "y", "z")))) == 4
def pixel_intensities_to_imagestack( intensities: IntensityTable, image_shape: Tuple[int, int, int]) -> ImageStack: """Re-create the pixel intensities from an IntensityTable Parameters ---------- intensities : IntensityTable intensities to transform into an ImageStack image_shape : Tuple[int, int, int] the dimensions of z, y, and x for the original image that the intensity table was generated from Returns ------- ImageStack : ImageStack containing Intensity information """ # reverses the process used to produce the intensity table in to_pixel_intensities data = intensities.values.reshape([ *image_shape, intensities.sizes[Axes.CH], intensities.sizes[Axes.ROUND] ]) data = data.transpose(4, 3, 0, 1, 2) return ImageStack.from_numpy(data)
def unique_tiles_imagestack( round_labels: Sequence[int], ch_labels: Sequence[int], z_labels: Sequence[int], tile_height: int, tile_width: int, crop_parameters: Optional[CropParameters] = None) -> ImageStack: """Build an imagestack with unique values per tile. """ collection = build_image( range(1), round_labels, ch_labels, z_labels, tile_fetcher_factory( UniqueTiles, True, len(round_labels), len(ch_labels), len(z_labels), tile_height, tile_width, ), ) tileset = list(collection.all_tilesets())[0][1] return ImageStack.from_tileset(tileset, crop_parameters)
def run(self, stack: ImageStack, verbose: bool = False, *args) -> TransformsList: """ Iterate over the given axes of an ImageStack and learn the translation transform based off the reference_stack passed into :py:class:`Translation`'s constructor. Only supports 2d data. Parameters ---------- stack : ImageStack Stack to calculate the transforms on. verbose : bool if True, report on transformation progress (default = False) Returns ------- List[Tuple[Mapping[Axes, int], SimilarityTransform]] : A list of tuples containing axes of the Imagestack and associated transform to apply. """ transforms = TransformsList() reference_image = np.squeeze(self.reference_stack.xarray) for a in stack.axis_labels(self.axes): target_image = np.squeeze(stack.sel({self.axes: a}).xarray) if len(target_image.shape) != 2: raise ValueError( f"Only axes: {self.axes.value} can have a length > 1, " f"please use the MaxProj filter.") shift, error, phasediff = phase_cross_correlation( reference_image=target_image.data, moving_image=reference_image.data, upsample_factor=self.upsampling) if verbose: print(f"For {self.axes}: {a}, Shift: {shift}, Error: {error}") selectors = {self.axes: a} # reverse shift because SimilarityTransform stores in y,x format shift = shift[::-1] transforms.append(selectors, TransformType.SIMILARITY, SimilarityTransform(translation=shift)) return transforms
def run( self, stack: ImageStack, in_place: bool = False, verbose: bool = False, n_processes: Optional[int] = None, *args, ) -> ImageStack: """Perform filtering of an image stack Parameters ---------- stack : ImageStack Stack to be filtered. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on filtering progress (default = False) n_processes : Optional[int] Number of parallel processes to devote to applying the filter. If None, defaults to the result of os.cpu_count(). (default None) Returns ------- ImageStack : If in-place is False, return the results of filter as a new stack. Otherwise return the original stack. """ # Align the axes of the multipliers with ImageStack mult_array_aligned: np.ndarray = self.mult_array.transpose( *stack.xarray.dims).values if not in_place: stack = deepcopy(stack) # stack._data contains the xarray stack._data *= mult_array_aligned if self.clip_method == Clip.CLIP: stack._data = preserve_float_range(stack._data, rescale=False) else: stack._data = preserve_float_range(stack._data, rescale=True) return stack
def run( # type: ignore self, image: ImageStack, markers: Optional[BinaryMaskCollection] = None, mask: Optional[BinaryMaskCollection] = None, *args, **kwargs ) -> BinaryMaskCollection: """Runs scikit-image's watershed """ if image.num_rounds != 1: raise ValueError( f"{WatershedSegment.__name__} given an image with more than one round " f"{image.num_rounds}") if image.num_chs != 1: raise ValueError( f"{WatershedSegment.__name__} given an image with more than one channel " f"{image.num_chs}") if mask is not None and len(mask) != 1: raise ValueError( f"{WatershedSegment.__name__} given a mask given a mask with more than one " f"channel {image.num_chs}") if len(args) != 0 or len(kwargs) != 0: raise ValueError( f"{WatershedSegment.__name__}'s run method should not have additional arguments.") image_npy = 1 - image._squeezed_numpy(Axes.ROUND, Axes.CH) markers_npy = np.asarray(markers.to_label_image().xarray) if markers is not None else None mask_npy = mask.uncropped_mask(0) if mask is not None else None watershed_output = watershed( image_npy, markers=markers_npy, mask=mask_npy, **self.watershed_kwargs ) pixel_ticks: Mapping[Axes, ArrayLike[int]] = { Axes(axis): axis_data for axis, axis_data in image.xarray.coords.items() if axis in _get_axes_names(3)[0] } physical_ticks: Mapping[Coordinates, ArrayLike[Number]] = { Coordinates(coord): coord_data for coord, coord_data in image.xarray.coords.items() if coord in _get_axes_names(3)[1] } return BinaryMaskCollection.from_label_array_and_ticks( watershed_output, pixel_ticks, physical_ticks, image.log, # FIXME: (ttung) this should somehow include the provenance of markers and # mask. )
def test_reshaping_between_stack_and_intensities(): """ transform an pixels of an ImageStack into an IntensityTable and back again, then verify that the created Imagestack is the same as the original """ np.random.seed(777) image = ImageStack.from_numpy(np.random.rand(1, 2, 3, 4, 5).astype(np.float32)) pixel_intensities = IntensityTable.from_image_stack(image, 0, 0, 0) image_shape = (image.shape['z'], image.shape['y'], image.shape['x']) image_from_pixels = pixel_intensities_to_imagestack(pixel_intensities, image_shape) assert np.array_equal(image.xarray, image_from_pixels.xarray)
def run( self, image_stack: ImageStack, reference_image: Optional[ImageStack] = None, n_processes: Optional[int] = None, *args, **kwargs ) -> SpotFindingResults: """ Find spots in the given ImageStack using a local maxima finding algorithm. If a reference image is provided the spots will be detected there then measured across all rounds and channels in the corresponding ImageStack. If a reference_image is not provided spots will be detected _independently_ in each channel. This assumes a non-multiplex imaging experiment, as only one (ch, round) will be measured for each spot. Parameters ---------- image_stack : ImageStack ImageStack where we find the spots in. reference_image : Optional[ImageStack] (Optional) a reference image. If provided, spots will be found in this image, and then the locations that correspond to these spots will be measured across each channel. n_processes : Optional[int] = None, Number of processes to devote to spot finding. """ spot_finding_method = partial(self.image_to_spots, **self.kwargs) if reference_image: shape = reference_image.shape assert shape[Axes.ROUND] == 1 assert shape[Axes.CH] == 1 spot_attributes_lists = reference_image.transform( func=spot_finding_method, group_by=determine_axes_to_group_by(self.is_volume), n_processes=n_processes ) spot_attributes_lists = combine_spot_attributes_by_round_channel(spot_attributes_lists) assert len(spot_attributes_lists) == 1 results = spot_finding_utils.measure_intensities_at_spot_locations_across_imagestack( data_image=image_stack, reference_spots=spot_attributes_lists[0][0], measurement_function=self.measurement_function) else: spot_attributes_lists = image_stack.transform( func=spot_finding_method, group_by=determine_axes_to_group_by(self.is_volume), n_processes=n_processes ) spot_attributes_lists = combine_spot_attributes_by_round_channel(spot_attributes_lists) results = SpotFindingResults(imagestack_coords=image_stack.xarray.coords, log=image_stack.log, spot_attributes_list=spot_attributes_lists) return results
def run( self, stack: ImageStack, *args, ) -> ImageStack: """Performs the dimension reduction with the specifed function Parameters ---------- stack : ImageStack Stack to be filtered. Returns ------- ImageStack : Return the results of filter as a new stack. """ # Apply the reducing function reduced = stack.xarray.reduce(self.func.resolve(), dim=[dim.value for dim in self.dims], **self.kwargs) # Add the reduced dims back and align with the original stack reduced = reduced.expand_dims(tuple(dim.value for dim in self.dims)) reduced = reduced.transpose(*stack.xarray.dims) if self.level_method == Levels.CLIP: reduced = levels(reduced) elif self.level_method in (Levels.SCALE_BY_CHUNK, Levels.SCALE_BY_IMAGE): reduced = levels(reduced, rescale=True) elif self.level_method in (Levels.SCALE_SATURATED_BY_CHUNK, Levels.SCALE_SATURATED_BY_IMAGE): reduced = levels(reduced, rescale_saturated=True) # Update the physical coordinates physical_coords: MutableMapping[Coordinates, ArrayLike[Number]] = {} for axis, coord in ((Axes.X, Coordinates.X), (Axes.Y, Coordinates.Y), (Axes.ZPLANE, Coordinates.Z)): if axis in self.dims: # this axis was projected out of existence. assert coord.value not in reduced.coords physical_coords[coord] = [ np.average(stack._data.coords[coord.value]) ] else: physical_coords[coord] = reduced.coords[coord.value] reduced_stack = ImageStack.from_numpy(reduced.values, coordinates=physical_coords) return reduced_stack
def run( self, stack: ImageStack, *args, ) -> ImageStack: """Performs the dimension reduction with the specifed function Parameters ---------- stack : ImageStack Stack to be filtered. Returns ------- ImageStack : Return the results of filter as a new stack. """ # Apply the reducing function reduced = stack.xarray.reduce(self.func, dim=[dim.value for dim in self.dims], **self.kwargs) # Add the reduced dims back and align with the original stack reduced = reduced.expand_dims(tuple(dim.value for dim in self.dims)) reduced = reduced.transpose(*stack.xarray.dims) if self.clip_method == Clip.CLIP: reduced = preserve_float_range(reduced, rescale=False) else: reduced = preserve_float_range(reduced, rescale=True) # Update the physical coordinates physical_coords: MutableMapping[Coordinates, Sequence[Number]] = {} for axis, coord in ((Axes.X, Coordinates.X), (Axes.Y, Coordinates.Y), (Axes.ZPLANE, Coordinates.Z)): if axis in self.dims: # this axis was projected out of existence. assert coord.value not in reduced.coords physical_coords[coord] = [ np.average(stack._data.coords[coord.value]) ] else: physical_coords[coord] = cast(Sequence[Number], reduced.coords[coord.value]) reduced_stack = ImageStack.from_numpy(reduced.values, coordinates=physical_coords) return reduced_stack
def imshow_plane( image_stack: ImageStack, sel: Optional[Mapping[Axes, Union[int, tuple]]] = None, ax=None, title: Optional[str] = None, **kwargs, ) -> None: """ Plot a single plane of an ImageStack. If passed a selection function (sel), the stack will be subset using :py:meth:`ImageStack.sel`. If ax is passed, the function will be plotted in the provided axis. Additional kwargs are passed to :py:func:`plt.imshow` Parameters ---------- image_stack : ImageStack imagestack from which to extract a 2-d image for plotting sel : Optional[Mapping[Axes, Union[int, tuple]]] Optional, but only if image_stack is already of shape (1, 1, 1, y, x). Selector to pass ImageStack.sel, Selects the (y, x) plane to be plotted. ax : Axes to plot on. If not passed, defaults to the current axes. title : Optional[str] Title to assign the Axes being plotted on. kwargs : additional keyword arguments to pass to plt.imshow """ if ax is None: ax = plt.gca() if sel is not None: image_stack = image_stack.sel(sel) if title is not None: ax.set_title(title) # verify imagestack is 2d before trying to plot it data: xr.DataArray = image_stack.xarray.squeeze() if set(data.sizes.keys()).intersection({Axes.CH, Axes.ROUND, Axes.ZPLANE}): raise ValueError( f"image_stack must be a 2d (x, y) array, not {data.sizes}") # set imshow default kwargs if "cmap" not in kwargs: kwargs["cmap"] = plt.cm.gray ax.imshow(data, **kwargs) ax.axis("off")
def run( self, image_stack: ImageStack, reference_image: Optional[ImageStack] = None, n_processes: Optional[int] = None, *args, ) -> SpotFindingResults: """ Find spots in the given ImageStack using a gaussian blob finding algorithm. If a reference image is provided the spots will be detected there then measured across all rounds and channels in the corresponding ImageStack. If a reference_image is not provided spots will be detected _independently_ in each channel. This assumes a non-multiplex imaging experiment, as only one (ch, round) will be measured for each spot. Parameters ---------- image_stack : ImageStack ImageStack where we find the spots in. reference_image : Optional[ImageStack] (Optional) a reference image. If provided, spots will be found in this image, and then the locations that correspond to these spots will be measured across each channel. n_processes : Optional[int] = None, Number of processes to devote to spot finding. """ spot_finding_method = partial(self.image_to_spots, *args) if reference_image: data_image = reference_image._squeezed_numpy( *{Axes.ROUND, Axes.CH}) if self.detector_method is blob_doh and data_image.ndim > 2: raise ValueError("blob_doh only support 2d images") reference_spots = spot_finding_method(data_image) results = spot_finding_utils.measure_intensities_at_spot_locations_across_imagestack( data_image=image_stack, reference_spots=reference_spots, measurement_function=self.measurement_function) else: if self.detector_method is blob_doh and self.is_volume: raise ValueError("blob_doh only support 2d images") spot_attributes_list = image_stack.transform( func=spot_finding_method, group_by=determine_axes_to_group_by(self.is_volume), n_processes=n_processes) results = SpotFindingResults( imagestack_coords=image_stack.xarray.coords, log=image_stack.log, spot_attributes_list=spot_attributes_list) return results
def run( self, stack: ImageStack, in_place: bool = False, verbose: bool = False, n_processes: Optional[int] = None, *args, ) -> Optional[ImageStack]: """Perform filtering of an image stack Parameters ---------- stack : ImageStack Stack to be filtered. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on filtering progress (default = False) n_processes : Optional[int] Number of parallel processes to devote to applying the filter. If None, defaults to the result of os.cpu_count(). (default None) Returns ------- ImageStack : If in-place is False, return the results of filter as a new stack. Otherwise return the original stack. """ bandpass_ = partial(self._bandpass, lshort=self.lshort, llong=self.llong, threshold=self.threshold, truncate=self.truncate) group_by = determine_axes_to_group_by(self.is_volume) result = stack.apply( bandpass_, group_by=group_by, in_place=in_place, n_processes=n_processes, level_method=self.level_method, verbose=verbose, ) return result
def run( self, stack: ImageStack, in_place: bool = False, verbose=False, n_processes: Optional[int] = None, *args, ) -> Optional[ImageStack]: """Perform filtering of an image stack Parameters ---------- stack : ImageStack Stack to be filtered. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on the percentage completed during processing (default = False) n_processes : Optional[int] Number of parallel processes to devote to applying the filter. If None, defaults to the result of os.cpu_count(). (default None) Returns ------- ImageStack : If in-place is False, return the results of filter as a new stack. Otherwise return the original stack. """ group_by = determine_axes_to_group_by(self.is_volume) func = partial( self._richardson_lucy_deconv, iterations=self.num_iter, psf=self.psf ) result = stack.apply( func, group_by=group_by, verbose=verbose, n_processes=n_processes, in_place=in_place, level_method=self.level_method, ) return result
def run(self, stack: ImageStack, in_place: bool = False, verbose: bool = False, n_processes: Optional[int] = None, *args) -> Optional[ImageStack]: """Perform filtering of an image stack Parameters ---------- stack : ImageStack Stack to be filtered. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on filtering progress (default = False) n_processes : Optional[int] Number of parallel processes to devote to applying the filter. If None, defaults to the result of os.cpu_count(). (default None) Returns ------- ImageStack : If in-place is False, return the results of filter as a new stack. Otherwise return the original stack. """ group_by = determine_axes_to_group_by(self.is_volume) clip_value_to_zero = partial( self._clip_value_to_zero, v_min=self.v_min, v_max=self.v_max, ) result = stack.apply( clip_value_to_zero, group_by=group_by, verbose=verbose, in_place=in_place, n_processes=n_processes, level_method=self.level_method, ) return result
def run( self, stack: ImageStack, verbose: bool = False, *args, ) -> Optional[ImageStack]: """Perform filtering of an image stack Parameters ---------- stack : ImageStack Stack to be filtered. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on filtering progress (default = False) n_processes : Optional[int] Number of parallel processes to devote to calculating the filter Returns ------- ImageStack : The max projection of an image across one or more axis. """ max_projection = stack.xarray.max([dim.value for dim in self.dims]) max_projection = max_projection.expand_dims(tuple(dim.value for dim in self.dims)) max_projection = max_projection.transpose(*stack.xarray.dims) physical_coords: MutableMapping[Coordinates, Sequence[Number]] = {} for axis, coord in ( (Axes.X, Coordinates.X), (Axes.Y, Coordinates.Y), (Axes.ZPLANE, Coordinates.Z)): if axis in self.dims: # this axis was projected out of existence. assert coord.value not in max_projection.coords physical_coords[coord] = [np.average(stack.xarray.coords[coord.value])] else: physical_coords[coord] = max_projection.coords[coord.value] max_proj_stack = ImageStack.from_numpy(max_projection.values, coordinates=physical_coords) return max_proj_stack
def run( self, stack: ImageStack, in_place: bool = False, verbose: bool = False, n_processes: Optional[int] = None, *args, ) -> ImageStack: """Perform filtering of an image stack Parameters ---------- stack : ImageStack Stack to be filtered. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on filtering progress (default = False) n_processes : Optional[int] Number of parallel processes to devote to applying the filter. If None, defaults to the result of os.cpu_count(). (default None) Returns ------- ImageStack : If in-place is False, return the results of filter as a new stack. Otherwise return the original stack. """ if verbose: print("Calculating reference distribution...") reference_image = self._compute_reference_distribution(stack) apply_function = partial(self._match_histograms, reference=reference_image) result = stack.apply(apply_function, group_by=self.group_by, verbose=verbose, in_place=in_place, n_processes=n_processes) return result