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: