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 _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 _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 Returns ------- np.ndarray : Blurred data in same shape as input image, converted to np.float32 dtype. """ 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 run(self, stack: ImageStack, in_place: bool = False, verbose=None, n_processes=None) -> 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 : None Not used. Elementwise multiply carries out a single vectorized multiplication that cannot provide a status bar. Included for consistency with Filter API. n_processes : None Not used. Elementwise multiplication scales slowly with additional processes due to the efficiency of vectorization on a single process. Included for consistency with Filter API. All computation happens on the main process. 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( 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. """ # 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(self, image: ImageStack, in_place: bool = False) -> Optional[ImageStack]: """Register an ImageStack against a reference image. Parameters ---------- image : ImageStack The stack to be registered in_place : bool If false, return a new registered stack. Else, register in-place (default False) Returns ------- """ if not in_place: image = deepcopy(image) # TODO: (ambrosejcarr) is this the appropriate way of dealing with Z in registration? mp = image.max_proj(Axes.CH, Axes.ZPLANE) mp_numpy = mp._squeezed_numpy(Axes.CH, Axes.ZPLANE) reference_image_mp = self.reference_stack.max_proj( Axes.ROUND, Axes.CH, Axes.ZPLANE) reference_image_numpy = reference_image_mp._squeezed_numpy( Axes.ROUND, Axes.CH, Axes.ZPLANE) for r in image.axis_labels(Axes.ROUND): # compute shift between maximum projection (across channels) and dots, for each round # TODO: make the max projection array ignorant of axes ordering. shift, error = compute_shift(mp_numpy[r, :, :], reference_image_numpy, self.upsampling) print(f"For round: {r}, Shift: {shift}, Error: {error}") for c in image.axis_labels(Axes.CH): for z in image.axis_labels(Axes.ZPLANE): # apply shift to all zplanes, channels, and imaging rounds selector = {Axes.ROUND: r, Axes.CH: c, Axes.ZPLANE: z} data, axes = image.get_slice(selector=selector) assert len(axes) == 0 result = shift_im(data, shift) result = preserve_float_range(result) image.set_slice(selector=selector, data=result) if not in_place: return image 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": """Create an IntensityTable containing synthetic spots with random locations Parameters ---------- codebook : Codebook starfish codebook object num_z : number of z-planes to use when localizing spots height : y dimension of each synthetic plane width : x dimension of each synthetic plane n_spots : number of spots to generate mean_fluor_per_spot : mean number of fluorophores per spot mean_photons_per_fluor : 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) 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. For instance, 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 (Default False) 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. 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, Clip] (Default Clip.CLIP) 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 """ # 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 = preserve_float_range(self._data, rescale=True) return self