diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index c9e09deaf..7ea7bf562 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -23,8 +23,8 @@ jobs: pip install .[docs] - name: parse notebooks - run: jupytext --to notebook --execute ./docs/source/notebooks/*.py - # continue-on-error: true + run: | + jupytext --to notebook --execute ./examples/starter_tutorial/minimal_tutorial.py --output ./docs/source/notebooks/minimal_tutorial.ipynb - name: remove notebook scripts run: rm ./docs/source/notebooks/*.py - name: Build and Commit diff --git a/dacapo/apply.py b/dacapo/apply.py index 0bbb66ea6..872e3c532 100644 --- a/dacapo/apply.py +++ b/dacapo/apply.py @@ -1,8 +1,8 @@ import logging from typing import Optional from funlib.geometry import Roi, Coordinate +from funlib.persistence import open_ds import numpy as np -from dacapo.experiments.datasplits.datasets.arrays.array import Array from dacapo.experiments.datasplits.datasets.dataset import Dataset from dacapo.experiments.run import Run @@ -12,7 +12,6 @@ import dacapo.experiments.tasks.post_processors as post_processors from dacapo.store.array_store import LocalArrayIdentifier from dacapo.predict import predict -from dacapo.experiments.datasplits.datasets.arrays import ZarrArray from dacapo.store.create_store import ( create_config_store, create_weights_store, @@ -164,7 +163,9 @@ def apply( # make array identifiers for input, predictions and outputs input_array_identifier = LocalArrayIdentifier(Path(input_container), input_dataset) - input_array = ZarrArray.open_from_array_identifier(input_array_identifier) + input_array = open_ds( + f"{input_array_identifier.container}/{input_array_identifier.dataset}" + ) if roi is None: _roi = input_array.roi else: @@ -226,7 +227,7 @@ def apply_run( output_dtype (np.dtype | str, optional): The output data type. Defaults to np.uint8. overwrite (bool, optional): Whether to overwrite existing output. Defaults to True. Raises: - ValueError: If the input array is not a ZarrArray. + ValueError: If the input array is not a zarr array. Examples: >>> apply_run( ... run=run, diff --git a/dacapo/blockwise/argmax_worker.py b/dacapo/blockwise/argmax_worker.py index 2c15a1625..2f0242cc8 100644 --- a/dacapo/blockwise/argmax_worker.py +++ b/dacapo/blockwise/argmax_worker.py @@ -1,8 +1,9 @@ from upath import UPath as Path import sys -from dacapo.experiments.datasplits.datasets.arrays.zarr_array import ZarrArray + from dacapo.store.array_store import LocalArrayIdentifier from dacapo.compute_context import create_compute_context +from dacapo.tmp import open_from_identifier import daisy @@ -82,12 +83,12 @@ def start_worker_fn( """ # get arrays input_array_identifier = LocalArrayIdentifier(Path(input_container), input_dataset) - input_array = ZarrArray.open_from_array_identifier(input_array_identifier) + input_array = open_from_identifier(input_array_identifier) output_array_identifier = LocalArrayIdentifier( Path(output_container), output_dataset ) - output_array = ZarrArray.open_from_array_identifier(output_array_identifier) + output_array = open_from_identifier(output_array_identifier) def io_loop(): # wait for blocks to run pipeline @@ -102,7 +103,7 @@ def io_loop(): # write to output array output_array[block.write_roi] = np.argmax( input_array[block.write_roi], - axis=input_array.axes.index("c"), + axis=input_array.axis_names.index("c^"), ) if return_io_loop: diff --git a/dacapo/blockwise/empanada_function.py b/dacapo/blockwise/empanada_function.py index 09871de88..06add79f2 100644 --- a/dacapo/blockwise/empanada_function.py +++ b/dacapo/blockwise/empanada_function.py @@ -374,7 +374,7 @@ def start_consensus_worker(trackers_dict): assert image.ndim in [3, 4], "Only 3D and 4D input images can be handled!" if image.ndim == 4: # channel dimensions are commonly 1, 3 and 4 - # check for dimensions on zeroeth and last axes + # check for dimensions on zeroeth and last axis_names shape = image.shape if shape[0] in [1, 3, 4]: image = image[0] diff --git a/dacapo/blockwise/predict_worker.py b/dacapo/blockwise/predict_worker.py index dea41e504..c196bfaf3 100644 --- a/dacapo/blockwise/predict_worker.py +++ b/dacapo/blockwise/predict_worker.py @@ -3,12 +3,12 @@ from typing import Optional import torch -from dacapo.experiments.datasplits.datasets.arrays import ZarrArray -from dacapo.gp import DaCapoArraySource + from dacapo.store.array_store import LocalArrayIdentifier from dacapo.store.create_store import create_config_store, create_weights_store from dacapo.experiments import Run from dacapo.compute_context import create_compute_context +from dacapo.tmp import open_from_identifier import gunpowder as gp import gunpowder.torch as gp_torch @@ -134,12 +134,12 @@ def io_loop(): input_array_identifier = LocalArrayIdentifier( Path(input_container), input_dataset ) - raw_array = ZarrArray.open_from_array_identifier(input_array_identifier) + raw_array = open_from_identifier(input_array_identifier) output_array_identifier = LocalArrayIdentifier( Path(output_container), output_dataset ) - output_array = ZarrArray.open_from_array_identifier(output_array_identifier) + output_array = open_from_identifier(output_array_identifier) # set benchmark flag to True for performance torch.backends.cudnn.benchmark = True @@ -163,7 +163,7 @@ def io_loop(): # assemble prediction pipeline # prepare data source - pipeline = DaCapoArraySource(raw_array, raw) + pipeline = gp.ArraySource(raw, raw_array) # raw: (c, d, h, w) pipeline += gp.Pad(raw, None) # raw: (c, d, h, w) diff --git a/dacapo/blockwise/segment_worker.py b/dacapo/blockwise/segment_worker.py index 2ccccf485..30cde1a3a 100644 --- a/dacapo/blockwise/segment_worker.py +++ b/dacapo/blockwise/segment_worker.py @@ -10,7 +10,8 @@ import numpy as np import yaml from dacapo.compute_context import create_compute_context -from dacapo.experiments.datasplits.datasets.arrays import ZarrArray +from dacapo.tmp import open_from_identifier + from dacapo.store.array_store import LocalArrayIdentifier @@ -93,13 +94,13 @@ def start_worker_fn( # get arrays input_array_identifier = LocalArrayIdentifier(Path(input_container), input_dataset) print(f"Opening input array {input_array_identifier}") - input_array = ZarrArray.open_from_array_identifier(input_array_identifier) + input_array = open_from_identifier(input_array_identifier) output_array_identifier = LocalArrayIdentifier( Path(output_container), output_dataset ) print(f"Opening output array {output_array_identifier}") - output_array = ZarrArray.open_from_array_identifier(output_array_identifier) + output_array = open_from_identifier(output_array_identifier) # Load segment function function_name = Path(function_path).stem diff --git a/dacapo/blockwise/threshold_worker.py b/dacapo/blockwise/threshold_worker.py index b6be79d22..be9fa944b 100644 --- a/dacapo/blockwise/threshold_worker.py +++ b/dacapo/blockwise/threshold_worker.py @@ -1,8 +1,9 @@ from upath import UPath as Path import sys -from dacapo.experiments.datasplits.datasets.arrays.zarr_array import ZarrArray + from dacapo.store.array_store import LocalArrayIdentifier from dacapo.compute_context import create_compute_context +from dacapo.tmp import open_from_identifier import daisy @@ -82,12 +83,12 @@ def start_worker_fn( """ # get arrays input_array_identifier = LocalArrayIdentifier(Path(input_container), input_dataset) - input_array = ZarrArray.open_from_array_identifier(input_array_identifier) + input_array = open_from_identifier(input_array_identifier) output_array_identifier = LocalArrayIdentifier( Path(output_container), output_dataset ) - output_array = ZarrArray.open_from_array_identifier(output_array_identifier) + output_array = open_from_identifier(output_array_identifier) def io_loop(): # wait for blocks to run pipeline diff --git a/dacapo/cli.py b/dacapo/cli.py index d1f7ab2ae..e0b86a1c1 100644 --- a/dacapo/cli.py +++ b/dacapo/cli.py @@ -7,6 +7,7 @@ import click import logging from funlib.geometry import Roi, Coordinate +from funlib.persistence import Array from dacapo.experiments.datasplits.datasets.dataset import Dataset from dacapo.experiments.tasks.post_processors.post_processor_parameters import ( PostProcessorParameters, @@ -16,7 +17,8 @@ segment_blockwise as _segment_blockwise, ) from dacapo.store.local_array_store import LocalArrayIdentifier -from dacapo.experiments.datasplits.datasets.arrays import ZarrArray +from dacapo.tmp import open_from_identifier, create_from_identifier + from dacapo.options import DaCapoConfig import os @@ -474,7 +476,7 @@ def run_blockwise( parameters = unpack_ctx(ctx) input_array_identifier = LocalArrayIdentifier(Path(input_container), input_dataset) - input_array = ZarrArray.open_from_array_identifier(input_array_identifier) + input_array = open_from_identifier(input_array_identifier) _total_roi, read_roi, write_roi, _ = get_rois( total_roi, read_roi_size, write_roi_size, input_array @@ -485,9 +487,9 @@ def run_blockwise( Path(output_container), output_dataset ) - ZarrArray.create_from_array_identifier( + create_from_identifier( output_array_identifier, - input_array.axes, + input_array.axis_names, _total_roi, channels_out, input_array.voxel_size, @@ -652,7 +654,7 @@ def segment_blockwise( parameters = unpack_ctx(ctx) input_array_identifier = LocalArrayIdentifier(Path(input_container), input_dataset) - input_array = ZarrArray.open_from_array_identifier(input_array_identifier) + input_array = open_from_identifier(input_array_identifier) _total_roi, read_roi, write_roi, _context = get_rois( total_roi, read_roi_size, write_roi_size, input_array @@ -668,9 +670,9 @@ def segment_blockwise( Path(output_container), output_dataset ) - ZarrArray.create_from_array_identifier( + create_from_identifier( output_array_identifier, - input_array.axes, + input_array.axis_names, _total_roi, channels_out, input_array.voxel_size, @@ -845,7 +847,7 @@ def unpack_ctx(ctx): return kwargs -def get_rois(total_roi, read_roi_size, write_roi_size, input_array): +def get_rois(total_roi, read_roi_size, write_roi_size, input_array: Array): """ Get the ROIs for processing. @@ -853,7 +855,7 @@ def get_rois(total_roi, read_roi_size, write_roi_size, input_array): total_roi (str): The total ROI to be processed. read_roi_size (str): The size of the ROI to be read for each block. write_roi_size (str): The size of the ROI to be written for each block. - input_array (ZarrArray): The input array. + input_array: The input array. Returns: tuple: A tuple containing the total ROI, read ROI, write ROI, and context. Raises: diff --git a/dacapo/experiments/datasplits/datasets/arrays/__init__.py b/dacapo/experiments/datasplits/datasets/arrays/__init__.py index 74091aba0..d8e6d6d7b 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/__init__.py +++ b/dacapo/experiments/datasplits/datasets/arrays/__init__.py @@ -1,25 +1,21 @@ -from .array import Array # noqa from .array_config import ArrayConfig # noqa # configurable arrays -from .dummy_array_config import DummyArray, DummyArrayConfig # noqa -from .zarr_array_config import ZarrArray, ZarrArrayConfig # noqa -from .binarize_array_config import BinarizeArray, BinarizeArrayConfig # noqa -from .resampled_array_config import ResampledArray, ResampledArrayConfig # noqa -from .intensity_array_config import IntensitiesArray, IntensitiesArrayConfig # noqa -from .missing_annotations_mask import MissingAnnotationsMask # noqa +from .dummy_array_config import DummyArrayConfig # noqa +from .zarr_array_config import ZarrArrayConfig # noqa +from .binarize_array_config import BinarizeArrayConfig # noqa +from .resampled_array_config import ResampledArrayConfig # noqa +from .intensity_array_config import IntensitiesArrayConfig # noqa from .missing_annotations_mask_config import MissingAnnotationsMaskConfig # noqa -from .ones_array_config import OnesArray, OnesArrayConfig # noqa -from .concat_array_config import ConcatArray, ConcatArrayConfig # noqa -from .logical_or_array_config import LogicalOrArray, LogicalOrArrayConfig # noqa -from .crop_array_config import CropArray, CropArrayConfig # noqa +from .ones_array_config import OnesArrayConfig # noqa +from .concat_array_config import ConcatArrayConfig # noqa +from .logical_or_array_config import LogicalOrArrayConfig # noqa +from .crop_array_config import CropArrayConfig # noqa from .merge_instances_array_config import ( - MergeInstancesArray, MergeInstancesArrayConfig, ) # noqa -from .dvid_array_config import DVIDArray, DVIDArrayConfig -from .sum_array_config import SumArray, SumArrayConfig +from .dvid_array_config import DVIDArrayConfig +from .sum_array_config import SumArrayConfig # nonconfigurable arrays (helpers) -from .numpy_array import NumpyArray # noqa -from .constant_array_config import ConstantArray, ConstantArrayConfig # noqa +from .constant_array_config import ConstantArrayConfig # noqa diff --git a/dacapo/experiments/datasplits/datasets/arrays/array.py b/dacapo/experiments/datasplits/datasets/arrays/array.py deleted file mode 100644 index da040067c..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/array.py +++ /dev/null @@ -1,325 +0,0 @@ -from funlib.geometry import Coordinate, Roi - -import numpy as np - -from typing import Optional, Dict, Any, List, Iterable -from abc import ABC, abstractmethod - - -class Array(ABC): - """ - An Array is a multi-dimensional array of data that can be read from and written to. It is - defined by a region of interest (ROI) in world units, a voxel size, and a number of spatial - dimensions. The data is stored in a numpy array, and can be accessed using numpy-like slicing - syntax. - - The Array class is an abstract base class that defines the interface for all Array - implementations. It provides a number of properties that must be implemented by subclasses, - such as the ROI, voxel size, and data type of the array. It also provides a method for fetching - data from the array, which is implemented by slicing the numpy array. - - The Array class also provides a method for checking if the array can be visualized in - Neuroglancer, and a method for generating a Neuroglancer layer for the array. These methods are - implemented by subclasses that support visualization in Neuroglancer. - - Attributes: - attrs (Dict[str, Any]): A dictionary of metadata attributes stored on this array. - axes (List[str]): The axes of this dataset as a string of characters, as they are indexed. - Permitted characters are: - * ``zyx`` for spatial dimensions - * ``c`` for channels - * ``s`` for samples - dims (int): The number of spatial dimensions. - voxel_size (Coordinate): The size of a voxel in physical units. - roi (Roi): The total ROI of this array, in world units. - dtype (Any): The dtype of this array, in numpy dtypes - num_channels (Optional[int]): The number of channels provided by this dataset. Should return - None if the channel dimension doesn't exist. - data (np.ndarray): A numpy-like readable and writable view into this array. - writable (bool): Can we write to this Array? - Methods: - __getitem__(self, roi: Roi) -> np.ndarray: Get a numpy like readable and writable view into - this array. - _can_neuroglance(self) -> bool: Check if this array can be visualized in Neuroglancer. - _neuroglancer_layer(self): Generate a Neuroglancer layer for this array. - _slices(self, roi: Roi) -> Iterable[slice]: Generate a list of slices for the given ROI. - Note: - This class is used to define the interface for all Array implementations. It provides a - number of properties that must be implemented by subclasses, such as the ROI, voxel size, and - data type of the array. It also provides a method for fetching data from the array, which is - implemented by slicing the numpy array. The Array class also provides a method for checking - if the array can be visualized in Neuroglancer, and a method for generating a Neuroglancer - layer for the array. These methods are implemented by subclasses that support visualization - in Neuroglancer. - """ - - @property - @abstractmethod - def attrs(self) -> Dict[str, Any]: - """ - Return a dictionary of metadata attributes stored on this array. - - Returns: - Dict[str, Any]: A dictionary of metadata attributes stored on this array. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.attrs - {} - Note: - This method must be implemented by the subclass. - """ - pass - - @property - @abstractmethod - def axes(self) -> List[str]: - """ - Returns the axes of this dataset as a string of charactes, as they - are indexed. Permitted characters are: - - * ``zyx`` for spatial dimensions - * ``c`` for channels - * ``s`` for samples - - Returns: - List[str]: The axes of this dataset as a string of characters, as they are indexed. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.axes - ['z', 'y', 'x'] - Note: - This method must be implemented by the subclass. - """ - pass - - @property - @abstractmethod - def dims(self) -> int: - """ - Returns the number of spatial dimensions. - - Returns: - int: The number of spatial dimensions. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.dims - 3 - Note: - This method must be implemented by the subclass. - """ - pass - - @property - @abstractmethod - def voxel_size(self) -> Coordinate: - """ - The size of a voxel in physical units. - - Returns: - Coordinate: The size of a voxel in physical units. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.voxel_size - Coordinate((1, 1, 1)) - Note: - This method must be implemented by the subclass. - """ - pass - - @property - @abstractmethod - def roi(self) -> Roi: - """ - The total ROI of this array, in world units. - - Returns: - Roi: The total ROI of this array, in world units. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.roi - Roi(offset=Coordinate((0, 0, 0)), shape=Coordinate((100, 100, 100))) - Note: - This method must be implemented by the subclass. - """ - pass - - @property - @abstractmethod - def dtype(self) -> Any: - """ - The dtype of this array, in numpy dtypes - - Returns: - Any: The dtype of this array, in numpy dtypes. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.dtype - np.dtype('uint8') - Note: - This method must be implemented by the subclass. - """ - pass - - @property - @abstractmethod - def num_channels(self) -> Optional[int]: - """ - The number of channels provided by this dataset. - Should return None if the channel dimension doesn't exist. - - Returns: - Optional[int]: The number of channels provided by this dataset. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.num_channels - 1 - Note: - This method must be implemented by the subclass. - """ - pass - - @property - @abstractmethod - def data(self) -> np.ndarray: - """ - Get a numpy like readable and writable view into this array. - - Returns: - np.ndarray: A numpy like readable and writable view into this array. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.data - np.ndarray - Note: - This method must be implemented by the subclass. - """ - pass - - @property - @abstractmethod - def writable(self) -> bool: - """ - Can we write to this Array? - - Returns: - bool: Can we write to this Array? - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array.writable - False - Note: - This method must be implemented by the subclass. - """ - pass - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Get a numpy like readable and writable view into this array. - - Args: - roi (Roi): The region of interest to fetch data from. - Returns: - np.ndarray: A numpy like readable and writable view into this array. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> roi = Roi(offset=Coordinate((0, 0, 0)), shape=Coordinate((100, 100, 100))) - >>> array[roi] - np.ndarray - Note: - This method must be implemented by the subclass. - """ - if not self.roi.contains(roi): - raise ValueError(f"Cannot fetch data from outside my roi: {self.roi}!") - - assert roi.offset % self.voxel_size == Coordinate( - (0,) * self.dims - ), f"Given roi offset: {roi.offset} is not a multiple of voxel_size: {self.voxel_size}" - assert roi.shape % self.voxel_size == Coordinate( - (0,) * self.dims - ), f"Given roi shape: {roi.shape} is not a multiple of voxel_size: {self.voxel_size}" - - slices = tuple(self._slices(roi)) - - return self.data[slices] - - def _can_neuroglance(self) -> bool: - """ - Check if this array can be visualized in Neuroglancer. - - Returns: - bool: Whether this array can be visualized in Neuroglancer. - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array._can_neuroglance() - False - Note: - This method must be implemented by the subclass. - """ - return False - - def _neuroglancer_layer(self): - """ - Generate a Neuroglancer layer for this array. - - Raises: - NotImplementedError: This method must be implemented by the subclass. - Examples: - >>> array = Array() - >>> array._neuroglancer_layer() - NotImplementedError - Note: - This method must be implemented by the subclass. - """ - pass - - def _slices(self, roi: Roi) -> Iterable[slice]: - """ - Generate a list of slices for the given ROI. - - Args: - roi (Roi): The region of interest to generate slices for. - Returns: - Iterable[slice]: A list of slices for the given ROI. - Examples: - >>> array = Array() - >>> roi = Roi(offset=Coordinate((0, 0, 0)), shape=Coordinate((100, 100, 100))) - >>> array._slices(roi) - [slice(None, None, None), slice(None, None, None), slice(None, None, None)] - Note: - This method must be implemented by the subclass. - """ - offset = (roi.offset - self.roi.offset) / self.voxel_size - shape = roi.shape / self.voxel_size - spatial_slices: Dict[str, slice] = { - a: slice(o, o + s) - for o, s, a in zip(offset, shape, self.axes[-self.dims :]) - } - slices: List[slice] = [] - for axis in self.axes: - if axis == "b" or axis == "c": - slices.append(slice(None, None)) - else: - slices.append(spatial_slices[axis]) - return slices diff --git a/dacapo/experiments/datasplits/datasets/arrays/array_config.py b/dacapo/experiments/datasplits/datasets/arrays/array_config.py index a8e51dfd2..15cec7b83 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/array_config.py @@ -1,10 +1,12 @@ import attr from typing import Tuple +from abc import ABC, abstractmethod +from funlib.persistence import Array @attr.s -class ArrayConfig: +class ArrayConfig(ABC): """ Base class for array configurations. Each subclass of an `Array` should have a corresponding config class derived from @@ -31,6 +33,10 @@ class ArrayConfig: } ) + @abstractmethod + def array(self, mode: str = "r") -> Array: + pass + def verify(self) -> Tuple[bool, str]: """ Check whether this is a valid Array diff --git a/dacapo/experiments/datasplits/datasets/arrays/binarize_array.py b/dacapo/experiments/datasplits/datasets/arrays/binarize_array.py deleted file mode 100644 index dc79fcae5..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/binarize_array.py +++ /dev/null @@ -1,319 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - -import neuroglancer - -import numpy as np - - -class BinarizeArray(Array): - """ - This is wrapper around a ZarrArray containing uint annotations. - Because we often want to predict classes that are a combination - of a set of labels we wrap a ZarrArray with the BinarizeArray - and provide something like `groupings=[("mito", [3,4,5])]` - where 4 corresponds to mito_mem (mitochondria membrane), 5 is mito_ribo - (mitochondria ribosomes), and 3 is everything else that is part of a - mitochondria. The BinarizeArray will simply combine labels 3,4,5 into - a single binary channel for the class of "mito". - - We use a single channel per class because some classes may overlap. - For example if you had `groupings=[("mito", [3,4,5]), ("membrane", [4, 8, 1])]` - where 4 is mito_mem, 8 is er_mem (ER membrane), and 1 is pm (plasma membrane). - Now you can have a binary classification for membrane or not which in - some cases overlaps with the channel for mitochondria which includes - the mito membrane. - - Attributes: - name (str): The name of the array. - source_array (Array): The source array to binarize. - background (int): The label to treat as background. - groupings (List[Tuple[str, List[int]]]): A list of tuples where the first - element is the name of the class and the second element is a list of - labels that should be combined into a single binary channel. - Methods: - __init__(self, array_config): This method initializes the BinarizeArray object. - __attrs_post_init__(self): This method is called after the instance has been initialized by the constructor. It is used to set the default_config to an instance of ArrayConfig if it is None. - __getitem__(self, roi: Roi) -> np.ndarray: This method returns the binary channels for the given region of interest. - _can_neuroglance(self): This method returns True if the source array can be visualized in neuroglance. - _neuroglancer_source(self): This method returns the source array for neuroglancer. - _neuroglancer_layer(self): This method returns the neuroglancer layer for the source array. - _source_name(self): This method returns the name of the source array. - Note: - This class is used to create a BinarizeArray object which is a wrapper around a ZarrArray containing uint annotations. - """ - - def __init__(self, array_config): - """ - This method initializes the BinarizeArray object. - - Args: - array_config (ArrayConfig): The array configuration. - Raises: - AssertionError: If the source array has channels. - Examples: - >>> binarize_array = BinarizeArray(array_config) - Note: - This method is used to initialize the BinarizeArray object. - """ - self.name = array_config.name - self._source_array = array_config.source_array_config.array_type( - array_config.source_array_config - ) - self.background = array_config.background - - assert ( - "c" not in self._source_array.axes - ), "Cannot initialize a BinarizeArray with a source array with channels" - - self._groupings = array_config.groupings - - @property - def attrs(self): - """ - This method returns the attributes of the source array. - - Returns: - Dict: The attributes of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.attrs - Note: - This method is used to return the attributes of the source array. - """ - return self._source_array.attrs - - @property - def axes(self): - """ - This method returns the axes of the source array. - - Returns: - List[str]: The axes of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.axes - Note: - This method is used to return the axes of the source array. - """ - return ["c"] + self._source_array.axes - - @property - def dims(self) -> int: - """ - This method returns the dimensions of the source array. - - Returns: - int: The dimensions of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.dims - Note: - This method is used to return the dimensions of the source array. - """ - return self._source_array.dims - - @property - def voxel_size(self) -> Coordinate: - """ - This method returns the voxel size of the source array. - - Returns: - Coordinate: The voxel size of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.voxel_size - Note: - This method is used to return the voxel size of the source array. - """ - return self._source_array.voxel_size - - @property - def roi(self) -> Roi: - """ - This method returns the region of interest of the source array. - - Returns: - Roi: The region of interest of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.roi - Note: - This method is used to return the region of interest of the source array. - """ - return self._source_array.roi - - @property - def writable(self) -> bool: - """ - This method returns True if the source array is writable. - - Returns: - bool: True if the source array is writable. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.writable - Note: - This method is used to return True if the source array is writable. - """ - return False - - @property - def dtype(self): - """ - This method returns the data type of the source array. - - Returns: - np.dtype: The data type of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.dtype - Note: - This method is used to return the data type of the source array. - """ - return np.uint8 - - @property - def num_channels(self) -> int: - """ - This method returns the number of channels in the source array. - - Returns: - int: The number of channels in the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.num_channels - Note: - This method is used to return the number of channels in the source array. - - """ - return len(self._groupings) - - @property - def data(self): - """ - This method returns the data of the source array. - - Returns: - np.ndarray: The data of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.data - Note: - This method is used to return the data of the source array. - """ - raise ValueError( - "Cannot get a writable view of this array because it is a virtual " - "array created by modifying another array on demand." - ) - - @property - def channels(self): - """ - This method returns the channel names of the source array. - - Returns: - Iterator[str]: The channel names of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array.channels - Note: - This method is used to return the channel names of the source array. - """ - return (name for name, _ in self._groupings) - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - This method returns the binary channels for the given region of interest. - - Args: - roi (Roi): The region of interest. - Returns: - np.ndarray: The binary channels for the given region of interest. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array[roi] - Note: - This method is used to return the binary channels for the given region of interest. - """ - labels = self._source_array[roi] - grouped = np.zeros((len(self._groupings), *labels.shape), dtype=np.uint8) - for i, (_, ids) in enumerate(self._groupings): - if len(ids) == 0: - grouped[i] += labels != self.background - for id in ids: - grouped[i] += labels == id - return grouped - - def _can_neuroglance(self): - """ - This method returns True if the source array can be visualized in neuroglance. - - Returns: - bool: True if the source array can be visualized in neuroglance. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._can_neuroglance() - Note: - This method is used to return True if the source array can be visualized in neuroglance. - """ - return self._source_array._can_neuroglance() - - def _neuroglancer_source(self): - """ - This method returns the source array for neuroglancer. - - Returns: - neuroglancer.LocalVolume: The source array for neuroglancer. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._neuroglancer_source() - Note: - This method is used to return the source array for neuroglancer. - """ - return self._source_array._neuroglancer_source() - - def _neuroglancer_layer(self): - """ - This method returns the neuroglancer layer for the source array. - - Returns: - neuroglancer.SegmentationLayer: The neuroglancer layer for the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._neuroglancer_layer() - Note: - This method is used to return the neuroglancer layer for the source array. - """ - layer = neuroglancer.SegmentationLayer(source=self._neuroglancer_source()) - return layer - - def _source_name(self): - """ - This method returns the name of the source array. - - Returns: - str: The name of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._source_name() - Note: - This method is used to return the name of the source array. - """ - return self._source_array._source_name() diff --git a/dacapo/experiments/datasplits/datasets/arrays/binarize_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/binarize_array_config.py index 195c9eb16..570739f63 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/binarize_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/binarize_array_config.py @@ -1,9 +1,13 @@ import attr from .array_config import ArrayConfig -from .binarize_array import BinarizeArray +from funlib.persistence import Array from typing import List, Tuple +from dacapo.tmp import num_channels_from_array + +import dask.array as da +import numpy as np @attr.s @@ -24,8 +28,6 @@ class BinarizeArrayConfig(ArrayConfig): """ - array_type = BinarizeArray - source_array_config: ArrayConfig = attr.ib( metadata={ "help_text": "The Array from which to pull annotated data. Is expected to contain a volume with uint64 voxels and no channel dimension" @@ -46,3 +48,31 @@ class BinarizeArrayConfig(ArrayConfig): "help_text": "The id considered background. Will never be binarized to 1, defaults to 0." }, ) + + def array(self, mode="r") -> Array: + array = self.source_array_config.array(mode) + num_channels = num_channels_from_array(array) + assert num_channels is None, "Input labels cannot have a channel dimension" + + def group_array(data): + out = da.zeros((len(self.groupings), *array.physical_shape), dtype=np.uint8) + for i, (_, group_ids) in enumerate(self.groupings): + if len(group_ids) == 0: + out[i] = data != self.background + else: + out[i] = da.isin(data, group_ids) + return out + + data = group_array(array.data) + out_array = Array( + data, + array.offset, + array.voxel_size, + ["c^"] + list(array.axis_names), + units=array.units, + ) + + # callable lazy op so funlib.persistence doesn't try to recoginize this data as writable + out_array.lazy_op(lambda data: data) + + return out_array diff --git a/dacapo/experiments/datasplits/datasets/arrays/concat_array.py b/dacapo/experiments/datasplits/datasets/arrays/concat_array.py deleted file mode 100644 index b970e10b1..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/concat_array.py +++ /dev/null @@ -1,540 +0,0 @@ -from .array import Array - -from funlib.geometry import Roi - -import numpy as np - -from typing import Dict, Any -import logging - -logger = logging.getLogger(__file__) - - -class ConcatArray(Array): - """ - This is a wrapper around other `source_arrays` that concatenates - them along the channel dimension. The `source_arrays` are expected - to have the same shape and ROI, but can have different data types. - - Attributes: - name: The name of the array. - channels: The list of channel names. - source_arrays: A dictionary mapping channel names to source arrays. - default_array: An optional default array to use for channels that are - not present in `source_arrays`. - Methods: - from_toml(cls, toml_path: str) -> ConcatArrayConfig: - Load the ConcatArrayConfig from a TOML file - to_toml(self, toml_path: str) -> None: - Save the ConcatArrayConfig to a TOML file - create_array(self) -> ConcatArray: - Create the ConcatArray from the config - Note: - This class is a subclass of Array and inherits all its attributes - and methods. The only difference is that the array_type is ConcatArray. - - """ - - def __init__(self, array_config): - """ - Initialize the ConcatArray from a ConcatArrayConfig. - - Args: - array_config (ConcatArrayConfig): The config to create the ConcatArray from. - Raises: - AssertionError: If the source arrays have different shapes or ROIs. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - Note: - The `source_arrays` are expected to have the same shape and ROI, - but can have different data types. - """ - self.name = array_config.name - self.channels = array_config.channels - self.source_arrays = { - channel: source_array_config.array_type(source_array_config) - for channel, source_array_config in array_config.source_array_configs.items() - } - self.default_array = ( - array_config.default_config.array_type(array_config.default_config) - if array_config.default_config is not None - else None - ) - - @property - def attrs(self): - """ - Return the attributes of the ConcatArray as a dictionary. - - Returns: - Dict[str, Any]: The attributes of the ConcatArray. - Raises: - AssertionError: If the source arrays have different attributes. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.attrs - {'axes': 'cxyz', 'roi': Roi(...), 'voxel_size': (1, 1, 1)} - Note: - The `source_arrays` are expected to have the same attributes. - """ - return dict() - - @property - def source_arrays(self) -> Dict[str, Array]: - """ - Return the source arrays of the ConcatArray. - - Returns: - Dict[str, Array]: The source arrays of the ConcatArray. - Raises: - AssertionError: If the source arrays are empty. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.source_arrays - {'A': Array(...), 'B': Array(...)} - Note: - The `source_arrays` are expected to have the same shape and ROI. - """ - return self._source_arrays - - @source_arrays.setter - def source_arrays(self, value: Dict[str, Array]): - """ - Set the source arrays of the ConcatArray. - - Args: - value (Dict[str, Array]): The source arrays to set. - Raises: - AssertionError: If the source arrays are empty. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.source_arrays = {'A': Array(...), 'B': Array(...)} - Note: - The `source_arrays` are expected to have the same shape and ROI. - """ - assert len(value) > 0, "Source arrays is empty!" - self._source_arrays = value - attrs: Dict[str, Any] = {} - for source_array in value.values(): - axes = attrs.get("axes", source_array.axes) - assert source_array.axes == axes - assert axes[0] == "c" or "c" not in axes - attrs["axes"] = axes - roi = attrs.get("roi", source_array.roi) - assert not (not roi.empty and source_array.roi.intersect(roi).empty), ( - self.name, - [x.roi for x in self._source_arrays.values()], - ) - attrs["roi"] = source_array.roi.intersect(roi) - voxel_size = attrs.get("voxel_size", source_array.voxel_size) - assert source_array.voxel_size == voxel_size - attrs["voxel_size"] = voxel_size - self._source_array = source_array - - @property - def source_array(self) -> Array: - """ - Return the source array of the ConcatArray. - - Returns: - Array: The source array of the ConcatArray. - Raises: - AssertionError: If the source array is None. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.source_array - Array(...) - Note: - The `source_array` is expected to have the same shape and ROI. - """ - return self._source_array - - @property - def axes(self): - """ - Return the axes of the ConcatArray. - - Returns: - str: The axes of the ConcatArray. - Raises: - AssertionError: If the source arrays have different axes. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.axes - 'cxyz' - Note: - The `source_arrays` are expected to have the same axes. - """ - source_axes = self.source_array.axes - if "c" not in source_axes: - source_axes = ["c"] + source_axes - return source_axes - - @property - def dims(self): - """ - Return the dimensions of the ConcatArray. - - Returns: - Tuple[int]: The dimensions of the ConcatArray. - Raises: - AssertionError: If the source arrays have different dimensions. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.dims - (2, 100, 100, 100) - Note: - The `source_arrays` are expected to have the same dimensions. - """ - return self.source_array.dims - - @property - def voxel_size(self): - """ - Return the voxel size of the ConcatArray. - - Returns: - Tuple[float]: The voxel size of the ConcatArray. - Raises: - AssertionError: If the source arrays have different voxel sizes. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.voxel_size - (1, 1, 1) - Note: - The `source_arrays` are expected to have the same voxel size. - """ - return self.source_array.voxel_size - - @property - def roi(self): - """ - Return the ROI of the ConcatArray. - - Returns: - Roi: The ROI of the ConcatArray. - Raises: - AssertionError: If the source arrays have different ROIs. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.roi - Roi(...) - Note: - The `source_arrays` are expected to have the same ROI. - """ - return self.source_array.roi - - @property - def writable(self) -> bool: - """ - Return whether the ConcatArray is writable. - - Returns: - bool: Whether the ConcatArray is writable. - Raises: - AssertionError: If the ConcatArray is writable. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.writable - False - Note: - The ConcatArray is not writable. - """ - return False - - @property - def data(self): - """ - Return the data of the ConcatArray. - - Returns: - np.ndarray: The data of the ConcatArray. - Raises: - RuntimeError: If the ConcatArray is not writable. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.data - np.ndarray(...) - Note: - The ConcatArray is not writable. - """ - raise RuntimeError("Cannot get writable version of this data!") - - @property - def dtype(self): - """ - Return the data type of the ConcatArray. - - Returns: - np.dtype: The data type of the ConcatArray. - Raises: - AssertionError: If the source arrays have different data types. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.dtype - np.float32 - Note: - The `source_arrays` are expected to have the same data type. - """ - return self.source_array.dtype - - @property - def num_channels(self): - """ - Return the number of channels of the ConcatArray. - - Returns: - int: The number of channels of the ConcatArray. - Raises: - AssertionError: If the source arrays have different numbers of channels. - Examples: - >>> config = ConcatArrayConfig( - ... name="my_concat_array", - ... channels=["A", "B"], - ... source_array_configs={ - ... "A": ArrayConfig(...), - ... "B": ArrayConfig(...), - ... }, - ... default_config=ArrayConfig(...), - ... ) - >>> array = ConcatArray(config) - >>> array.num_channels - 2 - Note: - The `source_arrays` are expected to have the same number of channels. - """ - return len(self.channels) - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Return the data of the ConcatArray for a given ROI. - - Args: - roi (Roi): The ROI to get the data for. - Returns: - np.ndarray: The data of the ConcatArray for the given ROI. - Raises: - AssertionError: If the source arrays have different shapes or ROIs. - Examples: - >>> roi = Roi(...) - >>> array[roi] - np.ndarray(...) - Note: - The `source_arrays` are expected to have the same shape and ROI. - """ - default = ( - np.zeros_like(self.source_array[roi]) - if self.default_array is None - else self.default_array[roi] - ) - arrays = [ - ( - self.source_arrays[channel][roi] - if channel in self.source_arrays - else default - ) - for channel in self.channels - ] - shapes = [array.shape for array in arrays] - ndims = max([len(shape) for shape in shapes]) - assert ndims <= len(self.axes), f"{self.axes}, {ndims}" - shapes = [(1,) * (len(self.axes) - len(shape)) + shape for shape in shapes] - for axis_shapes in zip(*shapes): - assert max(axis_shapes) == min(axis_shapes), f"{shapes}" - arrays = [array.reshape(shapes[0]) for array in arrays] - concatenated = np.concatenate( - arrays, - axis=0, - ) - if concatenated.shape[0] == 1: - logger.info( - f"Concatenated array has only one channel: {self.name} {concatenated.shape}" - ) - return concatenated - - def _can_neuroglance(self): - """ - This method returns True if the source array can be visualized in neuroglance. - - Returns: - bool: True if the source array can be visualized in neuroglance. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._can_neuroglance() - Note: - This method is used to return True if the source array can be visualized in neuroglance. - """ - return any( - [ - source_array._can_neuroglance() - for source_array in self.source_arrays.values() - ] - ) - - def _neuroglancer_source(self): - """ - This method returns the source array for neuroglancer. - - Returns: - neuroglancer.LocalVolume: The source array for neuroglancer. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._neuroglancer_source() - Note: - This method is used to return the source array for neuroglancer. - """ - # return self._source_array._neuroglancer_source() - return [ - source_array._neuroglancer_source() - for source_array in self.source_arrays.values() - ] - - def _neuroglancer_layer(self): - """ - This method returns the neuroglancer layer for the source array. - - Returns: - neuroglancer.SegmentationLayer: The neuroglancer layer for the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._neuroglancer_layer() - Note: - This method is used to return the neuroglancer layer for the source array. - """ - # layer = neuroglancer.SegmentationLayer(source=self._neuroglancer_source()) - return [ - source_array._neuroglancer_layer() - for source_array in self.source_arrays.values() - if source_array._can_neuroglance() - ] - - def _source_name(self): - """ - This method returns the name of the source array. - - Returns: - str: The name of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._source_name() - Note: - This method is used to return the name of the source array. - """ - # return self._source_array._source_name() - return [ - source_array._source_name() - for source_array in self.source_arrays.values() - if source_array._can_neuroglance() - ] diff --git a/dacapo/experiments/datasplits/datasets/arrays/concat_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/concat_array_config.py index cc734f70b..4de730b18 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/concat_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/concat_array_config.py @@ -1,9 +1,11 @@ import attr from .array_config import ArrayConfig -from .concat_array import ConcatArray from typing import List, Dict, Optional +from funlib.persistence import Array +import numpy as np +import dask.array as da @attr.s @@ -23,8 +25,6 @@ class ConcatArrayConfig(ArrayConfig): The source array is a dictionary with the key being the channel and the value being the array. """ - array_type = ConcatArray - channels: List[str] = attr.ib( metadata={"help_text": "An ordering for the source_arrays."} ) @@ -41,3 +41,22 @@ class ConcatArrayConfig(ArrayConfig): "not provided, missing channels will simply be filled with 0s" }, ) + + def array(self, mode: str = "r") -> Array: + arrays = [config.array(mode) for _, config in self.source_array_configs.items()] + + out_array = Array( + da.zeros(len(arrays), *arrays[0].physical_shape, dtype=arrays[0].dtype), + offset=arrays[0].offset, + voxel_size=arrays[0].voxel_size, + axis_names=["c^"] + arrays[0].axis_names, + units=arrays[0].units, + ) + + def set_channels(data): + for i, array in enumerate(arrays): + data[i] = array.data[:] + return data + + out_array.lazy_op(set_channels) + return out_array diff --git a/dacapo/experiments/datasplits/datasets/arrays/constant_array.py b/dacapo/experiments/datasplits/datasets/arrays/constant_array.py deleted file mode 100644 index 411591b78..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/constant_array.py +++ /dev/null @@ -1,493 +0,0 @@ -from .array import Array - -from funlib.geometry import Roi - -import numpy as np -import neuroglancer - - -class ConstantArray(Array): - """ - This is a wrapper around another `source_array` that simply provides constant value - with the same metadata as the `source_array`. - - This is useful for creating a mask array that is the same size as the - original array, but with all values set to 1. - - Attributes: - source_array: The source array that this array is based on. - Methods: - like: Create a new ConstantArray with the same metadata as another array. - attrs: Get the attributes of the array. - axes: Get the axes of the array. - dims: Get the dimensions of the array. - voxel_size: Get the voxel size of the array. - roi: Get the region of interest of the array. - writable: Check if the array is writable. - data: Get the data of the array. - dtype: Get the data type of the array. - num_channels: Get the number of channels of the array. - __getitem__: Get a subarray of the array. - Note: - This class is not meant to be instantiated directly. Instead, use the - `like` method to create a new ConstantArray with the same metadata as - another array. - """ - - def __init__(self, array_config): - """ - Initialize the ConstantArray with the given array configuration. - - Args: - array_config: The configuration of the source array. - Raises: - RuntimeError: If the source array is not specified in the - configuration. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> source_array_config = ArrayConfig(source_array) - >>> ones_array = ConstantArray(source_array_config) - >>> ones_array.source_array - NumpyArray(data=array([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]]), voxel_size=(1.0, 1.0, 1.0), roi=Roi((0, 0, 0), (10, 10, 10)), num_channels=1) - Notes: - This class is not meant to be instantiated directly. Instead, use the - `like` method to create a new ConstantArray with the same metadata as - another array. - """ - self._source_array = array_config.source_array_config.array_type( - array_config.source_array_config - ) - self._constant = array_config.constant - - @classmethod - def like(cls, array: Array): - """ - Create a new ConstantArray with the same metadata as another array. - - Args: - array: The source array. - Returns: - The new ConstantArray with the same metadata as the source array. - Raises: - RuntimeError: If the source array is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray.like(source_array) - >>> ones_array.source_array - NumpyArray(data=array([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]]), voxel_size=(1.0, 1.0, 1.0), roi=Roi((0, 0, 0), (10, 10, 10)), num_channels=1) - Notes: - This class is not meant to be instantiated directly. Instead, use the - `like` method to create a new ConstantArray with the same metadata as - another array. - - """ - instance = cls.__new__(cls) - instance._source_array = array - return instance - - @property - def attrs(self): - """ - Get the attributes of the array. - - Returns: - An empty dictionary. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.attrs - {} - Notes: - This method is used to get the attributes of the array. The attributes - are stored as key-value pairs in a dictionary. This method returns an - empty dictionary because the ConstantArray does not have any attributes. - """ - return dict() - - @property - def source_array(self) -> Array: - """ - Get the source array that this array is based on. - - Returns: - The source array. - Raises: - RuntimeError: If the source array is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.source_array - NumpyArray(data=array([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]]), voxel_size=(1.0, 1.0, 1.0), roi=Roi((0, 0, 0), (10, 10, 10)), num_channels=1) - Notes: - This method is used to get the source array that this array is based on. - The source array is the array that the ConstantArray is created from. This - method returns the source array that was specified when the ConstantArray - was created. - """ - return self._source_array - - @property - def axes(self): - """ - Get the axes of the array. - - Returns: - The axes of the array. - Raises: - RuntimeError: If the axes are not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.axes - 'zyx' - Notes: - This method is used to get the axes of the array. The axes are the - order of the dimensions of the array. This method returns the axes of - the array that was specified when the ConstantArray was created. - """ - return self.source_array.axes - - @property - def dims(self): - """ - Get the dimensions of the array. - - Returns: - The dimensions of the array. - Raises: - RuntimeError: If the dimensions are not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.dims - (10, 10, 10) - Notes: - This method is used to get the dimensions of the array. The dimensions - are the size of the array along each axis. This method returns the - dimensions of the array that was specified when the ConstantArray was created. - """ - return self.source_array.dims - - @property - def voxel_size(self): - """ - Get the voxel size of the array. - - Returns: - The voxel size of the array. - Raises: - RuntimeError: If the voxel size is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.voxel_size - (1.0, 1.0, 1.0) - Notes: - This method is used to get the voxel size of the array. The voxel size - is the size of each voxel in the array. This method returns the voxel - size of the array that was specified when the ConstantArray was created. - """ - return self.source_array.voxel_size - - @property - def roi(self): - """ - Get the region of interest of the array. - - Returns: - The region of interest of the array. - Raises: - RuntimeError: If the region of interest is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.roi - Roi((0, 0, 0), (10, 10, 10)) - Notes: - This method is used to get the region of interest of the array. The - region of interest is the region of the array that contains the data. - This method returns the region of interest of the array that was specified - when the ConstantArray was created. - """ - return self.source_array.roi - - @property - def writable(self) -> bool: - """ - Check if the array is writable. - - Returns: - False. - Raises: - RuntimeError: If the writability of the array is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.writable - False - Notes: - This method is used to check if the array is writable. An array is - writable if it can be modified in place. This method returns False - because the ConstantArray is read-only and cannot be modified. - """ - return False - - @property - def data(self): - """ - Get the data of the array. - - Returns: - The data of the array. - Raises: - RuntimeError: If the data is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.data - array([[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]]) - Notes: - This method is used to get the data of the array. The data is the - values that are stored in the array. This method returns a subarray - of the array with all values set to 1. - """ - raise RuntimeError("Cannot get writable version of this data!") - - @property - def dtype(self): - """ - Get the data type of the array. - - Returns: - The data type of the array. - Raises: - RuntimeError: If the data type is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.dtype - - Notes: - This method is used to get the data type of the array. The data type - is the type of the values that are stored in the array. This method - returns the data type of the array that was specified when the ConstantArray - was created. - """ - return bool - - @property - def num_channels(self): - """ - Get the number of channels of the array. - - Returns: - The number of channels of the array. - Raises: - RuntimeError: If the number of channels is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> ones_array.num_channels - 1 - Notes: - This method is used to get the number of channels of the array. The - number of channels is the number of values that are stored at each - voxel in the array. This method returns the number of channels of the - array that was specified when the ConstantArray was created. - """ - return self.source_array.num_channels - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Get a subarray of the array. - - Args: - roi: The region of interest. - Returns: - A subarray of the array with all values set to 1. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ConstantArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = ConstantArray(source_array) - >>> roi = Roi((0, 0, 0), (10, 10, 10)) - >>> ones_array[roi] - array([[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]]) - Notes: - This method is used to get a subarray of the array. The subarray is - specified by the region of interest. This method returns a subarray - of the array with all values set to 1. - """ - return ( - np.ones_like(self.source_array.__getitem__(roi), dtype=bool) - * self._constant - ) - - def _can_neuroglance(self): - """ - This method returns True if the source array can be visualized in neuroglance. - - Returns: - bool: True if the source array can be visualized in neuroglance. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._can_neuroglance() - Note: - This method is used to return True if the source array can be visualized in neuroglance. - """ - return True - - def _neuroglancer_source(self): - """ - This method returns the source array for neuroglancer. - - Returns: - neuroglancer.LocalVolume: The source array for neuroglancer. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._neuroglancer_source() - Note: - This method is used to return the source array for neuroglancer. - """ - # return self._source_array._neuroglancer_source() - shape = self.source_array[self.source_array.roi].shape - return np.ones(shape, dtype=np.uint64) * self._constant - - def _combined_neuroglancer_source(self) -> neuroglancer.LocalVolume: - """ - Combines dimensions and metadata from self._source_array._neuroglancer_source() - with data from self._neuroglancer_source(). - - Returns: - neuroglancer.LocalVolume: The combined neuroglancer source. - """ - source_array_volume = self._source_array._neuroglancer_source() - result_data = self._neuroglancer_source() - - return neuroglancer.LocalVolume( - data=result_data, - dimensions=source_array_volume.dimensions, - voxel_offset=source_array_volume.voxel_offset, - ) - - def _neuroglancer_layer(self): - """ - This method returns the neuroglancer layer for the source array. - - Returns: - neuroglancer.SegmentationLayer: The neuroglancer layer for the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._neuroglancer_layer() - Note: - This method is used to return the neuroglancer layer for the source array. - """ - # layer = neuroglancer.SegmentationLayer(source=self._neuroglancer_source()) - return neuroglancer.SegmentationLayer( - source=self._combined_neuroglancer_source() - ) - - def _source_name(self): - """ - This method returns the name of the source array. - - Returns: - str: The name of the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._source_name() - Note: - This method is used to return the name of the source array. - """ - # return self._source_array._source_name() - return f"{self._constant}_of_{self.source_array._source_name()}" diff --git a/dacapo/experiments/datasplits/datasets/arrays/constant_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/constant_array_config.py index 47c2b8689..a25c24390 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/constant_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/constant_array_config.py @@ -1,7 +1,7 @@ import attr from .array_config import ArrayConfig -from .constant_array import ConstantArray +from funlib.persistence import Array @attr.s @@ -21,8 +21,6 @@ class ConstantArrayConfig(ArrayConfig): This class is a subclass of ArrayConfig. """ - array_type = ConstantArray - source_array_config: ArrayConfig = attr.ib( metadata={"help_text": "The Array that you want to copy and fill with ones."} ) @@ -30,3 +28,13 @@ class ConstantArrayConfig(ArrayConfig): constant: int = attr.ib( metadata={"help_text": "The constant value to fill the array with."}, default=1 ) + + def array(self, mode: str = "r") -> Array: + array = self.source_array_config.array(mode) + + def set_constant(array): + array[:] = self.constant + return array + + array.lazy_op(set_constant) + return array diff --git a/dacapo/experiments/datasplits/datasets/arrays/crop_array.py b/dacapo/experiments/datasplits/datasets/arrays/crop_array.py deleted file mode 100644 index 96bdad0fd..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/crop_array.py +++ /dev/null @@ -1,508 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - -import numpy as np - - -class CropArray(Array): - """ - Used to crop a larger array to a smaller array. This is useful when you - want to work with a subset of a larger array, but don't want to copy the - data. The crop is done on demand, so the data is not copied until you - actually access it. - - Attributes: - name: The name of the array. - source_array: The array to crop. - crop_roi: The region of interest to crop to. - Methods: - attrs: Returns the attributes of the source array. - axes: Returns the axes of the source array. - dims: Returns the number of dimensions of the source array. - voxel_size: Returns the voxel size of the source array. - roi: Returns the region of interest of the source array. - writable: Returns whether the array is writable. - dtype: Returns the data type of the source array. - num_channels: Returns the number of channels of the source array. - data: Returns the data of the source array. - channels: Returns the channels of the source array. - __getitem__(roi): Returns the data of the source array within the - region of interest. - _can_neuroglance(): Returns whether the source array can be viewed in - Neuroglancer. - _neuroglancer_source(): Returns the source of the source array for - Neuroglancer. - _neuroglancer_layer(): Returns the layer of the source array for - Neuroglancer. - _source_name(): Returns the name of the source array. - Note: - This class is a subclass of Array. - - - """ - - def __init__(self, array_config): - """ - Initializes the CropArray. - - Args: - array_config: The configuration of the array to crop. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - Note: - The source array configuration must be an instance of ArrayConfig. - """ - self.name = array_config.name - self._source_array = array_config.source_array_config.array_type( - array_config.source_array_config - ) - self.crop_roi = array_config.roi - - @property - def attrs(self): - """ - Returns the attributes of the source array. - - Returns: - The attributes of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.attrs - {} - Note: - The attributes are empty because the source array is not modified. - """ - return self._source_array.attrs - - @property - def axes(self): - """ - Returns the axes of the source array. - - Returns: - The axes of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.axes - 'zyx' - Note: - The axes are 'zyx' because the source array is not modified. - """ - return self._source_array.axes - - @property - def dims(self) -> int: - """ - Returns the number of dimensions of the source array. - - Returns: - The number of dimensions of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.dims - 3 - Note: - The number of dimensions is 3 because the source array is not - modified. - """ - return self._source_array.dims - - @property - def voxel_size(self) -> Coordinate: - """ - Returns the voxel size of the source array. - - Returns: - The voxel size of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.voxel_size - Coordinate(x=1.0, y=1.0, z=1.0) - Note: - The voxel size is (1.0, 1.0, 1.0) because the source array is not - modified. - """ - return self._source_array.voxel_size - - @property - def roi(self) -> Roi: - """ - Returns the region of interest of the source array. - - Returns: - The region of interest of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.roi - Roi(offset=(0, 0, 0), shape=(10, 10, 10)) - Note: - The region of interest is (0, 0, 0) with shape (10, 10, 10) - because the source array is not modified. - """ - return self.crop_roi.intersect(self._source_array.roi) - - @property - def writable(self) -> bool: - """ - Returns whether the array is writable. - - Returns: - False - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.writable - False - Note: - The array is not writable because it is a virtual array created by - modifying another array on demand. - """ - return False - - @property - def dtype(self): - """ - Returns the data type of the source array. - - Returns: - The data type of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.dtype - numpy.dtype('uint8') - Note: - The data type is uint8 because the source array is not modified. - """ - return self._source_array.dtype - - @property - def num_channels(self) -> int: - """ - Returns the number of channels of the source array. - - Returns: - The number of channels of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.num_channels - 1 - Note: - The number of channels is 1 because the source array is not - modified. - """ - return self._source_array.num_channels - - @property - def data(self): - """ - Returns the data of the source array. - - Returns: - The data of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.data - array([[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], - [[0, 0, 0, 0, 0, 0, 0, 0, 0 - """ - raise ValueError( - "Cannot get a writable view of this array because it is a virtual " - "array created by modifying another array on demand." - ) - - @property - def channels(self): - """ - Returns the channels of the source array. - - Returns: - The channels of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array.channels - 1 - Note: - The channels is 1 because the source array is not modified. - """ - return self._source_array.channels - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Returns the data of the source array within the region of interest. - - Args: - roi: The region of interest. - Returns: - The data of the source array within the region of interest. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> array_config = ArrayConfig( - ... name='array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array[Roi((0, 0, 0), (5, 5, 5))] - array([[[ - Note: - The data is the same as the source array because the source array - is not modified. - """ - assert self.roi.contains(roi) - return self._source_array[roi] - - def _can_neuroglance(self): - """ - Returns whether the source array can be viewed in Neuroglancer. - - Returns: - Whether the source array can be viewed in Neuroglancer. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array_config = ArrayConfig( - ... name='source_array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array._can_neuroglance() - False - Note: - The source array cannot be viewed in Neuroglancer because the - source array is not modified. - """ - return self._source_array._can_neuroglance() - - def _neuroglancer_source(self): - """ - Returns the source of the source array for Neuroglancer. - - Returns: - The source of the source array for Neuroglancer. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array_config = ArrayConfig( - ... name='source_array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array._neuroglancer_source() - {'source': 'source_array'} - Note: - The source is the source array because the source array is not - modified. - """ - return self._source_array._neuroglancer_source() - - def _neuroglancer_layer(self): - """ - Returns the layer of the source array for Neuroglancer. - - Returns: - The layer of the source array for Neuroglancer. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array_config = ArrayConfig( - ... name='source_array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array._neuroglancer_layer() - {'source': 'source_array', 'type': 'image'} - Note: - The layer is an image because the source array is not modified. - """ - return self._source_array._neuroglancer_layer() - - def _source_name(self): - """ - Returns the name of the source array. - - Returns: - The name of the source array. - Raises: - ValueError: If the region of interest to crop to is not within the - region of interest of the source array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import CropArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array_config = ArrayConfig( - ... name='source_array', - ... source_array_config=source_array_config, - ... roi=Roi((0, 0, 0), (10, 10, 10)) - ... ) - >>> crop_array = CropArray(array_config) - >>> crop_array._source_name() - 'source_array' - Note: - The name is the source array because the source array is not - modified. - """ - return self._source_array._source_name() diff --git a/dacapo/experiments/datasplits/datasets/arrays/crop_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/crop_array_config.py index 899120e90..d8dd8d242 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/crop_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/crop_array_config.py @@ -1,7 +1,7 @@ import attr from .array_config import ArrayConfig -from .crop_array import CropArray +from funlib.persistence import Array from funlib.geometry import Roi @@ -29,10 +29,20 @@ class CropArrayConfig(ArrayConfig): and methods. The only difference is that the array_type is CropArray. """ - array_type = CropArray - source_array_config: ArrayConfig = attr.ib( metadata={"help_text": "The Array to crop"} ) roi: Roi = attr.ib(metadata={"help_text": "The ROI for cropping"}) + + def array(self, mode: str = "r") -> Array: + source_array = self.source_array_config.array(mode) + roi_slices = getattr(source_array, "_Array__slices")(self.roi) + out_array = Array( + source_array.data[roi_slices], + self.roi.offset, + source_array.voxel_size, + source_array.axis_names, + source_array.units, + ) + return out_array diff --git a/dacapo/experiments/datasplits/datasets/arrays/dummy_array.py b/dacapo/experiments/datasplits/datasets/arrays/dummy_array.py deleted file mode 100644 index 3d23ebf05..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/dummy_array.py +++ /dev/null @@ -1,193 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - -import numpy as np - - -class DummyArray(Array): - """ - This is just a dummy array for testing. It has a shape of (100, 50, 50) and is filled with zeros. - - Attributes: - array_config (ArrayConfig): The config object for the array - Methods: - __getitem__: Returns the intensities normalized to the range (0, 1) - Notes: - The array_config must be an ArrayConfig object. - The min and max values are used to normalize the intensities. - All intensities are converted to float32. - - """ - - def __init__(self, array_config): - """ - Initializes the IntensitiesArray object - - Args: - array_config (ArrayConfig): The config object for the array - Raises: - ValueError: If the array_config is not an ArrayConfig object - Examples: - >>> array_config = ArrayConfig(...) - >>> intensities_array = IntensitiesArray(array_config) - Notes: - The array_config must be an ArrayConfig object. - """ - super().__init__() - self._data = np.zeros((100, 50, 50)) - - @property - def attrs(self): - """ - Returns the attributes of the source array - - Returns: - dict: The attributes of the source array - Raises: - ValueError: If the attributes is not a dictionary - Examples: - >>> intensities_array.attrs - {'resolution': (1.0, 1.0, 1.0), 'unit': 'micrometer'} - """ - return dict() - - @property - def axes(self): - """ - Returns the axes of the source array - - Returns: - str: The axes of the source array - Raises: - ValueError: If the axes is not a string - Examples: - >>> intensities_array.axes - 'zyx' - Notes: - The axes are the same as the source array - """ - return ["z", "y", "x"] - - @property - def dims(self): - """ - Returns the number of dimensions of the source array - - Returns: - int: The number of dimensions of the source array - Raises: - ValueError: If the dims is not an integer - Examples: - >>> intensities_array.dims - 3 - Notes: - The dims are the same as the source array - """ - return 3 - - @property - def voxel_size(self): - """ - Returns the voxel size of the source array - - Returns: - Coordinate: The voxel size of the source array - Raises: - ValueError: If the voxel size is not a Coordinate object - Examples: - >>> intensities_array.voxel_size - Coordinate(x=1.0, y=1.0, z=1.0) - Notes: - The voxel size is the same as the source array - """ - return Coordinate(1, 2, 2) - - @property - def roi(self): - """ - Returns the region of interest of the source array - - Returns: - Roi: The region of interest of the source array - Raises: - ValueError: If the roi is not a Roi object - Examples: - >>> intensities_array.roi - Roi(offset=(0, 0, 0), shape=(100, 100, 100)) - Notes: - The roi is the same as the source array - """ - return Roi((0, 0, 0), (100, 100, 100)) - - @property - def writable(self) -> bool: - """ - Returns whether the array is writable - - Returns: - bool: Whether the array is writable - Examples: - >>> intensities_array.writable - True - Notes: - The array is always writable - """ - return True - - @property - def data(self): - """ - Returns the data of the source array - - Returns: - np.ndarray: The data of the source array - Raises: - ValueError: If the data is not a numpy array - Examples: - >>> intensities_array.data - array([[[0., 0., 0., ..., 0., 0., 0.], - [0., 0., 0., ..., 0., 0., 0.], - [0., 0., 0., ..., 0., 0., 0.], - ..., - [0., 0., 0., ..., 0., 0., 0.], - [0., 0., 0., ..., 0., 0., 0.], - [0., 0., 0., ..., 0., 0., 0.]], - Notes: - The data is the same as the source array - """ - return self._data - - @property - def dtype(self): - """ - Returns the data type of the array - - Returns: - type: The data type of the array - Raises: - ValueError: If the data type is not a type - Examples: - >>> intensities_array.dtype - numpy.float32 - Notes: - The data type is the same as the source array - """ - return self._data.dtype - - @property - def num_channels(self): - """ - Returns the number of channels in the source array - - Returns: - int: The number of channels in the source array - Raises: - ValueError: If the number of channels is not an integer - Examples: - >>> intensities_array.num_channels - 1 - Notes: - The number of channels is the same as the source array - """ - return None diff --git a/dacapo/experiments/datasplits/datasets/arrays/dummy_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/dummy_array_config.py index 44632ae2b..87dc4f7da 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/dummy_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/dummy_array_config.py @@ -1,9 +1,10 @@ import attr from .array_config import ArrayConfig -from .dummy_array import DummyArray from typing import Tuple +from funlib.persistence import Array +import numpy as np @attr.s @@ -21,7 +22,8 @@ class DummyArrayConfig(ArrayConfig): """ - array_type = DummyArray + def array(self, mode: str = "r") -> Array: + return Array(np.zeros((100, 50, 50))) def verify(self) -> Tuple[bool, str]: """ diff --git a/dacapo/experiments/datasplits/datasets/arrays/dvid_array.py b/dacapo/experiments/datasplits/datasets/arrays/dvid_array.py deleted file mode 100644 index b6abc29e1..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/dvid_array.py +++ /dev/null @@ -1,427 +0,0 @@ -from .array import Array -from dacapo.ext import NoSuchModule - -try: - from neuclease.dvid import fetch_info, fetch_labelmap_voxels, fetch_raw -except ImportError: - fetch_info = NoSuchModule("neuclease.dvid.fetch_info") - fetch_labelmap_voxels = NoSuchModule("neuclease.dvid.fetch_labelmap_voxels") - -from funlib.geometry import Coordinate, Roi -import funlib.persistence - -import neuroglancer - -import lazy_property -import numpy as np - -import logging -from typing import Dict, Tuple, Any, Optional, List - -logger = logging.getLogger(__name__) - - -class DVIDArray(Array): - """ - This is a DVID array. It is a wrapper around a DVID array that provides - the necessary methods to interact with the array. It is used to fetch data - from a DVID server. The source is a tuple of three strings: the server, the UUID, - and the data name. - - DVID: data management system for terabyte-sized 3D images - - Attributes: - name (str): The name of the array - source (tuple[str, str, str]): The source of the array - Methods: - __getitem__: Returns the data from the array for a given region of interest - Notes: - The source is a tuple of three strings: the server, the UUID, and the data name. - """ - - def __init__(self, array_config): - """ - Initializes the DVIDArray object - - Args: - array_config (ArrayConfig): The config object for the array - Returns: - DVIDArray: The DVIDArray object - Raises: - ValueError: If the array_config is not an ArrayConfig object - Examples: - >>> array_config = ArrayConfig(...) - >>> dvid_array = DVIDArray(array_config) - Notes: - The array_config must be an ArrayConfig object. - - """ - super().__init__() - self.name: str = array_config.name - self.source: tuple[str, str, str] = array_config.source - - def __str__(self): - """ - Returns the string representation of the DVIDArray object - - Returns: - str: The string representation of the DVIDArray object - Raises: - ValueError: If the source is not a tuple of three strings - Examples: - >>> str(dvid_array) - DVIDArray(('server', 'UUID', 'data_name')) - Notes: - The string representation is the source of the array - """ - return f"DVIDArray({self.source})" - - def __repr__(self): - """ - Returns the string representation of the DVIDArray object - - Returns: - str: The string representation of the DVIDArray object - Raises: - ValueError: If the source is not a tuple of three strings - Examples: - >>> repr(dvid_array) - DVIDArray(('server', 'UUID', 'data_name')) - Notes: - The string representation is the source of the array - """ - return f"DVIDArray({self.source})" - - @lazy_property.LazyProperty - def attrs(self): - """ - Returns the attributes of the DVID array - - Returns: - dict: The attributes of the DVID array - Raises: - ValueError: If the attributes is not a dictionary - Examples: - >>> dvid_array.attrs - {'Extended': {'VoxelSize': (1.0, 1.0, 1.0), 'Values': [{'DataType': 'uint64'}]}, 'Extents': {'MinPoint': (0, 0, 0), 'MaxPoint': (100, 100, 100)}} - Notes: - The attributes are the same as the source array - """ - return fetch_info(*self.source) - - @property - def axes(self): - """ - Returns the axes of the DVID array - - Returns: - str: The axes of the DVID array - Raises: - ValueError: If the axes is not a string - Examples: - >>> dvid_array.axes - 'zyx' - Notes: - The axes are the same as the source array - """ - return ["c", "z", "y", "x"][-self.dims :] - - @property - def dims(self) -> int: - """ - Returns the dimensions of the DVID array - - Returns: - int: The dimensions of the DVID array - Raises: - ValueError: If the dimensions is not an integer - Examples: - >>> dvid_array.dims - 3 - Notes: - The dimensions are the same as the source array - """ - return self.voxel_size.dims - - @lazy_property.LazyProperty - def _daisy_array(self) -> funlib.persistence.Array: - """ - Returns the DVID array as a Daisy array - - Returns: - funlib.persistence.Array: The DVID array as a Daisy array - Raises: - ValueError: If the DVID array is not a Daisy array - Examples: - >>> dvid_array._daisy_array - Array(...) - Notes: - The DVID array is a Daisy array - """ - raise NotImplementedError() - - @lazy_property.LazyProperty - def voxel_size(self) -> Coordinate: - """ - Returns the voxel size of the DVID array - - Returns: - Coordinate: The voxel size of the DVID array - Raises: - ValueError: If the voxel size is not a Coordinate object - Examples: - >>> dvid_array.voxel_size - Coordinate(x=1.0, y=1.0, z=1.0) - Notes: - The voxel size is the same as the source array - """ - return Coordinate(self.attrs["Extended"]["VoxelSize"]) - - @lazy_property.LazyProperty - def roi(self) -> Roi: - """ - Returns the region of interest of the DVID array - - Returns: - Roi: The region of interest of the DVID array - Raises: - ValueError: If the region of interest is not a Roi object - Examples: - >>> dvid_array.roi - Roi(...) - Notes: - The region of interest is the same as the source array - """ - return Roi( - Coordinate(self.attrs["Extents"]["MinPoint"]) * self.voxel_size, - Coordinate(self.attrs["Extents"]["MaxPoint"]) * self.voxel_size, - ) - return Roi( - Coordinate(self.attrs["Extents"]["MinPoint"]) * self.voxel_size, - Coordinate(self.attrs["Extents"]["MaxPoint"]) * self.voxel_size, - ) - - @property - def writable(self) -> bool: - """ - Returns whether the DVID array is writable - - Returns: - bool: Whether the DVID array is writable - Raises: - ValueError: If the writable is not a boolean - Examples: - >>> dvid_array.writable - False - Notes: - The writable is the same as the source array - """ - return False - - @property - def dtype(self) -> Any: - """ - Returns the data type of the DVID array - - Returns: - type: The data type of the DVID array - Raises: - ValueError: If the data type is not a type - Examples: - >>> dvid_array.dtype - numpy.uint64 - Notes: - The data type is the same as the source array - """ - return np.dtype(self.attrs["Extended"]["Values"][0]["DataType"]) - - @property - def num_channels(self) -> Optional[int]: - """ - Returns the number of channels of the DVID array - - Returns: - int: The number of channels of the DVID array - Raises: - ValueError: If the number of channels is not an integer - Examples: - >>> dvid_array.num_channels - 1 - Notes: - The number of channels is the same as the source array - """ - return None - - @property - def spatial_axes(self) -> List[str]: - """ - Returns the spatial axes of the DVID array - - Returns: - List[str]: The spatial axes of the DVID array - Raises: - ValueError: If the spatial axes is not a list - Examples: - >>> dvid_array.spatial_axes - ['z', 'y', 'x'] - Notes: - The spatial axes are the same as the source array - """ - return [ax for ax in self.axes if ax not in set(["c", "b"])] - - @property - def data(self) -> Any: - """ - Returns the number of channels of the DVID array - - Returns: - int: The number of channels of the DVID array - Raises: - ValueError: If the number of channels is not an integer - Examples: - >>> dvid_array.num_channels - 1 - Notes: - The number of channels is the same as the source array - """ - raise NotImplementedError() - - def __getitem__(self, roi: Roi) -> np.ndarray[Any, Any]: - """ - Returns the data of the DVID array for a given region of interest - - Args: - roi (Roi): The region of interest for which to get the data - Returns: - np.ndarray: The data of the DVID array for the region of interest - Raises: - ValueError: If the data is not a numpy array - Examples: - >>> dvid_array[roi] - array([[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], [[0.7, 0.8, 0.9], [1.0, 1.1, 1.2]]]) - Notes: - The data is the same as the source array - """ - box = np.array( - (roi.offset / self.voxel_size, (roi.offset + roi.shape) / self.voxel_size) - ) - if self.source[2] == "grayscale": - data = fetch_raw(*self.source, box) - elif self.source[2] == "segmentation": - data = fetch_labelmap_voxels(*self.source, box) - else: - raise Exception(self.source) - return data - - def _can_neuroglance(self) -> bool: - """ - Returns whether the DVID array can be used with neuroglance - - Returns: - bool: Whether the DVID array can be used with neuroglance - Raises: - ValueError: If the DVID array cannot be used with neuroglance - Examples: - >>> dvid_array._can_neuroglance() - True - Notes: - The DVID array can be used with neuroglance - """ - return True - - def _neuroglancer_source(self): - """ - Returns the neuroglancer source of the DVID array - - Returns: - Tuple[str, str, str]: The neuroglancer source of the DVID array - Raises: - ValueError: If the neuroglancer source is not a tuple of three strings - Examples: - >>> dvid_array._neuroglancer_source() - ('server', 'UUID', 'data_name') - Notes: - The neuroglancer source is the same as the source array - """ - raise NotImplementedError() - - def _neuroglancer_layer(self) -> Tuple[neuroglancer.ImageLayer, Dict[str, Any]]: - """ - Returns the neuroglancer layer of the DVID array - - Returns: - Tuple[neuroglancer.ImageLayer, dict]: The neuroglancer layer of the DVID array - Raises: - ValueError: If the neuroglancer layer is not a tuple of an ImageLayer and a dictionary - Examples: - >>> dvid_array._neuroglancer_layer() - (ImageLayer(...), {}) - Notes: - The neuroglancer layer is the same as the source array - """ - raise NotImplementedError() - - def _transform_matrix(self): - """ - Returns the transformation matrix of the DVID array - - Returns: - np.ndarray: The transformation matrix of the DVID array - Raises: - ValueError: If the transformation matrix is not a numpy array - Examples: - >>> dvid_array._transform_matrix() - array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) - Notes: - The transformation matrix is the same as the source array - """ - raise NotImplementedError() - - def _output_dimensions(self) -> Dict[str, Tuple[float, str]]: - """ - Returns the output dimensions of the DVID array - - Returns: - dict: The output dimensions of the DVID array - Raises: - ValueError: If the output dimensions is not a dictionary - Examples: - >>> dvid_array._output_dimensions() - {'z': (100, 'nm'), 'y': (100, 'nm'), 'x': (100, 'nm')} - Notes: - The output dimensions are the same as the source array - """ - raise NotImplementedError() - - def _source_name(self) -> str: - """ - Returns the source name of the DVID array - - Returns: - str: The source name of the DVID array - Raises: - ValueError: If the source name is not a string - Examples: - >>> dvid_array._source_name() - 'data_name' - Notes: - The source name is the same as the source array - """ - raise NotImplementedError() - - def add_metadata(self, metadata: Dict[str, Any]) -> None: - """ - Adds metadata to the DVID array - - Args: - metadata (dict): The metadata to add to the DVID array - Returns: - None - Raises: - ValueError: If the metadata is not a dictionary - Examples: - >>> dvid_array.add_metadata({'description': 'This is a DVID array'}) - Notes: - The metadata is added to the source array - """ - raise NotImplementedError() diff --git a/dacapo/experiments/datasplits/datasets/arrays/dvid_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/dvid_array_config.py index db63e2750..192849d24 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/dvid_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/dvid_array_config.py @@ -1,7 +1,8 @@ import attr from .array_config import ArrayConfig -from .dvid_array import DVIDArray + +from funlib.persistence import Array from typing import Tuple @@ -21,12 +22,34 @@ class DVIDArrayConfig(ArrayConfig): """ - array_type = DVIDArray - source: Tuple[str, str, str] = attr.ib( metadata={"help_text": "The source strings."} ) + def array(self, mode: str = "r") -> Array: + # DVID can't be easily wrapped in a dask array as far as I can tell + # To handle this case we would need to subclass `funlib.persistence.Array` to + # directly read from DVID + raise NotImplementedError("NotImplemented") + """ + from dacapo.ext import NoSuchModule + + try: + from neuclease.dvid import fetch_info, fetch_labelmap_voxels, fetch_raw + except ImportError: + fetch_info = NoSuchModule("neuclease.dvid.fetch_info") + fetch_labelmap_voxels = NoSuchModule("neuclease.dvid.fetch_labelmap_voxels") + + attrs = fetch_info(*self.source) + voxel_size = Coordinate(attrs["Extended"]["VoxelSize"]) + roi = Roi( + Coordinate(attrs["Extents"]["MinPoint"]) * voxel_size, + Coordinate(attrs["Extents"]["MaxPoint"]) * voxel_size, + ) + dtype = np.dtype(self.attrs["Extended"]["Values"][0]["DataType"]) + raise NotImplementedError + """ + def verify(self) -> Tuple[bool, str]: """ Check whether this is a valid Array diff --git a/dacapo/experiments/datasplits/datasets/arrays/intensity_array.py b/dacapo/experiments/datasplits/datasets/arrays/intensity_array.py deleted file mode 100644 index 7c1365106..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/intensity_array.py +++ /dev/null @@ -1,290 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - -import numpy as np - - -class IntensitiesArray(Array): - """ - This is wrapper another array that will normalize intensities to - the range (0, 1) and convert to float32. Use this if you have your - intensities stored as uint8 or similar and want your model to - have floats as input. - - Attributes: - array_config (ArrayConfig): The config object for the array - min (float): The minimum intensity value in the array - max (float): The maximum intensity value in the array - Methods: - __getitem__: Returns the intensities normalized to the range (0, 1) - Notes: - The array_config must be an ArrayConfig object. - The min and max values are used to normalize the intensities. - All intensities are converted to float32. - """ - - def __init__(self, array_config): - """ - Initializes the IntensitiesArray object - - Args: - array_config (ArrayConfig): The config object for the array - Raises: - ValueError: If the array_config is not an ArrayConfig object - Examples: - >>> array_config = ArrayConfig(...) - >>> intensities_array = IntensitiesArray(array_config) - Notes: - The array_config must be an ArrayConfig object. - """ - self.name = array_config.name - self._source_array = array_config.source_array_config.array_type( - array_config.source_array_config - ) - - self._min = array_config.min - self._max = array_config.max - - @property - def attrs(self): - """ - Returns the attributes of the source array - - Returns: - dict: The attributes of the source array - Raises: - ValueError: If the attributes is not a dictionary - Examples: - >>> intensities_array.attrs - {'resolution': (1.0, 1.0, 1.0), 'unit': 'micrometer'} - Notes: - The attributes are the same as the source array - """ - return self._source_array.attrs - - @property - def axes(self): - """ - Returns the axes of the source array - - Returns: - str: The axes of the source array - Raises: - ValueError: If the axes is not a string - Examples: - >>> intensities_array.axes - 'zyx' - Notes: - The axes are the same as the source array - """ - return self._source_array.axes - - @property - def dims(self) -> int: - """ - Returns the dimensions of the source array - - Returns: - int: The dimensions of the source array - Raises: - ValueError: If the dimensions is not an integer - Examples: - >>> intensities_array.dims - 3 - Notes: - The dimensions are the same as the source array - """ - return self._source_array.dims - - @property - def voxel_size(self) -> Coordinate: - """ - Returns the voxel size of the source array - - Returns: - Coordinate: The voxel size of the source array - Raises: - ValueError: If the voxel size is not a Coordinate object - Examples: - >>> intensities_array.voxel_size - Coordinate(x=1.0, y=1.0, z=1.0) - Notes: - The voxel size is the same as the source array - """ - return self._source_array.voxel_size - - @property - def roi(self) -> Roi: - """ - Returns the region of interest of the source array - - Returns: - Roi: The region of interest of the source array - Raises: - ValueError: If the region of interest is not a Roi object - Examples: - >>> intensities_array.roi - Roi(offset=(0, 0, 0), shape=(10, 20, 30)) - Notes: - The region of interest is the same as the source array - """ - return self._source_array.roi - - @property - def writable(self) -> bool: - """ - Returns whether the array is writable - - Returns: - bool: Whether the array is writable - Raises: - ValueError: If the array is not writable - Examples: - >>> intensities_array.writable - False - Notes: - The array is not writable because it is a virtual array created by modifying another array on demand. - """ - return False - - @property - def dtype(self): - """ - Returns the data type of the array - - Returns: - type: The data type of the array - Raises: - ValueError: If the data type is not a type - Examples: - >>> intensities_array.dtype - numpy.float32 - Notes: - The data type is always float32 - """ - return np.float32 - - @property - def num_channels(self) -> int: - """ - Returns the number of channels in the source array - - Returns: - int: The number of channels in the source array - Raises: - ValueError: If the number of channels is not an integer - Examples: - >>> intensities_array.num_channels - 3 - Notes: - The number of channels is the same as the source array - """ - return self._source_array.num_channels - - @property - def data(self): - """ - Returns the data of the source array - - Returns: - np.ndarray: The data of the source array - Raises: - ValueError: If the data is not a numpy array - Examples: - >>> intensities_array.data - array([[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], [[0.7, 0.8, 0.9], [1.0, 1.1, 1.2]]]) - Notes: - The data is the same as the source array - """ - raise ValueError( - "Cannot get a writable view of this array because it is a virtual " - "array created by modifying another array on demand." - ) - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Returns the intensities normalized to the range (0, 1) - - Args: - roi (Roi): The region of interest to get the intensities from - Returns: - np.ndarray: The intensities normalized to the range (0, 1) - Raises: - ValueError: If the intensities are not in the range (0, 1) - Examples: - >>> intensities_array[roi] - array([[[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]], [[0.7, 0.8, 0.9], [1.0, 1.1, 1.2]]]) - Notes: - The intensities are normalized to the range (0, 1) - """ - intensities = self._source_array[roi] - normalized = (intensities.astype(np.float32) - self._min) / ( - self._max - self._min - ) - return normalized - - def _can_neuroglance(self): - """ - Returns whether the array can be visualized with Neuroglancer - - Returns: - bool: Whether the array can be visualized with Neuroglancer - Raises: - ValueError: If the array cannot be visualized with Neuroglancer - Examples: - >>> intensities_array._can_neuroglance() - True - Notes: - The array can be visualized with Neuroglancer if the source array can be visualized with Neuroglancer - - """ - return self._source_array._can_neuroglance() - - def _neuroglancer_layer(self): - """ - Returns the Neuroglancer layer of the source array - - Returns: - dict: The Neuroglancer layer of the source array - Raises: - ValueError: If the Neuroglancer layer is not a dictionary - Examples: - >>> intensities_array._neuroglancer_layer() - {'type': 'image', 'source': 'precomputed://https://mybucket.s3.amazonaws.com/mydata'} - Notes: - The Neuroglancer layer is the same as the source array - """ - return self._source_array._neuroglancer_layer() - - def _source_name(self): - """ - Returns the name of the source array - - Returns: - str: The name of the source array - Raises: - ValueError: If the name is not a string - Examples: - >>> intensities_array._source_name() - 'mydata' - Notes: - The name is the same as the source array - """ - return self._source_array._source_name() - - def _neuroglancer_source(self): - """ - Returns the Neuroglancer source of the source array - - Returns: - str: The Neuroglancer source of the source array - Raises: - ValueError: If the Neuroglancer source is not a string - Examples: - >>> intensities_array._neuroglancer_source() - 'precomputed://https://mybucket.s3.amazonaws.com/mydata' - Notes: - The Neuroglancer source is the same as the source array - """ - return self._source_array._neuroglancer_source() diff --git a/dacapo/experiments/datasplits/datasets/arrays/intensity_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/intensity_array_config.py index 7ea13385c..98a787260 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/intensity_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/intensity_array_config.py @@ -1,7 +1,7 @@ import attr +from funlib.persistence import Array from .array_config import ArrayConfig -from .intensity_array import IntensitiesArray @attr.s @@ -21,8 +21,6 @@ class IntensitiesArrayConfig(ArrayConfig): The source_array_config must be an ArrayConfig object. """ - array_type = IntensitiesArray - source_array_config: ArrayConfig = attr.ib( metadata={ "help_text": "The Array from which to pull annotated data. Is expected to contain a volume with uint64 voxels and no channel dimension" @@ -31,3 +29,8 @@ class IntensitiesArrayConfig(ArrayConfig): min: float = attr.ib(metadata={"help_text": "The minimum intensity in your data"}) max: float = attr.ib(metadata={"help_text": "The maximum intensity in your data"}) + + def array(self, mode: str = "r") -> Array: + array = self.source_array_config.array(mode) + array.lazy_op(lambda data: (data - self.min) / (self.max - self.min)) + return array diff --git a/dacapo/experiments/datasplits/datasets/arrays/logical_or_array.py b/dacapo/experiments/datasplits/datasets/arrays/logical_or_array.py deleted file mode 100644 index 580f54d63..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/logical_or_array.py +++ /dev/null @@ -1,688 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - - -import neuroglancer - -import numpy as np - - -class LogicalOrArray(Array): - """ - Array that computes the logical OR of the instances in a list of source arrays. - - Attributes: - name: str - The name of the array - source_array: Array - The source array from which to take the logical OR - Methods: - axes: () -> List[str] - Get the axes of the array - dims: () -> int - Get the number of dimensions of the array - voxel_size: () -> Coordinate - Get the voxel size of the array - roi: () -> Roi - Get the region of interest of the array - writable: () -> bool - Get whether the array is writable - dtype: () -> type - Get the data type of the array - num_channels: () -> int - Get the number of channels in the array - data: () -> np.ndarray - Get the data of the array - attrs: () -> dict - Get the attributes of the array - __getitem__: (roi: Roi) -> np.ndarray - Get the data of the array in the region of interest - _can_neuroglance: () -> bool - Get whether the array can be visualized in neuroglance - _neuroglancer_source: () -> dict - Get the neuroglancer source of the array - _neuroglancer_layer: () -> Tuple[neuroglancer.Layer, dict] - Get the neuroglancer layer of the array - _source_name: () -> str - Get the name of the source array - Notes: - The LogicalOrArray class is used to create a LogicalOrArray. The LogicalOrArray - class is a subclass of the Array class. - """ - - def __init__(self, array_config): - """ - Create a LogicalOrArray instance from a configuration - Args: - array_config: MergeInstancesArrayConfig - The configuration for the array - Returns: - LogicalOrArray - The LogicalOrArray instance created from the configuration - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.name - 'logical_or' - >>> array.source_array.name - 'mask1' - >>> array.source_array.mask_id - 1 - Notes: - The create_array method is used to create a LogicalOrArray instance from a - configuration. The LogicalOrArray instance is created by taking the logical OR - of the instances in the source arrays. - """ - self.name = array_config.name - self._source_array = array_config.source_array_config.array_type( - array_config.source_array_config - ) - - @property - def axes(self): - """ - Get the axes of the array - - Returns: - List[str]: The axes of the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.axes - ['x', 'y', 'z'] - Notes: - The axes method is used to get the axes of the array. The axes are the dimensions - of the array. - """ - return [x for x in self._source_array.axes if x != "c"] - - @property - def dims(self) -> int: - """ - Get the number of dimensions of the array - - Returns: - int: The number of dimensions of the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.dims - 3 - Notes: - The dims method is used to get the number of dimensions of the array. The number - of dimensions is the number of axes of the array. - """ - return self._source_array.dims - - @property - def voxel_size(self) -> Coordinate: - """ - Get the voxel size of the array - - Returns: - Coordinate: The voxel size of the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.voxel_size - Coordinate(x=1.0, y=1.0, z=1.0) - Notes: - The voxel_size method is used to get the voxel size of the array. The voxel size - is the size of a voxel in the array. - - """ - return self._source_array.voxel_size - - @property - def roi(self) -> Roi: - """ - Get the region of interest of the array - - Returns: - Roi: The region of interest of the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.roi - Roi(offset=(0, 0, 0), shape=(10, 10, 10)) - Notes: - The roi method is used to get the region of interest of the array. The region of - interest is the shape and offset of the array. - """ - return self._source_array.roi - - @property - def writable(self) -> bool: - """ - Get whether the array is writable - - Returns: - bool: Whether the array is writable - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.writable - False - Notes: - The writable method is used to get whether the array is writable. An array is - writable if it can be modified. - """ - return False - - @property - def dtype(self): - """ - Get the data type of the array - - Returns: - type: The data type of the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.dtype - - Notes: - The dtype method is used to get the data type of the array. The data type is the - type of the data in the array. - """ - return np.uint8 - - @property - def num_channels(self): - """ - Get the number of channels in the array - - Returns: - int: The number of channels in the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.num_channels - 1 - Notes: - The num_channels method is used to get the number of channels in the array. The - number of channels is the number of channels in the array. - """ - return None - - @property - def data(self): - """ - Get the data of the array - - Returns: - np.ndarray: The data of the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.data - array([[[1, 1, 1, ..., 1, 1, 1], - [1, 1, 1, ..., 1, 1, 1], - [1, 1, 1, ..., 1, 1, 1], - ..., - [1, 1, 1, ..., 1, 1, 1], - [1, 1, 1, ..., 1, 1, 1], - [1, 1, 1, ..., 1, 1, 1]]], dtype=uint8) - Notes: - The data method is used to get the data of the array. The data is the content of - the array. - - """ - raise ValueError( - "Cannot get a writable view of this array because it is a virtual " - "array created by modifying another array on demand." - ) - - @property - def attrs(self): - """ - Get the attributes of the array - - Returns: - dict: The attributes of the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array.attrs - {'name': 'logical_or'} - Notes: - The attrs method is used to get the attributes of the array. The attributes are - the metadata of the array. - """ - return self._source_array.attrs - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Get the data of the array in the region of interest - - Args: - roi: Roi - The region of interest of the array - Returns: - np.ndarray: The data of the array in the region of interest - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> roi = Roi((0, 0, 0), (10, 10, 10)) - >>> array[roi] - array([[[1, 1, 1, ..., 1, 1, 1], - [1, 1, 1, ..., 1, 1, 1], - [1, 1, 1, ..., 1, 1, 1], - ..., - [1, 1, 1, ..., 1, 1, 1], - [1, 1, 1, ..., 1, 1, 1], - [1, 1, 1, ..., 1, 1, 1]]], dtype=uint8) - Notes: - The __getitem__ method is used to get the data of the array in the region of interest. - The data is the content of the array in the region of interest. - """ - mask = self._source_array[roi] - if "c" in self._source_array.axes: - mask = np.max(mask, axis=self._source_array.axes.index("c")) - return mask - - def _can_neuroglance(self): - """ - Get whether the array can be visualized in neuroglance - - Returns: - bool: Whether the array can be visualized in neuroglance - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array._can_neuroglance() - True - Notes: - The _can_neuroglance method is used to get whether the array can be visualized - in neuroglance. - """ - return self._source_array._can_neuroglance() - - def _neuroglancer_source(self): - """ - Get the neuroglancer source of the array - - Returns: - dict: The neuroglancer source of the array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array._neuroglancer_source() - {'source': 'precomputed://https://mybucket.storage.googleapis.com/path/to/logical_or'} - Notes: - The _neuroglancer_source method is used to get the neuroglancer source of the array. - The neuroglancer source is the source that is displayed in the neuroglancer viewer. - """ - # source_arrays - if hassattr(self._source_array, "source_arrays"): - source_arrays = list(self._source_array.source_arrays) - # apply logical or - mask = np.logical_or.reduce(source_arrays) - return mask - return self._source_array._neuroglancer_source() - - def _combined_neuroglancer_source(self) -> neuroglancer.LocalVolume: - """ - Combines dimensions and metadata from self._source_array._neuroglancer_source() - with data from self._neuroglancer_source(). - - Returns: - neuroglancer.LocalVolume: The combined neuroglancer source. - """ - source_array_volume = self._source_array._neuroglancer_source() - if isinstance(source_array_volume, list): - source_array_volume = source_array_volume[0] - result_data = self._neuroglancer_source() - - return neuroglancer.LocalVolume( - data=result_data, - dimensions=source_array_volume.dimensions, - voxel_offset=source_array_volume.voxel_offset, - ) - - def _neuroglancer_layer(self): - """ - This method returns the neuroglancer layer for the source array. - - Returns: - neuroglancer.SegmentationLayer: The neuroglancer layer for the source array. - Raises: - ValueError: If the source array is not writable. - Examples: - >>> binarize_array._neuroglancer_layer() - Note: - This method is used to return the neuroglancer layer for the source array. - """ - # layer = neuroglancer.SegmentationLayer(source=self._neuroglancer_source()) - return neuroglancer.SegmentationLayer( - source=self._combined_neuroglancer_source() - ) - - def _source_name(self): - """ - Get the name of the source array - - Returns: - str: The name of the source array - Raises: - ValueError: If the array is not writable - Examples: - >>> array_config = MergeInstancesArrayConfig( - ... name="logical_or", - ... source_array_configs=[ - ... ArrayConfig( - ... name="mask1", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask1", - ... mask_id=1, - ... ), - ... ), - ... ArrayConfig( - ... name="mask2", - ... array_type=MaskArray, - ... source_array_config=MaskArrayConfig( - ... name="mask2", - ... mask_id=2, - ... ), - ... ), - ... ], - ... ) - >>> array = array_config.create_array() - >>> array._source_name() - 'mask1' - Notes: - The _source_name method is used to get the name of the source array. The name - of the source array is the name of the array that is being modified. - """ - name = self._source_array._source_name() - if isinstance(name, list): - name = "_".join(name) - return "logical_or" + name diff --git a/dacapo/experiments/datasplits/datasets/arrays/logical_or_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/logical_or_array_config.py index a22591405..a9cde5daa 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/logical_or_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/logical_or_array_config.py @@ -1,7 +1,9 @@ import attr from .array_config import ArrayConfig -from .logical_or_array import LogicalOrArray +from funlib.persistence import Array +import dask.array as da +from dacapo.tmp import num_channels_from_array @attr.s @@ -18,8 +20,25 @@ class LogicalOrArrayConfig(ArrayConfig): The source_array_config must be an ArrayConfig object. """ - array_type = LogicalOrArray - source_array_config: ArrayConfig = attr.ib( metadata={"help_text": "The Array of masks from which to take the union"} ) + + def array(self, mode: str = "r") -> Array: + array = self.source_array_config.array(mode) + + assert num_channels_from_array(array) is not None + + out_array = Array( + da.zeros(*array.physical_shape, dtype=array.dtype), + offset=array.offset, + voxel_size=array.voxel_size, + axis_names=array.axis_names[1:], + units=array.units, + ) + + out_array.data = da.maximum(array.data, axis=0) + + # mark data as non-writable + out_array.lazy_op(lambda data: data) + return out_array diff --git a/dacapo/experiments/datasplits/datasets/arrays/merge_instances_array.py b/dacapo/experiments/datasplits/datasets/arrays/merge_instances_array.py deleted file mode 100644 index 4a36efc29..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/merge_instances_array.py +++ /dev/null @@ -1,641 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - - -import neuroglancer - -import numpy as np - - -class MergeInstancesArray(Array): - """ - This array merges multiple source arrays into a single array by summing them. This is useful for merging - instance segmentation arrays into a single array. NeuoGlancer will display each instance as a different color. - - Attributes: - name : str - The name of the array - source_array_configs : List[ArrayConfig] - A list of source arrays to merge - Methods: - __getitem__(roi: Roi) -> np.ndarray - Returns a numpy array with the requested region of interest - _can_neuroglance() -> bool - Returns True if the array can be visualized in neuroglancer - _neuroglancer_source() -> str - Returns the source name for the array in neuroglancer - _neuroglancer_layer() -> Tuple[neuroglancer.SegmentationLayer, Dict[str, Any]] - Returns a neuroglancer layer and its configuration - _source_name() -> str - Returns the source name for the array - Note: - This array is not writable - Source arrays must have the same shape. - - """ - - def __init__(self, array_config): - """ - Constructor for MergeInstancesArray - - Args: - array_config : MergeInstancesArrayConfig - The configuration for the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - ``` - Note: - This example shows how to create a MergeInstancesArray object - """ - self.name = array_config.name - self._source_arrays = [ - source_config.array_type(source_config) - for source_config in array_config.source_array_configs - ] - self._source_array = self._source_arrays[0] - - @property - def axes(self): - """ - Returns the axes of the array - - Returns: - List[str]: The axes of the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - axes = array.axes - ``` - Note: - This example shows how to get the axes of the array - - """ - return [x for x in self._source_array.axes if x != "c"] - - @property - def dims(self) -> int: - """ - Returns the number of dimensions of the array - - Returns: - int: The number of dimensions of the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - dims = array.dims - ``` - Note: - This example shows how to get the number of dimensions of the array - - - """ - return self._source_array.dims - - @property - def voxel_size(self) -> Coordinate: - """ - Returns the voxel size of the array - - Returns: - Coordinate: The voxel size of the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - voxel_size = array.voxel_size - ``` - Note: - This example shows how to get the voxel size of the array - """ - return self._source_array.voxel_size - - @property - def roi(self) -> Roi: - """ - Returns the region of interest of the array - - Returns: - Roi: The region of interest of the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - roi = array.roi - ``` - Note: - This example shows how to get the region of interest of the array - """ - return self._source_array.roi - - @property - def writable(self) -> bool: - """ - Returns True if the array is writable, False otherwise - - Returns: - bool: True if the array is writable, False otherwise - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - writable = array.writable - ``` - Note: - This example shows how to check if the array is writable - """ - return False - - @property - def dtype(self): - """ - Returns the data type of the array - - Returns: - np.dtype: The data type of the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - dtype = array.dtype - ``` - Note: - This example shows how to get the data type of the array - """ - return np.uint8 - - @property - def num_channels(self): - """ - Returns the number of channels of the array - - Returns: - int: The number of channels of the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - num_channels = array.num_channels - ``` - Note: - This example shows how to get the number of channels of the array - """ - return None - - @property - def data(self): - """ - Returns the data of the array - - Returns: - np.ndarray: The data of the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - data = array.data - ``` - Note: - This example shows how to get the data of the array - """ - raise ValueError( - "Cannot get a writable view of this array because it is a virtual " - "array created by modifying another array on demand." - ) - - @property - def attrs(self): - """ - Returns the attributes of the array - - Returns: - Dict[str, Any]: The attributes of the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - attributes = array.attrs - ``` - Note: - This example shows how to get the attributes of the array - """ - return self._source_array.attrs - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Returns a numpy array with the requested region of interest - - Args: - roi : Roi - The region of interest to get - Returns: - np.ndarray: A numpy array with the requested region of interest - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - roi = Roi((0, 0, 0), (100, 100, 100)) - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - array_data = array[roi] - ``` - Note: - This example shows how to get a numpy array with the requested region of interest - """ - arrays = [source_array[roi] for source_array in self._source_arrays] - offset = 0 - for array in arrays: - array[array > 0] += offset - offset = array.max() - return np.sum(arrays, axis=0) - - def _can_neuroglance(self): - """ - Returns True if the array can be visualized in neuroglancer, False otherwise - - Returns: - bool: True if the array can be visualized in neuroglancer, False otherwise - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - can_neuroglance = array._can_neuroglance() - ``` - Note: - This example shows how to check if the array can be visualized in neuroglancer - """ - return self._source_array._can_neuroglance() - - def _neuroglancer_source(self): - """ - Returns the source name for the array in neuroglancer - - Returns: - str: The source name for the array in neuroglancer - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - source = array._neuroglancer_source() - ``` - Note: - This example shows how to get the source name for the array in neuroglancer - """ - return self._source_array._neuroglancer_source() - - def _neuroglancer_layer(self): - """ - Returns a neuroglancer layer and its configuration - - Returns: - Tuple[neuroglancer.SegmentationLayer, Dict[str, Any]]: A neuroglancer layer and its configuration - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - layer, kwargs = array._neuroglancer_layer() - ``` - Note: - This example shows how to get a neuroglancer layer and its configuration - """ - # Generates an Segmentation layer - - layer = neuroglancer.SegmentationLayer(source=self._neuroglancer_source()) - kwargs = { - "visible": False, - } - return layer, kwargs - - def _source_name(self): - """ - Returns the source name for the array - - Returns: - str: The source name for the array - Raises: - ValueError: If the source arrays have different shapes - Example: - ```python - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArray - from dacapo.experiments.datasplits.datasets.arrays import MergeInstancesArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - from dacapo.experiments.datasplits.datasets.arrays import ArrayType - from funlib.geometry import Coordinate, Roi - array_config = MergeInstancesArrayConfig( - name="array", - source_array_configs=[ - ArrayConfig( - name="array1", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array1.h5", - ), - ArrayConfig( - name="array2", - array_type=ArrayType.INSTANCE_SEGMENTATION, - path="path/to/array2.h5", - ), - ], - ) - array = MergeInstancesArray(array_config) - source_name = array._source_name() - ``` - Note: - This example shows how to get the source name for the array - """ - return self._source_array._source_name() diff --git a/dacapo/experiments/datasplits/datasets/arrays/merge_instances_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/merge_instances_array_config.py index d7a523215..6f51b529b 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/merge_instances_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/merge_instances_array_config.py @@ -1,10 +1,11 @@ import attr from .array_config import ArrayConfig -from .merge_instances_array import MergeInstancesArray - +from funlib.persistence import Array from typing import List +import dask.array as da + @attr.s class MergeInstancesArrayConfig(ArrayConfig): @@ -23,8 +24,19 @@ class MergeInstancesArrayConfig(ArrayConfig): The MergeInstancesArrayConfig class is used to create a MergeInstancesArray """ - array_type = MergeInstancesArray - source_array_configs: List[ArrayConfig] = attr.ib( metadata={"help_text": "The Array of masks from which to take the union"} ) + + def array(self, mode: str = "r") -> Array: + arrays = [ + source_array.array(mode) for source_array in self.source_array_configs + ] + merged_data = da.stack([array.data for array in arrays], axis=0).sum(axis=0) + return Array( + data=merged_data, + offset=arrays[0].offset, + voxel_size=arrays[0].voxel_size, + axis_names=arrays[0].axis_names, + units=arrays[0].units, + ) diff --git a/dacapo/experiments/datasplits/datasets/arrays/missing_annotations_mask.py b/dacapo/experiments/datasplits/datasets/arrays/missing_annotations_mask.py deleted file mode 100644 index aaf59cb69..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/missing_annotations_mask.py +++ /dev/null @@ -1,366 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - -from fibsem_tools.metadata.groundtruth import LabelList - -import neuroglancer - -import numpy as np - - -class MissingAnnotationsMask(Array): - """ - This is wrapper around a ZarrArray containing uint annotations. - Complementary to the BinarizeArray class where we convert labels - into individual channels for training, we may find crops where a - specific label is present, but not annotated. In that case you - might want to avoid training specific channels for specific - training volumes. - See package fibsem_tools for appropriate metadata format for indicating - presence of labels in your ground truth. - "https://github.com/janelia-cosem/fibsem-tools" - - Attributes: - array_config: A BinarizeArrayConfig object - Methods: - __getitem__(roi: Roi) -> np.ndarray: Returns a binary mask of the - annotations that are present but not annotated. - Note: - This class is not meant to be used directly. It is used by the - BinarizeArray class to mask out annotations that are present but - not annotated. - """ - - def __init__(self, array_config): - """ - Initializes the MissingAnnotationsMask class - - Args: - array_config (BinarizeArrayConfig): A BinarizeArrayConfig object - Raises: - AssertionError: If the source array has channels - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> missing_annotations_mask = MissingAnnotationsMask(MissingAnnotationsMaskConfig(source_array, groupings)) - Notes: - This is a helper function for the BinarizeArray class - """ - self.name = array_config.name - self._source_array = array_config.source_array_config.array_type( - array_config.source_array_config - ) - - assert ( - "c" not in self._source_array.axes - ), "Cannot initialize a BinarizeArray with a source array with channels" - - self._groupings = array_config.groupings - - @property - def axes(self): - """ - Returns the axes of the source array - - Returns: - list: Axes of the source array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.axes - ['x', 'y', 'z'] - Notes: - This is a helper function for the BinarizeArray class - """ - return ["c"] + self._source_array.axes - - @property - def dims(self) -> int: - """ - Returns the number of dimensions of the source array - - Returns: - int: Number of dimensions of the source array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.dims - 3 - Notes: - This is a helper function for the BinarizeArray class - """ - return self._source_array.dims - - @property - def voxel_size(self) -> Coordinate: - """ - Returns the voxel size of the source array - - Returns: - Coordinate: Voxel size of the source array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.voxel_size - Coordinate(x=4, y=4, z=40) - Notes: - This is a helper function for the BinarizeArray class - - """ - return self._source_array.voxel_size - - @property - def roi(self) -> Roi: - """ - Returns the region of interest of the source array - - Returns: - Roi: Region of interest of the source array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.roi - Roi(offset=(0, 0, 0), shape=(100, 100, 100)) - Notes: - This is a helper function for the BinarizeArray class - """ - return self._source_array.roi - - @property - def writable(self) -> bool: - """ - Returns whether the source array is writable - - Returns: - bool: Whether the source array is writable - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.writable - False - Notes: - This is a helper function for the BinarizeArray class - - """ - return False - - @property - def dtype(self): - """ - Returns the data type of the source array - - Returns: - np.dtype: Data type of the source array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.dtype - np.uint8 - Notes: - This is a helper function for the BinarizeArray class - - """ - return np.uint8 - - @property - def num_channels(self) -> int: - """ - Returns the number of channels - - Returns: - int: Number of channels - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.num_channels - 2 - Notes: - This is a helper function for the BinarizeArray class - - - """ - return len(self._groupings) - - @property - def data(self): - """ - Returns the data of the source array - - Returns: - np.ndarray: Data of the source array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.data - np.ndarray(...) - Notes: - This is a helper function for the BinarizeArray class - - """ - raise ValueError( - "Cannot get a writable view of this array because it is a virtual " - "array created by modifying another array on demand." - ) - - @property - def attrs(self): - """ - Returns the attributes of the source array - - Returns: - dict: Attributes of the source array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.attrs - {'name': 'source_array', 'resolution': [4, 4, 40]} - Notes: - This is a helper function for the BinarizeArray class - """ - return self._source_array.attrs - - @property - def channels(self): - """ - Returns the names of the channels - - Returns: - Generator[str]: Names of the channels - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array.channels - Generator['channel1', 'channel2', ...] - Notes: - This is a helper function for the BinarizeArray class - """ - return (name for name, _ in self._groupings) - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Returns a binary mask of the annotations that are present but not annotated. - - Args: - roi (Roi): Region of interest to get the mask for - Returns: - np.ndarray: Binary mask of the annotations that are present but not annotated - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> missing_annotations_mask = MissingAnnotationsMask(MissingAnnotationsMaskConfig(source_array, groupings)) - >>> roi = Roi(...) - >>> missing_annotations_mask[roi] - np.ndarray(...) - Notes: - - This is a helper function for the BinarizeArray class - - Number of channels in the mask is equal to the number of groupings - - Nuclues is a special case where we mask out the whole channel if any of the - sub-organelles are present but not annotated - """ - labels = self._source_array[roi] - grouped = np.ones((len(self._groupings), *labels.shape), dtype=bool) - grouped[:] = labels > 0 - try: - labels_list = LabelList.parse_obj({"labels": self.attrs["labels"]}).labels - present_not_annotated = set( - [ - label.value - for label in labels_list - if label.annotationState.present - and not label.annotationState.annotated - ] - ) - for i, (_, ids) in enumerate(self._groupings): - if any([id in present_not_annotated for id in ids]): - grouped[i] = 0 - - except KeyError: - pass - return grouped - - def _can_neuroglance(self): - """ - Returns whether the array can be visualized in neuroglancer - - Returns: - bool: Whether the array can be visualized in neuroglancer - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array._can_neuroglance() - True - Notes: - This is a helper function for the neuroglancer layer - - """ - return self._source_array._can_neuroglance() - - def _neuroglancer_source(self): - """ - Returns a neuroglancer source for the array - - Returns: - neuroglancer.LocalVolume: Neuroglancer source for the array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array._neuroglancer_source() - neuroglancer.LocalVolume(...) - Notes: - This is a helper function for the neuroglancer layer - """ - return self._source_array._neuroglancer_source() - - def _neuroglancer_layer(self): - """ - Returns a neuroglancer Segmentation layer for the array - - Returns: - neuroglancer.SegmentationLayer: Segmentation layer for the array - dict: Keyword arguments for the layer - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array._neuroglancer_layer() - (neuroglancer.SegmentationLayer, dict) - Notes: - This is a helper function for the neuroglancer layer - """ - # Generates an Segmentation layer - - layer = neuroglancer.SegmentationLayer(source=self._neuroglancer_source()) - kwargs = { - "visible": False, - } - return layer, kwargs - - def _source_name(self): - """ - Returns the name of the source array - - Returns: - str: Name of the source array - Raises: - ValueError: If the source array does not have a name - Examples: - >>> source_array = ZarrArray(ZarrArrayConfig(...)) - >>> source_array._source_name() - 'source_array' - Notes: - This is a helper function for the neuroglancer layer name - """ - return self._source_array._source_name() diff --git a/dacapo/experiments/datasplits/datasets/arrays/missing_annotations_mask_config.py b/dacapo/experiments/datasplits/datasets/arrays/missing_annotations_mask_config.py index 08faece08..8f56f38ea 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/missing_annotations_mask_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/missing_annotations_mask_config.py @@ -1,9 +1,12 @@ import attr from .array_config import ArrayConfig -from .missing_annotations_mask import MissingAnnotationsMask from typing import List, Tuple +from funlib.persistence import Array +from fibsem_tools.metadata.groundtruth import LabelList + +import dask.array as da @attr.s @@ -23,8 +26,6 @@ class MissingAnnotationsMaskConfig(ArrayConfig): Each channel will be a binary mask of the ids in the groupings list. """ - array_type = MissingAnnotationsMask - source_array_config: ArrayConfig = attr.ib( metadata={ "help_text": "The Array from which to pull annotated data. Is expected to contain a volume with uint64 voxels and no channel dimension" @@ -37,3 +38,23 @@ class MissingAnnotationsMaskConfig(ArrayConfig): "Group i found in groupings[i] will be binarized and placed in channel i." } ) + + def array(self, mode: str = "r") -> Array: + labels = self.source_array_config.array(mode) + grouped = da.ones((len(self.groupings), *labels.shape), dtype=bool) + grouped[:] = labels.data > 0 + labels_list = LabelList.parse_obj({"labels": labels._source_data.attrs["labels"]}).labels + present_not_annotated = set( + [ + label.value + for label in labels_list + if label.annotationState.present and not label.annotationState.annotated + ] + ) + for i, (_, ids) in enumerate(self.groupings): + if any([id in present_not_annotated for id in ids]): + grouped[i] = 0 + + return Array( + grouped, labels.offset, labels.voxel_size, labels.axis_names, labels.units + ) diff --git a/dacapo/experiments/datasplits/datasets/arrays/numpy_array.py b/dacapo/experiments/datasplits/datasets/arrays/numpy_array.py deleted file mode 100644 index 63c73e228..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/numpy_array.py +++ /dev/null @@ -1,306 +0,0 @@ -from .array import Array - -import gunpowder as gp -from funlib.geometry import Coordinate, Roi - -import numpy as np - -from typing import List - - -class NumpyArray(Array): - """ - This is just a wrapper for a numpy array to make it fit the DaCapo Array interface. - - Attributes: - data: The numpy array. - dtype: The data type of the numpy array. - roi: The region of interest of the numpy array. - voxel_size: The voxel size of the numpy array. - axes: The axes of the numpy array. - Methods: - from_gp_array: Create a NumpyArray from a Gunpowder Array. - from_np_array: Create a NumpyArray from a numpy array. - Note: - This class is a subclass of Array. - """ - - _data: np.ndarray - _dtype: np.dtype - _roi: Roi - _voxel_size: Coordinate - _axes: List[str] - - def __init__(self, array_config): - """ - Create a NumpyArray from an array config. - - Args: - array_config: The array config. - Returns: - NumpyArray: The NumpyArray. - Raises: - ValueError: If the array does not have a data type. - Examples: - >>> array = NumpyArray(OnesArrayConfig(source_array_config=ArrayConfig())) - >>> array.data - array([[[1., 1., 1., 1.], - [1., 1., 1., 1.], - [1., 1., 1., 1.]], - - [[1., 1., 1., 1.], - [1., 1., 1., 1.], - [1., 1., 1., 1.]]]) - Note: - This method creates a NumpyArray from an array config. - """ - raise RuntimeError("Numpy Array cannot be built from a config file") - - @property - def attrs(self): - """ - Returns the attributes of the array. - - Returns: - dict: The attributes of the array. - Raises: - ValueError: If the array does not have attributes. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.attrs - {} - Note: - This method is a property. It returns the attributes of the array. - """ - return dict() - - @classmethod - def from_gp_array(cls, array: gp.Array): - """ - Create a NumpyArray from a Gunpowder Array. - - Args: - array (gp.Array): The Gunpowder Array. - Returns: - NumpyArray: The NumpyArray. - Raises: - ValueError: If the array does not have a data type. - Examples: - >>> array = gp.Array(data=np.zeros((2, 3, 4)), spec=gp.ArraySpec(roi=Roi((0, 0, 0), (2, 3, 4)), voxel_size=Coordinate((1, 1, 1)))) - >>> array = NumpyArray.from_gp_array(array) - >>> array.data - array([[[0., 0., 0., 0.], - [0., 0., 0., 0.], - [0., 0., 0., 0.]], - - [[0., 0., 0., 0.], - [0., 0., 0., 0.], - [0., 0., 0., 0.]]]) - Note: - This method creates a NumpyArray from a Gunpowder Array. - """ - instance = cls.__new__(cls) - instance._data = array.data - instance._dtype = array.data.dtype - instance._roi = array.spec.roi - instance._voxel_size = array.spec.voxel_size - instance._axes = ( - ((["b", "c"] if len(array.data.shape) == instance.dims + 2 else [])) - + (["c"] if len(array.data.shape) == instance.dims + 1 else []) - + [ - "c", - "z", - "y", - "x", - ][-instance.dims :] - ) - return instance - - @classmethod - def from_np_array(cls, array: np.ndarray, roi, voxel_size, axes): - """ - Create a NumpyArray from a numpy array. - - Args: - array (np.ndarray): The numpy array. - roi (Roi): The region of interest of the array. - voxel_size (Coordinate): The voxel size of the array. - axes (List[str]): The axes of the array. - Returns: - NumpyArray: The NumpyArray. - Raises: - ValueError: If the array does not have a data type. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.data - array([[[0., 0., 0., 0.], - [0., 0., 0., 0.], - [0., 0., 0., 0.]], - - [[0., 0., 0., 0.], - [0., 0., 0., 0.], - [0., 0., 0., 0.]]]) - Note: - This method creates a NumpyArray from a numpy array. - - """ - instance = cls.__new__(cls) - instance._data = array - instance._dtype = array.dtype - instance._roi = roi - instance._voxel_size = voxel_size - instance._axes = axes - return instance - - @property - def axes(self): - """ - Returns the axes of the array. - - Returns: - List[str]: The axes of the array. - Raises: - ValueError: If the array does not have axes. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.axes - ['z', 'y', 'x'] - Note: - This method is a property. It returns the axes of the array. - """ - return self._axes - - @property - def dims(self): - """ - Returns the number of dimensions of the array. - - Returns: - int: The number of dimensions of the array. - Raises: - ValueError: If the array does not have a dimension. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.dims - 3 - Note: - This method is a property. It returns the number of dimensions of the array. - """ - return self._roi.dims - - @property - def voxel_size(self): - """ - Returns the voxel size of the array. - - Returns: - Coordinate: The voxel size of the array. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.voxel_size - Coordinate((1, 1, 1)) - Note: - This method is a property. It returns the voxel size of the array. - """ - return self._voxel_size - - @property - def roi(self): - """ - Returns the region of interest of the array. - - Returns: - Roi: The region of interest of the array. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.roi - Roi((0, 0, 0), (2, 3, 4)) - Note: - This method is a property. It returns the region of interest of the array. - """ - return self._roi - - @property - def writable(self) -> bool: - """ - Returns whether the array is writable. - - Returns: - bool: Whether the array is writable. - Raises: - ValueError: If the array is not writable. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.writable - True - Note: - This method is a property. It returns whether the array is writable. - """ - return True - - @property - def data(self): - """ - Returns the numpy array. - - Returns: - np.ndarray: The numpy array. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.data - array([[[0., 0., 0., 0.], - [0., 0., 0., 0.], - [0., 0., 0., 0.]], - - [[0., 0., 0., 0.], - [0., 0., 0., 0.], - [0., 0., 0., 0.]]]) - Note: - This method is a property. It returns the numpy array. - """ - return self._data - - @property - def dtype(self): - """ - Returns the data type of the array. - - Returns: - np.dtype: The data type of the array. - Raises: - ValueError: If the array does not have a data type. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.dtype - dtype('float64') - Note: - This method is a property. It returns the data type of the array. - """ - return self.data.dtype - - @property - def num_channels(self): - """ - Returns the number of channels in the array. - - Returns: - int: The number of channels in the array. - Raises: - ValueError: If the array does not have a channel dimension. - Examples: - >>> array = NumpyArray.from_np_array(np.zeros((1, 2, 3, 4)), Roi((0, 0, 0), (1, 2, 3)), Coordinate((1, 1, 1)), ["b", "c", "z", "y", "x"]) - >>> array.num_channels - 1 - >>> array = NumpyArray.from_np_array(np.zeros((2, 3, 4)), Roi((0, 0, 0), (2, 3, 4)), Coordinate((1, 1, 1)), ["z", "y", "x"]) - >>> array.num_channels - Traceback (most recent call last): - ... - ValueError: Array does not have a channel dimension. - Note: - This method is a property. It returns the number of channels in the array. - """ - try: - channel_dim = self.axes.index("c") - return self.data.shape[channel_dim] - except ValueError: - return None diff --git a/dacapo/experiments/datasplits/datasets/arrays/ones_array.py b/dacapo/experiments/datasplits/datasets/arrays/ones_array.py deleted file mode 100644 index cf2c416fe..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/ones_array.py +++ /dev/null @@ -1,410 +0,0 @@ -from .array import Array - -from funlib.geometry import Roi - -import numpy as np - -import logging - -logger = logging.getLogger(__name__) - - -class OnesArray(Array): - """ - This is a wrapper around another `source_array` that simply provides ones - with the same metadata as the `source_array`. - - This is useful for creating a mask array that is the same size as the - original array, but with all values set to 1. - - Attributes: - source_array: The source array that this array is based on. - Methods: - like: Create a new OnesArray with the same metadata as another array. - attrs: Get the attributes of the array. - axes: Get the axes of the array. - dims: Get the dimensions of the array. - voxel_size: Get the voxel size of the array. - roi: Get the region of interest of the array. - writable: Check if the array is writable. - data: Get the data of the array. - dtype: Get the data type of the array. - num_channels: Get the number of channels of the array. - __getitem__: Get a subarray of the array. - Note: - This class is not meant to be instantiated directly. Instead, use the - `like` method to create a new OnesArray with the same metadata as - another array. - """ - - def __init__(self, array_config): - """ - Initialize the OnesArray with the given array configuration. - - Args: - array_config: The configuration of the source array. - Raises: - RuntimeError: If the source array is not specified in the - configuration. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> source_array_config = ArrayConfig(source_array) - >>> ones_array = OnesArray(source_array_config) - >>> ones_array.source_array - NumpyArray(data=array([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]]), voxel_size=(1.0, 1.0, 1.0), roi=Roi((0, 0, 0), (10, 10, 10)), num_channels=1) - Notes: - This class is not meant to be instantiated directly. Instead, use the - `like` method to create a new OnesArray with the same metadata as - another array. - """ - logger.warning("OnesArray is deprecated. Use ConstantArray instead.") - self._source_array = array_config.source_array_config.array_type( - array_config.source_array_config - ) - - @classmethod - def like(cls, array: Array): - """ - Create a new OnesArray with the same metadata as another array. - - Args: - array: The source array. - Returns: - The new OnesArray with the same metadata as the source array. - Raises: - RuntimeError: If the source array is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray.like(source_array) - >>> ones_array.source_array - NumpyArray(data=array([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]]), voxel_size=(1.0, 1.0, 1.0), roi=Roi((0, 0, 0), (10, 10, 10)), num_channels=1) - Notes: - This class is not meant to be instantiated directly. Instead, use the - `like` method to create a new OnesArray with the same metadata as - another array. - - """ - instance = cls.__new__(cls) - instance._source_array = array - return instance - - @property - def attrs(self): - """ - Get the attributes of the array. - - Returns: - An empty dictionary. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.attrs - {} - Notes: - This method is used to get the attributes of the array. The attributes - are stored as key-value pairs in a dictionary. This method returns an - empty dictionary because the OnesArray does not have any attributes. - """ - return dict() - - @property - def source_array(self) -> Array: - """ - Get the source array that this array is based on. - - Returns: - The source array. - Raises: - RuntimeError: If the source array is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.source_array - NumpyArray(data=array([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]]), voxel_size=(1.0, 1.0, 1.0), roi=Roi((0, 0, 0), (10, 10, 10)), num_channels=1) - Notes: - This method is used to get the source array that this array is based on. - The source array is the array that the OnesArray is created from. This - method returns the source array that was specified when the OnesArray - was created. - """ - return self._source_array - - @property - def axes(self): - """ - Get the axes of the array. - - Returns: - The axes of the array. - Raises: - RuntimeError: If the axes are not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.axes - 'zyx' - Notes: - This method is used to get the axes of the array. The axes are the - order of the dimensions of the array. This method returns the axes of - the array that was specified when the OnesArray was created. - """ - return self.source_array.axes - - @property - def dims(self): - """ - Get the dimensions of the array. - - Returns: - The dimensions of the array. - Raises: - RuntimeError: If the dimensions are not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.dims - (10, 10, 10) - Notes: - This method is used to get the dimensions of the array. The dimensions - are the size of the array along each axis. This method returns the - dimensions of the array that was specified when the OnesArray was created. - """ - return self.source_array.dims - - @property - def voxel_size(self): - """ - Get the voxel size of the array. - - Returns: - The voxel size of the array. - Raises: - RuntimeError: If the voxel size is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.voxel_size - (1.0, 1.0, 1.0) - Notes: - This method is used to get the voxel size of the array. The voxel size - is the size of each voxel in the array. This method returns the voxel - size of the array that was specified when the OnesArray was created. - """ - return self.source_array.voxel_size - - @property - def roi(self): - """ - Get the region of interest of the array. - - Returns: - The region of interest of the array. - Raises: - RuntimeError: If the region of interest is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.roi - Roi((0, 0, 0), (10, 10, 10)) - Notes: - This method is used to get the region of interest of the array. The - region of interest is the region of the array that contains the data. - This method returns the region of interest of the array that was specified - when the OnesArray was created. - """ - return self.source_array.roi - - @property - def writable(self) -> bool: - """ - Check if the array is writable. - - Returns: - False. - Raises: - RuntimeError: If the writability of the array is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.writable - False - Notes: - This method is used to check if the array is writable. An array is - writable if it can be modified in place. This method returns False - because the OnesArray is read-only and cannot be modified. - """ - return False - - @property - def data(self): - """ - Get the data of the array. - - Returns: - The data of the array. - Raises: - RuntimeError: If the data is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.data - array([[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]]) - Notes: - This method is used to get the data of the array. The data is the - values that are stored in the array. This method returns a subarray - of the array with all values set to 1. - """ - raise RuntimeError("Cannot get writable version of this data!") - - @property - def dtype(self): - """ - Get the data type of the array. - - Returns: - The data type of the array. - Raises: - RuntimeError: If the data type is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.dtype - - Notes: - This method is used to get the data type of the array. The data type - is the type of the values that are stored in the array. This method - returns the data type of the array that was specified when the OnesArray - was created. - """ - return bool - - @property - def num_channels(self): - """ - Get the number of channels of the array. - - Returns: - The number of channels of the array. - Raises: - RuntimeError: If the number of channels is not specified. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> ones_array.num_channels - 1 - Notes: - This method is used to get the number of channels of the array. The - number of channels is the number of values that are stored at each - voxel in the array. This method returns the number of channels of the - array that was specified when the OnesArray was created. - """ - return self.source_array.num_channels - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Get a subarray of the array. - - Args: - roi: The region of interest. - Returns: - A subarray of the array with all values set to 1. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import OnesArray - >>> from dacapo.experiments.datasplits.datasets.arrays import NumpyArray - >>> from funlib.geometry import Roi - >>> import numpy as np - >>> source_array = NumpyArray(np.zeros((10, 10, 10))) - >>> ones_array = OnesArray(source_array) - >>> roi = Roi((0, 0, 0), (10, 10, 10)) - >>> ones_array[roi] - array([[[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]]) - Notes: - This method is used to get a subarray of the array. The subarray is - specified by the region of interest. This method returns a subarray - of the array with all values set to 1. - """ - return np.ones_like(self.source_array.__getitem__(roi), dtype=bool) diff --git a/dacapo/experiments/datasplits/datasets/arrays/ones_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/ones_array_config.py index 152b357c2..577f34670 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/ones_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/ones_array_config.py @@ -1,7 +1,8 @@ import attr from .array_config import ArrayConfig -from .ones_array import OnesArray +import dask.array as da +from funlib.persistence import Array @attr.s @@ -21,8 +22,16 @@ class OnesArrayConfig(ArrayConfig): This class is a subclass of ArrayConfig. """ - array_type = OnesArray - source_array_config: ArrayConfig = attr.ib( metadata={"help_text": "The Array that you want to copy and fill with ones."} ) + + def array(self, mode: str = "r") -> Array: + source_array = self.source_array_config.array(mode) + return Array( + data=da.ones(source_array.shape, dtype=source_array.dtype), + offset=source_array.offset, + voxel_size=source_array.voxel_size, + axis_names=source_array.axis_names, + units=source_array.units, + ) diff --git a/dacapo/experiments/datasplits/datasets/arrays/resampled_array.py b/dacapo/experiments/datasplits/datasets/arrays/resampled_array.py deleted file mode 100644 index 86367e50b..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/resampled_array.py +++ /dev/null @@ -1,359 +0,0 @@ -from .array import Array - -import funlib.persistence -from funlib.geometry import Coordinate, Roi - -import numpy as np -from skimage.transform import rescale - - -class ResampledArray(Array): - """ - This is a zarr array that is a resampled version of another array. - - Resampling is done by rescaling the source array with the given - upsample and downsample factors. The voxel size of the resampled array - is the voxel size of the source array divided by the downsample factor - and multiplied by the upsample factor. - - Attributes: - name: str - The name of the array - source_array: Array - The source array - upsample: Coordinate - The upsample factor for each dimension - downsample: Coordinate - The downsample factor for each dimension - interp_order: int - The order of the interpolation used for resampling - Methods: - attrs: Dict - Returns the attributes of the source array - axes: str - Returns the axes of the source array - dims: int - Returns the number of dimensions of the source array - voxel_size: Coordinate - Returns the voxel size of the resampled array - roi: Roi - Returns the region of interest of the resampled array - writable: bool - Returns whether the resampled array is writable - dtype: np.dtype - Returns the data type of the resampled array - num_channels: int - Returns the number of channels of the resampled array - data: np.ndarray - Returns the data of the resampled array - scale: Tuple[float] - Returns the scale of the resampled array - __getitem__(roi: Roi) -> np.ndarray - Returns the data of the resampled array within the given region of interest - _can_neuroglance() -> bool - Returns whether the source array can be visualized with neuroglance - _neuroglancer_layer() -> Dict - Returns the neuroglancer layer of the source array - _neuroglancer_source() -> Dict - Returns the neuroglancer source of the source array - _source_name() -> str - Returns the name of the source array - Note: - This class is a subclass of Array. - - - """ - - def __init__(self, array_config): - """ - Constructor of the ResampledArray class. - - Args: - array_config: ArrayConfig - The configuration of the array - Raises: - AssertionError: If the voxel size of the resampled array is not equal to the voxel size of the source array divided by the downsample factor and multiplied by the upsample factor - Examples: - >>> resampled_array = ResampledArray(array_config) - Note: - This constructor resamples the source array with the given upsample and downsample factors. - """ - self.name = array_config.name - self._source_array = array_config.source_array_config.array_type( - array_config.source_array_config - ) - - self.upsample = Coordinate(max(u, 1) for u in array_config.upsample) - self.downsample = Coordinate(max(d, 1) for d in array_config.downsample) - self.interp_order = array_config.interp_order - - assert ( - self.voxel_size * self.upsample - ) / self.downsample == self._source_array.voxel_size, f"{self.name}, {self._source_array.voxel_size}, {self.voxel_size}, {self.upsample}, {self.downsample}" - - @property - def attrs(self): - """ - Returns the attributes of the source array. - - Returns: - Dict: The attributes of the source array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.attrs - Note: - This method returns the attributes of the source array. - - """ - return self._source_array.attrs - - @property - def axes(self): - """ - Returns the axes of the source array. - - Returns: - str: The axes of the source array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.axes - Note: - This method returns the axes of the source array. - """ - return self._source_array.axes - - @property - def dims(self) -> int: - """ - Returns the number of dimensions of the source array. - - Returns: - int: The number of dimensions of the source array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.dims - Note: - This method returns the number of dimensions of the source array. - """ - return self._source_array.dims - - @property - def voxel_size(self) -> Coordinate: - """ - Returns the voxel size of the resampled array. - - Returns: - Coordinate: The voxel size of the resampled array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.voxel_size - Note: - This method returns the voxel size of the resampled array. - """ - return (self._source_array.voxel_size * self.downsample) / self.upsample - - @property - def roi(self) -> Roi: - """ - Returns the region of interest of the resampled array. - - Returns: - Roi: The region of interest of the resampled array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.roi - Note: - This method returns the region of interest of the resampled array. - - """ - return self._source_array.roi.snap_to_grid( - np.lcm(self._source_array.voxel_size, self.voxel_size), mode="shrink" - ) - - @property - def writable(self) -> bool: - """ - Returns whether the resampled array is writable. - - Returns: - bool: True if the resampled array is writable, False otherwise - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.writable - Note: - This method returns whether the resampled array is writable. - - """ - return False - - @property - def dtype(self): - """ - Returns the data type of the resampled array. - - Returns: - np.dtype: The data type of the resampled array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.dtype - Note: - This method returns the data type of the resampled array. - """ - return self._source_array.dtype - - @property - def num_channels(self) -> int: - """ - Returns the number of channels of the resampled array. - - Returns: - int: The number of channels of the resampled array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.num_channels - Note: - This method returns the number of channels of the resampled array. - """ - return self._source_array.num_channels - - @property - def data(self): - """ - Returns the data of the resampled array. - - Returns: - np.ndarray: The data of the resampled array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.data - Note: - This method returns the data of the resampled array. - """ - return self._source_array.data - # raise ValueError( - # "Cannot get a writable view of this array because it is a virtual " - # "array created by modifying another array on demand." - # ) - - @property - def scale(self): - """ - Returns the scale of the resampled array. - - Returns: - Tuple[float]: The scale of the resampled array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array.scale - Note: - This method returns the scale of the resampled array. - - """ - spatial_scales = tuple(u / d for d, u in zip(self.downsample, self.upsample)) - if "c" in self.axes: - scales = list(spatial_scales) - scales.insert(self.axes.index("c"), 1.0) - return tuple(scales) - else: - return spatial_scales - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Returns the data of the resampled array within the given region of interest. - - Args: - roi: Roi - The region of interest - Returns: - np.ndarray: The data of the resampled array within the given region of interest - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array[roi] - Note: - This method returns the data of the resampled array within the given region of interest. - """ - snapped_roi = roi.snap_to_grid( - np.lcm(self._source_array.voxel_size, self.voxel_size), mode="grow" - ) - resampled_array = funlib.persistence.Array( - rescale( - self._source_array[snapped_roi].astype(np.float32), - self.scale, - order=self.interp_order, - anti_aliasing=self.interp_order != 0, - ).astype(self.dtype), - roi=snapped_roi, - voxel_size=self.voxel_size, - ) - return resampled_array.to_ndarray(roi) - - def _can_neuroglance(self): - """ - Returns whether the source array can be visualized with neuroglance. - - Returns: - bool: True if the source array can be visualized with neuroglance, False otherwise - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array._can_neuroglance() - Note: - This method returns whether the source array can be visualized with neuroglance. - """ - return self._source_array._can_neuroglance() - - def _neuroglancer_layer(self): - """ - Returns the neuroglancer layer of the source array. - - Returns: - Dict: The neuroglancer layer of the source array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array._neuroglancer_layer() - Note: - This method returns the neuroglancer layer of the source array. - """ - return self._source_array._neuroglancer_layer() - - def _neuroglancer_source(self): - """ - Returns the neuroglancer source of the source array. - - Returns: - Dict: The neuroglancer source of the source array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array._neuroglancer_source() - Note: - This method returns the neuroglancer source of the source array. - """ - return self._source_array._neuroglancer_source() - - def _source_name(self): - """ - Returns the name of the source array. - - Returns: - str: The name of the source array - Raises: - ValueError: If the resampled array is not writable - Examples: - >>> resampled_array._source_name() - Note: - This method returns the name of the source array. - """ - return self._source_array._source_name() diff --git a/dacapo/experiments/datasplits/datasets/arrays/resampled_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/resampled_array_config.py index c4c5a1c54..7a03f89eb 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/resampled_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/resampled_array_config.py @@ -1,9 +1,9 @@ import attr from .array_config import ArrayConfig -from .resampled_array import ResampledArray from funlib.geometry import Coordinate +from funlib.persistence import Array @attr.s @@ -23,8 +23,6 @@ class ResampledArrayConfig(ArrayConfig): """ - array_type = ResampledArray - source_array_config: ArrayConfig = attr.ib( metadata={"help_text": "The Array that you want to upsample or downsample."} ) @@ -38,3 +36,8 @@ class ResampledArrayConfig(ArrayConfig): interp_order: bool = attr.ib( metadata={"help_text": "The order of the interpolation!"} ) + + def array(self, mode: str = "r") -> Array: + # This is non trivial. We want to upsample or downsample the source + # array lazily. Not entirely sure how to do this with dask arrays. + raise NotImplementedError() diff --git a/dacapo/experiments/datasplits/datasets/arrays/sum_array.py b/dacapo/experiments/datasplits/datasets/arrays/sum_array.py deleted file mode 100644 index ce1dcd087..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/sum_array.py +++ /dev/null @@ -1,363 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - - -import neuroglancer - -import numpy as np - - -class SumArray(Array): - """ - This class provides a sum array. This array is a virtual array that is created by summing - multiple source arrays. The source arrays must have the same shape and ROI. - - Attributes: - name: str - The name of the array. - _source_arrays: List[Array] - The source arrays to sum. - _source_array: Array - The first source array. - Methods: - __getitem__(roi: Roi) -> np.ndarray - Get the data for the given region of interest. - _can_neuroglance() -> bool - Check if neuroglance can be used. - _neuroglancer_source() -> Dict - Return the source for neuroglance. - _neuroglancer_layer() -> Tuple[neuroglancer.SegmentationLayer, Dict] - Return the neuroglancer layer. - _source_name() -> str - Return the source name. - Note: - This class is a subclass of Array. - """ - - def __init__(self, array_config): - """ - Initialize the SumArray. - - Args: - array_config: SumArrayConfig - The configuration for the sum array. - Returns: - SumArray: The sum array. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays.sum_array import SumArray - >>> from dacapo.experiments.datasplits.datasets.arrays.sum_array_config import SumArrayConfig - >>> from dacapo.experiments.datasplits.datasets.arrays.tiff_array import TiffArray - >>> from dacapo.experiments.datasplits.datasets.arrays.tiff_array_config import TiffArrayConfig - >>> from funlib.geometry import Coordinate - >>> from pathlib import Path - >>> sum_array = SumArray(SumArrayConfig(name="sum", source_array_configs=[TiffArrayConfig(file_name=Path("data.tiff"), offset=Coordinate([0, 0, 0]), voxel_size=Coordinate([1, 1, 1]), axes=["x", "y", "z"])])) - Note: - This class is a subclass of Array. - - """ - self.name = array_config.name - self._source_arrays = [ - source_config.array_type(source_config) - for source_config in array_config.source_array_configs - ] - self._source_array = self._source_arrays[0] - - @property - def axes(self): - """ - The axes of the array. - - Returns: - List[str]: The axes of the array. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.axes - ['x', 'y', 'z'] - Note: - This class is a subclass of Array. - """ - return [x for x in self._source_array.axes if x != "c"] - - @property - def dims(self) -> int: - """ - The number of dimensions of the array. - - Returns: - int: The number of dimensions of the array. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.dims - 3 - Note: - This class is a subclass of Array. - """ - return self._source_array.dims - - @property - def voxel_size(self) -> Coordinate: - """ - The size of each voxel in each dimension. - - Returns: - Coordinate: The size of each voxel in each dimension. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.voxel_size - Coordinate([1, 1, 1]) - Note: - This class is a subclass of Array. - """ - return self._source_array.voxel_size - - @property - def roi(self) -> Roi: - """ - The region of interest of the array. - - Args: - roi: Roi - The region of interest. - Returns: - Roi: The region of interest. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.roi - Roi(Coordinate([0, 0, 0]), Coordinate([100, 100, 100])) - Note: - This class is a subclass of Array. - """ - return self._source_array.roi - - @property - def writable(self) -> bool: - """ - Check if the array is writable. - - Args: - writable: bool - Check if the array is writable. - Returns: - bool: True if the array is writable, otherwise False. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.writable - False - Note: - This class is a subclass of Array. - """ - return False - - @property - def dtype(self): - """ - The data type of the array. - - Args: - dtype: np.uint8 - The data type of the array. - Returns: - np.uint8: The data type of the array. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.dtype - np.uint8 - Note: - This class is a subclass of Array. - - """ - return np.uint8 - - @property - def num_channels(self): - """ - The number of channels in the array. - - Args: - num_channels: Optional[int] - The number of channels in the array. - Returns: - Optional[int]: The number of channels in the array. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.num_channels - None - Note: - This class is a subclass of Array. - - """ - return None - - @property - def data(self): - """ - Get the data of the array. - - Args: - data: np.ndarray - The data of the array. - Returns: - np.ndarray: The data of the array. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.data - np.array([[[0, 0], [0, 0]], [[0, 0], [0, 0]]]) - Note: - This class is a subclass of Array. - """ - raise ValueError( - "Cannot get a writable view of this array because it is a virtual " - "array created by modifying another array on demand." - ) - - @property - def attrs(self): - """ - Return the attributes of the array. - - Args: - attrs: Dict - The attributes of the array. - Returns: - Dict: The attributes of the array. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array.attrs - {} - Note: - This class is a subclass of Array. - """ - return self._source_array.attrs - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Get the data for the given region of interest. - - Args: - roi: Roi - The region of interest. - Returns: - np.ndarray: The data for the given region of interest. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array[roi] - np.array([[[0, 0], [0, 0]], [[0, 0], [0, 0]]]) - Note: - This class is a subclass of Array. - """ - return np.sum( - [source_array[roi] for source_array in self._source_arrays], axis=0 - ) - - def _can_neuroglance(self): - """ - Check if neuroglance can be used. - - Args: - can_neuroglance: bool - Check if neuroglance can be used. - Returns: - bool: True if neuroglance can be used, otherwise False. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array._can_neuroglance() - False - Note: - This class is a subclass of Array. - """ - return self._source_array._can_neuroglance() - - def _neuroglancer_source(self): - """ - Return the source for neuroglance. - - Args: - source: Dict - The source for neuroglance. - Returns: - Dict: The source for neuroglance. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array._neuroglancer_source() - {'source': 'precomputed://https://mybucket/segmentation', 'type': 'segmentation', 'voxel_size': [1, 1, 1]} - Note: - This class is a subclass of Array. - - """ - return self._source_array._neuroglancer_source() - - def _neuroglancer_layer(self): - """ - Return the neuroglancer layer. - - Args: - layer: Tuple[neuroglancer.SegmentationLayer, Dict] - The neuroglancer layer. - Returns: - Tuple[neuroglancer.SegmentationLayer, Dict]: The neuroglancer layer. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array._neuroglancer_layer() - (SegmentationLayer(source={'source': 'precomputed://https://mybucket/segmentation', 'type': 'segmentation', 'voxel_size': [1, 1, 1]}, visible=False), {}) - Note: - This class is a subclass of Array. - - """ - # Generates an Segmentation layer - - layer = neuroglancer.SegmentationLayer(source=self._neuroglancer_source()) - kwargs = { - "visible": False, - } - return layer, kwargs - - def _source_name(self): - """ - Return the source name. - - Args: - source_name: str - The source name. - Returns: - str: The source name. - Raises: - ValueError: - Cannot get a writable view of this array because it is a virtual array created by modifying another array on demand. - Examples: - >>> sum_array._source_name() - 'data.tiff' - Note: - This class is a subclass of Array. - - """ - return self._source_array._source_name() diff --git a/dacapo/experiments/datasplits/datasets/arrays/sum_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/sum_array_config.py index 0c2912140..5ebadeca8 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/sum_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/sum_array_config.py @@ -1,9 +1,10 @@ import attr from .array_config import ArrayConfig -from .sum_array import SumArray from typing import List +from funlib.persistence import Array +import dask.array as da @attr.s @@ -19,8 +20,18 @@ class SumArrayConfig(ArrayConfig): This class is a subclass of ArrayConfig. """ - array_type = SumArray - source_array_configs: List[ArrayConfig] = attr.ib( metadata={"help_text": "The Array of masks from which to take the union"} ) + + def array(self, mode: str = "r") -> Array: + arrays = [ + source_array.array(mode) for source_array in self.source_array_configs + ] + return Array( + data=da.stack([array.data for array in arrays], axis=0).sum(axis=0), + offset=arrays[0].offset, + voxel_size=arrays[0].voxel_size, + axis_names=arrays[0].axis_names, + units=arrays[0].units, + ) diff --git a/dacapo/experiments/datasplits/datasets/arrays/tiff_array.py b/dacapo/experiments/datasplits/datasets/arrays/tiff_array.py deleted file mode 100644 index 34e582b4e..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/tiff_array.py +++ /dev/null @@ -1,274 +0,0 @@ -from .array import Array - -from funlib.geometry import Coordinate, Roi - -import lazy_property -import tifffile - -import logging -from upath import UPath as Path -from typing import List, Optional - -logger = logging.getLogger(__name__) - - -class TiffArray(Array): - """ - This class provides the necessary configuration for a tiff array. - - Attributes: - _offset: Coordinate - The offset of the array. - _file_name: Path - The file name of the tiff. - _voxel_size: Coordinate - The voxel size of the array. - _axes: List[str] - The axes of the array. - Methods: - attrs() -> Dict - Return the attributes of the tiff. - Note: - This class is a subclass of Array. - - """ - - _offset: Coordinate - _file_name: Path - _voxel_size: Coordinate - _axes: List[str] - - def __init__(self, array_config): - """ - Initialize the TiffArray. - - Args: - array_config: TiffArrayConfig - The configuration for the tiff array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays.tiff_array import TiffArray - >>> from dacapo.experiments.datasplits.datasets.arrays.tiff_array_config import TiffArrayConfig - >>> from funlib.geometry import Coordinate - >>> from pathlib import Path - >>> tiff_array = TiffArray(TiffArrayConfig(file_name=Path("data.tiff"), offset=Coordinate([0, 0, 0]), voxel_size=Coordinate([1, 1, 1]), axes=["x", "y", "z"])) - Note: - This class is a subclass of Array. - """ - super().__init__() - - self._file_name = array_config.file_name - self._offset = array_config.offset - self._voxel_size = array_config.voxel_size - self._axes = array_config.axes - - @property - def attrs(self): - """ - Return the attributes of the tiff. - - Returns: - Dict: The attributes of the tiff. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.attrs - {'axes': ['x', 'y', 'z'], 'offset': [0, 0, 0], 'voxel_size': [1, 1, 1]} - Note: - Tiffs have tons of different locations for metadata. - """ - raise NotImplementedError( - "Tiffs have tons of different locations for metadata." - ) - - @property - def axes(self) -> List[str]: - """ - Return the axes of the array. - - Returns: - List[str]: The axes of the array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.axes - ['x', 'y', 'z'] - Note: - Tiffs have tons of different locations for metadata. - """ - return self._axes - - @property - def dims(self) -> int: - """ - Return the number of dimensions of the array. - - Returns: - int: The number of dimensions of the array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.dims - 3 - Note: - Tiffs have tons of different locations for metadata. - """ - return self.voxel_size.dims - - @lazy_property.LazyProperty - def shape(self) -> Coordinate: - """ - Return the shape of the array. - - Returns: - Coordinate: The shape of the array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.shape - Coordinate([100, 100, 100]) - Note: - Tiffs have tons of different locations for metadata. - """ - data_shape = self.data.shape - spatial_shape = Coordinate( - [data_shape[self.axes.index(axis)] for axis in self.spatial_axes] - ) - return spatial_shape - - @lazy_property.LazyProperty - def voxel_size(self) -> Coordinate: - """ - Return the voxel size of the array. - - Returns: - Coordinate: The voxel size of the array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.voxel_size - Coordinate([1, 1, 1]) - Note: - Tiffs have tons of different locations for metadata. - """ - return self._voxel_size - - @lazy_property.LazyProperty - def roi(self) -> Roi: - """ - Return the region of interest of the array. - - Returns: - Roi: The region of interest of the array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.roi - Roi([0, 0, 0], [100, 100, 100]) - Note: - Tiffs have tons of different locations for metadata. - """ - return Roi(self._offset, self.shape) - - @property - def writable(self) -> bool: - """ - Return whether the array is writable. - - Returns: - bool: Whether the array is writable. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.writable - False - Note: - Tiffs have tons of different locations for metadata. - """ - return False - - @property - def dtype(self): - """ - Return the data type of the array. - - Returns: - np.dtype: The data type of the array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.dtype - np.float32 - Note: - Tiffs have tons of different locations for metadata. - - """ - return self.data.dtype - - @property - def num_channels(self) -> Optional[int]: - """ - Return the number of channels of the array. - - Returns: - Optional[int]: The number of channels of the array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.num_channels - 1 - Note: - Tiffs have tons of different locations for metadata. - - """ - if "c" in self.axes: - return self.data.shape[self.axes.index("c")] - else: - return None - - @property - def spatial_axes(self) -> List[str]: - """ - Return the spatial axes of the array. - - Returns: - List[str]: The spatial axes of the array. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.spatial_axes - ['x', 'y', 'z'] - Note: - Tiffs have tons of different locations for metadata. - """ - return [c for c in self.axes if c != "c"] - - @lazy_property.LazyProperty - def data(self): - """ - Return the data of the tiff. - - Returns: - np.ndarray: The data of the tiff. - Raises: - NotImplementedError: - Tiffs have tons of different locations for metadata. - Examples: - >>> tiff_array.data - np.ndarray - Note: - Tiffs have tons of different locations for metadata. - """ - return tifffile.TiffFile(self._file_name).values diff --git a/dacapo/experiments/datasplits/datasets/arrays/tiff_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/tiff_array_config.py index 27b4e623a..2f123010d 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/tiff_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/tiff_array_config.py @@ -1,16 +1,17 @@ import attr from .array_config import ArrayConfig -from .tiff_array import TiffArray from funlib.geometry import Coordinate +from funlib.persistence import Array from upath import UPath as Path from typing import List +import tifffile @attr.s -class ZarrArrayConfig(ArrayConfig): +class TiffArrayConfig(ArrayConfig): """ This config class provides the necessary configuration for a tiff array @@ -21,14 +22,12 @@ class ZarrArrayConfig(ArrayConfig): The offset of the array. voxel_size: Coordinate The voxel size of the array. - axes: List[str] - The axes of the array. + axis_names: List[str] + The axis_names of the array. Note: This class is a subclass of ArrayConfig. """ - array_type = TiffArray - file_name: Path = attr.ib( metadata={"help_text": "The file name of the zarr container."} ) @@ -41,4 +40,17 @@ class ZarrArrayConfig(ArrayConfig): voxel_size: Coordinate = attr.ib( metadata={"help_text": "The size of each voxel in each dimension."} ) - axes: List[str] = attr.ib(metadata={"help_text": "The axes of your array"}) + axis_names: list[str] = attr.ib( + metadata={"help_text": "The axis_names of your array"} + ) + units: list[str] = attr.ib(metadata={"help_text": "The units of your array"}) + + def array(self, mode: str = "r") -> Array: + + return Array( + data=tifffile.TiffFile(self.file_name).values, + offset=self.offset, + voxel_size=self.voxel_size, + axis_names=self.axis_names, + units=self.units, + ) diff --git a/dacapo/experiments/datasplits/datasets/arrays/zarr_array.py b/dacapo/experiments/datasplits/datasets/arrays/zarr_array.py deleted file mode 100644 index f9a26bd09..000000000 --- a/dacapo/experiments/datasplits/datasets/arrays/zarr_array.py +++ /dev/null @@ -1,736 +0,0 @@ -from .array import Array -from dacapo import Options -from funlib.persistence import open_ds -from funlib.geometry import Coordinate, Roi -import funlib.persistence - -import neuroglancer - -import lazy_property -import numpy as np -import zarr -from zarr.n5 import N5FSStore - -from collections import OrderedDict -import logging -from typing import Dict, Tuple, Any, Optional, List - -logger = logging.getLogger(__name__) - - -class ZarrArray(Array): - """ - This is a zarr array. - - Attributes: - name (str): The name of the array. - file_name (Path): The file name of the array. - dataset (str): The dataset name. - _axes (Optional[List[str]]): The axes of the array. - snap_to_grid (Optional[Coordinate]): The snap to grid. - Methods: - __init__(array_config): - Initializes the array type 'raw' and name for the DummyDataset instance. - __str__(): - Returns the string representation of the ZarrArray. - __repr__(): - Returns the string representation of the ZarrArray. - attrs(): - Returns the attributes of the array. - axes(): - Returns the axes of the array. - dims(): - Returns the dimensions of the array. - _daisy_array(): - Returns the daisy array. - voxel_size(): - Returns the voxel size of the array. - roi(): - Returns the region of interest of the array. - writable(): - Returns the boolean value of the array. - dtype(): - Returns the data type of the array. - num_channels(): - Returns the number of channels of the array. - spatial_axes(): - Returns the spatial axes of the array. - data(): - Returns the data of the array. - __getitem__(roi): - Returns the data of the array for the given region of interest. - __setitem__(roi, value): - Sets the data of the array for the given region of interest. - create_from_array_identifier(array_identifier, axes, roi, num_channels, voxel_size, dtype, write_size=None, name=None, overwrite=False): - Creates a new ZarrArray given an array identifier. - open_from_array_identifier(array_identifier, name=""): - Opens a new ZarrArray given an array identifier. - _can_neuroglance(): - Returns the boolean value of the array. - _neuroglancer_source(): - Returns the neuroglancer source of the array. - _neuroglancer_layer(): - Returns the neuroglancer layer of the array. - _transform_matrix(): - Returns the transform matrix of the array. - _output_dimensions(): - Returns the output dimensions of the array. - _source_name(): - Returns the source name of the array. - add_metadata(metadata): - Adds metadata to the array. - Notes: - This class is used to create a zarr array. - """ - - def __init__(self, array_config): - """ - Initializes the array type 'raw' and name for the DummyDataset instance. - - Args: - array_config (object): an instance of a configuration class that includes the name and - raw configuration of the data. - Raises: - NotImplementedError - If the method is not implemented in the derived class. - Examples: - >>> dataset = DummyDataset(dataset_config) - Notes: - This method is used to initialize the dataset. - """ - super().__init__() - self.name = array_config.name - self.file_name = array_config.file_name - self.dataset = array_config.dataset - self._mode = array_config.mode - self._attributes = self.data.attrs - self._axes = array_config._axes - self.snap_to_grid = array_config.snap_to_grid - - def __str__(self): - """ - Returns the string representation of the ZarrArray. - - Args: - ZarrArray (str): The string representation of the ZarrArray. - Returns: - str: The string representation of the ZarrArray. - Raises: - NotImplementedError - Examples: - >>> print(ZarrArray) - Notes: - This method is used to return the string representation of the ZarrArray. - """ - return f"ZarrArray({self.file_name}, {self.dataset})" - - def __repr__(self): - """ - Returns the string representation of the ZarrArray. - - Args: - ZarrArray (str): The string representation of the ZarrArray. - Returns: - str: The string representation of the ZarrArray. - Raises: - NotImplementedError - Examples: - >>> print(ZarrArray) - Notes: - This method is used to return the string representation of the ZarrArray. - - """ - return f"ZarrArray({self.file_name}, {self.dataset})" - - @property - def mode(self): - if not hasattr(self, "_mode"): - self._mode = "a" - if self._mode not in ["r", "w", "a"]: - raise ValueError(f"Mode {self._mode} not in ['r', 'w', 'a']") - return self._mode - - @property - def attrs(self): - """ - Returns the attributes of the array. - - Args: - attrs (Any): The attributes of the array. - Returns: - Any: The attributes of the array. - Raises: - NotImplementedError - Examples: - >>> attrs() - Notes: - This method is used to return the attributes of the array. - - """ - return self.data.attrs - - @property - def axes(self): - """ - Returns the axes of the array. - - Args: - axes (List[str]): The axes of the array. - Returns: - List[str]: The axes of the array. - Raises: - NotImplementedError - Examples: - >>> axes() - Notes: - This method is used to return the axes of the array. - """ - if self._axes is not None: - return self._axes - try: - return self._attributes["axes"] - except KeyError: - logger.debug( - "DaCapo expects Zarr datasets to have an 'axes' attribute!\n" - f"Zarr {self.file_name} and dataset {self.dataset} has attributes: {list(self._attributes.items())}\n" - f"Using default {['s', 'c', 'z', 'y', 'x'][-self.dims::]}", - ) - return ["s", "c", "z", "y", "x"][-self.dims : :] - - @property - def dims(self) -> int: - """ - Returns the dimensions of the array. - - Args: - dims (int): The dimensions of the array. - Returns: - int: The dimensions of the array. - Raises: - NotImplementedError - Examples: - >>> dims() - Notes: - This method is used to return the dimensions of the array. - - """ - return self.voxel_size.dims - - @lazy_property.LazyProperty - def _daisy_array(self) -> funlib.persistence.Array: - """ - Returns the daisy array. - - Args: - voxel_size (Coordinate): The voxel size. - Returns: - funlib.persistence.Array: The daisy array. - Raises: - NotImplementedError - Examples: - >>> _daisy_array() - Notes: - This method is used to return the daisy array. - - """ - return funlib.persistence.open_ds(f"{self.file_name}", self.dataset) - - @lazy_property.LazyProperty - def voxel_size(self) -> Coordinate: - """ - Returns the voxel size of the array. - - Args: - voxel_size (Coordinate): The voxel size. - Returns: - Coordinate: The voxel size of the array. - Raises: - NotImplementedError - Examples: - >>> voxel_size() - Notes: - This method is used to return the voxel size of the array. - - """ - return self._daisy_array.voxel_size - - @lazy_property.LazyProperty - def roi(self) -> Roi: - """ - Returns the region of interest of the array. - - Args: - roi (Roi): The region of interest. - Returns: - Roi: The region of interest of the array. - Raises: - NotImplementedError - Examples: - >>> roi() - Notes: - This method is used to return the region of interest of the array. - """ - if self.snap_to_grid is not None: - return self._daisy_array.roi.snap_to_grid( - np.lcm(self.voxel_size, self.snap_to_grid), mode="shrink" - ) - else: - return self._daisy_array.roi - - @property - def writable(self) -> bool: - """ - Returns the boolean value of the array. - - Args: - writable (bool): The boolean value of the array. - Returns: - bool: The boolean value of the array. - Raises: - NotImplementedError - Examples: - >>> writable() - Notes: - This method is used to return the boolean value of the array. - """ - return True - - @property - def dtype(self) -> Any: - """ - Returns the data type of the array. - - Args: - dtype (Any): The data type of the array. - Returns: - Any: The data type of the array. - Raises: - NotImplementedError - Examples: - >>> dtype() - Notes: - This method is used to return the data type of the array. - """ - return self.data.dtype - - @property - def num_channels(self) -> Optional[int]: - """ - Returns the number of channels of the array. - - Args: - num_channels (Optional[int]): The number of channels of the array. - Returns: - Optional[int]: The number of channels of the array. - Raises: - NotImplementedError - Examples: - >>> num_channels() - Notes: - This method is used to return the number of channels of the array. - - """ - return None if "c" not in self.axes else self.data.shape[self.axes.index("c")] - - @property - def spatial_axes(self) -> List[str]: - """ - Returns the spatial axes of the array. - - Args: - spatial_axes (List[str]): The spatial axes of the array. - Returns: - List[str]: The spatial axes of the array. - Raises: - NotImplementedError - Examples: - >>> spatial_axes() - Notes: - This method is used to return the spatial axes of the array. - - """ - return [ax for ax in self.axes if ax not in set(["c", "b"])] - - @property - def data(self) -> Any: - """ - Returns the data of the array. - - Args: - data (Any): The data of the array. - Returns: - Any: The data of the array. - Raises: - NotImplementedError - Examples: - >>> data() - Notes: - This method is used to return the data of the array. - """ - file_name = str(self.file_name) - # Zarr library does not detect the store for N5 datasets - try: - if file_name.endswith(".n5"): - zarr_container = zarr.open(N5FSStore(str(file_name)), mode=self.mode) - else: - zarr_container = zarr.open(str(file_name), mode=self.mode) - return zarr_container[self.dataset] - except Exception as e: - logger.error( - f"Could not open dataset {self.dataset} in file {file_name} in mode {self.mode}" - ) - raise e - - def __getitem__(self, roi: Roi) -> np.ndarray: - """ - Returns the data of the array for the given region of interest. - - Args: - roi (Roi): The region of interest. - Returns: - np.ndarray: The data of the array for the given region of interest. - Raises: - NotImplementedError - Examples: - >>> __getitem__(roi) - Notes: - This method is used to return the data of the array for the given region of interest. - """ - data: np.ndarray = funlib.persistence.Array( - self.data, self.roi, self.voxel_size - ).to_ndarray(roi=roi) - return data - - def __setitem__(self, roi: Roi, value: np.ndarray): - """ - Sets the data of the array for the given region of interest. - - Args: - roi (Roi): The region of interest. - value (np.ndarray): The value to set. - Raises: - NotImplementedError - Examples: - >>> __setitem__(roi, value) - Notes: - This method is used to set the data of the array for the given region of interest. - """ - funlib.persistence.Array(self.data, self.roi, self.voxel_size)[roi] = value - - @classmethod - def create_from_array_identifier( - cls, - array_identifier, - axes, - roi, - num_channels, - voxel_size, - dtype, - mode="a", - write_size=None, - name=None, - overwrite=False, - ): - """ - Create a new ZarrArray given an array identifier. It is assumed that - this array_identifier points to a dataset that does not yet exist. - - Args: - array_identifier (ArrayIdentifier): The array identifier. - axes (List[str]): The axes of the array. - roi (Roi): The region of interest. - num_channels (int): The number of channels. - voxel_size (Coordinate): The voxel size. - dtype (Any): The data type. - write_size (Optional[Coordinate]): The write size. - name (Optional[str]): The name of the array. - overwrite (bool): The boolean value to overwrite the array. - Returns: - ZarrArray: The ZarrArray. - Raises: - NotImplementedError - Examples: - >>> create_from_array_identifier(array_identifier, axes, roi, num_channels, voxel_size, dtype, write_size=None, name=None, overwrite=False) - Notes: - This method is used to create a new ZarrArray given an array identifier. - """ - if write_size is None: - # total storage per block is approx c*x*y*z*dtype_size - # appropriate block size about 5MB. - axis_length = ( - ( - 1024**2 - * 5 - / (num_channels if num_channels is not None else 1) - / np.dtype(dtype).itemsize - ) - ** (1 / voxel_size.dims) - ) // 1 - write_size = Coordinate((axis_length,) * voxel_size.dims) * voxel_size - write_size = Coordinate((min(a, b) for a, b in zip(write_size, roi.shape))) - zarr_container = zarr.open(array_identifier.container, "a") - if num_channels is None: - axes = [axis for axis in axes if "c" not in axis] - num_channels = None - else: - axes = ["c"] + [axis for axis in axes if "c" not in axis] - try: - funlib.persistence.prepare_ds( - f"{array_identifier.container}", - array_identifier.dataset, - roi, - voxel_size, - dtype, - num_channels=num_channels, - write_size=write_size, - delete=overwrite, - force_exact_write_size=True, - ) - zarr_dataset = zarr_container[array_identifier.dataset] - if array_identifier.container.name.endswith("n5"): - zarr_dataset.attrs["offset"] = roi.offset[::-1] - zarr_dataset.attrs["resolution"] = voxel_size[::-1] - zarr_dataset.attrs["axes"] = axes[::-1] - # to make display right in neuroglancer: TODO ADD CHANNELS - zarr_dataset.attrs["dimension_units"] = [ - f"{size} nm" for size in voxel_size[::-1] - ] - zarr_dataset.attrs["_ARRAY_DIMENSIONS"] = [ - a if a != "c" else "c^" for a in axes[::-1] - ] - else: - zarr_dataset.attrs["offset"] = roi.offset - zarr_dataset.attrs["resolution"] = voxel_size - zarr_dataset.attrs["axes"] = axes - # to make display right in neuroglancer: TODO ADD CHANNELS - zarr_dataset.attrs["dimension_units"] = [ - f"{size} nm" for size in voxel_size - ] - zarr_dataset.attrs["_ARRAY_DIMENSIONS"] = [ - a if a != "c" else "c^" for a in axes - ] - if "c" in axes: - if axes.index("c") == 0: - zarr_dataset.attrs["dimension_units"] = [ - str(num_channels) - ] + zarr_dataset.attrs["dimension_units"] - else: - zarr_dataset.attrs["dimension_units"] = zarr_dataset.attrs[ - "dimension_units" - ] + [str(num_channels)] - except zarr.errors.ContainsArrayError: - zarr_dataset = zarr_container[array_identifier.dataset] - assert ( - tuple(zarr_dataset.attrs["offset"]) == roi.offset - ), f"{zarr_dataset.attrs['offset']}, {roi.offset}" - assert ( - tuple(zarr_dataset.attrs["resolution"]) == voxel_size - ), f"{zarr_dataset.attrs['resolution']}, {voxel_size}" - assert tuple(zarr_dataset.attrs["axes"]) == tuple( - axes - ), f"{zarr_dataset.attrs['axes']}, {axes}" - assert ( - zarr_dataset.shape - == ((num_channels,) if num_channels is not None else ()) - + roi.shape / voxel_size - ), f"{zarr_dataset.shape}, {((num_channels,) if num_channels is not None else ()) + roi.shape / voxel_size}" - zarr_dataset[:] = np.zeros(zarr_dataset.shape, dtype) - - zarr_array = cls.__new__(cls) - zarr_array.file_name = array_identifier.container - zarr_array.dataset = array_identifier.dataset - zarr_array._axes = None - zarr_array._attributes = zarr_array.data.attrs - zarr_array.snap_to_grid = None - return zarr_array - - @classmethod - def open_from_array_identifier(cls, array_identifier, name=""): - """ - Opens a new ZarrArray given an array identifier. - - Args: - array_identifier (ArrayIdentifier): The array identifier. - name (str): The name of the array. - Returns: - ZarrArray: The ZarrArray. - Raises: - NotImplementedError - Examples: - >>> open_from_array_identifier(array_identifier, name="") - Notes: - This method is used to open a new ZarrArray given an array identifier. - """ - zarr_array = cls.__new__(cls) - zarr_array.name = name - zarr_array.file_name = array_identifier.container - zarr_array.dataset = array_identifier.dataset - zarr_array._axes = None - zarr_array._attributes = zarr_array.data.attrs - zarr_array.snap_to_grid = None - return zarr_array - - def _can_neuroglance(self) -> bool: - """ - Returns the boolean value of the array. - - Args: - can_neuroglance (bool): The boolean value of the array. - Returns: - bool: The boolean value of the array. - Raises: - NotImplementedError - Examples: - >>> can_neuroglance() - Notes: - This method is used to return the boolean value of the array. - """ - return True - - def _neuroglancer_source(self): - """ - Returns the neuroglancer source of the array. - - Args: - neuroglancer.LocalVolume: The neuroglancer source of the array. - Returns: - neuroglancer.LocalVolume: The neuroglancer source of the array. - Raises: - NotImplementedError - Examples: - >>> neuroglancer_source() - Notes: - This method is used to return the neuroglancer source of the array. - - """ - d = open_ds(str(self.file_name), self.dataset) - return neuroglancer.LocalVolume( - data=d.data, - dimensions=neuroglancer.CoordinateSpace( - names=["z", "y", "x"], - units=["nm", "nm", "nm"], - scales=self.voxel_size, - ), - voxel_offset=self.roi.get_begin() / self.voxel_size, - ) - - def _neuroglancer_layer(self) -> Tuple[neuroglancer.ImageLayer, Dict[str, Any]]: - """ - Returns the neuroglancer layer of the array. - - Args: - layer (neuroglancer.ImageLayer): The neuroglancer layer of the array. - Returns: - Tuple[neuroglancer.ImageLayer, Dict[str, Any]]: The neuroglancer layer of the array. - Raises: - NotImplementedError - Examples: - >>> neuroglancer_layer() - Notes: - This method is used to return the neuroglancer layer of the array. - """ - layer = neuroglancer.ImageLayer(source=self._neuroglancer_source()) - return layer - - def _transform_matrix(self): - """ - Returns the transform matrix of the array. - - Args: - transform_matrix (List[List[float]]): The transform matrix of the array. - Returns: - List[List[float]]: The transform matrix of the array. - Raises: - NotImplementedError - Examples: - >>> transform_matrix() - Notes: - This method is used to return the transform matrix of the array. - """ - is_zarr = self.file_name.name.endswith(".zarr") - if is_zarr: - offset = self.roi.offset - voxel_size = self.voxel_size - matrix = [ - [0] * (self.dims - i - 1) + [1e-9 * vox] + [0] * i + [off / vox] - for i, (vox, off) in enumerate(zip(voxel_size[::-1], offset[::-1])) - ] - if "c" in self.axes: - matrix = [[1] + [0] * (self.dims + 1)] + [[0] + row for row in matrix] - return matrix - else: - offset = self.roi.offset[::-1] - voxel_size = self.voxel_size[::-1] - matrix = [ - [0] * (self.dims - i - 1) + [1] + [0] * i + [off] - for i, (vox, off) in enumerate(zip(voxel_size[::-1], offset[::-1])) - ] - if "c" in self.axes: - matrix = [[1] + [0] * (self.dims + 1)] + [[0] + row for row in matrix] - return matrix - return [[0] * i + [1] + [0] * (self.dims - i) for i in range(self.dims)] - - def _output_dimensions(self) -> Dict[str, Tuple[float, str]]: - """ - Returns the output dimensions of the array. - - Args: - output_dimensions (Dict[str, Tuple[float, str]]): The output dimensions of the array. - Returns: - Dict[str, Tuple[float, str]]: The output dimensions of the array. - Raises: - NotImplementedError - Examples: - >>> output_dimensions() - Notes: - This method is used to return the output dimensions of the array. - """ - is_zarr = self.file_name.name.endswith(".zarr") - if is_zarr: - spatial_dimensions = OrderedDict() - if "c" in self.axes: - spatial_dimensions["c^"] = (1.0, "") - for dim, vox in zip(self.spatial_axes[::-1], self.voxel_size[::-1]): - spatial_dimensions[dim] = (vox * 1e-9, "m") - return spatial_dimensions - else: - return { - dim: (1e-9, "m") - for dim, vox in zip(self.spatial_axes[::-1], self.voxel_size[::-1]) - } - - def _source_name(self) -> str: - """ - Returns the source name of the array. - - Args: - source_name (str): The source name of the array. - Returns: - str: The source name of the array. - Raises: - NotImplementedError - Examples: - >>> source_name() - Notes: - This method is used to return the source name of the array. - - """ - return self.name - - def add_metadata(self, metadata: Dict[str, Any]) -> None: - """ - Adds metadata to the array. - - Args: - metadata (Dict[str, Any]): The metadata to add to the array. - Raises: - NotImplementedError - Examples: - >>> add_metadata(metadata) - Notes: - This method is used to add metadata to the array. - - """ - dataset = zarr.open(self.file_name, mode="a")[self.dataset] - for k, v in metadata.items(): - dataset.attrs[k] = v diff --git a/dacapo/experiments/datasplits/datasets/arrays/zarr_array_config.py b/dacapo/experiments/datasplits/datasets/arrays/zarr_array_config.py index b67717647..6f03a31a0 100644 --- a/dacapo/experiments/datasplits/datasets/arrays/zarr_array_config.py +++ b/dacapo/experiments/datasplits/datasets/arrays/zarr_array_config.py @@ -1,9 +1,9 @@ import attr from .array_config import ArrayConfig -from .zarr_array import ZarrArray from funlib.geometry import Coordinate +from funlib.persistence import open_ds from upath import UPath as Path @@ -28,7 +28,7 @@ class ZarrArrayConfig(ArrayConfig): snap_to_grid: Optional[Coordinate] If you need to make sure your ROI's align with a specific voxel_size _axes: Optional[List[str]] - The axes of your data! + The axis_names of your data! Methods: verify() -> Tuple[bool, str] Check whether this is a valid Array @@ -36,8 +36,6 @@ class ZarrArrayConfig(ArrayConfig): This class is a subclass of ArrayConfig. """ - array_type = ZarrArray - file_name: Path = attr.ib( metadata={"help_text": "The file name of the zarr container."} ) @@ -53,12 +51,15 @@ class ZarrArrayConfig(ArrayConfig): }, ) _axes: Optional[List[str]] = attr.ib( - default=None, metadata={"help_text": "The axes of your data!"} + default=None, metadata={"help_text": "The axis_names of your data!"} ) mode: Optional[str] = attr.ib( default="a", metadata={"help_text": "The access mode!"} ) + def array(self, mode="r"): + return open_ds(f"{self.file_name}/{self.dataset}", mode=mode) + def verify(self) -> Tuple[bool, str]: """ Check whether this is a valid Array diff --git a/dacapo/experiments/datasplits/datasets/dataset.py b/dacapo/experiments/datasplits/datasets/dataset.py index ef8ad2a1d..0eb19ee8f 100644 --- a/dacapo/experiments/datasplits/datasets/dataset.py +++ b/dacapo/experiments/datasplits/datasets/dataset.py @@ -1,5 +1,5 @@ -from .arrays import Array from funlib.geometry import Coordinate +from funlib.persistence import Array from abc import ABC from typing import Optional, Any, List diff --git a/dacapo/experiments/datasplits/datasets/dummy_dataset.py b/dacapo/experiments/datasplits/datasets/dummy_dataset.py index 4fc34e84b..b8e6a2ae0 100644 --- a/dacapo/experiments/datasplits/datasets/dummy_dataset.py +++ b/dacapo/experiments/datasplits/datasets/dummy_dataset.py @@ -1,5 +1,5 @@ from .dataset import Dataset -from .arrays import Array +from funlib.persistence import Array class DummyDataset(Dataset): @@ -35,4 +35,4 @@ def __init__(self, dataset_config): """ super().__init__() self.name = dataset_config.name - self.raw = dataset_config.raw_config.array_type(dataset_config.raw_config) + self.raw = dataset_config.raw_config.array() diff --git a/dacapo/experiments/datasplits/datasets/raw_gt_dataset.py b/dacapo/experiments/datasplits/datasets/raw_gt_dataset.py index 8539e8339..8af1068f9 100644 --- a/dacapo/experiments/datasplits/datasets/raw_gt_dataset.py +++ b/dacapo/experiments/datasplits/datasets/raw_gt_dataset.py @@ -1,5 +1,5 @@ from .dataset import Dataset -from .arrays import Array +from funlib.persistence import Array from funlib.geometry import Coordinate @@ -49,10 +49,10 @@ def __init__(self, dataset_config): This method is used to initialize the dataset. """ self.name = dataset_config.name - self.raw = dataset_config.raw_config.array_type(dataset_config.raw_config) - self.gt = dataset_config.gt_config.array_type(dataset_config.gt_config) + self.raw = dataset_config.raw_config.array() + self.gt = dataset_config.gt_config.array() self.mask = ( - dataset_config.mask_config.array_type(dataset_config.mask_config) + dataset_config.mask_config.array() if dataset_config.mask_config is not None else None ) diff --git a/dacapo/experiments/datasplits/datasplit_generator.py b/dacapo/experiments/datasplits/datasplit_generator.py index 2f860bdbf..f968b0fa1 100644 --- a/dacapo/experiments/datasplits/datasplit_generator.py +++ b/dacapo/experiments/datasplits/datasplit_generator.py @@ -1,4 +1,5 @@ from dacapo.experiments.tasks import TaskConfig +from dacapo.experiments.datasplits.datasets.arrays import ArrayConfig from upath import UPath as Path from typing import List, Union, Optional, Sequence from enum import Enum, EnumMeta @@ -8,9 +9,6 @@ from zarr.n5 import N5FSStore import numpy as np from dacapo.experiments.datasplits.datasets.arrays import ( - ArrayConfig, - ZarrArrayConfig, - ZarrArray, ResampledArrayConfig, BinarizeArrayConfig, IntensitiesArrayConfig, @@ -18,6 +16,7 @@ LogicalOrArrayConfig, ConstantArrayConfig, CropArrayConfig, + ZarrArrayConfig, ) from dacapo.experiments.datasplits import TrainValidateDataSplitConfig from dacapo.experiments.datasplits.datasets import RawGTDatasetConfig @@ -76,7 +75,7 @@ def resize_if_needed( Notes: This function is used to resize the array if needed. """ - zarr_array = ZarrArray(array_config) + zarr_array = array_config.array() raw_voxel_size = zarr_array.voxel_size raw_upsample = raw_voxel_size / target_resolution @@ -102,7 +101,7 @@ def resize_if_needed( def limit_validation_crop_size(gt_config, mask_config, max_size): - gt_array = gt_config.array_type(gt_config) + gt_array = gt_config.array() voxel_shape = gt_array.roi.shape / gt_array.voxel_size crop = False while np.prod(voxel_shape) > max_size: @@ -173,7 +172,7 @@ def get_right_resolution_array_config( snap_to_grid=target_resolution, mode="r", ) - zarr_array = ZarrArray(zarr_config) + zarr_array = zarr_config.array() while ( all([z < t for (z, t) in zip(zarr_array.voxel_size, target_resolution)]) and Path(container, Path(dataset, f"s{level+1}")).exists() @@ -187,7 +186,7 @@ def get_right_resolution_array_config( mode="r", ) - zarr_array = ZarrArray(zarr_config) + zarr_array = zarr_config.array() return resize_if_needed(zarr_config, target_resolution, extra_str) diff --git a/dacapo/experiments/tasks/evaluators/binary_segmentation_evaluator.py b/dacapo/experiments/tasks/evaluators/binary_segmentation_evaluator.py index 178dd8f4b..896d215b4 100644 --- a/dacapo/experiments/tasks/evaluators/binary_segmentation_evaluator.py +++ b/dacapo/experiments/tasks/evaluators/binary_segmentation_evaluator.py @@ -5,7 +5,8 @@ MultiChannelBinarySegmentationEvaluationScores, ) -from dacapo.experiments.datasplits.datasets.arrays import ZarrArray +from dacapo.tmp import open_from_identifier + import numpy as np import SimpleITK as sitk @@ -110,7 +111,7 @@ def evaluate(self, output_array_identifier, evaluation_array): Args: output_array_identifier : str the identifier of the output array - evaluation_array : ZarrArray + evaluation_array : Zarr Array the evaluation array Returns: BinarySegmentationEvaluationScores or MultiChannelBinarySegmentationEvaluationScores @@ -120,13 +121,13 @@ def evaluate(self, output_array_identifier, evaluation_array): Examples: >>> binary_segmentation_evaluator = BinarySegmentationEvaluator(clip_distance=200, tol_distance=40, channels=["channel1", "channel2"]) >>> output_array_identifier = "output_array" - >>> evaluation_array = ZarrArray.open_from_array_identifier("evaluation_array") + >>> evaluation_array = open_from_identifier("evaluation_array") >>> binary_segmentation_evaluator.evaluate(output_array_identifier, evaluation_array) BinarySegmentationEvaluationScores(dice=0.0, jaccard=0.0, hausdorff=0.0, false_negative_rate=0.0, false_positive_rate=0.0, false_discovery_rate=0.0, voi=0.0, mean_false_distance=0.0, mean_false_negative_distance=0.0, mean_false_positive_distance=0.0, mean_false_distance_clipped=0.0, mean_false_negative_distance_clipped=0.0, mean_false_positive_distance_clipped=0.0, precision_with_tolerance=0.0, recall_with_tolerance=0.0, f1_score_with_tolerance=0.0, precision=0.0, recall=0.0, f1_score=0.0) Note: This function is used to evaluate the output array against the evaluation array. """ - output_array = ZarrArray.open_from_array_identifier(output_array_identifier) + output_array = open_from_identifier(output_array_identifier) # removed the .squeeze() because it was used for batch size and now we are feeding 4d c, z, y, x evaluation_data = evaluation_array[evaluation_array.roi] output_data = output_array[output_array.roi] @@ -136,14 +137,16 @@ def evaluate(self, output_array_identifier, evaluation_array): assert ( evaluation_data.shape == output_data.shape ), f"{evaluation_data.shape} vs {output_data.shape}" - if "c" in evaluation_array.axes and "c" in output_array.axes: + if "c^" in evaluation_array.axis_names and "c^" in output_array.axis_names: score_dict = [] - for indx, channel in enumerate(evaluation_array.channels): + for indx, channel in enumerate( + range(evaluation_array.shape[evaluation_array.axis_names.index("c^")]) + ): evaluation_channel_data = evaluation_data.take( - indices=indx, axis=evaluation_array.axes.index("c") + indices=indx, axis=evaluation_array.axis_names.index("c^") ) output_channel_data = output_data.take( - indices=indx, axis=output_array.axes.index("c") + indices=indx, axis=output_array.axis_names.index("c^") ) evaluator = ArrayEvaluator( evaluation_channel_data, diff --git a/dacapo/experiments/tasks/evaluators/evaluator.py b/dacapo/experiments/tasks/evaluators/evaluator.py index beccc57c5..9c709bc4e 100644 --- a/dacapo/experiments/tasks/evaluators/evaluator.py +++ b/dacapo/experiments/tasks/evaluators/evaluator.py @@ -4,11 +4,11 @@ from typing import Tuple, Dict, Optional, List, TYPE_CHECKING, Union import math import itertools +from funlib.persistence import Array if TYPE_CHECKING: from dacapo.experiments.tasks.evaluators.evaluation_scores import EvaluationScores from dacapo.experiments.datasplits.datasets import Dataset - from dacapo.experiments.datasplits.datasets.arrays import Array from dacapo.store.local_array_store import LocalArrayIdentifier from dacapo.experiments.tasks.post_processors import PostProcessorParameters from dacapo.experiments.validation_scores import ValidationScores @@ -57,7 +57,7 @@ class Evaluator(ABC): @abstractmethod def evaluate( - self, output_array_identifier: "LocalArrayIdentifier", evaluation_array: "Array" + self, output_array_identifier: "LocalArrayIdentifier", evaluation_array: Array ) -> "EvaluationScores": """ Compares and evaluates the output array against the evaluation array. diff --git a/dacapo/experiments/tasks/evaluators/instance_evaluator.py b/dacapo/experiments/tasks/evaluators/instance_evaluator.py index d2e179eaa..8a2a67934 100644 --- a/dacapo/experiments/tasks/evaluators/instance_evaluator.py +++ b/dacapo/experiments/tasks/evaluators/instance_evaluator.py @@ -1,9 +1,10 @@ from typing import List -from dacapo.experiments.datasplits.datasets.arrays import ZarrArray + from .evaluator import Evaluator from .instance_evaluation_scores import InstanceEvaluationScores from dacapo.utils.voi import voi as _voi +from dacapo.tmp import open_from_identifier import numpy as np import numpy_indexed as npi @@ -100,7 +101,7 @@ def evaluate(self, output_array_identifier, evaluation_array): Args: output_array_identifier : str the identifier of the output array - evaluation_array : ZarrArray + evaluation_array : Zarr Array the evaluation array Returns: InstanceEvaluationScores @@ -110,14 +111,14 @@ def evaluate(self, output_array_identifier, evaluation_array): Examples: >>> instance_evaluator = InstanceEvaluator() >>> output_array_identifier = "output_array" - >>> evaluation_array = ZarrArray.open_from_array_identifier("evaluation_array") + >>> evaluation_array = open_from_identifier("evaluation_array") >>> instance_evaluator.evaluate(output_array_identifier, evaluation_array) InstanceEvaluationScores(voi_merge=0.0, voi_split=0.0) Note: This function is used to evaluate the output array against the evaluation array. """ - output_array = ZarrArray.open_from_array_identifier(output_array_identifier) + output_array = open_from_identifier(output_array_identifier) evaluation_data = evaluation_array[evaluation_array.roi].astype(np.uint64) output_data = output_array[output_array.roi].astype(np.uint64) results = voi(evaluation_data, output_data) diff --git a/dacapo/experiments/tasks/post_processors/argmax_post_processor.py b/dacapo/experiments/tasks/post_processors/argmax_post_processor.py index 4dc605e10..f736d3e17 100644 --- a/dacapo/experiments/tasks/post_processors/argmax_post_processor.py +++ b/dacapo/experiments/tasks/post_processors/argmax_post_processor.py @@ -1,13 +1,16 @@ -import daisy -from daisy import Roi, Coordinate -from funlib.persistence import open_ds -from dacapo.utils.array_utils import to_ndarray, save_ndarray -from dacapo.experiments.datasplits.datasets.arrays.zarr_array import ZarrArray +from upath import UPath as Path +from dacapo.blockwise import run_blockwise +import dacapo.blockwise + from dacapo.store.array_store import LocalArrayIdentifier from .argmax_post_processor_parameters import ArgmaxPostProcessorParameters from .post_processor import PostProcessor import numpy as np from daisy import Roi, Coordinate +from dacapo.tmp import create_from_identifier, open_from_identifier, np_to_funlib_array +from funlib.persistence import open_ds + +import daisy class ArgmaxPostProcessor(PostProcessor): @@ -81,9 +84,7 @@ def set_prediction(self, prediction_array_identifier): `prediction_array_identifier` attribute. """ self.prediction_array_identifier = prediction_array_identifier - self.prediction_array = ZarrArray.open_from_array_identifier( - prediction_array_identifier - ) + self.prediction_array = open_from_identifier(prediction_array_identifier) def process( self, @@ -112,47 +113,42 @@ def process( This method must be implemented in the subclass. It should process the predictions and return the output array. """ - if self.prediction_array._daisy_array.chunk_shape is not None: - block_size = Coordinate( - self.prediction_array._daisy_array.chunk_shape[ - -self.prediction_array.dims : - ] - ) - - write_size = [ - b * v - for b, v in zip( - block_size[-self.prediction_array.dims :], - self.prediction_array.voxel_size, + if self.prediction_array._source_data.chunks is not None: + block_size = ( + Coordinate( + self.prediction_array._source_data.chunks[ + -self.prediction_array.spatial_dims : + ] + ) + * self.prediction_array.voxel_size ) - ] - output_array = ZarrArray.create_from_array_identifier( + output_array = create_from_identifier( output_array_identifier, - [dim for dim in self.prediction_array.axes if dim != "c"], + [dim for dim in self.prediction_array.axis_names if dim != "c^"], self.prediction_array.roi, None, self.prediction_array.voxel_size, np.uint8, + overwrite=True, ) - read_roi = Roi((0, 0, 0), write_size[-self.prediction_array.dims :]) + read_roi = Roi((0, 0, 0), block_size[-self.prediction_array.dims :]) input_array = open_ds( - self.prediction_array_identifier.container.path, - self.prediction_array_identifier.dataset, + f"{self.prediction_array_identifier.container.path}/{self.prediction_array_identifier.dataset}" ) def process_block(block): # Apply argmax to each block of data data = np.argmax( - to_ndarray(input_array, block.read_roi), - axis=self.prediction_array.axes.index("c"), + input_array[block.write_roi], + axis=self.prediction_array.axis_names.index("c^"), ).astype(np.uint8) - save_ndarray(data, block.write_roi, output_array) + output_array[block.write_roi] = data # Define the task for blockwise processing task = daisy.Task( - f"argmax_{output_array.dataset}", + f"argmax_{output_array_identifier.dataset}", total_roi=self.prediction_array.roi, read_roi=read_roi, write_roi=read_roi, diff --git a/dacapo/experiments/tasks/post_processors/post_processor.py b/dacapo/experiments/tasks/post_processors/post_processor.py index 2b63b15c0..7495e6d6a 100644 --- a/dacapo/experiments/tasks/post_processors/post_processor.py +++ b/dacapo/experiments/tasks/post_processors/post_processor.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from funlib.geometry import Coordinate +from funlib.persistence import Array from typing import Iterable, TYPE_CHECKING @@ -7,7 +8,6 @@ from dacapo.experiments.tasks.post_processors.post_processor_parameters import ( PostProcessorParameters, ) - from dacapo.experiments.datasplits.datasets.arrays import Array from dacapo.store.local_array_store import LocalArrayIdentifier @@ -86,7 +86,7 @@ def process( output_array_identifier: "LocalArrayIdentifier", num_workers: int = 16, chunk_size: Coordinate = Coordinate((64, 64, 64)), - ) -> "Array": + ) -> Array: """ Convert predictions into the final output. diff --git a/dacapo/experiments/tasks/post_processors/threshold_post_processor.py b/dacapo/experiments/tasks/post_processors/threshold_post_processor.py index 2cf719d44..59059e516 100644 --- a/dacapo/experiments/tasks/post_processors/threshold_post_processor.py +++ b/dacapo/experiments/tasks/post_processors/threshold_post_processor.py @@ -1,4 +1,6 @@ -from dacapo.experiments.datasplits.datasets.arrays.zarr_array import ZarrArray +from upath import UPath as Path +from dacapo.blockwise.scheduler import run_blockwise + from .threshold_post_processor_parameters import ThresholdPostProcessorParameters from dacapo.store.array_store import LocalArrayIdentifier from .post_processor import PostProcessor @@ -8,6 +10,13 @@ from dacapo.utils.array_utils import to_ndarray, save_ndarray from funlib.persistence import open_ds +from dacapo.tmp import ( + open_from_identifier, + create_from_identifier, + num_channels_from_array, +) +from funlib.persistence import Array + from typing import Iterable @@ -60,9 +69,7 @@ def set_prediction(self, prediction_array_identifier): This method should set the prediction array using the given identifier. """ self.prediction_array_identifier = prediction_array_identifier - self.prediction_array = ZarrArray.open_from_array_identifier( - prediction_array_identifier - ) + self.prediction_array = open_from_identifier(prediction_array_identifier) def process( self, @@ -70,7 +77,7 @@ def process( output_array_identifier: "LocalArrayIdentifier", num_workers: int = 12, block_size: Coordinate = Coordinate((256, 256, 256)), - ) -> ZarrArray: + ) -> Array: """ Process the prediction with the given parameters. @@ -79,15 +86,13 @@ def process( output_array_identifier (LocalArrayIdentifier): The identifier of the output array. num_workers (int): The number of workers to use for processing. block_size (Coordinate): The block size to use for processing. - Returns: - ZarrArray: The output array. Raises: NotImplementedError: If the method is not implemented. Examples: >>> post_processor.process(parameters, output_array_identifier) Note: This method should process the prediction with the given parameters and return the output array. The method uses the `run_blockwise` function from the `dacapo.blockwise.scheduler` module to run the blockwise post-processing. - The output array is created using the `ZarrArray.create_from_array_identifier` function from the `dacapo.experiments.datasplits.datasets.arrays` module. + The output array is created using the `create_from_identifier` function from the `dacapo.experiments.datasplits.datasets.arrays` module. """ # TODO: Investigate Liskov substitution princple and whether it is a problem here # OOP theory states the super class should always be replaceable with its subclasses @@ -100,8 +105,8 @@ def process( # so our subclasses aren't directly replaceable anyway. # Might be missing something since I only did a quick google, leaving this here # for me or someone else to investigate further in the future. - if self.prediction_array._daisy_array.chunk_shape is not None: - block_size = self.prediction_array._daisy_array.chunk_shape + if self.prediction_array._source_data.chunks is not None: + block_size = self.prediction_array._source_data.chunks write_size = [ b * v @@ -110,31 +115,32 @@ def process( self.prediction_array.voxel_size, ) ] - output_array = ZarrArray.create_from_array_identifier( + output_array = create_from_identifier( output_array_identifier, - self.prediction_array.axes, + self.prediction_array.axis_names, self.prediction_array.roi, - self.prediction_array.num_channels, + num_channels_from_array(self.prediction_array), self.prediction_array.voxel_size, np.uint8, + overwrite=True, ) read_roi = Roi((0, 0, 0), write_size[-self.prediction_array.dims :]) input_array = open_ds( - self.prediction_array_identifier.container.path, - self.prediction_array_identifier.dataset, + f"{self.prediction_array_identifier.container.path}/{self.prediction_array_identifier.dataset}" ) def process_block(block): - data = to_ndarray(input_array, block.read_roi) > parameters.threshold + write_roi = block.write_roi.intersect(input_array.roi) + data = input_array[write_roi] > parameters.threshold data = data.astype(np.uint8) if int(data.max()) == 0: - print("No data in block", block.read_roi) + print("No data in block", write_roi) return - save_ndarray(data, block.write_roi, output_array) + output_array[write_roi] = data task = daisy.Task( - f"threshold_{output_array.dataset}", + f"threshold_{output_array_identifier.dataset}", total_roi=self.prediction_array.roi, read_roi=read_roi, write_roi=read_roi, diff --git a/dacapo/experiments/tasks/post_processors/watershed_post_processor.py b/dacapo/experiments/tasks/post_processors/watershed_post_processor.py index b57f07f42..f43fa14eb 100644 --- a/dacapo/experiments/tasks/post_processors/watershed_post_processor.py +++ b/dacapo/experiments/tasks/post_processors/watershed_post_processor.py @@ -1,7 +1,7 @@ from upath import UPath as Path import dacapo.blockwise from dacapo.blockwise.scheduler import segment_blockwise -from dacapo.experiments.datasplits.datasets.arrays import ZarrArray + from dacapo.store.array_store import LocalArrayIdentifier from dacapo.utils.array_utils import to_ndarray, save_ndarray from funlib.persistence import open_ds @@ -12,6 +12,7 @@ from .post_processor import PostProcessor from funlib.geometry import Coordinate, Roi +from dacapo.tmp import create_from_identifier, open_from_identifier import numpy as np @@ -71,9 +72,7 @@ def enumerate_parameters(self): def set_prediction(self, prediction_array_identifier): self.prediction_array_identifier = prediction_array_identifier - self.prediction_array = ZarrArray.open_from_array_identifier( - prediction_array_identifier - ) + self.prediction_array = open_from_identifier(prediction_array_identifier) """ Set the prediction array. @@ -84,7 +83,7 @@ def set_prediction(self, prediction_array_identifier): Examples: >>> post_processor.set_prediction(prediction_array_identifier) Note: - This method should be implemented by the subclass. To set the prediction array, the method uses the `ZarrArray.open_from_array_identifier` function from the `dacapo.experiments.datasplits.datasets.arrays` module. + This method should be implemented by the subclass. To set the prediction array, the method uses the `open_from_identifier` function from the `dacapo.experiments.datasplits.datasets.arrays` module. """ def process( @@ -111,31 +110,31 @@ def process( Note: This method should be implemented by the subclass. To run the watershed transformation, the method uses the `segment_blockwise` function from the `dacapo.blockwise.scheduler` module. """ - if self.prediction_array._daisy_array.chunk_shape is not None: + if self.prediction_array._source_data.chunks is not None: block_size = Coordinate( - self.prediction_array._daisy_array.chunk_shape[ - -self.prediction_array.dims : + self.prediction_array._source_data.chunks[ + -self.prediction_array.spatial_dims : ] ) - output_array = ZarrArray.create_from_array_identifier( + output_array = create_from_identifier( output_array_identifier, - [axis for axis in self.prediction_array.axes if axis != "c"], + [axis for axis in self.prediction_array.axis_names if axis != "c^"], self.prediction_array.roi, None, self.prediction_array.voxel_size, np.uint64, - block_size * self.prediction_array.voxel_size, + write_size=block_size * self.prediction_array.voxel_size, + overwrite=True, ) input_array = open_ds( - self.prediction_array_identifier.container.path, - self.prediction_array_identifier.dataset, + f"{self.prediction_array_identifier.container.path}/{self.prediction_array_identifier.dataset}", ) - data = to_ndarray(input_array, output_array.roi).astype(float) + data = input_array.to_ndarray(output_array.roi).astype(float) segmentation = mws.agglom( data - parameters.bias, offsets=self.offsets, randomized_strides=True ) - save_ndarray(segmentation, self.prediction_array.roi, output_array) + output_array[self.prediction_array.roi] = segmentation return output_array_identifier diff --git a/dacapo/experiments/tasks/predictors/affinities_predictor.py b/dacapo/experiments/tasks/predictors/affinities_predictor.py index e4084270a..ed8f8fe29 100644 --- a/dacapo/experiments/tasks/predictors/affinities_predictor.py +++ b/dacapo/experiments/tasks/predictors/affinities_predictor.py @@ -1,10 +1,12 @@ from .predictor import Predictor from dacapo.experiments import Model from dacapo.experiments.arraytypes import EmbeddingArray -from dacapo.experiments.datasplits.datasets.arrays import NumpyArray +from dacapo.tmp import np_to_funlib_array from dacapo.utils.affinities import seg_to_affgraph, padding as aff_padding from dacapo.utils.balance_weights import balance_weights +from dacapo.tmp import np_to_funlib_array from funlib.geometry import Coordinate +from funlib.persistence import Array from lsd.train import LsdExtractor from scipy import ndimage import numpy as np @@ -173,20 +175,6 @@ def lsd_pad(self, voxel_size): padding = Coordinate(self.sigma(voxel_size) * multiplier) return padding - @property - def num_channels(self): - """ - Get the number of channels. - - Returns: - int: The number of channels. - Raises: - NotImplementedError: This method is not implemented. - Examples: - >>> predictor.num_channels - """ - return len(self.neighborhood) + self.num_lsds - def create_model(self, architecture): """ Create the model. @@ -202,11 +190,11 @@ def create_model(self, architecture): """ if self.dims == 2: head = torch.nn.Conv2d( - architecture.num_out_channels, self.num_channels, kernel_size=1 + architecture.num_out_channels, len(self.neighborhood), kernel_size=1 ) elif self.dims == 3: head = torch.nn.Conv3d( - architecture.num_out_channels, self.num_channels, kernel_size=1 + architecture.num_out_channels, len(self.neighborhood), kernel_size=1 ) else: raise NotImplementedError( @@ -215,7 +203,7 @@ def create_model(self, architecture): return Model(architecture, head, eval_activation=torch.nn.Sigmoid()) - def create_target(self, gt): + def create_target(self, gt: Array): """ Create the target data. @@ -230,16 +218,19 @@ def create_target(self, gt): """ # zeros - assert gt.num_channels is None or gt.num_channels == 1, ( - "Cannot create affinities from ground truth with multiple channels.\n" - f"GT axes: {gt.axes} with {gt.num_channels} channels" + assert np.prod(gt.physical_shape) == np.prod(gt.shape), ( + "Cannot create affinities from ground truth with nonspatial dimensions.\n" + f"GT axis_names: {gt.axis_names}" ) + assert ( + gt.channel_dims <= 1 + ), "Cannot create affinities from ground truth with more than one channel dimension." label_data = gt[gt.roi] - axes = gt.axes - if gt.num_channels is not None: + axis_names = gt.axis_names + if gt.channel_dims == 1: label_data = label_data[0] else: - axes = ["c"] + axes + axis_names = ["c^"] + axis_names affinities = seg_to_affgraph( label_data + int(self.background_as_object), self.neighborhood ).astype(np.float32) @@ -248,17 +239,15 @@ def create_target(self, gt): segmentation=label_data + int(self.background_as_object), voxel_size=gt.voxel_size, ) - return NumpyArray.from_np_array( + return np_to_funlib_array( np.concatenate([affinities, descriptors], axis=0, dtype=np.float32), - gt.roi, + gt.roi.offset, gt.voxel_size, - axes, ) - return NumpyArray.from_np_array( + return np_to_funlib_array( affinities, - gt.roi, + gt.roi.offset, gt.voxel_size, - axes, ) def _grow_boundaries(self, mask, slab): @@ -297,7 +286,9 @@ def _grow_boundaries(self, mask, slab): mask[background] = 0 return mask - def create_weight(self, gt, target, mask, moving_class_counts=None): + def create_weight( + self, gt: Array, target: Array, mask: Array, moving_class_counts=None + ): """ Create the weight data. @@ -318,14 +309,15 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): ) if self.grow_boundary_iterations > 0: mask_data = self._grow_boundaries( - mask[target.roi], slab=tuple(1 if c == "c" else -1 for c in target.axes) + mask[target.roi], + slab=tuple(1 if c == "c^" else -1 for c in target.axis_names), ) else: mask_data = mask[target.roi] aff_weights, moving_class_counts = balance_weights( - target[target.roi][: self.num_channels - self.num_lsds].astype(np.uint8), + target[target.roi][: len(self.neighborhood)].astype(np.uint8), 2, - slab=tuple(1 if c == "c" else -1 for c in target.axes), + slab=tuple(1 if c == "c^" else -1 for c in target.axis_names), masks=[mask_data], moving_counts=moving_class_counts, clipmin=self.affs_weight_clipmin, @@ -335,7 +327,7 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): lsd_weights, moving_lsd_class_counts = balance_weights( (gt[target.roi] > 0).astype(np.uint8), 2, - slab=(-1,) * len(gt.axes), + slab=(-1,) * len(gt.axis_names), masks=[mask_data], moving_counts=moving_lsd_class_counts, clipmin=self.lsd_weight_clipmin, @@ -344,17 +336,15 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): lsd_weights = np.ones( (self.num_lsds,) + aff_weights.shape[1:], dtype=aff_weights.dtype ) * lsd_weights.reshape((1,) + aff_weights.shape[1:]) - return NumpyArray.from_np_array( + return np_to_funlib_array( np.concatenate([aff_weights, lsd_weights], axis=0), - target.roi, + target.roi.offset, target.voxel_size, - target.axes, ), (moving_class_counts, moving_lsd_class_counts) - return NumpyArray.from_np_array( + return np_to_funlib_array( aff_weights, - target.roi, + target.roi.offset, target.voxel_size, - target.axes, ), (moving_class_counts, moving_lsd_class_counts) def gt_region_for_roi(self, target_spec): diff --git a/dacapo/experiments/tasks/predictors/distance_predictor.py b/dacapo/experiments/tasks/predictors/distance_predictor.py index 403565b00..861a9e1dd 100644 --- a/dacapo/experiments/tasks/predictors/distance_predictor.py +++ b/dacapo/experiments/tasks/predictors/distance_predictor.py @@ -1,10 +1,11 @@ from .predictor import Predictor from dacapo.experiments import Model from dacapo.experiments.arraytypes import DistanceArray -from dacapo.experiments.datasplits.datasets.arrays import NumpyArray from dacapo.utils.balance_weights import balance_weights +from dacapo.tmp import np_to_funlib_array from funlib.geometry import Coordinate +from funlib.persistence import Array from scipy.ndimage.morphology import distance_transform_edt import numpy as np @@ -125,28 +126,15 @@ def create_model(self, architecture): return Model(architecture, head) - def create_target(self, gt): + def create_target(self, gt: Array): """ - Create the target array for training. - - Args: - gt: The ground-truth array. - Returns: - NumpyArray: The created target array. - Raises: - NotImplementedError: This method is not implemented. - Examples: - >>> predictor.create_target(gt) - + Turn the ground truth labels into a distance transform. """ - distances = self.process( - gt.data, gt.voxel_size, self.norm, self.dt_scale_factor - ) - return NumpyArray.from_np_array( + distances = self.process(gt[:], gt.voxel_size, self.norm, self.dt_scale_factor) + return np_to_funlib_array( distances, - gt.roi, + gt.roi.offset, gt.voxel_size, - gt.axes, ) def create_weight(self, gt, target, mask, moving_class_counts=None): @@ -181,18 +169,17 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): weights, moving_class_counts = balance_weights( gt[target.roi], 2, - slab=tuple(1 if c == "c" else -1 for c in gt.axes), + slab=tuple(1 if c == "c^" else -1 for c in gt.axis_names), masks=[mask[target.roi], distance_mask], moving_counts=moving_class_counts, clipmin=self.clipmin, clipmax=self.clipmax, ) return ( - NumpyArray.from_np_array( + np_to_funlib_array( weights, - gt.roi, + gt.roi.offset, gt.voxel_size, - gt.axes, ), moving_class_counts, ) @@ -347,7 +334,7 @@ def process( return all_distances - def __find_boundaries(self, labels): + def __find_boundaries(self, labels: np.ndarray): """ Find the boundaries in the labels. @@ -366,6 +353,10 @@ def __find_boundaries(self, labels): # diff : 0 0 0 1 0 1 0 0 0 1 0 n - 1 # bound.: 00000001000100000001000 2n - 1 + if labels.dtype == bool: + raise ValueError("Labels should not be bools") + labels = labels.astype(np.uint8) + logger.debug(f"computing boundaries for {labels.shape}") dims = len(labels.shape) diff --git a/dacapo/experiments/tasks/predictors/dummy_predictor.py b/dacapo/experiments/tasks/predictors/dummy_predictor.py index 3fb64b9ac..3293f6423 100644 --- a/dacapo/experiments/tasks/predictors/dummy_predictor.py +++ b/dacapo/experiments/tasks/predictors/dummy_predictor.py @@ -1,7 +1,7 @@ from .predictor import Predictor from dacapo.experiments import Model from dacapo.experiments.arraytypes import EmbeddingArray -from dacapo.experiments.datasplits.datasets.arrays import NumpyArray +from dacapo.tmp import np_to_funlib_array import numpy as np import torch @@ -69,11 +69,11 @@ def create_target(self, gt): >>> predictor.create_target(gt) """ # zeros - return NumpyArray.from_np_array( + return np_to_funlib_array( np.zeros((self.embedding_dims,) + gt.data.shape[-gt.dims :]), gt.roi, gt.voxel_size, - ["c"] + gt.axes, + ["c^"] + gt.axis_names, ) def create_weight(self, gt, target, mask, moving_class_counts=None): @@ -94,11 +94,11 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): """ # ones return ( - NumpyArray.from_np_array( + np_to_funlib_array( np.ones(target.data.shape), target.roi, target.voxel_size, - target.axes, + target.axis_names, ), None, ) diff --git a/dacapo/experiments/tasks/predictors/hot_distance_predictor.py b/dacapo/experiments/tasks/predictors/hot_distance_predictor.py index 9b067f230..607c426f0 100644 --- a/dacapo/experiments/tasks/predictors/hot_distance_predictor.py +++ b/dacapo/experiments/tasks/predictors/hot_distance_predictor.py @@ -2,7 +2,7 @@ from .predictor import Predictor from dacapo.experiments import Model from dacapo.experiments.arraytypes import DistanceArray -from dacapo.experiments.datasplits.datasets.arrays import NumpyArray +from dacapo.tmp import np_to_funlib_array from dacapo.utils.balance_weights import balance_weights from funlib.geometry import Coordinate @@ -142,11 +142,11 @@ def create_target(self, gt): >>> target = predictor.create_target(gt) """ target = self.process(gt.data, gt.voxel_size, self.norm, self.dt_scale_factor) - return NumpyArray.from_np_array( + return np_to_funlib_array( target, gt.roi, gt.voxel_size, - gt.axes, + gt.axis_names, ) def create_weight(self, gt, target, mask, moving_class_counts=None): @@ -170,7 +170,7 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): one_hot_weights, one_hot_moving_class_counts = balance_weights( gt[target.roi], 2, - slab=tuple(1 if c == "c" else -1 for c in gt.axes), + slab=tuple(1 if c == "c^" else -1 for c in gt.axis_names), masks=[mask[target.roi]], moving_counts=( None @@ -193,7 +193,7 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): distance_weights, distance_moving_class_counts = balance_weights( gt[target.roi], 2, - slab=tuple(1 if c == "c" else -1 for c in gt.axes), + slab=tuple(1 if c == "c^" else -1 for c in gt.axis_names), masks=[mask[target.roi], distance_mask], moving_counts=( None @@ -207,11 +207,11 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): (one_hot_moving_class_counts, distance_moving_class_counts) ) return ( - NumpyArray.from_np_array( + np_to_funlib_array( weights, gt.roi, gt.voxel_size, - gt.axes, + gt.axis_names, ), moving_class_counts, ) diff --git a/dacapo/experiments/tasks/predictors/inner_distance_predictor.py b/dacapo/experiments/tasks/predictors/inner_distance_predictor.py index 7e168c0e5..b2f50b59a 100644 --- a/dacapo/experiments/tasks/predictors/inner_distance_predictor.py +++ b/dacapo/experiments/tasks/predictors/inner_distance_predictor.py @@ -1,7 +1,7 @@ from .predictor import Predictor from dacapo.experiments import Model from dacapo.experiments.arraytypes import DistanceArray -from dacapo.experiments.datasplits.datasets.arrays import NumpyArray +from dacapo.tmp import np_to_funlib_array from dacapo.utils.balance_weights import balance_weights from funlib.geometry import Coordinate @@ -118,11 +118,11 @@ def create_target(self, gt): distances = self.process( gt.data, gt.voxel_size, self.norm, self.dt_scale_factor ) - return NumpyArray.from_np_array( + return np_to_funlib_array( distances, gt.roi, gt.voxel_size, - gt.axes, + gt.axis_names, ) def create_weight(self, gt, target, mask, moving_class_counts=None): @@ -148,16 +148,16 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): weights, moving_class_counts = balance_weights( gt[target.roi], 2, - slab=tuple(1 if c == "c" else -1 for c in gt.axes), + slab=tuple(1 if c == "c^" else -1 for c in gt.axis_names), masks=[mask[target.roi]], moving_counts=moving_class_counts, ) return ( - NumpyArray.from_np_array( + np_to_funlib_array( weights, gt.roi, gt.voxel_size, - gt.axes, + gt.axis_names, ), moving_class_counts, ) diff --git a/dacapo/experiments/tasks/predictors/one_hot_predictor.py b/dacapo/experiments/tasks/predictors/one_hot_predictor.py index abf90be7e..1ad7fdeec 100644 --- a/dacapo/experiments/tasks/predictors/one_hot_predictor.py +++ b/dacapo/experiments/tasks/predictors/one_hot_predictor.py @@ -1,7 +1,8 @@ from .predictor import Predictor from dacapo.experiments import Model from dacapo.experiments.arraytypes import ProbabilityArray -from dacapo.experiments.datasplits.datasets.arrays import NumpyArray +from dacapo.tmp import np_to_funlib_array +from funlib.persistence import Array import numpy as np import torch @@ -75,26 +76,20 @@ def create_model(self, architecture): return Model(architecture, head) - def create_target(self, gt): + def create_target(self, gt: Array): """ - Create the target array for training. - - Args: - gt: The ground truth array. - Returns: - NumpyArray: The created target array. - Raises: - NotImplementedError: This method is not implemented. - Examples: - >>> target = predictor.create_target(gt) - + Turn labels into a one hot encoding """ - one_hots = self.process(gt.data) - return NumpyArray.from_np_array( + label_data = gt[:] + if gt.channel_dims == 0: + label_data = label_data[np.newaxis] + elif gt.channel_dims > 1: + raise ValueError(f"Cannot handle multiple channel dims: {gt.channel_dims}") + one_hots = self.process(label_data) + return np_to_funlib_array( one_hots, - gt.roi, + gt.roi.offset, gt.voxel_size, - gt.axes, ) def create_weight(self, gt, target, mask, moving_class_counts=None): @@ -115,11 +110,10 @@ def create_weight(self, gt, target, mask, moving_class_counts=None): """ return ( - NumpyArray.from_np_array( + np_to_funlib_array( np.ones(target.data.shape), - target.roi, + target.roi.offset, target.voxel_size, - target.axes, ), None, ) diff --git a/dacapo/experiments/tasks/predictors/predictor.py b/dacapo/experiments/tasks/predictors/predictor.py index 8c1dce00d..bb236ce60 100644 --- a/dacapo/experiments/tasks/predictors/predictor.py +++ b/dacapo/experiments/tasks/predictors/predictor.py @@ -1,4 +1,5 @@ from funlib.geometry import Coordinate +from funlib.persistence import Array from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Tuple @@ -6,7 +7,6 @@ if TYPE_CHECKING: from dacapo.experiments.architectures.architecture import Architecture from dacapo.experiments.model import Model - from dacapo.experiments.datasplits.datasets.arrays import Array class Predictor(ABC): @@ -19,8 +19,8 @@ class Predictor(ABC): Methods: create_model(self, architecture: "Architecture") -> "Model": Given a training architecture, create a model for this predictor. - create_target(self, gt: "Array") -> "Array": Create the target array for training, given a ground-truth array. - create_weight(self, gt: "Array", target: "Array", mask: "Array", moving_class_counts: Any) -> Tuple["Array", Any]: Create the weight array for training, given a ground-truth and associated target array. + create_target(self, gt: Array) -> Array: Create the target array for training, given a ground-truth array. + create_weight(self, gt: Array, target: Array, mask: Array, moving_class_counts: Any) -> Tuple[Array, Any]: Create the weight array for training, given a ground-truth and associated target array. gt_region_for_roi(self, target_spec): Report how much spatial context this predictor needs to generate a target for the given ROI. padding(self, gt_voxel_size: Coordinate) -> Coordinate: Return the padding needed for the ground-truth array. Notes: @@ -48,7 +48,7 @@ def create_model(self, architecture: "Architecture") -> "Model": pass @abstractmethod - def create_target(self, gt: "Array") -> "Array": + def create_target(self, gt: Array) -> Array: """ Create the target array for training, given a ground-truth array. @@ -83,11 +83,11 @@ def create_target(self, gt: "Array") -> "Array": @abstractmethod def create_weight( self, - gt: "Array", - target: "Array", - mask: "Array", + gt: Array, + target: Array, + mask: Array, moving_class_counts: Any, - ) -> Tuple["Array", Any]: + ) -> Tuple[Array, Any]: """ Create the weight array for training, given a ground-truth and associated target array. diff --git a/dacapo/experiments/trainers/gunpowder_trainer.py b/dacapo/experiments/trainers/gunpowder_trainer.py index 104c5fa9c..dcb40c115 100644 --- a/dacapo/experiments/trainers/gunpowder_trainer.py +++ b/dacapo/experiments/trainers/gunpowder_trainer.py @@ -1,20 +1,20 @@ from ..training_iteration_stats import TrainingIterationStats from .trainer import Trainer +from dacapo.tmp import ( + create_from_identifier, + open_from_identifier, + gp_to_funlib_array, + np_to_funlib_array, +) from dacapo.gp import ( - DaCapoArraySource, GraphSource, DaCapoTargetFilter, CopyMask, - Product, -) -from dacapo.experiments.datasplits.datasets.arrays import ( - NumpyArray, - ZarrArray, - OnesArray, ) from funlib.geometry import Coordinate +from funlib.persistence import Array import gunpowder as gp import zarr @@ -172,12 +172,12 @@ def build_batch_provider(self, datasets, model, task, snapshot_container=None): weights.append(dataset.weight) assert isinstance(dataset.weight, int), dataset - raw_source = DaCapoArraySource(dataset.raw, raw_key) + raw_source = gp.ArraySource(raw_key, dataset.raw) if self.clip_raw: raw_source += gp.Crop( raw_key, dataset.gt.roi.snap_to_grid(dataset.raw.voxel_size) ) - gt_source = DaCapoArraySource(dataset.gt, gt_key) + gt_source = gp.ArraySource(gt_key, dataset.gt) sample_points = dataset.sample_points points_source = None if sample_points is not None: @@ -188,14 +188,23 @@ def build_batch_provider(self, datasets, model, task, snapshot_container=None): ) points_source = GraphSource(sample_points_key, graph) if dataset.mask is not None: - mask_source = DaCapoArraySource(dataset.mask, mask_key) + mask_source = gp.ArraySource(mask_key, dataset.mask) else: # Always provide a mask. By default it is simply an array # of ones with the same shape/roi as gt. Avoids making us # specially handle no mask case and allows padding of the # ground truth without worrying about training on incorrect # data. - mask_source = DaCapoArraySource(OnesArray.like(dataset.gt), mask_key) + mask_source = gp.ArraySource( + mask_key, + Array( + np.ones(dataset.gt.data.shape, dtype=dataset.gt.data.dtype), + offset=dataset.gt.roi.offset, + voxel_size=dataset.gt.voxel_size, + axis_names=dataset.gt.axis_names, + units=dataset.gt.units, + ), + ) array_sources = [raw_source, gt_source, mask_source] + ( [points_source] if points_source is not None else [] ) @@ -324,22 +333,29 @@ def iterate(self, num_iterations, model, optimizer, device): and iteration % self.snapshot_iteration == 0 ): snapshot_zarr = zarr.open(self.snapshot_container.container, "a") + # remove batch dim from all snapshot arrays snapshot_arrays = { - "volumes/raw": raw, - "volumes/gt": gt, - "volumes/target": target, - "volumes/weight": weight, - "volumes/prediction": NumpyArray.from_np_array( - predicted.detach().cpu().numpy(), - target.roi, - target.voxel_size, - target.axes, + "volumes/raw": np_to_funlib_array( + raw[0], offset=raw.offset, voxel_size=raw.voxel_size + ), + "volumes/gt": np_to_funlib_array( + gt[0], offset=gt.offset, voxel_size=gt.voxel_size + ), + "volumes/target": np_to_funlib_array( + target[0], offset=target.offset, voxel_size=target.voxel_size + ), + "volumes/weight": np_to_funlib_array( + weight[0], offset=weight.offset, voxel_size=weight.voxel_size + ), + "volumes/prediction": np_to_funlib_array( + predicted.detach().cpu().numpy()[0], + offset=target.roi.offset, + voxel_size=target.voxel_size, ), - "volumes/gradients": NumpyArray.from_np_array( - predicted.grad.detach().cpu().numpy(), - target.roi, - target.voxel_size, - target.axes, + "volumes/gradients": np_to_funlib_array( + predicted.grad.detach().cpu().numpy()[0], + offset=target.roi.offset, + voxel_size=target.voxel_size, ), } if mask is not None: @@ -350,43 +366,40 @@ def iterate(self, num_iterations, model, optimizer, device): ) for k, v in snapshot_arrays.items(): k = f"{iteration}/{k}" + snapshot_array_identifier = ( + self.snapshot_container.array_identifier(k) + ) if k not in snapshot_zarr: - snapshot_array_identifier = ( - self.snapshot_container.array_identifier(k) - ) - if v.num_channels == 1: - channels = None - else: - channels = v.num_channels - ZarrArray.create_from_array_identifier( + array = create_from_identifier( snapshot_array_identifier, - v.axes, + v.axis_names, v.roi, - channels, + ( + v.shape[0] + if (v.channel_dims == 1 and v.shape[0] > 1) + else None + ), v.voxel_size, v.dtype if not v.dtype == bool else np.float32, model.output_shape * v.voxel_size, + overwrite=True, ) - dataset = snapshot_zarr[k] else: - dataset = snapshot_zarr[k] - # remove batch dimension. Everything has a batch - # and channel dim because of torch. + array = open_from_identifier( + snapshot_array_identifier, mode="a" + ) + + # neuroglancer doesn't allow bools if not v.dtype == bool: - data = v[v.roi][0] + data = v[:] else: - data = v[v.roi][0].astype(np.float32) - if v.num_channels is None or v.num_channels == 1: - # remove channel dimension - assert data.shape[0] == 1, ( - f"Data for array {k} should not have channels but has shape: " - f"{v.shape}. The first dimension is channels" - ) + data = v[:].astype(np.float32) + + # remove channel dim if there is only 1 channel + if v.channel_dims == 1 and v.shape[0] == 1: data = data[0] - dataset[:] = data - dataset.attrs["offset"] = v.roi.offset - dataset.attrs["resolution"] = v.voxel_size - dataset.attrs["axes"] = v.axes + + array[:] = data logger.debug( f"Trainer step took {time.time() - t_start_prediction} seconds" @@ -425,7 +438,7 @@ def next(self): Fetches the next batch of data. Returns: - Tuple[NumpyArray, NumpyArray, NumpyArray, NumpyArray, NumpyArray]: A tuple containing the raw data, ground truth data, target data, weight data, and mask data. + Tuple[Array, Array, Array, Array, Array]: A tuple containing the raw data, ground truth data, target data, weight data, and mask data. Raises: NotImplementedError: If the method is not implemented by the subclass. Examples: @@ -435,12 +448,14 @@ def next(self): batch = next(self._iter) self._iter.send(False) return ( - NumpyArray.from_gp_array(batch[self._raw_key]), - NumpyArray.from_gp_array(batch[self._gt_key]), - NumpyArray.from_gp_array(batch[self._target_key]), - NumpyArray.from_gp_array(batch[self._weight_key]), + gp_to_funlib_array( + batch[self._raw_key], + ), + gp_to_funlib_array(batch[self._gt_key]), + gp_to_funlib_array(batch[self._target_key]), + gp_to_funlib_array(batch[self._weight_key]), ( - NumpyArray.from_gp_array(batch[self._mask_key]) + gp_to_funlib_array(batch[self._mask_key]) if self._mask_key is not None else None ), diff --git a/dacapo/gp/__init__.py b/dacapo/gp/__init__.py index 0e81de5d4..e0273fccb 100644 --- a/dacapo/gp/__init__.py +++ b/dacapo/gp/__init__.py @@ -1,4 +1,3 @@ -from .dacapo_array_source import DaCapoArraySource from .dacapo_create_target import DaCapoTargetFilter from .gamma_noise import GammaAugment from .elastic_augment_fuse import ElasticAugment diff --git a/dacapo/gp/dacapo_array_source.py b/dacapo/gp/dacapo_array_source.py deleted file mode 100644 index 2fb750c8b..000000000 --- a/dacapo/gp/dacapo_array_source.py +++ /dev/null @@ -1,98 +0,0 @@ -# from dacapo.stateless.arraysources.helpers import ArraySource - -from dacapo.experiments.datasplits.datasets.arrays import Array - -import gunpowder as gp -from gunpowder.profiling import Timing -from gunpowder.array_spec import ArraySpec - -import numpy as np - - -class DaCapoArraySource(gp.BatchProvider): - """ - A DaCapo Array source node - - Attributes: - array (Array): The array to be served. - key (gp.ArrayKey): The key of the array to be served. - Methods: - setup(): Set up the provider. - provide(request): Provides the array for the requested ROI. - Note: - This class is a subclass of gunpowder.BatchProvider and is used to - serve array data to gunpowder pipelines. - """ - - def __init__(self, array: Array, key: gp.ArrayKey): - """ - Create a DaCapoArraySource object. - - Args: - array (Array): The array to be served. - key (gp.ArrayKey): The key of the array to be served. - Raises: - TypeError: If key is not of type gp.ArrayKey. - TypeError: If array is not of type Array. - Examples: - >>> from dacapo.experiments.datasplits.datasets.arrays import Array - >>> from gunpowder import ArrayKey - >>> array = Array() - >>> array_source = DaCapoArraySource(array, gp.ArrayKey("ARRAY")) - """ - self.array = array - self.array_spec = ArraySpec( - roi=self.array.roi, voxel_size=self.array.voxel_size - ) - self.key = key - - def setup(self): - """ - Adds the key and the array spec to the provider. - - Raises: - RuntimeError: If the key is already provided. - Examples: - >>> array_source.setup() - - """ - self.provides(self.key, self.array_spec.copy()) - - def provide(self, request): - """ - Provides data based on the given request. - - Args: - request (gp.BatchRequest): The request for data - Returns: - gp.Batch: The batch containing the provided data - Raises: - ValueError: If the input data contains NaN values - Examples: - >>> array_source.provide(request) - - """ - output = gp.Batch() - - timing_provide = Timing(self, "provide") - timing_provide.start() - - spec = self.array_spec.copy() - spec.roi = request[self.key].roi - - if spec.roi.empty: - data = np.zeros((0,) * len(self.array.axes)) - else: - data = self.array[spec.roi] - if "c" not in self.array.axes: - # add a channel dimension - data = np.expand_dims(data, 0) - if np.any(np.isnan(data)): - raise ValueError("INPUT DATA CAN'T BE NAN") - output[self.key] = gp.Array(data, spec=spec) - - timing_provide.stop() - - output.profiling_stats.add(timing_provide) - - return output diff --git a/dacapo/gp/dacapo_create_target.py b/dacapo/gp/dacapo_create_target.py index 13514cebc..1f26bd4e4 100644 --- a/dacapo/gp/dacapo_create_target.py +++ b/dacapo/gp/dacapo_create_target.py @@ -1,5 +1,5 @@ from dacapo.experiments.tasks.predictors import Predictor -from dacapo.experiments.datasplits.datasets.arrays import NumpyArray +from dacapo.tmp import gp_to_funlib_array import gunpowder as gp @@ -152,9 +152,9 @@ def process(self, batch, request): """ output = gp.Batch() - gt_array = NumpyArray.from_gp_array(batch[self.gt_key]) + gt_array = gp_to_funlib_array(batch[self.gt_key]) target_array = self.predictor.create_target(gt_array) - mask_array = NumpyArray.from_gp_array(batch[self.mask_key]) + mask_array = gp_to_funlib_array(batch[self.mask_key]) if self.target_key is not None: request_spec = request[self.target_key] diff --git a/dacapo/plot.py b/dacapo/plot.py index f367ca9a4..1ac82e965 100644 --- a/dacapo/plot.py +++ b/dacapo/plot.py @@ -403,9 +403,9 @@ def plot_runs( include_validation_figure = False include_loss_figure = False - fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(15, 10)) - loss_ax = axes[0] - validation_ax = axes[1] + fig, axis_names = plt.subplots(nrows=2, ncols=1, figsize=(15, 10)) + loss_ax = axis_names[0] + validation_ax = axis_names[1] for run, color in zip(runs, colors): name = run.name diff --git a/dacapo/predict.py b/dacapo/predict.py index ca0f028b9..7eda281b5 100644 --- a/dacapo/predict.py +++ b/dacapo/predict.py @@ -4,8 +4,9 @@ from dacapo.experiments import Run from dacapo.store.create_store import create_config_store, create_weights_store from dacapo.store.local_array_store import LocalArrayIdentifier -from dacapo.experiments.datasplits.datasets.arrays import ZarrArray + from dacapo.compute_context import create_compute_context, LocalTorch +from dacapo.tmp import open_from_identifier, create_from_identifier from funlib.geometry import Coordinate, Roi import numpy as np @@ -56,7 +57,7 @@ def predict( # get arrays input_array_identifier = LocalArrayIdentifier(Path(input_container), input_dataset) - raw_array = ZarrArray.open_from_array_identifier(input_array_identifier) + raw_array = open_from_identifier(input_array_identifier) if isinstance(output_path, LocalArrayIdentifier): output_array_identifier = output_path else: @@ -124,10 +125,19 @@ def predict( print(f"Total input ROI: {_input_roi}, output ROI: {output_roi}") # prepare prediction dataset - ZarrArray.create_from_array_identifier( + if raw_array.channel_dims == 0: + axis_names = ["c^"] + raw_array.axis_names + else: + axis_names = raw_array.axis_names + + if isinstance(output_roi, Roi): + out_roi: Roi = output_roi + else: + raise ValueError("out_roi must be a roi") + create_from_identifier( output_array_identifier, - raw_array.axes, - output_roi, + axis_names, + out_roi, num_out_channels, output_voxel_size, output_dtype, diff --git a/dacapo/predict_local.py b/dacapo/predict_local.py index 76858012b..674d00a40 100644 --- a/dacapo/predict_local.py +++ b/dacapo/predict_local.py @@ -2,7 +2,6 @@ from dacapo.store.local_array_store import LocalArrayIdentifier from funlib.persistence import open_ds, prepare_ds, Array from dacapo.utils.array_utils import to_ndarray -from dacapo.experiments.datasplits.datasets.arrays.zarr_array import ZarrArray from funlib.geometry import Coordinate, Roi import numpy as np from dacapo.compute_context import create_compute_context @@ -12,6 +11,7 @@ import torch import os from dacapo.utils.array_utils import to_ndarray, save_ndarray +from dacapo.tmp import create_from_identifier logger = logging.getLogger(__name__) @@ -25,7 +25,7 @@ def predict( # get the model's input and output size if isinstance(raw_array_identifier, LocalArrayIdentifier): raw_array = open_ds( - str(raw_array_identifier.container), raw_array_identifier.dataset + f"{raw_array_identifier.container}/{raw_array_identifier.dataset}" ) else: raw_array = raw_array_identifier @@ -47,17 +47,18 @@ def predict( read_roi = Roi((0, 0, 0), input_size) write_roi = read_roi.grow(-context, -context) - axes = ["c", "z", "y", "x"] + axes = ["c^", "z", "y", "x"] num_channels = model.num_out_channels - result_dataset = ZarrArray.create_from_array_identifier( + result_dataset = create_from_identifier( prediction_array_identifier, axes, output_roi, num_channels, output_voxel_size, np.float32, + overwrite=True, ) logger.info("Total input ROI: %s, output ROI: %s", input_size, output_roi) @@ -71,20 +72,39 @@ def predict( device = compute_context.device def predict_fn(block): - raw_input = to_ndarray(raw_array, block.read_roi) - # expend batch dim - # no need to normalize, done by datasplit - raw_input = np.expand_dims(raw_input, (0, 1)) + raw_input = raw_array.to_ndarray(block.read_roi) + + # expand batch dimension + # this is done in case models use BatchNorm or similar layers that + # expect a batch dimension + raw_input = np.expand_dims(raw_input, 0) + + # raw may or may not have channel dimensions. + axis_names = raw_array.axis_names + if raw_array.channel_dims == 0: + raw_input = np.expand_dims(raw_input, 0) + axis_names = ["c^"] + axis_names + with torch.no_grad(): + model.eval() predictions = ( model.forward(torch.from_numpy(raw_input).float().to(device)) .detach() .cpu() .numpy()[0] ) + model.train() + predictions = Array( + predictions, + block.write_roi.offset, + raw_array.voxel_size, + axis_names, + raw_array.units, + ) - save_ndarray(predictions, block.write_roi, result_dataset) - # result_dataset[block.write_roi] = predictions + result_dataset[block.write_roi.intersect(result_dataset.roi)] = predictions[ + block.write_roi.intersect(result_dataset.roi) + ] # fixing the input roi to be a multiple of the output voxel size input_roi = input_roi.snap_to_grid( diff --git a/dacapo/store/array_store.py b/dacapo/store/array_store.py index 0e48a0882..fef838ee2 100644 --- a/dacapo/store/array_store.py +++ b/dacapo/store/array_store.py @@ -1,5 +1,3 @@ -from dacapo.experiments.datasplits.datasets.arrays.zarr_array import ZarrArray - import zarr import neuroglancer import attr @@ -264,13 +262,13 @@ def add_element(name, obj): with viewer.txn() as s: snapshot_layers = {} for snapshot in snapshots: - snapshot_layers[snapshot] = ZarrArray.open_from_array_identifier( + snapshot_layers[snapshot] = open_from_identifier( snapshot_container.array_identifier(snapshot), name=snapshot )._neuroglancer_layer() validation_layers = {} for validation in validations: - validation_layers[validation] = ZarrArray.open_from_array_identifier( + validation_layers[validation] = open_from_identifier( validation_container.array_identifier(validation), name=validation )._neuroglancer_layer() diff --git a/dacapo/tmp.py b/dacapo/tmp.py new file mode 100644 index 000000000..672745c90 --- /dev/null +++ b/dacapo/tmp.py @@ -0,0 +1,90 @@ +from funlib.persistence import open_ds, prepare_ds, Array +from funlib.geometry import Roi, Coordinate + +from pathlib import Path + + +def num_channels_from_array(array: Array) -> int | None: + if array.channel_dims == 1: + assert array.axis_names[0] == "c^", array.axis_names + return array.shape[0] + elif array.channel_dims == 0: + return None + else: + raise ValueError( + "Trying to get number of channels from an array with multiple channel dimensions:", + array.axis_names, + ) + + +def gp_to_funlib_array(gp_array) -> Array: + n_dims = len(gp_array.data.shape) + physical_dims = gp_array.spec.roi.dims + channel_dims = n_dims - physical_dims + axis_names = (["b^", "c^"][-channel_dims:] if channel_dims > 0 else []) + [ + "z", + "y", + "x", + ][-physical_dims:] + return Array( + gp_array.data, + offset=gp_array.spec.roi.offset, + voxel_size=gp_array.spec.voxel_size, + axis_names=axis_names, + ) + + +def np_to_funlib_array(np_array, offset: Coordinate, voxel_size: Coordinate) -> Array: + n_dims = len(np_array.shape) + physical_dims = offset.dims + channel_dims = n_dims - physical_dims + axis_names = (["b^", "c^"][-channel_dims:] if channel_dims > 0 else []) + [ + "z", + "y", + "x", + ][-physical_dims:] + return Array( + np_array, + offset=offset, + voxel_size=voxel_size, + axis_names=axis_names, + ) + + +def create_from_identifier( + array_identifier, + axis_names, + roi: Roi, + num_channels: int | None, + voxel_size: Coordinate, + dtype, + mode="a+", + write_size=None, + name=None, + overwrite=False, +) -> Array: + out_path = Path(f"{array_identifier.container}/{array_identifier.dataset}") + if not out_path.parent.exists(): + out_path.parent.mkdir(parents=True) + + list_num_channels = [num_channels] if num_channels is not None else [] + return prepare_ds( + out_path, + shape=(*list_num_channels, *roi.shape / voxel_size), + offset=roi.offset / voxel_size, + voxel_size=voxel_size, + axis_names=axis_names, + dtype=dtype, + chunk_shape=( + (*list_num_channels, *write_size / voxel_size) + if write_size is not None + else None + ), + mode=mode if overwrite is False else "w", + ) + + +def open_from_identifier(array_identifier, name: str = "", mode: str = "r") -> Array: + return open_ds( + f"{array_identifier.container}/{array_identifier.dataset}", mode=mode + ) diff --git a/dacapo/validate.py b/dacapo/validate.py index 495f9cccc..4e091ff55 100644 --- a/dacapo/validate.py +++ b/dacapo/validate.py @@ -1,8 +1,9 @@ from .predict_local import predict from .experiments import Run, ValidationIterationScores -from .experiments.datasplits.datasets.arrays import ZarrArray -from dacapo.store.create_store import ( +from dacapo.tmp import create_from_identifier, num_channels_from_array + +from .store.create_store import ( create_array_store, create_config_store, create_stats_store, @@ -140,26 +141,28 @@ def validate_run(run: Run, iteration: int, datasets_config=None): .snap_to_grid(validation_dataset.raw.voxel_size, mode="grow") .intersect(validation_dataset.raw.roi) ) - input_raw = ZarrArray.create_from_array_identifier( + input_raw = create_from_identifier( input_raw_array_identifier, - validation_dataset.raw.axes, + validation_dataset.raw.axis_names, input_roi, - validation_dataset.raw.num_channels, + num_channels_from_array(validation_dataset.raw), validation_dataset.raw.voxel_size, validation_dataset.raw.dtype, name=f"{run.name}_validation_raw", write_size=input_size, + overwrite=True, ) - input_raw[input_roi] = validation_dataset.raw[input_roi] - input_gt = ZarrArray.create_from_array_identifier( + input_raw[input_roi] = validation_dataset.raw[input_roi].squeeze() + input_gt = create_from_identifier( input_gt_array_identifier, - validation_dataset.gt.axes, + validation_dataset.gt.axis_names, output_roi, - validation_dataset.gt.num_channels, + num_channels_from_array(validation_dataset.gt), validation_dataset.gt.voxel_size, validation_dataset.gt.dtype, name=f"{run.name}_validation_gt", write_size=output_size, + overwrite=True, ) input_gt[output_roi] = validation_dataset.gt[output_roi] else: @@ -188,40 +191,67 @@ def validate_run(run: Run, iteration: int, datasets_config=None): parameters, output_array_identifier ) - scores = evaluator.evaluate(output_array_identifier, validation_dataset.gt) - - # for criterion in run.validation_scores.criteria: - # # replace predictions in array with the new better predictions - # if evaluator.is_best( - # validation_dataset, - # parameters, - # criterion, - # scores, - # ): - # best_array_identifier = array_store.best_validation_array( - # run.name, criterion, index=validation_dataset.name - # ) - # best_array = ZarrArray.create_from_array_identifier( - # best_array_identifier, - # post_processed_array.axes, - # post_processed_array.roi, - # post_processed_array.num_channels, - # post_processed_array.voxel_size, - # post_processed_array.dtype, - # ) - # best_array[best_array.roi] = post_processed_array[ - # post_processed_array.roi - # ] - # best_array.add_metadata( - # { - # "iteration": iteration, - # criterion: getattr(scores, criterion), - # "parameters_id": parameters.id, - # } - # ) - # weights_store.store_best( - # run, iteration, validation_dataset.name, criterion - # ) + try: + scores = evaluator.evaluate( + output_array_identifier, + validation_dataset.gt, # type: ignore + ) + # for criterion in run.validation_scores.criteria: + # # replace predictions in array with the new better predictions + # if evaluator.is_best( + # validation_dataset, + # parameters, + # criterion, + # scores, + # ): + # # then this is the current best score for this parameter, but not necessarily the overall best + # # initial_best_score = overall_best_scores[criterion] + # current_score = getattr(scores, criterion) + # if not overall_best_scores[criterion] or evaluator.compare( + # current_score, overall_best_scores[criterion], criterion + # ): + # any_overall_best = True + # overall_best_scores[criterion] = current_score + + # # For example, if parameter 2 did better this round than it did in other rounds, but it was still worse than parameter 1 + # # the code would have overwritten it below since all parameters write to the same file. Now each parameter will be its own file + # # Either we do that, or we only write out the overall best, regardless of parameters + # best_array_identifier = array_store.best_validation_array( + # run.name, + # criterion, + # index=validation_dataset.name, + # ) + # best_array = create_from_identifier( + # best_array_identifier, + # post_processed_array.axis_names, + # post_processed_array.roi, + # num_channels_from_array(post_processed_array), + # post_processed_array.voxel_size, + # post_processed_array.dtype, + # output_size, + # ) + # best_array[best_array.roi] = post_processed_array[ + # post_processed_array.roi + # ] + # best_array.add_metadata( + # { + # "iteration": iteration, + # criterion: getattr(scores, criterion), + # "parameters_id": parameters.id, + # } + # ) + # weights_store.store_best( + # run.name, + # iteration, + # validation_dataset.name, + # criterion, + # ) + except: + logger.error( + f"Could not evaluate run {run.name} on dataset {validation_dataset.name} with parameters {parameters}.", + exc_info=True, + stack_info=True, + ) # delete current output. We only keep the best outputs as determined by # the evaluator diff --git a/docs/source/conf.py b/docs/source/conf.py index 0f3330788..7a239919e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -29,6 +29,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "nbsphinx", "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", @@ -38,6 +39,10 @@ "myst_nb", # integrate ipynb ] +nbsphinx_custom_formats = { + ".py": ["jupytext.reads", {"fmt", "py:percent"}], +} + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/source/notebooks/minimal_tutorial.ipynb b/docs/source/notebooks/minimal_tutorial.ipynb deleted file mode 100644 index be8c81c17..000000000 --- a/docs/source/notebooks/minimal_tutorial.ipynb +++ /dev/null @@ -1,699 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "c31a8544", - "metadata": { - "lines_to_next_cell": 2 - }, - "source": [ - "# Minimal Tutorial\n", - "DaCapo is a framework for easy application of established machine learning techniques on large, multi-dimensional images.\n", - "![DaCapo Diagram](https://raw.githubusercontent.com/janelia-cellmap/dacapo/main/docs/source/_static/dacapo_diagram.png)" - ] - }, - { - "cell_type": "markdown", - "id": "7a3fc568", - "metadata": {}, - "source": [ - "## Needed Libraries for this Tutorial\n", - "For the tutorial we will use data from the `skimage` library, and we will use `matplotlib` to visualize the data. You can install these libraries using the following commands:\n", - "\n", - "```bash\n", - "pip install 'scikit-image[data]'\n", - "pip install matplotlib\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "63c9c494", - "metadata": {}, - "source": [ - "## Introduction and overview\n", - "\n", - "In this tutorial we will cover the basics of running an ML experiment with DaCapo.\n", - "\n", - "DaCapo has 4 major configurable components:\n", - "\n", - "1. **dacapo.datasplits.DataSplit**\n", - "\n", - "2. **dacapo.tasks.Task**\n", - "\n", - "3. **dacapo.architectures.Architecture**\n", - "\n", - "4. **dacapo.trainers.Trainer**\n", - "\n", - "These are then combined in a single **dacapo.experiments.Run** that includes\n", - "your starting point (whether you want to start training from scratch or\n", - "continue off of a previously trained model) and stopping criterion (the number\n", - "of iterations you want to train)." - ] - }, - { - "cell_type": "markdown", - "id": "9c131cfe", - "metadata": {}, - "source": [ - "## Environment setup\n", - "If you have not already done so, you will need to install DaCapo. You can do this\n", - "by first creating a new environment and then installing DaCapo using pip.\n", - "\n", - "```bash\n", - "conda create -n dacapo python=3.10\n", - "conda activate dacapo\n", - "```\n", - "\n", - "Then, you can install DaCapo using pip, via GitHub:\n", - "\n", - "```bash\n", - "pip install git+https://github.com/janelia-cellmap/dacapo.git\n", - "```\n", - "```bash\n", - "pip install dacapo-ml\n", - "```\n", - "\n", - "Be sure to select this environment in your Jupyter notebook or JupyterLab." - ] - }, - { - "cell_type": "markdown", - "id": "a552197b", - "metadata": {}, - "source": [ - "## Config Store\n", - "To define where the data goes, create a dacapo.yaml configuration file either in `~/.config/dacapo/dacapo.yaml` or in `./dacapo.yaml`. Here is a template:\n", - "\n", - "```yaml\n", - "type: files\n", - "runs_base_dir: /path/to/my/data/storage\n", - "```\n", - "The `runs_base_dir` defines where your on-disk data will be stored. The `type` setting determines the database backend. The default is `files`, which stores the data in a file tree on disk. Alternatively, you can use `mongodb` to store the data in a MongoDB database. To use MongoDB, you will need to provide a `mongodbhost` and `mongodbname` in the configuration file:\n", - "\n", - "```yaml\n", - "mongodbhost: mongodb://dbuser:dbpass@dburl:dbport/\n", - "mongodbname: dacapo\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "e253df12", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:36.201154Z", - "iopub.status.busy": "2024-10-23T13:40:36.200557Z", - "iopub.status.idle": "2024-10-23T13:40:40.170857Z", - "shell.execute_reply": "2024-10-23T13:40:40.169984Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Creating FileConfigStore:\n", - "\tpath: /Users/zouinkhim/dacapo/configs\n" - ] - } - ], - "source": [ - "# First we need to create a config store to store our configurations\n", - "import multiprocessing\n", - "\n", - "multiprocessing.set_start_method(\"fork\", force=True)\n", - "from dacapo.store.create_store import create_config_store, create_stats_store\n", - "\n", - "config_store = create_config_store()" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "56eb9e67", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.175418Z", - "iopub.status.busy": "2024-10-23T13:40:40.174729Z", - "iopub.status.idle": "2024-10-23T13:40:40.631183Z", - "shell.execute_reply": "2024-10-23T13:40:40.630881Z" - }, - "title": "Create some data" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data saved to cells3d.zarr\n" - ] - } - ], - "source": [ - "\n", - "# import random\n", - "\n", - "import matplotlib.pyplot as plt\n", - "from matplotlib.colors import ListedColormap\n", - "import numpy as np\n", - "from funlib.geometry import Coordinate, Roi\n", - "from funlib.persistence import prepare_ds\n", - "from scipy.ndimage import label\n", - "from skimage import data\n", - "from skimage.filters import gaussian\n", - "\n", - "from dacapo.utils.affinities import seg_to_affgraph\n", - "\n", - "# Download the data\n", - "cell_data = (data.cells3d().transpose((1, 0, 2, 3)) / 256).astype(np.uint8)\n", - "\n", - "# Handle metadata\n", - "offset = Coordinate(0, 0, 0)\n", - "voxel_size = Coordinate(290, 260, 260)\n", - "axis_names = [\"c^\", \"z\", \"y\", \"x\"]\n", - "units = [\"nm\", \"nm\", \"nm\"]\n", - "\n", - "# Create the zarr array with appropriate metadata\n", - "cell_array = prepare_ds(\n", - " \"cells3d.zarr\",\n", - " \"raw\",\n", - " Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size,\n", - " voxel_size=voxel_size,\n", - " dtype=np.uint8,\n", - " num_channels=None,\n", - ")\n", - "\n", - "# Save the cell data to the zarr array\n", - "cell_array[cell_array.roi] = cell_data[1]\n", - "\n", - "# Generate and save some pseudo ground truth data\n", - "mask_array = prepare_ds(\n", - " \"cells3d.zarr\",\n", - " \"mask\",\n", - " Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size,\n", - " voxel_size=voxel_size,\n", - " dtype=np.uint8,\n", - ")\n", - "cell_mask = np.clip(gaussian(cell_data[1] / 255.0, sigma=1), 0, 255) * 255 > 30\n", - "not_membrane_mask = np.clip(gaussian(cell_data[0] / 255.0, sigma=1), 0, 255) * 255 < 10\n", - "mask_array[mask_array.roi] = cell_mask * not_membrane_mask\n", - "\n", - "# Generate labels via connected components\n", - "labels_array = prepare_ds(\n", - " \"cells3d.zarr\",\n", - " \"labels\",\n", - " Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size,\n", - " voxel_size=voxel_size,\n", - " dtype=np.uint8,\n", - ")\n", - "labels_array[labels_array.roi] = label(mask_array.to_ndarray(mask_array.roi))[0]\n", - "\n", - "print(\"Data saved to cells3d.zarr\")\n", - "\n", - "\n", - "# Create a custom label color map for showing instances\n", - "np.random.seed(1)\n", - "colors = [[0, 0, 0]] + [list(np.random.choice(range(256), size=3)) for _ in range(254)]\n", - "label_cmap = ListedColormap(colors)" - ] - }, - { - "cell_type": "markdown", - "id": "aaf096dc", - "metadata": { - "lines_to_next_cell": 0 - }, - "source": [ - "Here we show a slice of the raw data:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "87c4087b", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.632935Z", - "iopub.status.busy": "2024-10-23T13:40:40.632611Z", - "iopub.status.idle": "2024-10-23T13:40:40.925047Z", - "shell.execute_reply": "2024-10-23T13:40:40.924032Z" - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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+0wOZ5+p4ewzPZyNpsZ09iEijI6OoqurC3wywtsUqMtQIeccCxcuBAAUi0U3aKaAs8/UcPaZGs4+08PZaGpMZZ+WlpZ39JlX5uYcHBwcHBxmGM5xOTg4ODjUFerWcUVRhC1btryjouXZAGefqeHsMzWcfaaHs9HUuJb2qUtyhoODg4PD7EXd7rgcHBwcHGYnnONycHBwcKgrOMfl4ODg4FBXcI7LwcHBwaGuUJeO69lnn8V73vMeNDQ0oKen56LGlLMF3/72t1PBSv1z++232+OVSgX9/f2YN28empub8elPf9o27bxR8corr+CTn/wkurq6wBjDr3/969xxIsJTTz2Fzs5OFAoF9PX14a233sqdc/bsWWzcuBHFYhGtra344he/iLGxset4F9cO09nngQceuGhMrV+/PnfOjWqfp59+Gh/+8IcxZ84ctLe341Of+hSGhoZy51zOnDp27BjuuusuNDY2or29HY8//jiSbE+5Osbl2OjjH//4RWPo4Ycfzp3zbm1Ud47rF7/4Bb72ta9hy5Yt+Otf/4pVq1Zh3bp1OH369Exf2ozg/e9/P06ePGl/Xn31VXvs0UcfxW9+8xu88MIL2LNnD95++23ce++9M3i11x6lUgmrVq3Cs88+O+nxrVu34oc//CF+8pOfYO/evWhqasK6detQqVTsORs3bsQbb7yBXbt22U7fDz300PW6hWuK6ewDAOvXr8+Nqe3bt+eO36j22bNnD/r7+/Haa69h165diOMYa9euRalUsudMN6eEELjrrrtQq9Xwpz/9CT/96U+xbds2PPXUUzNxS1cdl2MjAHjwwQdzY2jr1q322FWxEdUZ7rzzTurv77f/F0JQV1cXPf300zN4VTODLVu20KpVqyY9du7cOQqCgF544QX7u7///e8EgAYHB6/TFc4sANCOHTvs/6WU1NHRQd///vft786dO0dRFNH27duJiOjNN98kAPSXv/zFnvO73/2OGGP0r3/967pd+/XARPsQEW3atInuvvvuS/7NbLLP6dOnCQDt2bOHiC5vTr344ovEOaeRkRF7zo9//GMqFotUrVav7w1cB0y0ERHRxz72MfrKV75yyb+5Gjaqqx1XrVbDvn370NfXZ3/HOUdfXx8GBwdn8MpmDm+99Ra6urqwZMkSbNy4EceOHQMA7Nu3D3Ec52x1++23Y9GiRbPWVkeOHMHIyEjOJi0tLejp6bE2GRwcRGtrKz70oQ/Zc/r6+sA5x969e6/7Nc8EBgYG0N7ejttuuw2bN2/GmTNn7LHZZJ/z588DAObOnQvg8ubU4OAgVqxYgQULFthz1q1bhwsXLuCNN964jld/fTDRRgY/+9nP0NbWhuXLl+OJJ55AuVy2x66GjepKZPc///kPhBC5GwaABQsW4B//+McMXdXMoaenB9u2bcNtt92GkydP4jvf+Q4++tGP4tChQxgZGUEYhmhtbc39zYIFCzAyMjIzFzzDMPc92fgxx0ZGRtDe3p477vs+5s6dOyvstn79etx7773o7u7G4cOH8c1vfhMbNmzA4OAgPM+bNfaRUuKrX/0qPvKRj2D58uUAcFlzamRkZNLxZY7dSJjMRgDw2c9+FosXL0ZXVxcOHjyIb3zjGxgaGsKvfvUrAFfHRnXluBzy2LBhg/33ypUr0dPTg8WLF+OXv/wlCoXCDF6ZQ73iM5/5jP33ihUrsHLlSixduhQDAwNYs2bNDF7Z9UV/fz8OHTqUyxk75HEpG2XznStWrEBnZyfWrFmDw4cPY+nSpVflu+sqVNjW1gbP8y5i8Zw6dQodHR0zdFX/O2htbcX73vc+DA8Po6OjA7VaDefOncudM5ttZe57qvHT0dFxEdEnSRKcPXt2VtptyZIlaGtrw/DwMIDZYZ9HHnkEv/3tb/Hyyy/nGhxezpzq6OiYdHyZYzcKLmWjydDT0wMAuTH0bm1UV44rDEPccccdeOmll+zvpJR46aWX0NvbO4NX9r+BsbExHD58GJ2dnbjjjjsQBEHOVkNDQzh27NistVV3dzc6OjpyNrlw4QL27t1rbdLb24tz585h37599pzdu3dDSmkn4GzCiRMncObMGXR2dgK4se1DRHjkkUewY8cO7N69G93d3bnjlzOnent78be//S3n3Hft2oVisYhly5Zdnxu5hpjORpPhwIEDAJAbQ+/aRu+QTDJj+PnPf05RFNG2bdvozTffpIceeohaW1tzDJXZgscee4wGBgboyJEj9Mc//pH6+vqora2NTp8+TUREDz/8MC1atIh2795Nr7/+OvX29lJvb+8MX/W1xejoKO3fv5/2799PAOgHP/gB7d+/n44ePUpERM888wy1trbSzp076eDBg3T33XdTd3c3jY+P289Yv349feADH6C9e/fSq6++Srfeeivdf//9M3VLVxVT2Wd0dJS+/vWv0+DgIB05coT+8Ic/0Ac/+EG69dZbqVKp2M+4Ue2zefNmamlpoYGBATp58qT9KZfL9pzp5lSSJLR8+XJau3YtHThwgH7/+9/T/Pnz6YknnpiJW7rqmM5Gw8PD9N3vfpdef/11OnLkCO3cuZOWLFlCq1evtp9xNWxUd46LiOhHP/oRLVq0iMIwpDvvvJNee+21mb6kGcF9991HnZ2dFIYhLVy4kO677z4aHh62x8fHx+lLX/oS3XTTTdTY2Ej33HMPnTx5cgav+Nrj5ZdfJgAX/WzatImIFCX+W9/6Fi1YsICiKKI1a9bQ0NBQ7jPOnDlD999/PzU3N1OxWKQvfOELNDo6OgN3c/UxlX3K5TKtXbuW5s+fT0EQ0OLFi+nBBx+8aFF4o9pnMrsAoOeee86eczlz6p///Cdt2LCBCoUCtbW10WOPPUZxHF/nu7k2mM5Gx44do9WrV9PcuXMpiiJ673vfS48//jidP38+9znv1kaurYmDg4ODQ12hrnJcDg4ODg4OznE5ODg4ONQVnONycHBwcKgrOMfl4ODg4FBXcI7LwcHBwaGu4ByXg4ODg0NdwTkuBwcHB4e6gnNcDg4ODg51Bee4HBwcHBzqCs5xOTg4ODjUFZzjcnBwcHCoKzjH5eDg4OBQV/h/aYW5Aan7uVEAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.imshow(cell_array.data[30])" - ] - }, - { - "cell_type": "markdown", - "id": "f0ba959d", - "metadata": {}, - "source": [ - "## Datasplit\n", - "Where can you find your data? What format is it in? Does it need to be normalized?\n", - "What data do you want to use for validation?\n", - "\n", - "We have already saved some data in `cells3d.zarr`. We will use this data for\n", - "training and validation. We only have one dataset, so we will be using the\n", - "same data for both training and validation. This is not recommended for real\n", - "experiments, but is useful for this tutorial." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "9dc9fa43", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.930236Z", - "iopub.status.busy": "2024-10-23T13:40:40.929063Z", - "iopub.status.idle": "2024-10-23T13:40:40.946285Z", - "shell.execute_reply": "2024-10-23T13:40:40.942908Z" - }, - "lines_to_next_cell": 2 - }, - "outputs": [], - "source": [ - "from dacapo.experiments.datasplits import DataSplitGenerator, DatasetSpec\n", - "\n", - "dataspecs = [\n", - " DatasetSpec(\n", - " dataset_type=\"train\",\n", - " raw_container=\"cells3d.zarr\",\n", - " raw_dataset=\"raw\",\n", - " gt_container=\"cells3d.zarr\",\n", - " gt_dataset=\"labels\",\n", - " ),\n", - " DatasetSpec(\n", - " dataset_type=\"val\",\n", - " raw_container=\"cells3d.zarr\",\n", - " raw_dataset=\"raw\",\n", - " gt_container=\"cells3d.zarr\",\n", - " gt_dataset=\"labels\",\n", - " ),\n", - "]\n", - "\n", - "datasplit_config = DataSplitGenerator(\n", - " name=\"skimage_tutorial_data\",\n", - " datasets=dataspecs,\n", - " input_resolution=voxel_size,\n", - " output_resolution=voxel_size,\n", - " targets=[\"cell\"],\n", - ").compute()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "a2512f62", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.953135Z", - "iopub.status.busy": "2024-10-23T13:40:40.951108Z", - "iopub.status.idle": "2024-10-23T13:40:40.962816Z", - "shell.execute_reply": "2024-10-23T13:40:40.961667Z" - } - }, - "outputs": [], - "source": [ - "datasplit = datasplit_config.datasplit_type(datasplit_config)\n", - "# viewer = datasplit._neuroglancer()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "311e0f81", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.973844Z", - "iopub.status.busy": "2024-10-23T13:40:40.971874Z", - "iopub.status.idle": "2024-10-23T13:40:40.981327Z", - "shell.execute_reply": "2024-10-23T13:40:40.981091Z" - } - }, - "outputs": [], - "source": [ - "config_store.store_datasplit_config(datasplit_config)" - ] - }, - { - "cell_type": "markdown", - "id": "69bc34e8", - "metadata": {}, - "source": [ - "## Task\n", - "What do you want to learn? An instance segmentation? If so, how? Affinities,\n", - "Distance Transform, Foreground/Background, etc. Each of these tasks are commonly learned\n", - "and evaluated with specific loss functions and evaluation metrics. Some tasks may\n", - "also require specific non-linearities or output formats from your model." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "aa236c16", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.982722Z", - "iopub.status.busy": "2024-10-23T13:40:40.982634Z", - "iopub.status.idle": "2024-10-23T13:40:40.986471Z", - "shell.execute_reply": "2024-10-23T13:40:40.986252Z" - } - }, - "outputs": [], - "source": [ - "from dacapo.experiments.tasks import DistanceTaskConfig, AffinitiesTaskConfig\n", - "\n", - "# an example distance task configuration\n", - "# note that the clip_distance, tol_distance, and scale_factor are in nm\n", - "dist_task_config = DistanceTaskConfig(\n", - " name=\"example_dist\",\n", - " channels=[\"cell\"],\n", - " clip_distance=260 * 10.0,\n", - " tol_distance=260 * 10.0,\n", - " scale_factor=260 * 20.0,\n", - ")\n", - "# config_store.delete_task_config(dist_task_config.name)\n", - "config_store.store_task_config(dist_task_config)\n", - "\n", - "# an example affinities task configuration\n", - "affs_task_config = AffinitiesTaskConfig(\n", - " name=\"example_affs\",\n", - " neighborhood=[(1, 0, 0), (0, 1, 0), (0, 0, 1)],\n", - ")\n", - "# config_store.delete_task_config(dist_task_config.name)\n", - "config_store.store_task_config(affs_task_config)" - ] - }, - { - "cell_type": "markdown", - "id": "cf128bbd", - "metadata": {}, - "source": [ - "## Architecture\n", - "\n", - "The setup of the network you will train. Biomedical image to image translation\n", - "often utilizes a UNet, but even after choosing a UNet you still need to provide\n", - "some additional parameters. How much do you want to downsample? How many\n", - "convolutional layers do you want?" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "12d2bc85", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.987839Z", - "iopub.status.busy": "2024-10-23T13:40:40.987748Z", - "iopub.status.idle": "2024-10-23T13:40:40.991692Z", - "shell.execute_reply": "2024-10-23T13:40:40.991442Z" - } - }, - "outputs": [], - "source": [ - "from dacapo.experiments.architectures import CNNectomeUNetConfig\n", - "\n", - "# Note we make this UNet 2D by defining kernel_size_down, kernel_size_up, and downsample_factors\n", - "# all with 1s in z meaning no downsampling or convolving in the z direction.\n", - "architecture_config = CNNectomeUNetConfig(\n", - " name=\"example_unet\",\n", - " input_shape=(2, 132, 132),\n", - " eval_shape_increase=(8, 32, 32),\n", - " fmaps_in=1,\n", - " num_fmaps=8,\n", - " fmaps_out=8,\n", - " fmap_inc_factor=2,\n", - " downsample_factors=[(1, 4, 4), (1, 4, 4)],\n", - " kernel_size_down=[[(1, 3, 3)] * 2] * 3,\n", - " kernel_size_up=[[(1, 3, 3)] * 2] * 2,\n", - " constant_upsample=True,\n", - " padding=\"valid\",\n", - ")\n", - "config_store.store_architecture_config(architecture_config)" - ] - }, - { - "cell_type": "markdown", - "id": "3bda4dcf", - "metadata": {}, - "source": [ - "## Trainer\n", - "\n", - "How do you want to train? This config defines the training loop and how\n", - "the other three components work together. What sort of augmentations to\n", - "apply during training, what learning rate and optimizer to use, what\n", - "batch size to train with." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "a59ea7ae", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.993554Z", - "iopub.status.busy": "2024-10-23T13:40:40.993472Z", - "iopub.status.idle": "2024-10-23T13:40:40.996744Z", - "shell.execute_reply": "2024-10-23T13:40:40.996309Z" - } - }, - "outputs": [], - "source": [ - "from dacapo.experiments.trainers import GunpowderTrainerConfig\n", - "\n", - "trainer_config = GunpowderTrainerConfig(\n", - " name=\"example\",\n", - " batch_size=10,\n", - " learning_rate=0.0001,\n", - " num_data_fetchers=8,\n", - " snapshot_interval=1000,\n", - " min_masked=0.05,\n", - " clip_raw=False,\n", - ")\n", - "config_store.store_trainer_config(trainer_config)" - ] - }, - { - "cell_type": "markdown", - "id": "55e43081", - "metadata": {}, - "source": [ - "## Run\n", - "Now that we have our components configured, we just need to combine them\n", - "into a run and start training. We can have multiple repetitions of a single\n", - "set of configs in order to increase our chances of finding an optimum." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "45547c67", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:40.998754Z", - "iopub.status.busy": "2024-10-23T13:40:40.998657Z", - "iopub.status.idle": "2024-10-23T13:40:41.008905Z", - "shell.execute_reply": "2024-10-23T13:40:41.008647Z" - } - }, - "outputs": [], - "source": [ - "from dacapo.experiments import RunConfig\n", - "from dacapo.experiments.run import Run\n", - "\n", - "iterations = 2000\n", - "validation_interval = iterations // 4\n", - "run_config = RunConfig(\n", - " name=\"example_run\",\n", - " datasplit_config=datasplit_config,\n", - " task_config=affs_task_config,\n", - " architecture_config=architecture_config,\n", - " trainer_config=trainer_config,\n", - " num_iterations=iterations,\n", - " validation_interval=validation_interval,\n", - " repetition=0,\n", - ")\n", - "config_store.store_run_config(run_config)" - ] - }, - { - "cell_type": "markdown", - "id": "aa2a2d14", - "metadata": {}, - "source": [ - "## Train\n", - "\n", - "NOTE: The run stats are stored in the `runs_base_dir/stats` directory.\n", - "You can delete this directory to remove all stored stats if you want to re-run training.\n", - "Otherwise, the stats will be appended to the existing files, and the run won't start\n", - "from scratch. This may cause errors." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "51f82d4f", - "metadata": { - "execution": { - "iopub.execute_input": "2024-10-23T13:40:41.010644Z", - "iopub.status.busy": "2024-10-23T13:40:41.010555Z", - "iopub.status.idle": "2024-10-23T13:40:41.317330Z", - "shell.execute_reply": "2024-10-23T13:40:41.317055Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Creating FileConfigStore:\n", - "\tpath: /Users/zouinkhim/dacapo/configs\n" - ] - } - ], - "source": [ - "from dacapo.train import train_run\n", - "\n", - "# from dacapo.validate import validate\n", - "from dacapo.experiments.run import Run\n", - "\n", - "from dacapo.store.create_store import create_config_store\n", - "\n", - "config_store = create_config_store()\n", - "\n", - "run = Run(config_store.retrieve_run_config(\"example_run\"))\n", - "\n", - "# if __name__ == \"__main__\":\n", - "# train_run(run)\n", - "\n", - "# # %% [markdown]\n", - "# # ## Visualize\n", - "# # Let's visualize the results of the training run. DaCapo saves a few artifacts during training\n", - "# # including snapshots, validation results, and the loss.\n", - "\n", - "# # %%\n", - "# stats_store = create_stats_store()\n", - "# training_stats = stats_store.retrieve_training_stats(run_config.name)\n", - "# stats = training_stats.to_xarray()\n", - "# plt.plot(stats)\n", - "# plt.title(\"Training Loss\")\n", - "# plt.xlabel(\"Iteration\")\n", - "# plt.ylabel(\"Loss\")\n", - "# plt.show()\n", - "\n", - "# # %%\n", - "# import zarr\n", - "\n", - "# run_path = config_store.path / run_config.name\n", - "\n", - "# num_snapshots = run_config.num_iterations // run_config.trainer_config.snapshot_interval\n", - "# fig, ax = plt.subplots(num_snapshots, 3, figsize=(10, 2 * num_snapshots))\n", - "\n", - "# # Set column titles\n", - "# column_titles = [\"Raw\", \"Target\", \"Prediction\"]\n", - "# for col in range(3):\n", - "# ax[0, col].set_title(column_titles[col])\n", - "\n", - "# for snapshot in range(num_snapshots):\n", - "# snapshot_it = snapshot * run_config.trainer_config.snapshot_interval\n", - "# # break\n", - "# raw = zarr.open(f\"{run_path}/snapshot.zarr/{snapshot_it}/volumes/raw\")[:]\n", - "# target = zarr.open(f\"{run_path}/snapshot.zarr/{snapshot_it}/volumes/target\")[0]\n", - "# prediction = zarr.open(\n", - "# f\"{run_path}/snapshot.zarr/{snapshot_it}/volumes/prediction\"\n", - "# )[0]\n", - "# c = (raw.shape[1] - target.shape[1]) // 2\n", - "# ax[snapshot, 0].imshow(raw[raw.shape[0] // 2, c:-c, c:-c])\n", - "# ax[snapshot, 1].imshow(target[target.shape[0] // 2])\n", - "# ax[snapshot, 2].imshow(prediction[prediction.shape[0] // 2])\n", - "# ax[snapshot, 0].set_ylabel(f\"Snapshot {snapshot_it}\")\n", - "# plt.show()\n", - "\n", - "# # %%\n", - "# # Visualize validations\n", - "# import zarr\n", - "\n", - "# num_validations = run_config.num_iterations // run_config.validation_interval\n", - "# fig, ax = plt.subplots(num_validations, 4, figsize=(10, 2 * num_validations))\n", - "\n", - "# # Set column titles\n", - "# column_titles = [\"Raw\", \"Ground Truth\", \"Prediction\", \"Segmentation\"]\n", - "# for col in range(len(column_titles)):\n", - "# ax[0, col].set_title(column_titles[col])\n", - "\n", - "# for validation in range(1, num_validations + 1):\n", - "# dataset = run.datasplit.validate[0].name\n", - "# validation_it = validation * run_config.validation_interval\n", - "# # break\n", - "# raw = zarr.open(f\"{run_path}/validation.zarr/inputs/{dataset}/raw\")[:]\n", - "# gt = zarr.open(f\"{run_path}/validation.zarr/inputs/{dataset}/gt\")[0]\n", - "# pred_path = f\"{run_path}/validation.zarr/{validation_it}/ds_{dataset}/prediction\"\n", - "# out_path = f\"{run_path}/validation.zarr/{validation_it}/ds_{dataset}/output/WatershedPostProcessorParameters(id=2, bias=0.5, context=(32, 32, 32))\"\n", - "# output = zarr.open(out_path)[:]\n", - "# prediction = zarr.open(pred_path)[0]\n", - "# c = (raw.shape[1] - gt.shape[1]) // 2\n", - "# if c != 0:\n", - "# raw = raw[:, c:-c, c:-c]\n", - "# ax[validation - 1, 0].imshow(raw[raw.shape[0] // 2])\n", - "# ax[validation - 1, 1].imshow(\n", - "# gt[gt.shape[0] // 2], cmap=label_cmap, interpolation=\"none\"\n", - "# )\n", - "# ax[validation - 1, 2].imshow(prediction[prediction.shape[0] // 2])\n", - "# ax[validation - 1, 3].imshow(\n", - "# output[output.shape[0] // 2], cmap=label_cmap, interpolation=\"none\"\n", - "# )\n", - "# ax[validation - 1, 0].set_ylabel(f\"Validation {validation_it}\")\n", - "# plt.show()" - ] - } - ], - "metadata": { - "jupytext": { - "cell_metadata_filter": "title,-all" - }, - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/source/notebooks/minimal_tutorial.py b/docs/source/notebooks/minimal_tutorial.py index f2794bb16..946b7ac55 100644 --- a/docs/source/notebooks/minimal_tutorial.py +++ b/docs/source/notebooks/minimal_tutorial.py @@ -109,23 +109,28 @@ # Create the zarr array with appropriate metadata cell_array = prepare_ds( - "cells3d.zarr", - "raw", - Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size, + "cells3d.zarr/raw", + cell_data.shape, + offset=offset, voxel_size=voxel_size, + axis_names=axis_names, + units=units, + mode="w", dtype=np.uint8, - num_channels=None, ) # Save the cell data to the zarr array -cell_array[cell_array.roi] = cell_data[1] +cell_array[cell_array.roi] = cell_data # Generate and save some pseudo ground truth data mask_array = prepare_ds( - "cells3d.zarr", - "mask", - Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size, + "cells3d.zarr/mask", + cell_data.shape[1:], + offset=offset, voxel_size=voxel_size, + axis_names=axis_names[1:], + units=units, + mode="w", dtype=np.uint8, ) cell_mask = np.clip(gaussian(cell_data[1] / 255.0, sigma=1), 0, 255) * 255 > 30 @@ -134,10 +139,13 @@ # Generate labels via connected components labels_array = prepare_ds( - "cells3d.zarr", - "labels", - Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size, + "cells3d.zarr/labels", + cell_data.shape[1:], + offset=offset, voxel_size=voxel_size, + axis_names=axis_names[1:], + units=units, + mode="w", dtype=np.uint8, ) labels_array[labels_array.roi] = label(mask_array.to_ndarray(mask_array.roi))[0] @@ -155,7 +163,7 @@ fig, axes = plt.subplots(1, 2, figsize=(12, 6)) # Show the raw data -axes[0].imshow(cell_array.data[30]) +axes[0].imshow(cell_array.data[0, 30]) axes[0].set_title("Raw Data") # Show the labels using the custom label color map @@ -176,26 +184,59 @@ # experiments, but is useful for this tutorial. # %% -from dacapo.experiments.datasplits import DataSplitGenerator, DatasetSpec - -dataspecs = [ - DatasetSpec( - dataset_type=type_crop, - raw_container="cells3d.zarr", - raw_dataset="raw", - gt_container="cells3d.zarr", - gt_dataset="labels", - ) - for type_crop in ["train", "val"] -] - -datasplit_config = DataSplitGenerator( - name="skimage_tutorial_data", - datasets=dataspecs, - input_resolution=voxel_size, - output_resolution=voxel_size, - targets=["cell"], -).compute() +from dacapo.experiments.datasplits import TrainValidateDataSplitConfig +from dacapo.experiments.datasplits.datasets import RawGTDatasetConfig +from dacapo.experiments.datasplits.datasets.arrays import ( + ZarrArrayConfig, + IntensitiesArrayConfig, +) +from funlib.geometry import Coordinate + +datasplit_config = TrainValidateDataSplitConfig( + name="example_datasplit", + train_configs=[ + RawGTDatasetConfig( + name="example_dataset", + raw_config=IntensitiesArrayConfig( + name="example_raw_normalized", + source_array_config=ZarrArrayConfig( + name="example_raw", + file_name="cells3d.zarr", + dataset="raw", + ), + min=0, + max=255, + ), + gt_config=ZarrArrayConfig( + name="example_gt", + file_name="cells3d.zarr", + dataset="mask", + ), + ) + ], + validate_configs=[ + RawGTDatasetConfig( + name="example_dataset", + raw_config=IntensitiesArrayConfig( + name="example_raw_normalized", + source_array_config=ZarrArrayConfig( + name="example_raw", + file_name="cells3d.zarr", + dataset="raw", + ), + min=0, + max=255, + ), + gt_config=ZarrArrayConfig( + name="example_gt", + file_name="cells3d.zarr", + dataset="labels", + ), + ) + ], +) +datasplit = datasplit_config.datasplit_type(datasplit_config) +config_store.store_datasplit_config(datasplit_config) # %% @@ -259,7 +300,7 @@ name="example_unet", input_shape=(2, 132, 132), eval_shape_increase=(8, 32, 32), - fmaps_in=1, + fmaps_in=2, num_fmaps=8, fmaps_out=8, fmap_inc_factor=2, @@ -286,7 +327,7 @@ name="example", batch_size=10, learning_rate=0.0001, - num_data_fetchers=8, + num_data_fetchers=1, snapshot_interval=1000, min_masked=0.05, clip_raw=False, @@ -365,9 +406,9 @@ config_store = create_config_store() run = Run(config_store.retrieve_run_config("example_run")) - if __name__ == "__main__": train_run(run) + pass # %% [markdown] # ## Visualize @@ -375,7 +416,15 @@ # including snapshots, validation results, and the loss. # %% -run.validation_scores.to_xarray()["criteria"].values +stats_store = create_stats_store() +training_stats = stats_store.retrieve_training_stats(run_config.name) +stats = training_stats.to_xarray() +print(stats) +plt.plot(stats) +plt.title("Training Loss") +plt.xlabel("Iteration") +plt.ylabel("Loss") +plt.show() # %% from dacapo.plot import plot_runs @@ -405,28 +454,31 @@ run_path = config_store.path.parent / run_config.name +# BROWSER = False num_snapshots = run_config.num_iterations // run_config.trainer_config.snapshot_interval -fig, ax = plt.subplots(num_snapshots, 3, figsize=(10, 2 * num_snapshots)) - -# Set column titles -column_titles = ["Raw", "Target", "Prediction"] -for col in range(3): - ax[0, col].set_title(column_titles[col]) -for snapshot in range(num_snapshots): - snapshot_it = snapshot * run_config.trainer_config.snapshot_interval - # break - raw = zarr.open(f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/raw")[:] - target = zarr.open(f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/target")[0] - prediction = zarr.open( - f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/prediction" - )[0] - c = (raw.shape[1] - target.shape[1]) // 2 - ax[snapshot, 0].imshow(raw[raw.shape[0] // 2, c:-c, c:-c]) - ax[snapshot, 1].imshow(target[target.shape[0] // 2]) - ax[snapshot, 2].imshow(prediction[prediction.shape[0] // 2]) - ax[snapshot, 0].set_ylabel(f"Snapshot {snapshot_it}") -plt.show() +if num_snapshots > 0: + fig, ax = plt.subplots(num_snapshots, 3, figsize=(10, 2 * num_snapshots)) + + # Set column titles + column_titles = ["Raw", "Target", "Prediction"] + for col in range(3): + ax[0, col].set_title(column_titles[col]) + + for snapshot in range(num_snapshots): + snapshot_it = snapshot * run_config.trainer_config.snapshot_interval + # break + raw = zarr.open(f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/raw")[:] + target = zarr.open(f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/target")[0] + prediction = zarr.open( + f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/prediction" + )[0] + c = (raw.shape[2] - target.shape[1]) // 2 + ax[snapshot, 0].imshow(raw[1, raw.shape[0] // 2, c:-c, c:-c]) + ax[snapshot, 1].imshow(target[target.shape[0] // 2]) + ax[snapshot, 2].imshow(prediction[prediction.shape[0] // 2]) + ax[snapshot, 0].set_ylabel(f"Snapshot {snapshot_it}") + plt.show() # # %% # Visualize validations @@ -444,16 +496,16 @@ dataset = run.datasplit.validate[0].name validation_it = validation * run_config.validation_interval # break - raw = zarr.open(f"{run_path}/validation.zarr/inputs/{dataset}/raw")[:] - gt = zarr.open(f"{run_path}/validation.zarr/inputs/{dataset}/gt")[0] + raw = zarr.open(f"{run_path}/validation.zarr/inputs/{dataset}/raw") + gt = zarr.open(f"{run_path}/validation.zarr/inputs/{dataset}/gt") pred_path = f"{run_path}/validation.zarr/{validation_it}/ds_{dataset}/prediction" out_path = f"{run_path}/validation.zarr/{validation_it}/ds_{dataset}/output/WatershedPostProcessorParameters(id=2, bias=0.5, context=(32, 32, 32))" output = zarr.open(out_path)[:] prediction = zarr.open(pred_path)[0] - c = (raw.shape[1] - gt.shape[1]) // 2 + c = (raw.shape[2] - gt.shape[1]) // 2 if c != 0: - raw = raw[:, c:-c, c:-c] - ax[validation - 1, 0].imshow(raw[raw.shape[0] // 2]) + raw = raw[:, :, c:-c, c:-c] + ax[validation - 1, 0].imshow(raw[1, raw.shape[1] // 2]) ax[validation - 1, 1].imshow( gt[gt.shape[0] // 2], cmap=label_cmap, interpolation="none" ) diff --git a/examples/blockwise/synthetic_source_worker.py b/examples/blockwise/synthetic_source_worker.py index cd2744fa0..76bb53e31 100644 --- a/examples/blockwise/synthetic_source_worker.py +++ b/examples/blockwise/synthetic_source_worker.py @@ -4,9 +4,10 @@ from pathlib import Path import sys -from dacapo.experiments.datasplits.datasets.arrays.zarr_array import ZarrArray + from dacapo.store.array_store import LocalArrayIdentifier from dacapo.compute_context import create_compute_context +from dacapo.tmp import create_from_identifier, open_from_identifier import dacapo import daisy @@ -69,13 +70,13 @@ def generate_synthetic_dataset( raw_output_array_identifier = LocalArrayIdentifier( Path(output_container), raw_output_dataset ) - raw_output_array = ZarrArray.create_from_array_identifier( + raw_output_array = create_from_identifier( raw_output_array_identifier, roi=roi, dtype=np.uint8, voxel_size=_voxel_size, num_channels=None, - axes=["z", "y", "x"], + axis_names=["z", "y", "x"], overwrite=overwrite, write_size=_write_shape * voxel_size, ) @@ -83,13 +84,13 @@ def generate_synthetic_dataset( labels_output_array_identifier = LocalArrayIdentifier( Path(output_container), labels_output_dataset ) - labels_output_array = ZarrArray.create_from_array_identifier( + labels_output_array = create_from_identifier( labels_output_array_identifier, roi=roi, dtype=np.uint64, voxel_size=_voxel_size, num_channels=None, - axes=["z", "y", "x"], + axis_names=["z", "y", "x"], overwrite=overwrite, write_size=_write_shape * voxel_size, ) @@ -121,14 +122,12 @@ def start_worker( raw_output_array_identifier = LocalArrayIdentifier( Path(output_container), raw_output_dataset ) - raw_output_array = ZarrArray.open_from_array_identifier(raw_output_array_identifier) + raw_output_array = open_from_identifier(raw_output_array_identifier) labels_output_array_identifier = LocalArrayIdentifier( Path(output_container), labels_output_dataset ) - labels_output_array = ZarrArray.open_from_array_identifier( - labels_output_array_identifier - ) + labels_output_array = open_from_identifier(labels_output_array_identifier) # get data generator diff --git a/examples/starter_tutorial/minimal_tutorial.py b/examples/starter_tutorial/minimal_tutorial.py index f2794bb16..5479d86bd 100644 --- a/examples/starter_tutorial/minimal_tutorial.py +++ b/examples/starter_tutorial/minimal_tutorial.py @@ -109,23 +109,28 @@ # Create the zarr array with appropriate metadata cell_array = prepare_ds( - "cells3d.zarr", - "raw", - Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size, + "cells3d.zarr/raw", + cell_data.shape, + offset=offset, voxel_size=voxel_size, + axis_names=axis_names, + units=units, + mode="w", dtype=np.uint8, - num_channels=None, ) # Save the cell data to the zarr array -cell_array[cell_array.roi] = cell_data[1] +cell_array[cell_array.roi] = cell_data # Generate and save some pseudo ground truth data mask_array = prepare_ds( - "cells3d.zarr", - "mask", - Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size, + "cells3d.zarr/mask", + cell_data.shape[1:], + offset=offset, voxel_size=voxel_size, + axis_names=axis_names[1:], + units=units, + mode="w", dtype=np.uint8, ) cell_mask = np.clip(gaussian(cell_data[1] / 255.0, sigma=1), 0, 255) * 255 > 30 @@ -134,10 +139,13 @@ # Generate labels via connected components labels_array = prepare_ds( - "cells3d.zarr", - "labels", - Roi((0, 0, 0), cell_data.shape[1:]) * voxel_size, + "cells3d.zarr/labels", + cell_data.shape[1:], + offset=offset, voxel_size=voxel_size, + axis_names=axis_names[1:], + units=units, + mode="w", dtype=np.uint8, ) labels_array[labels_array.roi] = label(mask_array.to_ndarray(mask_array.roi))[0] @@ -155,7 +163,7 @@ fig, axes = plt.subplots(1, 2, figsize=(12, 6)) # Show the raw data -axes[0].imshow(cell_array.data[30]) +axes[0].imshow(cell_array.data[0, 30]) axes[0].set_title("Raw Data") # Show the labels using the custom label color map @@ -176,26 +184,59 @@ # experiments, but is useful for this tutorial. # %% -from dacapo.experiments.datasplits import DataSplitGenerator, DatasetSpec - -dataspecs = [ - DatasetSpec( - dataset_type=type_crop, - raw_container="cells3d.zarr", - raw_dataset="raw", - gt_container="cells3d.zarr", - gt_dataset="labels", - ) - for type_crop in ["train", "val"] -] - -datasplit_config = DataSplitGenerator( - name="skimage_tutorial_data", - datasets=dataspecs, - input_resolution=voxel_size, - output_resolution=voxel_size, - targets=["cell"], -).compute() +from dacapo.experiments.datasplits import TrainValidateDataSplitConfig +from dacapo.experiments.datasplits.datasets import RawGTDatasetConfig +from dacapo.experiments.datasplits.datasets.arrays import ( + ZarrArrayConfig, + IntensitiesArrayConfig, +) +from funlib.geometry import Coordinate + +datasplit_config = TrainValidateDataSplitConfig( + name="example_datasplit", + train_configs=[ + RawGTDatasetConfig( + name="example_dataset", + raw_config=IntensitiesArrayConfig( + name="example_raw_normalized", + source_array_config=ZarrArrayConfig( + name="example_raw", + file_name="cells3d.zarr", + dataset="raw", + ), + min=0, + max=255, + ), + gt_config=ZarrArrayConfig( + name="example_gt", + file_name="cells3d.zarr", + dataset="mask", + ), + ) + ], + validate_configs=[ + RawGTDatasetConfig( + name="example_dataset", + raw_config=IntensitiesArrayConfig( + name="example_raw_normalized", + source_array_config=ZarrArrayConfig( + name="example_raw", + file_name="cells3d.zarr", + dataset="raw", + ), + min=0, + max=255, + ), + gt_config=ZarrArrayConfig( + name="example_gt", + file_name="cells3d.zarr", + dataset="labels", + ), + ) + ], +) +datasplit = datasplit_config.datasplit_type(datasplit_config) +config_store.store_datasplit_config(datasplit_config) # %% @@ -259,7 +300,7 @@ name="example_unet", input_shape=(2, 132, 132), eval_shape_increase=(8, 32, 32), - fmaps_in=1, + fmaps_in=2, num_fmaps=8, fmaps_out=8, fmap_inc_factor=2, @@ -286,7 +327,7 @@ name="example", batch_size=10, learning_rate=0.0001, - num_data_fetchers=8, + num_data_fetchers=1, snapshot_interval=1000, min_masked=0.05, clip_raw=False, @@ -365,7 +406,6 @@ config_store = create_config_store() run = Run(config_store.retrieve_run_config("example_run")) - if __name__ == "__main__": train_run(run) @@ -375,7 +415,15 @@ # including snapshots, validation results, and the loss. # %% -run.validation_scores.to_xarray()["criteria"].values +stats_store = create_stats_store() +training_stats = stats_store.retrieve_training_stats(run_config.name) +stats = training_stats.to_xarray() +print(stats) +plt.plot(stats) +plt.title("Training Loss") +plt.xlabel("Iteration") +plt.ylabel("Loss") +plt.show() # %% from dacapo.plot import plot_runs @@ -405,28 +453,31 @@ run_path = config_store.path.parent / run_config.name +# BROWSER = False num_snapshots = run_config.num_iterations // run_config.trainer_config.snapshot_interval -fig, ax = plt.subplots(num_snapshots, 3, figsize=(10, 2 * num_snapshots)) - -# Set column titles -column_titles = ["Raw", "Target", "Prediction"] -for col in range(3): - ax[0, col].set_title(column_titles[col]) -for snapshot in range(num_snapshots): - snapshot_it = snapshot * run_config.trainer_config.snapshot_interval - # break - raw = zarr.open(f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/raw")[:] - target = zarr.open(f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/target")[0] - prediction = zarr.open( - f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/prediction" - )[0] - c = (raw.shape[1] - target.shape[1]) // 2 - ax[snapshot, 0].imshow(raw[raw.shape[0] // 2, c:-c, c:-c]) - ax[snapshot, 1].imshow(target[target.shape[0] // 2]) - ax[snapshot, 2].imshow(prediction[prediction.shape[0] // 2]) - ax[snapshot, 0].set_ylabel(f"Snapshot {snapshot_it}") -plt.show() +if num_snapshots > 0: + fig, ax = plt.subplots(num_snapshots, 3, figsize=(10, 2 * num_snapshots)) + + # Set column titles + column_titles = ["Raw", "Target", "Prediction"] + for col in range(3): + ax[0, col].set_title(column_titles[col]) + + for snapshot in range(num_snapshots): + snapshot_it = snapshot * run_config.trainer_config.snapshot_interval + # break + raw = zarr.open(f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/raw")[:] + target = zarr.open(f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/target")[0] + prediction = zarr.open( + f"{run_path}/snapshot.zarr/{snapshot_it}/volumes/prediction" + )[0] + c = (raw.shape[2] - target.shape[1]) // 2 + ax[snapshot, 0].imshow(raw[1, raw.shape[0] // 2, c:-c, c:-c]) + ax[snapshot, 1].imshow(target[target.shape[0] // 2]) + ax[snapshot, 2].imshow(prediction[prediction.shape[0] // 2]) + ax[snapshot, 0].set_ylabel(f"Snapshot {snapshot_it}") + plt.show() # # %% # Visualize validations @@ -444,16 +495,16 @@ dataset = run.datasplit.validate[0].name validation_it = validation * run_config.validation_interval # break - raw = zarr.open(f"{run_path}/validation.zarr/inputs/{dataset}/raw")[:] - gt = zarr.open(f"{run_path}/validation.zarr/inputs/{dataset}/gt")[0] + raw = zarr.open(f"{run_path}/validation.zarr/inputs/{dataset}/raw") + gt = zarr.open(f"{run_path}/validation.zarr/inputs/{dataset}/gt") pred_path = f"{run_path}/validation.zarr/{validation_it}/ds_{dataset}/prediction" out_path = f"{run_path}/validation.zarr/{validation_it}/ds_{dataset}/output/WatershedPostProcessorParameters(id=2, bias=0.5, context=(32, 32, 32))" output = zarr.open(out_path)[:] prediction = zarr.open(pred_path)[0] - c = (raw.shape[1] - gt.shape[1]) // 2 + c = (raw.shape[2] - gt.shape[1]) // 2 if c != 0: - raw = raw[:, c:-c, c:-c] - ax[validation - 1, 0].imshow(raw[raw.shape[0] // 2]) + raw = raw[:, :, c:-c, c:-c] + ax[validation - 1, 0].imshow(raw[1, raw.shape[1] // 2]) ax[validation - 1, 1].imshow( gt[gt.shape[0] // 2], cmap=label_cmap, interpolation="none" ) diff --git a/pyproject.toml b/pyproject.toml index 1c5b726db..ffa21a923 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "lazy-property", "neuroglancer", "torch", - "fibsem_tools<=6.3", + "fibsem_tools >= 6.0, <=6.3", "attrs", "bokeh", "numpy-indexed>=0.3.7", @@ -47,8 +47,8 @@ dependencies = [ "funlib.geometry>=0.2", "mwatershed>=0.1", "cellmap-models", - "funlib.persistence==0.4.0", - "gunpowder>=1.3", + "funlib.persistence>=0.5.3", + "gunpowder>=1.4", "lsds", "xarray", "cattrs", @@ -74,12 +74,18 @@ dev = [ "pre-commit", ] docs = [ + "matplotlib", + "pooch", + "sphinx", + "nbsphinx", "sphinx-autodoc-typehints", "sphinx-autoapi", "sphinx-click", "sphinx-rtd-theme", "myst-parser", - "pooch", + "jupytext", + "ipykernel", + "myst_nb", ] examples = [ "ipython", diff --git a/tests/components/test_arrays.py b/tests/components/test_arrays.py index d91863ad7..f9e9f638e 100644 --- a/tests/components/test_arrays.py +++ b/tests/components/test_arrays.py @@ -1,9 +1,11 @@ from ..fixtures import * from dacapo.store.create_store import create_config_store +from dacapo.tmp import num_channels_from_array import pytest from pytest_lazy_fixtures import lf +from funlib.persistence import Array @pytest.mark.parametrize( @@ -24,23 +26,23 @@ def test_array_api(options, array_config): assert fetched_array_config == array_config # Create Array from config - array = array_config.array_type(array_config) + array: Array = array_config.array("r+") # Test API - # channels/axes - if "c" in array.axes: - assert array.num_channels is not None + # channels/axis_names + if "c^" in array.axis_names: + assert num_channels_from_array(array) is not None else: - assert array.num_channels is None + assert num_channels_from_array(array) is None # dims/voxel_size/roi - assert array.dims == array.voxel_size.dims - assert array.dims == array.roi.dims + assert array.spatial_dims == array.voxel_size.dims + assert array.spatial_dims == array.roi.dims # fetching data: expected_data_shape = array.roi.shape / array.voxel_size - assert array[array.roi].shape[-array.dims :] == expected_data_shape + assert array[array.roi].shape[-array.spatial_dims :] == expected_data_shape # setting data: - if array.writable: - data_slice = array.data[0].copy() - array.data[0] = data_slice + 1 + if array.is_writeable: + data_slice = array[0] + array[0] = data_slice + 1 assert data_slice.sum() == 0 - assert (array.data[0] - data_slice).sum() == data_slice.size + assert (array[0] - data_slice).sum() == data_slice.size diff --git a/tests/components/test_gp_arraysource.py b/tests/components/test_gp_arraysource.py index 69fee515f..58a4b23ba 100644 --- a/tests/components/test_gp_arraysource.py +++ b/tests/components/test_gp_arraysource.py @@ -1,11 +1,10 @@ from ..fixtures import * -from dacapo.gp import DaCapoArraySource - import gunpowder as gp import pytest from pytest_lazy_fixtures import lf +import numpy as np @pytest.mark.parametrize( @@ -18,16 +17,19 @@ ) def test_gp_dacapo_array_source(array_config): # Create Array from config - array = array_config.array_type(array_config) + array = array_config.array() # Make sure the DaCapoArraySource can properly read # the data in `array` key = gp.ArrayKey("TEST") - source_node = DaCapoArraySource(array, key) + source_node = gp.ArraySource(key, array) with gp.build(source_node): request = gp.BatchRequest() request[key] = gp.ArraySpec(roi=array.roi) batch = source_node.request_batch(request) data = batch[key].data + if data.dtype == bool: + raise ValueError("Data should not be bools") + data = data.astype(np.uint8) assert (data - array[array.roi]).sum() == 0 diff --git a/tests/conf.py b/tests/conf.py new file mode 100644 index 000000000..57a8708d5 --- /dev/null +++ b/tests/conf.py @@ -0,0 +1,3 @@ +import multiprocessing as mp + +mp.set_start_method("fork", force=True) diff --git a/tests/fixtures/arrays.py b/tests/fixtures/arrays.py index 8af4e90f2..6c94c4b73 100644 --- a/tests/fixtures/arrays.py +++ b/tests/fixtures/arrays.py @@ -28,7 +28,7 @@ def zarr_array(tmp_path): ) dataset.attrs["offset"] = (12, 12, 12) dataset.attrs["resolution"] = (1, 2, 4) - dataset.attrs["axes"] = ["zyx"] + dataset.attrs["axis_names"] = ["z", "y", "x"] yield zarr_array_config @@ -46,7 +46,7 @@ def cellmap_array(tmp_path): ) dataset.attrs["offset"] = (12, 12, 12) dataset.attrs["resolution"] = (1, 2, 4) - dataset.attrs["axes"] = ["z", "y", "x"] + dataset.attrs["axis_names"] = ["z", "y", "x"] cellmap_array_config = BinarizeArrayConfig( name="cellmap_zarr_array", diff --git a/tests/fixtures/datasplits.py b/tests/fixtures/datasplits.py index 7bb5672c6..448c9c834 100644 --- a/tests/fixtures/datasplits.py +++ b/tests/fixtures/datasplits.py @@ -74,10 +74,10 @@ def twelve_class_datasplit(tmp_path): raw_dataset[:] = random_data raw_dataset.attrs["offset"] = (0, 0, 0) raw_dataset.attrs["resolution"] = (2, 2, 2) - raw_dataset.attrs["axes"] = ("z", "y", "x") + raw_dataset.attrs["axis_names"] = ("z", "y", "x") gt_dataset.attrs["offset"] = (0, 0, 0) gt_dataset.attrs["resolution"] = (2, 2, 2) - gt_dataset.attrs["axes"] = ("z", "y", "x") + gt_dataset.attrs["axis_names"] = ("z", "y", "x") crop1 = RawGTDatasetConfig(name="crop1", raw_config=crop1_raw, gt_config=crop1_gt) crop2 = RawGTDatasetConfig(name="crop2", raw_config=crop2_raw, gt_config=crop2_gt) @@ -185,10 +185,10 @@ def six_class_datasplit(tmp_path): raw_dataset[:] = random_data raw_dataset.attrs["offset"] = (0, 0, 0) raw_dataset.attrs["resolution"] = (2, 2, 2) - raw_dataset.attrs["axes"] = ("z", "y", "x") + raw_dataset.attrs["axis_names"] = ("z", "y", "x") gt_dataset.attrs["offset"] = (0, 0, 0) gt_dataset.attrs["resolution"] = (2, 2, 2) - gt_dataset.attrs["axes"] = ("z", "y", "x") + gt_dataset.attrs["axis_names"] = ("z", "y", "x") crop1 = RawGTDatasetConfig( name="crop1", raw_config=crop1_raw, gt_config=crop1_distances