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 test_multiple_tiles_of_different_kind(): with pytest.raises(TypeError): ImageStack.synthetic_stack( NUM_HYB, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_data_provider=create_tile_data_provider( np.uint32, np.float32), )
def labeled_synthetic_dataset(): stp = synthesize.SyntheticSpotTileProvider() image = ImageStack.synthetic_stack(tile_data_provider=stp.tile) max_proj = image.max_proj(Indices.HYB, Indices.CH, Indices.Z) view = max_proj.reshape((1, 1, 1) + max_proj.shape) dots = ImageStack.from_numpy_array(view) def labeled_synthetic_dataset_factory(): return deepcopy(image), deepcopy(dots), deepcopy(stp.codebook) return labeled_synthetic_dataset_factory
def test_from_numpy_array_raises_error_when_incorrect_dims_passed(): array = np.ones((2, 2)) # 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 filter(self, stack: ImageStack, in_place: bool = True) -> 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 Returns ------- Optional[ImageStack] : if in-place is False, return the results of filter as a new stack """ func: Callable = partial(self.richardson_lucy_deconv, num_iter=self.num_iter, psf=self.psf, clip=self.clip) result = stack.apply(func, in_place=in_place, verbose=self.verbose) if not in_place: return result return None
def test_iss_pipeline(): np.random.seed(2) synthesizer = SyntheticData(n_spots=5) codebook = synthesizer.codebook() true_intensities = synthesizer.intensities(codebook=codebook) image = synthesizer.spots(intensities=true_intensities) dots_data = image.max_proj(Indices.HYB, Indices.CH, Indices.Z) dots = ImageStack.from_numpy_array( dots_data.reshape((1, 1, 1, *dots_data.shape))) wth = WhiteTophat(disk_size=15) wth.filter(image) wth.filter(dots) fsr = FourierShiftRegistration(upsampling=1000, reference_stack=dots) fsr.register(image) min_sigma = 1.5 max_sigma = 5 num_sigma = 10 threshold = 1e-4 gsd = GaussianSpotDetector( min_sigma=min_sigma, max_sigma=max_sigma, num_sigma=num_sigma, threshold=threshold, blobs_stack=dots, measurement_type='max', ) intensities = gsd.find(hybridization_image=image) assert intensities.shape[0] == 5 codebook.decode_euclidean(intensities)
def __init__(self, min_sigma, max_sigma, num_sigma, threshold, blobs_stack, overlap=0.5, measurement_type='max', is_volume: bool = True, **kwargs) -> None: """Multi-dimensional gaussian spot detector Parameters ---------- min_sigma : float The minimum standard deviation for Gaussian Kernel. Keep this low to detect smaller blobs. max_sigma : float The maximum standard deviation for Gaussian Kernel. Keep this high to detect larger blobs. num_sigma : int The number of intermediate values of standard deviations to consider between `min_sigma` and `max_sigma`. threshold : float The absolute lower bound for scale space maxima. Local maxima smaller than thresh are ignored. Reduce this to detect blobs with less intensities. overlap : float [0, 1] If two spots have more than this fraction of overlap, the spots are combined (default = 0.5) blobs_stack : Union[ImageStack, str] ImageStack or the path or URL that references the ImageStack that contains the blobs. measurement_type : str ['max', 'mean'] name of the function used to calculate the intensity for each identified spot area Notes ----- This spot detector is very sensitive to the threshold that is selected, and the threshold is defined as an absolute value -- therefore it must be adjusted depending on the datatype of the passed image. """ self.min_sigma = min_sigma self.max_sigma = max_sigma self.num_sigma = num_sigma self.threshold = threshold self.overlap = overlap self.is_volume = is_volume if isinstance(blobs_stack, ImageStack): self.blobs_stack = blobs_stack else: self.blobs_stack = ImageStack.from_path_or_url(blobs_stack) self.blobs_image: np.ndarray = self.blobs_stack.max_proj( Indices.HYB, Indices.CH) try: self.measurement_function = getattr(np, measurement_type) except AttributeError: raise ValueError( f'measurement_type must be a numpy reduce function such as "max" or "mean". {measurement_type} ' f'not found.')
def _cli(cls, args, print_help=False): """Runs the spot finder component based on parsed arguments.""" if args.spot_finder_algorithm_class is None or print_help: cls.spot_finder_group.print_help() cls.spot_finder_group.exit(status=2) print('Detecting Spots ...') hybridization_stack = ImageStack.from_path_or_url(args.input) instance = args.spot_finder_algorithm_class(**vars(args)) spot_attributes, encoded_spots = instance.find(hybridization_stack) if args.show: encoded_spots.show(figsize=(10, 10)) path = os.path.join(args.output, 'spots.geojson') print(f"Writing | spots geojson to: {path}") spot_attributes.save_geojson(path) path = os.path.join(args.output, 'spots.json') print(f"Writing | spot_id | x | y | z | to: {path}") spot_attributes.save(path) path = os.path.join(args.output, 'encoder_table.json') print(f"Writing | spot_id | hyb | ch | val | to: {path}") encoded_spots.save(path)
def test_max_projection_preserves_dtype(): original_dtype = np.uint16 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.HYB, Indices.Z) assert max_projection.dtype == original_dtype
def filter(self, stack: ImageStack, in_place: bool = True) -> 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 Returns ------- Optional[ImageStack] : if in-place is False, return the results of filter as a new stack """ clip = partial(self.clip, p_min=self.p_min, p_max=self.p_max) result = stack.apply(clip, is_volume=self.is_volume, verbose=self.verbose, in_place=in_place) if not in_place: return result return None
def filter(self, stack: ImageStack, in_place: bool = True) -> 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 Returns ------- Optional[ImageStack] : if in-place is False, return the results of filter as a new stack """ high_pass: Callable = partial(self.high_pass, sigma=self.sigma) result = stack.apply(high_pass, is_volume=self.is_volume, verbose=self.verbose, in_place=in_place) if not in_place: return result return None
def __init__(self, upsampling: int, reference_stack: Union[str, ImageStack], **kwargs) -> None: self.upsampling = upsampling if isinstance(reference_stack, ImageStack): self.reference_stack = reference_stack else: self.reference_stack = ImageStack.from_path_or_url(reference_stack)
def _cli(cls, args, print_help=False): """Runs the segmentation component based on parsed arguments.""" if args.segmentation_algorithm_class is None or print_help: cls.segmentation_group.print_help() cls.segmentation_group.exit(status=2) instance = args.segmentation_algorithm_class(**vars(args)) print('Segmenting ...') hybridization_stack = ImageStack.from_path_or_url(args.hybridization_stack) nuclei_stack = ImageStack.from_path_or_url(args.nuclei_stack) regions = instance.segment(hybridization_stack, nuclei_stack) geojson = regions_to_geojson(regions, use_hull=False) print("Writing | regions geojson to: {}".format(args.output)) with open(args.output, "w") as f: f.write(json.dumps(geojson))
def test_synthetic_spot_creation_produces_an_imagestack( synthetic_intensity_table): num_z = 12 height = 50 width = 40 image = ImageStack.synthetic_spots(synthetic_intensity_table, num_z, height, width) assert isinstance(image, ImageStack)
def spots(self, intensities=None) -> ImageStack: if intensities is None: intensities = self.intensities() return ImageStack.synthetic_spots( intensities, self.n_z, self.height, self.width, self.n_photons_background, self.point_spread_function, self.camera_detection_efficiency, self.background_electrons, self.gray_level, self.ad_coversion_bits)
def _cli(cls, args, print_help=False): """Runs the registration component based on parsed arguments.""" if args.registration_algorithm_class is None or print_help: cls.register_group.print_help() cls.register_group.exit(status=2) print('Registering ...') stack = ImageStack.from_path_or_url(args.input) instance = args.registration_algorithm_class(**vars(args)) instance.register(stack) stack.write(args.output)
def _cli(cls, args, print_help=False): """Runs the spot finder component based on parsed arguments.""" if args.spot_finder_algorithm_class is None or print_help: cls.spot_finder_group.print_help() cls.spot_finder_group.exit(status=2) print('Detecting Spots ...') hybridization_stack = ImageStack.from_path_or_url(args.input) instance = args.spot_finder_algorithm_class(**vars(args)) intensities = instance.find(hybridization_stack) intensities.save(os.path.join(args.output, 'spots.nc'))
def test_multiple_tiles_of_same_dtype(): stack = ImageStack.synthetic_stack( NUM_HYB, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_data_provider=create_tile_data_provider(np.uint32, np.uint32), ) expected = np.ones((NUM_HYB, NUM_CH, NUM_Z, HEIGHT, WIDTH), dtype=np.uint32) assert np.array_equal(stack.numpy_array, expected)
def synthetic_spot_pass_through_stack(synthetic_dataset_with_truth_values): codebook, true_intensities, _ = synthetic_dataset_with_truth_values true_intensities = true_intensities[:2] # transfer the intensities to the stack but don't do anything to them. img_stack = ImageStack.synthetic_spots(true_intensities, num_z=12, height=50, width=45, n_photons_background=0, point_spread_function=(0, 0, 0), camera_detection_efficiency=1.0, background_electrons=0, graylevel=1) return codebook, true_intensities, img_stack
def register(self, image: ImageStack): # 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.HYB, Indices.CH, Indices.Z) for h in range(image.num_hybs): # compute shift between maximum projection (across channels) and dots, for each hyb round # TODO: make the max projection array ignorant of axes ordering. shift, error = compute_shift(mp[h, :, :], reference_image, self.upsampling) print("For hyb: {}, Shift: {}, Error: {}".format(h, shift, error)) for c in range(image.num_chs): for z in range(image.num_zlayers): # apply shift to all zlayers, channels, and hyb rounds indices = {Indices.HYB: h, Indices.CH: c, Indices.Z: z} data, axes = image.get_slice(indices=indices) assert len(axes) == 0 result = shift_im(data, shift) image.set_slice(indices=indices, data=result) return image
def test_set_slice_range(): """ Sets a slice across a range of one of the dimensions. """ stack = ImageStack.synthetic_stack() zrange = slice(1, 3) y, x = stack.tile_shape expected = np.ones((stack.shape[Indices.HYB], stack.shape[Indices.CH], zrange.stop - zrange.start, y, x)) * 10 index = {Indices.Z: zrange} stack.set_slice(index, expected) assert np.array_equal(stack.get_slice(index)[0], expected)
def synthetic_stack( num_hyb: int=DEFAULT_NUM_HYB, num_ch: int=DEFAULT_NUM_CH, num_z: int=DEFAULT_NUM_Z, tile_height: int=DEFAULT_HEIGHT, tile_width: int=DEFAULT_WIDTH, tile_data_provider: Callable[[int, int, int, int, int], np.ndarray]=default_tile_data_provider, tile_extras_provider: Callable[[int, int, int], Any]=default_tile_extras_provider ) -> ImageStack: """generate a synthetic ImageStack Returns ------- ImageStack : imagestack containing a tensor of (2, 3, 4, 30, 20) whose values are all 1. """ img = TileSet( {Coordinates.X, Coordinates.Y, Indices.HYB, Indices.CH, Indices.Z}, { Indices.HYB: num_hyb, Indices.CH: num_ch, Indices.Z: num_z, }, default_tile_shape=(tile_height, tile_width), ) for hyb in range(num_hyb): for ch in range(num_ch): for z in range(num_z): tile = Tile( { Coordinates.X: (0.0, 0.001), Coordinates.Y: (0.0, 0.001), Coordinates.Z: (0.0, 0.001), }, { Indices.HYB: hyb, Indices.CH: ch, Indices.Z: z, }, extras=tile_extras_provider(hyb, ch, z), ) tile.numpy_array = tile_data_provider(hyb, ch, z, tile_height, tile_width) img.add_tile(tile) stack = ImageStack(img) return stack
def test_set_slice_simple_index(): """ Sets a slice across one of the indices at the end. For instance, if the dimensions are (P, Q0,..., Qn-1, R), sets a slice across either P or R. """ stack = ImageStack.synthetic_stack() hyb = 1 y, x = stack.tile_shape expected = np.ones( (stack.shape[Indices.CH], stack.shape[Indices.Z], y, x)) * 2 index = {Indices.HYB: hyb} stack.set_slice(index, expected) assert np.array_equal(stack.get_slice(index)[0], expected)
def test_set_slice_middle_index(): """ Sets a slice across one of the indices in the middle. For instance, if the dimensions are (P, Q0,..., Qn-1, R), slice across one of the Q axes. """ stack = ImageStack.synthetic_stack() ch = 1 y, x = stack.tile_shape expected = np.ones( (stack.shape[Indices.HYB], stack.shape[Indices.Z], y, x)) * 2 index = {Indices.CH: ch} stack.set_slice(index, expected) assert np.array_equal(stack.get_slice(index)[0], expected)
def test_float_type_promotion(): with warnings.catch_warnings(record=True) as w: stack = ImageStack.synthetic_stack( NUM_HYB, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_data_provider=create_tile_data_provider( np.float64, np.float32), ) assert len(w) == 1 assert issubclass(w[0].category, DataFormatWarning) expected = np.ones((NUM_HYB, NUM_CH, NUM_Z, HEIGHT, WIDTH), dtype=np.float64) assert np.array_equal(stack.numpy_array, expected)
def test_conflict(): """ Tiles that have extras that conflict with indices should produce an error. """ def tile_extras_provider(hyb: int, ch: int, z: int) -> Any: return { Indices.HYB: hyb, Indices.CH: ch, Indices.Z: z, } stack = ImageStack.synthetic_stack( num_hyb=NUM_HYB, num_ch=NUM_CH, num_z=NUM_Z, tile_extras_provider=tile_extras_provider, ) with pytest.raises(ValueError): stack.tile_metadata
def test_int_type_promotion(): with warnings.catch_warnings(record=True) as w: stack = ImageStack.synthetic_stack( NUM_HYB, NUM_CH, NUM_Z, HEIGHT, WIDTH, tile_data_provider=create_tile_data_provider(np.int32, np.int8), ) assert len(w) == 1 assert issubclass(w[0].category, DataFormatWarning) expected = np.ones((NUM_HYB, NUM_CH, NUM_Z, HEIGHT, WIDTH), dtype=np.int32) corner = np.empty((HEIGHT, WIDTH), dtype=np.int32) corner.fill(16777216) expected[0, 0, 0] = corner assert np.array_equal(stack.numpy_array, expected)
def test_metadata(): """ Normal situation where all the tiles have uniform keys for both indices and extras. """ def tile_extras_provider(hyb: int, ch: int, z: int) -> Any: return { 'random_key': { Indices.HYB: hyb, Indices.CH: ch, Indices.Z: z, } } stack = ImageStack.synthetic_stack( num_hyb=NUM_HYB, num_ch=NUM_CH, num_z=NUM_Z, tile_extras_provider=tile_extras_provider, ) table = stack.tile_metadata assert len(table) == NUM_HYB * NUM_CH * NUM_Z
def _measure_spot_intensities( self, stack: ImageStack, spot_attributes: pd.DataFrame) -> IntensityTable: n_ch = stack.shape[Indices.CH] n_hyb = stack.shape[Indices.HYB] spot_attribute_index = dataframe_to_multiindex(spot_attributes) intensity_table = IntensityTable.empty_intensity_table( spot_attribute_index, n_ch, n_hyb) indices = product(range(n_ch), range(n_hyb)) for c, h in indices: image, _ = stack.get_slice({Indices.CH: c, Indices.HYB: h}) blob_intensities: pd.Series = self._measure_blob_intensity( image, spot_attributes, self.measurement_function) intensity_table[:, c, h] = blob_intensities return intensity_table
def test_get_slice_range(): """ Retrieve a slice across a range of one of the dimensions. """ stack = ImageStack.synthetic_stack() zrange = slice(1, 3) imageslice, axes = stack.get_slice({Indices.Z: zrange}) y, x = stack.tile_shape assert axes == [Indices.HYB, Indices.CH, Indices.Z] for hyb in range(stack.shape[Indices.HYB]): for ch in range(stack.shape[Indices.CH]): for z in range(zrange.stop - zrange.start): data = np.empty((y, x)) data.fill((hyb * stack.shape[Indices.CH] + ch) * stack.shape[Indices.Z] + (z + zrange.start)) assert data.all() == imageslice[hyb, ch, z].all()