From e5418fc9fea7de1e4bc6be1c9c7ee4d723966fc4 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 25 Jun 2023 16:40:10 -0400 Subject: [PATCH] docs: adding docs (#149) * docs: adding docs * docs: tweaks * docs: fixup --- .github/workflows/docs.yml | 20 ++ .pre-commit-config.yaml | 12 +- README.md | 4 +- docs/API/nd2.md | 5 + docs/API/nd2_binary.md | 8 + docs/API/structures.md | 5 + docs/index.md | 138 +++++++++ docs/styles/extra.css | 68 +++++ mkdocs.yml | 71 +++++ pyproject.toml | 5 + src/nd2/_binary.py | 30 +- src/nd2/_pysdk/_chunk_decode.py | 2 +- src/nd2/nd2file.py | 505 +++++++++++++++++++++++++++++--- src/nd2/structures.py | 6 +- 14 files changed, 812 insertions(+), 67 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/API/nd2.md create mode 100644 docs/API/nd2_binary.md create mode 100644 docs/API/structures.md create mode 100644 docs/index.md create mode 100644 docs/styles/extra.css create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..3b78f2a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,20 @@ +name: ci +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + + - run: pip install .[docs] + - run: mkdocs gh-deploy --force diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 386ec79..4f2636b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.272 + rev: v0.0.275 hooks: - id: ruff args: [--fix] @@ -37,9 +37,17 @@ repos: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.4.0 hooks: - id: mypy files: "^src/" exclude: scripts additional_dependencies: [numpy, lxml-stubs] + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.5 + hooks: + - id: codespell + exclude: CHANGELOG.md + args: + - "-L=nd2,nd,pevents" diff --git a/README.md b/README.md index 5ec86fa..8a985ea 100644 --- a/README.md +++ b/README.md @@ -111,11 +111,11 @@ f.events() # returns tabular "Recorded Data" view from in NIS Elements/ # allll the metadata we can find... # no attempt made to standardize or parse it -# look in here if you're searching for metdata that isn't exposed in the above +# look in here if you're searching for metadata that isn't exposed in the above # but try not to rely on it, as it's not guaranteed to be stable f.unstructured_metadata() -f.close() # don't forget to close when not using a contet manager! +f.close() # don't forget to close when not using a context manager! f.closed # boolean, whether the file is closed ``` diff --git a/docs/API/nd2.md b/docs/API/nd2.md new file mode 100644 index 0000000..f893ac9 --- /dev/null +++ b/docs/API/nd2.md @@ -0,0 +1,5 @@ +# nd2 + +::: nd2 + options: + filters: ["!^Binary", "!^_"] diff --git a/docs/API/nd2_binary.md b/docs/API/nd2_binary.md new file mode 100644 index 0000000..1df58da --- /dev/null +++ b/docs/API/nd2_binary.md @@ -0,0 +1,8 @@ +# nd2 (binary masks) + +::: nd2 + options: + filters: ["!^_"] + members: + - BinaryLayers + - BinaryLayer diff --git a/docs/API/structures.md b/docs/API/structures.md new file mode 100644 index 0000000..bdc74d0 --- /dev/null +++ b/docs/API/structures.md @@ -0,0 +1,5 @@ +# nd2.structures + +::: nd2.structures + options: + show_if_no_docstring: true diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..367ac01 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,138 @@ +# Quickstart + +[![License](https://img.shields.io/pypi/l/nd2.svg?style=flat-square&color=yellow)](https://github.com/tlambert03/nd2/raw/main/LICENSE) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/nd2?style=flat-square&color=yellow)](https://pypi.org/project/nd2) +[![PyPI](https://img.shields.io/pypi/v/nd2.svg?style=flat-square&color=yellow)](https://pypi.org/project/nd2) +[![Conda](https://img.shields.io/conda/v/conda-forge/nd2?style=flat-square&color=yellow)](https://anaconda.org/conda-forge/nd2) + +.nd2 (Nikon NIS Elements) file reader. + +Features complete metadata retrieval, and many array outputs, including +[`to_dask()`][nd2.ND2File.to_dask] and [`to_xarray()`][nd2.ND2File.to_xarray] +options for lazy and/or annotated arrays (in addition to numpy arrays). + +This library is thoroughly tested against many nd2 files with the goal of +maximizing compatibility and data extraction. (If you find an nd2 file that +fails in any way, please [open an +issue](https://github.com/tlambert03/nd2/issues/new) with the file!) + +!!! Note + This library is not affiliated with Nikon in any way, but we are + grateful for assistance from the SDK developers at [Laboratory + Imaging](https://www.lim.cz). + +## Installation + +From pip: + +```sh +pip install nd2 +``` + +From conda: + +```sh +conda install -c conda-forge nd2 +``` + +### With legacy nd2 file support + +Legacy nd2 (JPEG2000) files are also supported, but require `imagecodecs`. To +install with support for these files use the `legacy` extra: + +```sh +pip install nd2[legacy] +``` + +### Faster XML parsing + +Much of the metadata in the file stored as XML. If found in the environment, +`nd2` will use [`lxml`](https://pypi.org/project/lxml/) which is faster than the +built-in `xml` module. To install with support for `lxml` use: + +```sh +pip install nd2 lxml +``` + +## Usage overview + +For complete usage details, see the [API](API/nd2.md) + +### Reading nd2 files into arrays + +To quickly read an nd2 file into a numpy, dask, or xarray array, +use `nd2.imread()`: + +```python +import nd2 + +# read to numpy array +my_array = nd2.imread('some_file.nd2') + +# read to dask array +my_array = nd2.imread('some_file.nd2', dask=True) + +# read to xarray +my_array = nd2.imread('some_file.nd2', xarray=True) + +# read file to dask-xarray +my_array = nd2.imread('some_file.nd2', xarray=True, dask=True) +``` + +### Extracting metadata + +If you want to get metadata, then use the [`nd2.ND2File`][] class directly: + +```python +myfile = nd2.ND2File('some_file.nd2') +``` + +!!! tip + It's best to use it as a context manager, so that the file is closed + automatically when you're done with it. + + ```python + with nd2.ND2File('some_file.nd2') as myfile: + print(myfile.metadata) + ... + ``` + +The primary metadata is available as attributes on the file object: + +The key metadata outputs are: + +- [`ND2File.attributes`][nd2.ND2File.attributes] +- [`ND2File.metadata`][nd2.ND2File.metadata] / [`ND2File.frame_metadata()`][nd2.ND2File.frame_metadata] +- [`ND2File.experiment`][nd2.ND2File.experiment] +- [`ND2File.text_info`][nd2.ND2File.text_info] +- [`ND2File.events()`][nd2.ND2File.events] + +Other attributes of note include: + +| ATTRIBUTE | EXAMPLE OUTPUT | +|--------------------|------------------------------------------| +| `myfile.shape` | `(10, 2, 256, 256)` | +| `myfile.ndim` | `4` | +| `myfile.dtype` | `np.dtype('uint16')` | +| `myfile.size` | `1310720` (total voxel elements) | +| `myfile.sizes` | `{'T': 10, 'C': 2, 'Y': 256, 'X': 256}` | +| `myfile.voxel_size()` | `VoxelSize(x=0.65, y=0.65, z=1.0)` | +| `myfile.is_rgb` | `False` (whether the file is rgb) | + +### Binary masks and ROIs + +Binary masks, if present, can be accessed at +[`ND2File.binary_data`][nd2.ND2File.binary_data]. + +ROIs, if present, can be accessed at [`ND2File.rois`][nd2.ND2File.rois]. + +### There's more in there! + +If you're still looking for something that you don't see in the above +properties and methods, try looking through: + +- [ND2File.custom_data][nd2.ND2File.custom_data] +- [ND2File.unstructured_metadata()][nd2.ND2File.unstructured_metadata] + +These methods parse and return more of the metadata found in the file, +but no attempt is made to extract it into a more useful form. diff --git a/docs/styles/extra.css b/docs/styles/extra.css new file mode 100644 index 0000000..a5562e8 --- /dev/null +++ b/docs/styles/extra.css @@ -0,0 +1,68 @@ +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: 0.05rem solid var(--md-typeset-table-color); +} + +.md-typeset .doc-heading code { + background-color: transparent; + display: inline-block; + max-width: 100%; + margin-left: 20px; + text-indent: -20px; +} + +.md-typeset .doc-heading code span { + font-weight: 600; + color: rgb(166, 23, 25); +} +.md-typeset h3.doc-heading code span { + font-size: 0.8rem; +} + + +div.doc-function h2.doc-heading { + line-height: 1; + margin-left: 20px; + text-indent: -20px; + margin-bottom: 30px; +} + +/* Mark external links as such. */ +a.external::after, +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + mask-image: url('data:image/svg+xml,'); + content: " "; + + display: inline-block; + vertical-align: middle; + position: relative; + + height: 1em; + width: 1em; + background-color: var(--md-typeset-a-color); +} + +a.external:hover::after, +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); +} + +article:has(> h1:first-child:nth-child(1)[id="nd2structures"]) .doc-label { + display: none; +} + +article:has(> h1:first-child:nth-child(1)[id="nd2structures"]) h3 { + line-height: 1; + margin-top: 0; +} + + +.md-typeset__table { + min-width: 100%; +} + +.md-typeset table:not([class]) { + display: table; +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..e7968a0 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,71 @@ +site_name: nd2 +site_author: Talley Lambert +repo_name: tlambert03/nd2 +repo_url: https://github.com/tlambert03/nd2 +edit_uri: edit/main/docs/ +site_description: A Python package for reading Nikon ND2 files +copyright: 'Talley Lambert © 2021' +strict: true + +watch: + - src + +theme: + name: material + icon: + logo: material/camera-iris + repo: fontawesome/brands/github + features: + - navigation.sections + palette: + # Palette toggle for light mode + - scheme: default + primary: yellow + accent: teal + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - scheme: slate + primary: black + accent: yellow + toggle: + icon: material/brightness-4 + name: Switch to light mode + +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - tables + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + - toc: + permalink: true + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + show_bases: false + show_source: false + show_root_toc_entry: false + docstring_style: numpy + docstring_section_style: list + show_signature_annotations: true + signature_crossrefs: true + filters: + - "!^__" + - "!^_" + import: + - https://numpy.org/doc/stable/objects.inv + - https://docs.python.org/3/objects.inv + - https://docs.xarray.dev/en/stable/objects.inv + - https://docs.dask.org/en/stable/objects.inv + +extra_css: + - styles/extra.css diff --git a/pyproject.toml b/pyproject.toml index 035cddd..5cd9448 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,11 @@ dev = [ "xarray", "lxml-stubs", ] +docs = [ + "mkdocs", + "mkdocs-material", + "mkdocstrings-python", +] [project.urls] homepage = "https://github.com/tlambert03/nd2" diff --git a/src/nd2/_binary.py b/src/nd2/_binary.py index 411aa26..2f0ccac 100644 --- a/src/nd2/_binary.py +++ b/src/nd2/_binary.py @@ -20,37 +20,37 @@ class BinaryLayer(NamedTuple): - """Wrapper for data from a single binary layer in an ND2 file. + """Wrapper for data from a single binary layer in an [`nd2.ND2File`][]. `data` will have length of num_sequences, with `None` for any frames that lack binary data. - Parameters + Attributes ---------- - data : list of numpy.ndarray or None + data : list[numpy.ndarray] | None The data for each frame. If a frame has no binary data, the value will be None. Data will have the same length as the number of sequences in the file. - name: str + name : str The name of the binary layer. - comp_name: str + comp_name : str The name of the associated component, if Any. - comp_order: int + comp_order : int The order of the associated component, if Any. - color: int + color : int The color of the binary layer. - color_mode: int + color_mode : int The color mode of the binary layer. I believe this is related to how colors are chosen in NIS-Elements software. Where "0" is direct color (i.e. use, the color value), "8" is color by 3D ... and I'm not sure about the rest :) - state: int + state : int The state of the binary layer. (meaning still unclear) - file_tag: str + file_tag : str The key for the binary layer in the CustomData metadata, e.g. `RleZipBinarySequence_1_v1` - layer_id: int + layer_id : int The ID of the binary layer. - coordinate_shape: tuple of int + coordinate_shape : tuple[int, ...] The shape of the coordinates for the associated nd2 file. This is used to reshape the data into a 3D array in `asarray`. """ @@ -111,8 +111,8 @@ class BinaryLayers(Sequence[BinaryLayer]): present in that frame. The wrapper can be cast to a numpy array (with `BinaryLayers.asarray()` or - np.asarray(BinaryLayers)) to stack all the layers into a single array. The output - array will have shape (n_layers, *coord_shape, *frame_shape). + `np.asarray(BinaryLayers)`) to stack all the layers into a single array. The output + array will have shape `(n_layers, *coord_shape, *frame_shape)`. """ def __init__(self, data: list[BinaryLayer]) -> None: @@ -139,7 +139,7 @@ def __repr__(self) -> str: return f"<{type(self).__name__} with {len(self)} layers>" def __array__(self) -> np.ndarray: - """Compatibility with np.asarray(BinaryLayers).""" + """Compatibility with `np.asarray(BinaryLayers)`.""" return self.asarray() def asarray(self) -> np.ndarray: diff --git a/src/nd2/_pysdk/_chunk_decode.py b/src/nd2/_pysdk/_chunk_decode.py index c8afa76..e0965d6 100644 --- a/src/nd2/_pysdk/_chunk_decode.py +++ b/src/nd2/_pysdk/_chunk_decode.py @@ -315,7 +315,7 @@ def rescue_nd2( Parameters ---------- - handle : Union[BinaryIO,str] + handle : BinaryIO | str Filepath string, or binary file handle (For example `handle = open('some.nd2', 'rb')`) frame_shape : Tuple[int, ...], optional diff --git a/src/nd2/nd2file.py b/src/nd2/nd2file.py index 2f3c075..84f34f7 100644 --- a/src/nd2/nd2file.py +++ b/src/nd2/nd2file.py @@ -24,6 +24,7 @@ import mmap from typing import Any, Sequence, Sized, SupportsInt + import dask.array import dask.array.core import xarray as xr from typing_extensions import Literal @@ -46,6 +47,27 @@ class ND2File: """Main objecting for opening and extracting data from an nd2 file. + ```python + with nd2.ND2File("path/to/file.nd2") as nd2_file: + ... + ``` + + The key metadata outputs are: + + - [attributes][nd2.ND2File.attributes] + - [metadata][nd2.ND2File.metadata] / [frame_metadata][nd2.ND2File.frame_metadata] + - [experiment][nd2.ND2File.experiment] + - [text_info][nd2.ND2File.text_info] + + Some files may also have: + + - [binary_data][nd2.ND2File.binary_data] + - [rois][nd2.ND2File.rois] + + !!! tip + + For a simple way to read nd2 file data into an array, see [nd2.imread][]. + Parameters ---------- path : Path | str @@ -59,7 +81,8 @@ class ND2File: When validate_frames is true, this is the search window (in KB) that will be used to try to find the actual chunk position. by default 100 KB read_using_sdk : Optional[bool] - DEPRECATED. No longer does anything. + :warning: **DEPRECATED**. No longer does anything. + If `True`, use the SDK to read the file. If `False`, inspects the chunkmap and reads from a `numpy.memmap`. If `None` (the default), uses the SDK if the file is compressed, otherwise uses the memmap. Note: using @@ -97,12 +120,19 @@ def __init__( @staticmethod def is_supported_file(path: StrOrBytesPath) -> bool: - """Return True if the file is supported by this reader.""" + """Return `True` if the file is supported by this reader.""" return is_supported_file(path) @property def version(self) -> tuple[int, ...]: - """Return the file format version as a tuple of ints.""" + """Return the file format version as a tuple of ints. + + Likely values are: + + - `(1, 0)` = a legacy nd2 file (JPEG2000) + - `(2, 0)`, `(2, 1)` = non-JPEG2000 nd2 with xml metadata + - `(3, 0)` = new format nd2 file with lite variant metadata + """ if self._version is None: try: self._version = get_version(self._rdr._fh or self._rdr._path) @@ -122,20 +152,44 @@ def is_legacy(self) -> bool: return self._is_legacy def open(self) -> None: - """Open file for reading.""" + """Open file for reading. + + !!! note + + Files are best opened using a context manager: + + ```python + with nd2.ND2File("path/to/file.nd2") as nd2_file: + ... + ``` + + This will automatically close the file when the context exits. + """ if self.closed: self._rdr.open() self._closed = False def close(self) -> None: - """Close file (may cause segfault if read when closed in some cases).""" + """Close file. + + !!! note + + Files are best opened using a context manager: + + ```python + with nd2.ND2File("path/to/file.nd2") as nd2_file: + ... + ``` + + This will automatically close the file when the context exits. + """ if not self.closed: self._rdr.close() self._closed = True @property def closed(self) -> bool: - """Whether the file is closed.""" + """Return `True` if the file is closed.""" return self._closed def __enter__(self) -> ND2File: @@ -174,17 +228,67 @@ def __setstate__(self, d: dict[str, Any]) -> None: @cached_property def attributes(self) -> Attributes: - """Core image attributes.""" + """Core image attributes. + + !!! example "Example Output" + + ```python + Attributes( + bitsPerComponentInMemory=16, + bitsPerComponentSignificant=16, + componentCount=2, + heightPx=32, + pixelDataType='unsigned', + sequenceCount=60, + widthBytes=128, + widthPx=32, + compressionLevel=None, + compressionType=None, + tileHeightPx=None, + tileWidthPx=None, + channelCount=2 + ) + ``` + + + Returns + ------- + attrs : Attributes + Core image attributes + """ return self._rdr.attributes @cached_property def text_info(self) -> TextInfo | dict: - """Misc text info.""" + r"""Miscellaneous text info. + + ??? example "Example Output" + + ```python + { + 'description': 'Metadata:\r\nDimensions: T(3) x XY(4) x λ(2) x Z(5)...' + 'capturing': 'Flash4.0, SN:101412\r\nSample 1:\r\n Exposure: 100 ms...' + 'date': '9/28/2021 9:41:27 AM', + 'optics': 'Plan Fluor 10x Ph1 DLL' + } + ``` + Returns + ------- + TextInfo | dict + If the file is a legacy nd2 file, a dict is returned. Otherwise, a + `TextInfo` object is returned. + """ return self._rdr.text_info() @cached_property def rois(self) -> dict[int, ROI]: - """Return dict of {id: ROI} for all ROIs found in the metadata.""" + """Return dict of `{id: ROI}` for all ROIs found in the metadata. + + Returns + ------- + dict[int, ROI] + The dict of ROIs is keyed by the ROI ID. + """ key = b"CustomData|RoiMetadata_v1!" if self.is_legacy or key not in self._rdr.chunkmap: # type: ignore return {} # pragma: no cover @@ -200,7 +304,45 @@ def rois(self) -> dict[int, ROI]: @cached_property def experiment(self) -> list[ExpLoop]: - """Loop information for each nd axis.""" + """Loop information for each axis of an nD acquisition. + + ??? example "Example Output" + + ```python + [ + TimeLoop( + count=3, + nestingLevel=0, + parameters=TimeLoopParams( + startMs=0.0, + periodMs=1.0, + durationMs=0.0, + periodDiff=PeriodDiff( + avg=3674.199951171875, + max=3701.219970703125, + min=3647.179931640625 + ) + ), + type='TimeLoop' + ), + ZStackLoop( + count=5, + nestingLevel=1, + parameters=ZStackLoopParams( + homeIndex=2, + stepUm=1.0, + bottomToTop=True, + deviceName='Ti2 ZDrive' + ), + type='ZStackLoop' + ) + ] + ``` + + Returns + ------- + list[ExpLoop] + """ return self._rdr.experiment() @overload @@ -250,6 +392,12 @@ def events( - 'list' : dict of list - `{column -> [value, ...]}` null_value : Any, default float('nan') The value to use for missing data. + + + Returns + ------- + ListOfDicts | DictOfLists | DictOfDicts + Tabular data in the format specified by `orient`. """ if orient not in ("records", "dict", "list"): # pragma: no cover raise ValueError("orient must be one of 'records', 'dict', or 'list'") @@ -312,17 +460,17 @@ def unstructured_metadata( Whether to strip the type information from the front of the keys in the dict. For example, if `True`: `uiModeFQ` becomes `ModeFQ` and `bUsePFS` becomes `UsePFS`, etc... by default `True` - include : Optional[Set[str]], optional + include : set[str] | None, optional If provided, only include the specified keys in the output. by default, all metadata sections found in the file are included. - exclude : Optional[Set[str]], optional + exclude : set[str] | None, optional If provided, exclude the specified keys from the output. by default `None` unnest : bool, optional - DEPRECATED. No longer does anything. + :warning: **DEPRECATED**. No longer does anything. Returns ------- - Dict[str, Any] + dict[str, Any] A dict of the unstructured metadata, with keys that are the type of the metadata chunk (things like 'CustomData|RoiMetadata_v1' or 'ImageMetadataLV'), and values that are associated metadata chunk. @@ -368,15 +516,261 @@ def unstructured_metadata( @cached_property def metadata(self) -> Metadata | dict: - """Various metadata (will be dict if legacy format).""" + """Various metadata (will be `dict` only if legacy format). + + ??? example "Example output" + + ```python + Metadata( + contents=Contents(channelCount=2, frameCount=15), + channels=[ + Channel( + channel=ChannelMeta( + name='Widefield Green', + index=0, + colorRGB=65371, + emissionLambdaNm=535.0, + excitationLambdaNm=None + ), + loops=LoopIndices( + NETimeLoop=None, + TimeLoop=0, + XYPosLoop=None, + ZStackLoop=1 + ), + microscope=Microscope( + objectiveMagnification=10.0, + objectiveName='Plan Fluor 10x Ph1 DLL', + objectiveNumericalAperture=0.3, + zoomMagnification=1.0, + immersionRefractiveIndex=1.0, + projectiveMagnification=None, + pinholeDiameterUm=None, + modalityFlags=['fluorescence'] + ), + volume=Volume( + axesCalibrated=[True, True, True], + axesCalibration=[ + 0.652452890023035, + 0.652452890023035, + 1.0 + ], + axesInterpretation=['distance', 'distance', 'distance'], + bitsPerComponentInMemory=16, + bitsPerComponentSignificant=16, + cameraTransformationMatrix=[ + -0.9998932296054086, + -0.014612644841559427, + 0.014612644841559427, + -0.9998932296054086 + ], + componentCount=1, + componentDataType='unsigned', + voxelCount=[32, 32, 5], + componentMaxima=[0.0], + componentMinima=[0.0], + pixelToStageTransformationMatrix=None + ) + ), + Channel( + channel=ChannelMeta( + name='Widefield Red', + index=1, + colorRGB=22015, + emissionLambdaNm=620.0, + excitationLambdaNm=None + ), + loops=LoopIndices( + NETimeLoop=None, + TimeLoop=0, + XYPosLoop=None, + ZStackLoop=1 + ), + microscope=Microscope( + objectiveMagnification=10.0, + objectiveName='Plan Fluor 10x Ph1 DLL', + objectiveNumericalAperture=0.3, + zoomMagnification=1.0, + immersionRefractiveIndex=1.0, + projectiveMagnification=None, + pinholeDiameterUm=None, + modalityFlags=['fluorescence'] + ), + volume=Volume( + axesCalibrated=[True, True, True], + axesCalibration=[ + 0.652452890023035, + 0.652452890023035, + 1.0 + ], + axesInterpretation=['distance', 'distance', 'distance'], + bitsPerComponentInMemory=16, + bitsPerComponentSignificant=16, + cameraTransformationMatrix=[ + -0.9998932296054086, + -0.014612644841559427, + 0.014612644841559427, + -0.9998932296054086 + ], + componentCount=1, + componentDataType='unsigned', + voxelCount=[32, 32, 5], + componentMaxima=[0.0], + componentMinima=[0.0], + pixelToStageTransformationMatrix=None + ) + ) + ] + ) + ``` + + Returns + ------- + Metadata | dict + dict if legacy format, else `Metadata` + """ return self._rdr.metadata() def frame_metadata(self, seq_index: int | tuple) -> FrameMetadata | dict: """Metadata for specific frame. + :eyes: **See also:** [metadata][nd2.ND2File.metadata] + This includes the global metadata from the metadata function. (will be dict if legacy format). + ??? example "Example output" + + ```python + FrameMetadata( + contents=Contents(channelCount=2, frameCount=15), + channels=[ + FrameChannel( + channel=ChannelMeta( + name='Widefield Green', + index=0, + colorRGB=65371, + emissionLambdaNm=535.0, + excitationLambdaNm=None + ), + loops=LoopIndices( + NETimeLoop=None, + TimeLoop=0, + XYPosLoop=None, + ZStackLoop=1 + ), + microscope=Microscope( + objectiveMagnification=10.0, + objectiveName='Plan Fluor 10x Ph1 DLL', + objectiveNumericalAperture=0.3, + zoomMagnification=1.0, + immersionRefractiveIndex=1.0, + projectiveMagnification=None, + pinholeDiameterUm=None, + modalityFlags=['fluorescence'] + ), + volume=Volume( + axesCalibrated=[True, True, True], + axesCalibration=[ + 0.652452890023035, + 0.652452890023035, + 1.0 + ], + axesInterpretation=['distance', 'distance', 'distance'], + bitsPerComponentInMemory=16, + bitsPerComponentSignificant=16, + cameraTransformationMatrix=[ + -0.9998932296054086, + -0.014612644841559427, + 0.014612644841559427, + -0.9998932296054086 + ], + componentCount=1, + componentDataType='unsigned', + voxelCount=[32, 32, 5], + componentMaxima=[0.0], + componentMinima=[0.0], + pixelToStageTransformationMatrix=None + ), + position=Position( + stagePositionUm=StagePosition( + x=26950.2, + y=-1801.6000000000001, + z=494.3 + ), + pfsOffset=None, + name=None + ), + time=TimeStamp( + absoluteJulianDayNumber=2459486.0682717753, + relativeTimeMs=580.3582921028137 + ) + ), + FrameChannel( + channel=ChannelMeta( + name='Widefield Red', + index=1, + colorRGB=22015, + emissionLambdaNm=620.0, + excitationLambdaNm=None + ), + loops=LoopIndices( + NETimeLoop=None, + TimeLoop=0, + XYPosLoop=None, + ZStackLoop=1 + ), + microscope=Microscope( + objectiveMagnification=10.0, + objectiveName='Plan Fluor 10x Ph1 DLL', + objectiveNumericalAperture=0.3, + zoomMagnification=1.0, + immersionRefractiveIndex=1.0, + projectiveMagnification=None, + pinholeDiameterUm=None, + modalityFlags=['fluorescence'] + ), + volume=Volume( + axesCalibrated=[True, True, True], + axesCalibration=[ + 0.652452890023035, + 0.652452890023035, + 1.0 + ], + axesInterpretation=['distance', 'distance', 'distance'], + bitsPerComponentInMemory=16, + bitsPerComponentSignificant=16, + cameraTransformationMatrix=[ + -0.9998932296054086, + -0.014612644841559427, + 0.014612644841559427, + -0.9998932296054086 + ], + componentCount=1, + componentDataType='unsigned', + voxelCount=[32, 32, 5], + componentMaxima=[0.0], + componentMinima=[0.0], + pixelToStageTransformationMatrix=None + ), + position=Position( + stagePositionUm=StagePosition( + x=26950.2, + y=-1801.6000000000001, + z=494.3 + ), + pfsOffset=None, + name=None + ), + time=TimeStamp( + absoluteJulianDayNumber=2459486.0682717753, + relativeTimeMs=580.3582921028137 + ) + ) + ] + ) + ``` + Parameters ---------- seq_index : Union[int, tuple] @@ -384,7 +778,7 @@ def frame_metadata(self, seq_index: int | tuple) -> FrameMetadata | dict: Returns ------- - Union[FrameMetadata, dict] + FrameMetadata | dict dict if legacy format, else FrameMetadata """ idx = cast( @@ -402,17 +796,34 @@ def custom_data(self) -> dict[str, Any]: @cached_property def ndim(self) -> int: - """Number of dimensions.""" + """Number of dimensions (i.e. `len(`[`self.shape`][nd2.ND2File.shape]`)`).""" return len(self.shape) @cached_property def shape(self) -> tuple[int, ...]: - """Size of each axis.""" + """Size of each axis. + + Examples + -------- + >>> ndfile.shape + (3, 5, 2, 512, 512) + """ return self._coord_shape + self._frame_shape @cached_property def sizes(self) -> dict[str, int]: - """Names and sizes for each axis.""" + """Names and sizes for each axis. + + This is an ordered dict, with the same order + as the corresponding [shape][nd2.ND2File.shape] + + Examples + -------- + >>> ndfile.sizes + {'T': 3, 'Z': 5, 'C': 2, 'Y': 512, 'X': 512} + >>> ndfile.shape + (3, 5, 2, 512, 512) + """ attrs = self.attributes dims = {AXIS._MAP[c[1]]: c[2] for c in self._rdr._coord_info()} dims[AXIS.CHANNEL] = ( @@ -431,7 +842,7 @@ def sizes(self) -> dict[str, int]: @property def is_rgb(self) -> bool: - """Whether the image is rgb.""" + """Whether the image is rgb (i.e. it has 3 or 4 components per channel).""" return self.components_per_channel in (3, 4) @property @@ -442,7 +853,7 @@ def components_per_channel(self) -> int: @property def size(self) -> int: - """Total number of pixels in the volume.""" + """Total number of voxels in the volume (the product of the shape).""" return int(np.prod(self.shape)) @property @@ -473,7 +884,9 @@ def voxel_size(self, channel: int = 0) -> _util.VoxelSize: return _util.VoxelSize(*self._rdr.voxel_size()) def asarray(self, position: int | None = None) -> np.ndarray: - """Read image into numpy array. + """Read image into a [numpy.ndarray][]. + + For a simple way to read a file into a numpy array, see [nd2.imread][]. Parameters ---------- @@ -482,7 +895,7 @@ def asarray(self, position: int | None = None) -> np.ndarray: Returns ------- - np.ndarray + array : np.ndarray Raises ------ @@ -544,15 +957,14 @@ def to_dask(self, wrapper: bool = True, copy: bool = True) -> dask.array.core.Ar Parameters ---------- wrapper : bool - If True (the default), the returned obect will be a thin subclass of - a :class:`dask.array.Array` (an - `ResourceBackedDaskArray`) that manages the opening and closing of this file - when getting chunks via compute(). If `wrapper` is `False`, then a pure - `dask.array.core.Array` will be returned. However, when that array is - computed, it will incur a file open/close on *every* chunk that is read (in - the `_dask_block` method). As such `wrapper` will generally be much faster, - however, it *may* fail (i.e. result in segmentation faults) with certain - dask schedulers. + If `True` (the default), the returned object will be a thin subclass of a + [`dask.array.Array`][] (a `ResourceBackedDaskArray`) that manages the + opening and closing of this file when getting chunks via compute(). If + `wrapper` is `False`, then a pure `dask.array.core.Array` will be returned. + However, when that array is computed, it will incur a file open/close on + *every* chunk that is read (in the `_dask_block` method). As such `wrapper` + will generally be much faster, however, it *may* fail (i.e. result in + segmentation faults) with certain dask schedulers. copy : bool If `True` (the default), the dask chunk-reading function will return an array copy. This can avoid segfaults in certain cases, though it @@ -560,7 +972,8 @@ def to_dask(self, wrapper: bool = True, copy: bool = True) -> dask.array.core.Ar Returns ------- - dask.array.core.Array + dask_array: dask.array.Array + A dask array representing the image data. """ from dask.array.core import map_blocks @@ -618,7 +1031,10 @@ def to_xarray( position: int | None = None, copy: bool = True, ) -> xr.DataArray: - """Create labeled xarray representing image. + """Return a labeled [xarray.DataArray][] representing image. + + Xarrays are a powerful way to label and manipulate n-dimensional data with + axis-associated coordinates. `array.dims` will be populated according to image metadata, and coordinates will be populated based on pixel spacings. Additional metadata is available @@ -781,19 +1197,15 @@ def __repr__(self) -> str: return f"" @property - def recorded_data( - self, - ) -> DictOfLists: + def recorded_data(self) -> DictOfLists: """Return tabular data recorded for each frame of the experiment. - This method returns a dict of equal-length sequences (passable to - pd.DataFrame()). It matches the tabular data reported in the Image Properties > - Recorded Data tab of the NIS Viewer. - - (There will be a column for each tag in the `CustomDataV2_0` section of - `ND2File.custom_data`) + !!! warning "Deprecated" - Legacy ND2 files are not supported. + This method is deprecated and will be removed in a future version. + Please use the [`events`][nd2.ND2File.events] method instead. To get the + same dict-of-lists output that `recorded_data` returns, use + `ndfile.events(orient='list')` """ warnings.warn( "recorded_data is deprecated and will be removed in a future version." @@ -892,7 +1304,7 @@ def imread( Parameters ---------- - file : Union[Path, str] + file : Path | str Filepath (`str`) or `Path` object to ND2 file. dask : bool If `True`, returns a (delayed) `dask.array.Array`. This will avoid reading @@ -910,7 +1322,8 @@ def imread( This comes at a slight performance penalty at file open, but may "rescue" some corrupt files. by default False. read_using_sdk : Optional[bool] - DEPRECATED: no longer used. + :warning: **DEPRECATED**. No longer used. + If `True`, use the SDK to read the file. If `False`, inspects the chunkmap and reads from a `numpy.memmap`. If `None` (the default), uses the SDK if the file is compressed, otherwise uses the memmap. diff --git a/src/nd2/structures.py b/src/nd2/structures.py index 75a71fc..7f77c21 100644 --- a/src/nd2/structures.py +++ b/src/nd2/structures.py @@ -115,6 +115,8 @@ class CustomLoop(_Loop): @dataclass class TimeLoop(_Loop): + """The time dimension of an experiment.""" + parameters: TimeLoopParams type: Literal["TimeLoop"] = "TimeLoop" @@ -150,6 +152,8 @@ class PeriodDiff: @dataclass class NETimeLoop(_Loop): + """The time dimension of an nD experiment.""" + parameters: NETimeLoopParams type: Literal["NETimeLoop"] = "NETimeLoop" @@ -393,7 +397,7 @@ class XYZPoint(NamedTuple): class ExtrudedShape(NamedTuple): sizeZ: float = 0 - basePoints: list[XYPoint] = [] + basePoints: list[XYPoint] = field(default_factory=list) @classmethod def _from_meta_dict(cls, val: dict) -> ExtrudedShape: