def __init__( self, pixel_ticks: Union[Mapping[Axes, ArrayLike[int]], Mapping[str, ArrayLike[int]]], physical_ticks: Union[Mapping[Coordinates, ArrayLike[Number]], Mapping[str, ArrayLike[Number]]], masks: Sequence[MaskData], log: Optional[Log], ): self._pixel_ticks: Mapping[Axes, ArrayLike[int]] = _normalize_pixel_ticks(pixel_ticks) self._physical_ticks: Mapping[Coordinates, ArrayLike[Number]] = _normalize_physical_ticks( physical_ticks) self._masks: MutableMapping[int, MaskData] = {} self._log: Log = log or Log() for ix, mask_data in enumerate(masks): if mask_data.binary_mask.ndim not in (2, 3): raise TypeError(f"expected 2 or 3 dimensions; got {mask_data.binary_mask.ndim}") if mask_data.binary_mask.dtype != bool: raise ValueError(f"expected dtype of bool; got {mask_data.binary_mask.dtype}") self._masks[ix] = mask_data if len(self._pixel_ticks) != len(self._physical_ticks): raise ValueError( "pixel_ticks should have the same cardinality as physical_ticks") for axis, coord in zip(*_get_axes_names(len(self._pixel_ticks))): if axis not in self._pixel_ticks: raise ValueError(f"pixel ticks missing {axis.value} data") if coord not in self._physical_ticks: raise ValueError(f"physical coordinate ticks missing {coord.value} data") if len(self._pixel_ticks[axis]) != len(self._physical_ticks[coord]): raise ValueError( f"pixel ticks for {axis.name} does not have the same cardinality as physical " f"coordinates ticks for {coord.name}")
def from_label_array_and_ticks( cls, array: np.ndarray, pixel_ticks: Optional[Union[Mapping[Axes, ArrayLike[int]], Mapping[str, ArrayLike[int]]]], physical_ticks: Union[Mapping[Coordinates, ArrayLike[Number]], Mapping[str, ArrayLike[Number]]], log: Optional[Log], ) -> "BinaryMaskCollection": """Constructs a BinaryMaskCollection from an array containing the labels, a set of physical coordinates, and an optional log of how this label image came to be. Masks are cropped to the smallest size that contains the non-zero values, but pixel and physical coordinates ticks are retained. Masks extracted from BinaryMaskCollections will be cropped. To extract masks sized to the original label image, use :py:meth:`starfish.morphology.BinaryMaskCollection.uncropped_mask`. Parameters ---------- array : np.ndarray A 2D or 3D array containing the labels. The ordering of the axes must be Y, X for 2D images and ZPLANE, Y, X for 3D images. pixel_ticks : Optional[Union[Mapping[Axes, ArrayLike[int]], Mapping[str, ArrayLike[int]]]] A map from the axis to the values for that axis. For any axis that exist in the array but not in pixel_ticks, the pixel coordinates are assigned from 0..N-1, where N is the size along that axis. physical_ticks : Union[Mapping[Coordinates, ArrayLike[Number]], Mapping[str, ArrayLike[Number]]] A map from the physical coordinate type to the values for axis. For 2D label images, X and Y physical coordinates must be provided. For 3D label images, Z physical coordinates must also be provided. log : Optional[Log] A log of how this label image came to be. Returns ------- masks : BinaryMaskCollection Masks generated from the label image. """ # normalize the pixel coordinates to Mapping[Axes, ArrayLike[int]] pixel_ticks = _normalize_pixel_ticks(pixel_ticks) # normalize the physical coordinates to Mapping[Coordinates, ArrayLike[Number]] physical_ticks = _normalize_physical_ticks(physical_ticks) for ix, (axis, coord) in enumerate(zip(*_get_axes_names(array.ndim))): # We verify that the physical coordinate array's size matches the array's size. In # BinaryMaskCollection's constructor, it is verified that the pixel coordinate array's # size matches that of the physical coordinate array's size. if coord not in physical_ticks: raise ValueError( f"Missing physical coordinates for {coord.name}") if len(physical_ticks[coord]) != array.shape[ix]: raise ValueError( f"The size of the array for physical coordinates for {coord.name} " f"({len(physical_ticks[coord])} does not match the array's shape " f"({array.shape[ix]}).") if axis not in pixel_ticks: pixel_ticks[axis] = np.arange(0, array.shape[ix]) props = regionprops(array) masks: Sequence[MaskData] = [ MaskData(prop.image, prop.bbox[:array.ndim], prop) for prop in props ] log = deepcopy(log) return cls( pixel_ticks, physical_ticks, masks, log, )
def from_binary_arrays_and_ticks( cls, arrays: Sequence[np.ndarray], pixel_ticks: Optional[Union[Mapping[Axes, ArrayLike[int]], Mapping[str, ArrayLike[int]]]], physical_ticks: Union[Mapping[Coordinates, ArrayLike[Number]], Mapping[str, ArrayLike[Number]]], log: Optional[Log], ) -> "BinaryMaskCollection": """Constructs a BinaryMaskCollection from an array containing the labels, a set of physical coordinates, and an optional log of how this label image came to be. Masks are cropped to the smallest size that contains the non-zero values, but pixel and physical coordinates ticks are retained. Masks extracted from BinaryMaskCollections will be cropped. To extract masks sized to the original label image, use :py:meth:`starfish.morphology.BinaryMaskCollection.uncropped_mask`. Parameters ---------- arrays : Sequence[np.ndarray] A set of 2D or 3D binary arrays. The ordering of the axes must be Y, X for 2D images and ZPLANE, Y, X for 3D images. The arrays must have identical sizes and match the sizes of pixel_ticks and physical_ticks. pixel_ticks : Optional[Union[Mapping[Axes, ArrayLike[int]], Mapping[str, ArrayLike[int]]]] A map from the axis to the values for that axis. For any axis that exist in the array but not in pixel_ticks, the pixel coordinates are assigned from 0..N-1, where N is the size along that axis. physical_ticks : Union[Mapping[Coordinates, ArrayLike[Number]], Mapping[str, ArrayLike[Number]]] A map from the physical coordinate type to the values for axis. For 2D label images, X and Y physical coordinates must be provided. For 3D label images, Z physical coordinates must also be provided. log : Optional[Log] A log of how this label image came to be. Returns ------- masks : BinaryMaskCollection Masks generated from the label image. """ array_shapes = set(array.shape for array in arrays) array_dtypes = set(array.dtype for array in arrays) if len(array_shapes) > 1: raise ValueError("all masks must be identically sized") for array_dtype in array_dtypes: if array_dtype != np.bool: raise TypeError("arrays must be binary data") # normalize the pixel coordinates to Mapping[Axes, ArrayLike[int]] pixel_ticks = _normalize_pixel_ticks(pixel_ticks) # normalize the physical coordinates to Mapping[Coordinates, ArrayLike[Number]] physical_ticks = _normalize_physical_ticks(physical_ticks) # If we have 1 or more arrays, verify that the physical coordinate array's size matches the # array's size. In BinaryMaskCollection's constructor, it is verified that the pixel # tick array's size matches that of the physical coordinate array's size. if len(array_shapes) == 1: array_shape = next(iter(array_shapes)) # verify that we don't have extra expected_axes, expected_physical_coords = _get_axes_names( len(array_shape)) actual_real_coordinates_provided = set(expected_physical_coords) actual_real_coordinates_provided.intersection_update( set(physical_ticks.keys())) if len(array_shape) != len(actual_real_coordinates_provided): raise ValueError( f"physical coordinates provided for {len(actual_real_coordinates_provided)} " f"axes ({actual_real_coordinates_provided}), but the data has " f"{len(array_shape)} axes") for ix, (axis, coord) in enumerate( zip(*_get_axes_names(len(array_shape)))): if (coord in physical_ticks and len(physical_ticks[coord]) != array_shape[ix]): raise ValueError( f"The size of the array for physical coordinates for {coord.name} " f"({len(physical_ticks[coord])} does not match the array's shape " f"({array_shape[ix]}).") # Add all pixel ticks not present. for axis, coord in zip(*_get_axes_names(3)): if coord in physical_ticks and axis not in pixel_ticks: pixel_ticks[axis] = np.arange(0, len(physical_ticks[coord])) masks: MutableSequence[MaskData] = list() for array in arrays: selection_range: Tuple[ slice, ...] = BinaryMaskCollection._crop_mask(array) masks.append( MaskData( array[selection_range], tuple(selection.start for selection in selection_range), None)) log = deepcopy(log) return cls( pixel_ticks, physical_ticks, masks, log, )
def from_label_array_and_ticks( cls, array: np.ndarray, pixel_ticks: Optional[Union[Mapping[Axes, ArrayLike[int]], Mapping[str, ArrayLike[int]]]], physical_ticks: Union[Mapping[Coordinates, ArrayLike[Number]], Mapping[str, ArrayLike[Number]]], log: Optional[Log], ) -> "LabelImage": """Constructs a LabelImage from an array containing the labels, a set of physical coordinates, and an optional log of how this label image came to be. Parameters ---------- array : np.ndarray A 2D or 3D array containing the labels. The ordering of the axes must be Y, X for 2D images and ZPLANE, Y, X for 3D images. pixel_ticks : Optional[Union[Mapping[Axes, ArrayLike[int]], Mapping[str, ArrayLike[int]]]] A map from the axis to the values for that axis. For any axis that exist in the array but not in pixel_coordinates, the pixel coordinates are assigned from 0..N-1, where N is the size along that axis. physical_ticks : Union[Mapping[Coordinates, ArrayLike[Number]], Mapping[str, ArrayLike[Number]]] A map from the physical coordinate type to the values for axis. For 2D label images, X and Y physical coordinates must be provided. For 3D label images, Z physical coordinates must also be provided. log : Optional[Log] A log of how this label image came to be. """ # normalize the pixel coordinates to Mapping[Axes, ArrayLike[int]] pixel_ticks = _normalize_pixel_ticks(pixel_ticks) # normalize the physical coordinates to Mapping[Coordinates, ArrayLike[Number]] physical_ticks = _normalize_physical_ticks(physical_ticks) img_axes, img_coords = _get_axes_names(array.ndim) xr_axes = [axis.value for axis in img_axes] try: xr_coords: MutableMapping[Hashable, Any] = { coord.value: (axis.value, physical_ticks[coord]) for axis, coord in zip(img_axes, img_coords) } except KeyError as ex: raise KeyError( f"missing physical coordinates {ex.args[0]}") from ex for ix, axis in enumerate(img_axes): xr_coords[axis.value] = pixel_ticks.get( axis, np.arange(0, array.shape[ix])) dataarray = xr.DataArray( array, dims=xr_axes, coords=xr_coords, ) dataarray.attrs.update({ AttrKeys.LOG: (log or Log()).encode(), AttrKeys.DOCTYPE: DOCTYPE_STRING, AttrKeys.VERSION: str(CURRENT_VERSION), }) return LabelImage(dataarray)