Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
 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
Esempio n. 4
0
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
Esempio n. 5
0
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
Esempio n. 6
0
    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)
Esempio n. 7
0
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
Esempio n. 9
0
    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))
Esempio n. 10
0
    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)
Esempio n. 11
0
    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)
Esempio n. 12
0
    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)
Esempio n. 13
0
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)
Esempio n. 14
0
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)
Esempio n. 16
0
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)
Esempio n. 17
0
    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
Esempio n. 18
0
    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
Esempio n. 19
0
    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
Esempio n. 22
0
    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
Esempio n. 23
0
    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
Esempio n. 24
0
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")
Esempio n. 25
0
    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
Esempio n. 26
0
    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
Esempio n. 28
0
    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
Esempio n. 29
0
    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
Esempio n. 30
0
    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