def test_multiple_tiles_of_different_kind(): with pytest.raises(TypeError): ImageStack.synthetic_stack( NUM_ROUND, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_fetcher=CornerDifferentDtype(np.uint32, np.float32), )
def run( self, stack: ImageStack, in_place: bool=False, verbose=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 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._data.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) return self.run(new_stack, in_place=True) # 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 stack
def test_synthetic_spot_creation_raises_error_with_coords_too_small( synthetic_intensity_table): num_z = 0 height = 40 width = 50 with pytest.raises(ValueError): ImageStack.synthetic_spots(synthetic_intensity_table, num_z, height, width)
def _cli(ctx, primary_images, nuclei, output): print('Segmenting ...') ctx.obj = dict( component=Segmentation, output=output, primary_images=ImageStack.from_path_or_url(primary_images), nuclei=ImageStack.from_path_or_url(nuclei), )
def _cli(ctx, primary_images, nuclei, output): """define polygons for cell boundaries and assign spots""" print('Segmenting ...') ctx.obj = dict( component=Segmentation, output=output, primary_images=ImageStack.from_path_or_url(primary_images), nuclei=ImageStack.from_path_or_url(nuclei), )
def test_imagestack_export(tmpdir, format, count, recwarn): """ Save a synthetic stack to files and check the results """ stack = ImageStack.synthetic_stack() stack_json = tmpdir / "output.json" stack.export(str(stack_json), tile_format=format) files = list([x for x in tmpdir.listdir() if str(x).endswith(format.file_ext)]) assert ImageStack.from_path_or_url(str(stack_json)) assert count == len(files) with open(files[0], "rb") as fh: format.reader_func(fh)
def test_fov_order(): data = SyntheticData() codebook = data.codebook() stack1 = ImageStack.synthetic_stack() stack2 = ImageStack.synthetic_stack() fovs = [ FieldOfView("stack2", {"primary": stack2}), FieldOfView("stack1", {"primary": stack1}) ] extras = {"synthetic": True} experiment = Experiment(fovs, codebook, extras) assert "stack1" == experiment.fov().name assert ["stack1", "stack2"] == [x.name for x in experiment.fovs()]
def test_from_numpy_array_raises_error_when_incorrect_dims_passed(): array = np.ones((2, 2), dtype=np.float32) # verify this method works with the correct shape image = ImageStack.from_numpy_array(array.reshape((1, 1, 1, 2, 2))) assert isinstance(image, ImageStack) with pytest.raises(ValueError): ImageStack.from_numpy_array(array.reshape((1, 1, 2, 2))) ImageStack.from_numpy_array(array.reshape((1, 2, 2))) ImageStack.from_numpy_array(array) ImageStack.from_numpy_array(array.reshape((1, 1, 1, 1, 2, 2)))
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(Indices.CH, Indices.Z) reference_image = self.reference_stack.max_proj( Indices.ROUND, Indices.CH, Indices.Z) for r in range(image.num_rounds): # 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[r, :, :], reference_image, self.upsampling) print(f"For round: {r}, Shift: {shift}, Error: {error}") for c in range(image.num_chs): for z in range(image.num_zlayers): # apply shift to all zlayers, channels, and imaging rounds indices = {Indices.ROUND: r, Indices.CH: c, Indices.Z: z} data, axes = image.get_slice(indices=indices) assert len(axes) == 0 result = shift_im(data, shift) result = preserve_float_range(result) image.set_slice(indices=indices, data=result) if not in_place: return image return None
def test_apply_3d(): """test that apply correctly applies a simple function across 3d volumes of a Stack""" stack = ImageStack.synthetic_stack() assert np.all(stack.xarray == 1) stack.apply(divide, in_place=True, value=4, group_by={Axes.ROUND, Axes.CH}) assert (stack.xarray == 0.25).all()
def test_spot_detection_with_reference_image( data_stack: ImageStack, spot_detector: SpotFinderAlgorithmBase, radius_is_gyration: bool, ): """ This testing method uses a reference image to identify spot locations. Thus, it should detect two spots, each with max intensity 7. Because the channels and rounds are aggregated, this method should recognize the 1-hot code used in the testing data, and see one channel "on" per round. Thus, the total intensity across all channels and round for each spot should be 14. """ reference_image = data_stack.max_proj(Indices.CH, Indices.ROUND) intensity_table = detect_spots( data_stack=data_stack, spot_finding_method=spot_detector.image_to_spots, reference_image=reference_image, measurement_function=np.max, radius_is_gyration=radius_is_gyration, ) assert intensity_table.shape == (2, 2, 2), "wrong number of spots detected" expected = [0.01587425, 0.01587425] assert np.allclose(intensity_table.sum((Indices.ROUND, Indices.CH)).values, expected), \ "wrong spot intensities detected"
def test_imagestack_export(tmpdir, format, count, recwarn): """ Save a synthetic stack to files and check the results """ stack_shape = OrderedDict([(Axes.ROUND, 3), (Axes.CH, 2), (Axes.ZPLANE, 1), (Axes.Y, 50), (Axes.X, 40)]) physical_coords = OrderedDict([ (PhysicalCoordinateTypes.X_MIN, X_COORDS[0]), (PhysicalCoordinateTypes.X_MAX, X_COORDS[1]), (PhysicalCoordinateTypes.Y_MIN, Y_COORDS[0]), (PhysicalCoordinateTypes.Y_MAX, Y_COORDS[1]), (PhysicalCoordinateTypes.Z_MIN, Z_COORDS[0]), (PhysicalCoordinateTypes.Z_MAX, Z_COORDS[1]) ]) stack = test_utils.imagestack_with_coords_factory(stack_shape, physical_coords) stack_json = tmpdir / "output.json" stack.export(str(stack_json), tile_format=format) files = list( [x for x in tmpdir.listdir() if str(x).endswith(format.file_ext)]) loaded_stack = ImageStack.from_path_or_url(str(stack_json)) verify_physical_coordinates( loaded_stack, X_COORDS, Y_COORDS, get_physical_coordinates_of_z_plane(Z_COORDS), ) assert count == len(files) with open(files[0], "rb") as fh: format.reader_func(fh)
def test_missing_extras(): """ If the extras are not present on some of the tiles, it should still work. """ class OnesTilesWithExtrasMostly(OnesTile): def __init__(self, fov, r, ch, z, extras: dict) -> None: super().__init__((10, 10)) self.fov = fov self._extras = extras @property def extras(self): if self.fov == 0: return None return self._extras tile_fetcher = tile_fetcher_factory(OnesTilesWithExtrasMostly, True, {'random_key': { 'hello': "world", }}) stack = ImageStack.synthetic_stack( num_round=NUM_ROUND, num_ch=NUM_CH, num_z=NUM_Z, tile_fetcher=tile_fetcher, ) table = stack.tile_metadata assert len(table) == NUM_ROUND * NUM_CH * NUM_Z
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 test_coordinates(): """Set up an ImageStack with tiles that are offset based on round. Verify that the coordinates retrieved match. """ stack = ImageStack.synthetic_stack(NUM_ROUND, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_fetcher=tile_fetcher_factory( OffsettedTiles, True, )) for _round in range(NUM_ROUND): for ch in range(NUM_CH): for z in range(NUM_Z): indices = {Indices.ROUND: _round, Indices.CH: ch, Indices.Z: z} xmin, xmax = stack.tile_coordinates(indices, Coordinates.X) ymin, ymax = stack.tile_coordinates(indices, Coordinates.Y) zmin, zmax = stack.tile_coordinates(indices, Coordinates.Z) expected_xmin, expected_xmax = round_to_x(_round) expected_ymin, expected_ymax = round_to_y(_round) expected_zmin, expected_zmax = round_to_z(_round) assert np.isclose(xmin, expected_xmin) assert np.isclose(xmax, expected_xmax) assert np.isclose(ymin, expected_ymin) assert np.isclose(ymax, expected_ymax) assert np.isclose(zmin, expected_zmin) assert np.isclose(zmax, expected_zmax)
def test_scalar_coordinates(): """Set up an ImageStack where only a single scalar physical coordinate is provided per axis. Internally, this should be converted to a range where the two endpoints are identical to the physical coordinate provided. """ stack = ImageStack.synthetic_stack(NUM_ROUND, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_fetcher=tile_fetcher_factory( OffsettedScalarTiles, True, )) for _round in range(NUM_ROUND): for ch in range(NUM_CH): for z in range(NUM_Z): indices = {Indices.ROUND: _round, Indices.CH: ch, Indices.Z: z} xmin, xmax = stack.tile_coordinates(indices, Coordinates.X) ymin, ymax = stack.tile_coordinates(indices, Coordinates.Y) zmin, zmax = stack.tile_coordinates(indices, Coordinates.Z) expected_x = round_to_x(_round)[0] expected_y = round_to_y(_round)[0] expected_z = round_to_z(_round)[0] assert np.isclose(xmin, expected_x) assert np.isclose(xmax, expected_x) assert np.isclose(ymin, expected_y) assert np.isclose(ymax, expected_y) assert np.isclose(zmin, expected_z) assert np.isclose(zmax, expected_z)
def run(self, stack: ImageStack, in_place: bool = False, verbose: bool = False, n_processes: Optional[int] = 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 : bool If True, report on the percentage completed (default = False) during processing 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. """ group_by = determine_axes_to_group_by(self.is_volume) clip = partial(self._clip, p_min=self.p_min, p_max=self.p_max) result = stack.apply(clip, group_by=group_by, verbose=verbose, in_place=in_place, n_processes=n_processes) return result
def run( self, stack: ImageStack, in_place: bool=False, verbose=False, n_processes: Optional[int]=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 : bool if True, report on the percentage completed during processing (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. """ func = partial( self._richardson_lucy_deconv, iterations=self.num_iter, psf=self.psf, clip=self.clip ) result = stack.apply( func, in_place=in_place, verbose=verbose, n_processes=n_processes ) return result
def run(self, stack: ImageStack, in_place: bool = False, verbose: bool = False, n_processes: Optional[int] = 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 : bool If True, report on the percentage completed (default = False) during processing 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. """ group_by = {Axes.ROUND, Axes.ZPLANE} unmix = partial(self._unmix, coeff_mat=self.coeff_mat) result = stack.apply(unmix, group_by=group_by, verbose=verbose, in_place=in_place, n_processes=n_processes) return result
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: """ 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 ------- The instantiated ImageStack """ 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 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, in_place: bool = False, verbose: bool = True, n_processes: Optional[int] = 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 : 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. """ group_by = determine_axes_to_group_by(self.is_volume) high_pass: Callable = partial(self._high_pass, sigma=self.sigma) result = stack.apply(high_pass, group_by=group_by, verbose=verbose, in_place=in_place, n_processes=n_processes) return result
def test_scalar_coordinates(): """Set up an ImageStack where only a single scalar physical coordinate is provided per axis. Internally, this should be converted to a range where the two endpoints are identical to the physical coordinate provided. """ stack = ImageStack.synthetic_stack(NUM_ROUND, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_fetcher=tile_fetcher_factory( OffsettedScalarTiles, True, )) assert stack.tiles_aligned is False for selectors in stack._iter_axes({Axes.ROUND, Axes.CH, Axes.ZPLANE}): expected_x = round_to_x(selectors[Axes.ROUND])[0] expected_y = round_to_y(selectors[Axes.ROUND])[0] expected_z = round_to_z(selectors[Axes.ROUND])[0] verify_physical_coordinates( stack, selectors, (expected_x, expected_x), (expected_y, expected_y), (expected_z, expected_z), )
def test_unaligned_tiles(): """Test that imagestack error is thrown when constructed with unaligned tiles""" try: ImageStack.synthetic_stack(NUM_ROUND, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_fetcher=tile_fetcher_factory( OffsettedTiles, True, )) except ValueError as e: # Assert value error is thrown with right message assert e.args[0] == "Tiles must be aligned"
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_array(data)
def run(self, stack: ImageStack, in_place: bool = True, verbose: bool = True, n_processes: Optional[int] = 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 : bool if True, report on filtering progress (default = False) Returns ------- ImageStack : if in-place is False, return the results of filter as a new stack """ group_by = determine_axes_to_group_by(self.is_volume) apply_filtering: Callable = partial(self._gaussian_laplace, sigma=self.sigma) return stack.apply( apply_filtering, group_by=group_by, verbose=verbose, in_place=in_place, n_processes=n_processes, )
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_array(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 test_max_projection_preserves_dtype(): original_dtype = np.float32 array = np.ones((2, 2, 2), dtype=original_dtype) image = ImageStack.from_numpy_array(array.reshape((1, 1, 2, 2, 2))) max_projection = image.max_proj(Indices.CH, Indices.ROUND, Indices.Z) assert max_projection.dtype == original_dtype
def run(self, stack: ImageStack, transforms_list: TransformsList, in_place: bool = False, verbose: bool = False, *args, **kwargs) -> ImageStack: """Applies a list of transformations to an ImageStack Parameters ---------- stack : ImageStack Stack to be transformed. transforms_list: TransformsList The list of transform objects to apply to the ImageStack. in_place : bool if True, process ImageStack in-place, otherwise return a new stack verbose : bool if True, report on transformation progress (default = False) Returns ------- ImageStack : If in-place is False, return the results of the transforms as a new stack. Otherwise return the original stack. """ if not in_place: # create a copy of the ImageStack, call apply on that stack with in_place=True image_stack = deepcopy(stack) return self.run(image_stack, transforms_list, in_place=True, **kwargs) 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 stack
def verify_physical_coordinates( stack: ImageStack, selectors: Mapping[Axes, int], expected_x_coordinates: Tuple[float, float], expected_y_coordinates: Tuple[float, float], expected_z_coordinates: Tuple[float, float]) -> None: """Given an imagestack and a set of selectors, verify that the physical coordinates for the data referred to by the selectors match the expected physical coordinates. """ assert np.all( np.isclose(stack.tile_coordinates(selectors, Coordinates.X), expected_x_coordinates)) assert np.all( np.isclose(stack.tile_coordinates(selectors, Coordinates.Y), expected_y_coordinates)) assert np.all( np.isclose(stack.tile_coordinates(selectors, Coordinates.Z), expected_z_coordinates))