def _in_place_apply(apply_func: Callable[..., Union[xr.DataArray, np.ndarray]], data: np.ndarray, clip_method: Union[str, Clip], **kwargs) -> None: result = apply_func(data, **kwargs) if clip_method == Clip.CLIP: data[:] = preserve_float_range(result, rescale=False) elif clip_method == Clip.SCALE_BY_CHUNK: data[:] = preserve_float_range(result, rescale=True) else: data[:] = result
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 _high_pass(image: Union[xr.DataArray, np.ndarray], size: Number, rescale: bool = False) -> np.ndarray: """ Applies a mean high pass filter to an image Parameters ---------- image : Union[xr.DataArray, numpy.ndarray] 2-d or 3-d image data size : Union[Number, Tuple[Number]] width of the kernel rescale : bool If true scales data by max value, if false clips max values to one Returns ------- np.ndarray[np.float32]: Filtered image, same shape as input """ blurred: np.ndarray = uniform_filter(image, size) filtered: np.ndarray = image - blurred filtered = preserve_float_range(filtered, rescale) return filtered
def _low_pass(image: Union[xr.DataArray, np.ndarray], sigma: Union[Number, Tuple[Number]], rescale: bool = False) -> np.ndarray: """ Apply a Gaussian blur operation over a multi-dimensional image. Parameters ---------- image : Union[xr.DataArray, np.ndarray] 2-d or 3-d image data sigma : Union[Number, Tuple[Number]] Standard deviation of the Gaussian kernel that will be applied. If a float, an isotropic kernel will be assumed, otherwise the dimensions of the kernel give (z, y, x) rescale : bool If true scales data by max value, if false clips max values to one (default False) Returns ------- np.ndarray : Blurred data in same shape as input image """ filtered = gaussian(image, sigma=sigma, output=None, cval=0, multichannel=False, preserve_range=True, truncate=4.0) filtered = preserve_float_range(filtered, rescale) return filtered
def _high_pass(image: Union[xr.DataArray, np.ndarray], sigma: Union[Number, Tuple[Number]], rescale: bool = False) -> Union[xr.DataArray, np.ndarray]: """ Applies a gaussian high pass filter to an image Parameters ---------- image : Union[xr.DataArray, np.ndarray] 2-d or 3-d image data sigma : Union[Number, Tuple[Number]] Standard deviation of gaussian kernel rescale : bool If true scales data by max value, if false clips max values to one Returns ------- np.ndarray : filtered image of the same shape as the input image """ blurred = GaussianLowPass._low_pass(image, sigma) filtered = image - blurred filtered = preserve_float_range(filtered, rescale) return filtered
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. """ # 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) self.run(stack, in_place=True) return stack stack.xarray.values *= mult_array_aligned if self.clip_method == Clip.CLIP: stack.xarray.values = preserve_float_range(stack.xarray.values, rescale=False) else: stack.xarray.values = preserve_float_range(stack.xarray.values, rescale=True) return None
def synthetic_intensities(cls, codebook, num_z: int = 12, height: int = 50, width: int = 40, n_spots=10, mean_fluor_per_spot=200, mean_photons_per_fluor=50) -> "IntensityTable": """ Creates an IntensityTable with synthetic spots, that correspond to valid codes in a provided codebook. Parameters ---------- codebook : Codebook Starfish codebook object. num_z : int Number of z-planes to use when localizing spots. height : int y dimension of each synthetic plane. width : int x dimension of each synthetic plane. n_spots : int Number of spots to generate. mean_fluor_per_spot : int Mean number of fluorophores per spot. mean_photons_per_fluor : int Mean number of photons per fluorophore. Returns ------- IntensityTable """ # TODO nsofroniew: right now there is no jitter on x-y positions of the spots z = np.random.randint(0, num_z, size=n_spots) y = np.random.randint(0, height, size=n_spots) x = np.random.randint(0, width, size=n_spots) r = np.empty(n_spots) r.fill( np.nan) # radius is a function of the point-spread gaussian size spot_attributes = SpotAttributes( pd.DataFrame({ Axes.ZPLANE.value: z, Axes.Y.value: y, Axes.X.value: x, Features.SPOT_RADIUS: r })) # empty data tensor data = np.zeros(shape=(n_spots, *codebook.shape[1:])) targets = np.random.choice(codebook.coords[Features.TARGET], size=n_spots, replace=True) expected_bright_locations = np.where(codebook.loc[targets]) # create a binary matrix where "on" spots are 1 data[expected_bright_locations] = 1 # add physical properties of fluorescence data *= np.random.poisson(mean_photons_per_fluor, size=data.shape) data *= np.random.poisson(mean_fluor_per_spot, size=data.shape) # convert data to float for consistency with starfish data = preserve_float_range(data) assert 0 < data.max() <= 1 intensities = cls.from_spot_data(data, spot_attributes, np.arange(data.shape[1]), np.arange(data.shape[2])) intensities[Features.TARGET] = (Features.AXIS, targets) return intensities
def apply(self, func: Callable, group_by: Set[Axes] = None, in_place=False, verbose: bool = False, n_processes: Optional[int] = None, clip_method: Union[str, Clip] = Clip.CLIP, **kwargs) -> "ImageStack": """Split the image along a set of axes and apply a function across all the components. This function should yield data of the same dimensionality as the input components. These resulting components are then constituted into an ImageStack and returned. Parameters ---------- func : Callable Function to apply. must expect a first argument which is a 2d or 3d numpy array and return an array of the same shape. group_by : Set[Axes] Axes to split the data along. `ex. splitting a 2D array (axes: X, Y; size: 3, 4) by X results in 3 arrays of size 4. (default {Axes.ROUND, Axes.CH, Axes.ZPLANE})` in_place : bool If True, function is executed in place. If n_proc is not 1, the tile or volume will be copied once during execution. If false, a new ImageStack object will be produced. (Default False) verbose : bool If True, report on the percentage completed (default = False) during processing n_processes : Optional[int] The number of processes to use for apply. If None, uses the output of os.cpu_count() (default = None). kwargs : dict Additional arguments to pass to func clip_method : Union[str, :py:class:`~starfish.types.Clip`] - Clip.CLIP (default): Controls the way that data are scaled to retain skimage dtype requirements that float data fall in [0, 1]. - Clip.CLIP: data above 1 are set to 1, and below 0 are set to 0 - Clip.SCALE_BY_IMAGE: data above 1 are scaled by the maximum value, with the maximum value calculated over the entire ImageStack - Clip.SCALE_BY_CHUNK: data above 1 are scaled by the maximum value, with the maximum value calculated over each slice, where slice shapes are determined by the group_by parameters Returns ------- ImageStack : If inplace is False, return a new ImageStack, otherwise return a reference to the original stack with data modified by application of func Raises ------ TypeError : If no Clip method given. """ # default grouping is by (x, y) tile if group_by is None: group_by = {Axes.ROUND, Axes.CH, Axes.ZPLANE} if not isinstance(clip_method, (str, Clip)): raise TypeError( "must pass a Clip method. See starfish.types.Clip for valid options" ) if not in_place: # create a copy of the ImageStack, call apply on that stack with in_place=True image_stack = deepcopy(self) return image_stack.apply(func=func, group_by=group_by, in_place=True, verbose=verbose, n_processes=n_processes, clip_method=clip_method, **kwargs) # wrapper adds a target `data` parameter where the results from func will be stored # data are clipped or scaled by chunk using preserve_float_range if clip_method != 2 bound_func = partial(ImageStack._in_place_apply, func, clip_method=clip_method) # execute the processing workflow self.transform(func=bound_func, group_by=group_by, verbose=verbose, n_processes=n_processes, **kwargs) # scale based on values of whole image if clip_method == Clip.SCALE_BY_IMAGE: self._data.data.values = preserve_float_range( self._data.data.values, rescale=True) return self