Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: make binarylayer a dataclass #178

Merged
merged 4 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions src/nd2/_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

import io
import struct
import sys
import warnings
import zlib
from typing import TYPE_CHECKING, Iterator, NamedTuple, Sequence, cast, overload
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Iterator, Sequence, cast, overload

import numpy as np

Expand All @@ -17,12 +19,21 @@
I9 = struct.Struct("<" + "I" * 9)
I2 = struct.Struct("<" + "I" * 2)

SLOTS = {}
if sys.version_info >= (3, 10):
SLOTS["slots"] = True

class BinaryLayer(NamedTuple):

@dataclass(frozen=True, **SLOTS)
class BinaryLayer:
"""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.
A "layer" is a set of binary data that can be associated with a
specific component in an ND2 file, such as a single channel.

This object behaves like a `list[numpy.ndarray] | None`.
It will have a length matching the number of frames in the file, with `None` for
any frames that lack binary data.

Attributes
----------
Expand Down Expand Up @@ -54,7 +65,7 @@ class BinaryLayer(NamedTuple):
to reshape the data into a 3D array in `asarray`.
"""

data: list[np.ndarray | None]
data: list[np.ndarray | None] = field(repr=False)
name: str
file_tag: str
comp_name: str | None
Expand All @@ -65,6 +76,14 @@ class BinaryLayer(NamedTuple):
layer_id: int | None
coordinate_shape: tuple[int, ...]

def __len__(self) -> int:
"""Return the number of frames in the data."""
return len(self.data)

def __getitem__(self, key: int) -> np.ndarray | None:
"""Return the data for a single frame."""
return self.data[key]

@property
def frame_shape(self) -> tuple[int, ...]:
"""Shape (Y, X) of each mask in `data`."""
Expand Down Expand Up @@ -94,12 +113,6 @@ def asarray(self) -> np.ndarray | None:
"np.ndarray", np.stack(d).reshape(self.coordinate_shape + frame_shape)
)

def __repr__(self) -> str:
"""Return a nicely formatted string."""
field_names = (f for f in self._fields if f != "data")
repr_fmt = "(" + ", ".join(f"{name}=%r" for name in field_names) + ")"
return self.__class__.__name__ + repr_fmt % self[1:]


class BinaryLayers(Sequence[BinaryLayer]):
"""Sequence of Binary Layers found in an ND2 file.
Expand Down
23 changes: 15 additions & 8 deletions src/nd2/nd2file.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,11 +1155,15 @@ def binary_data(self) -> BinaryLayers | None:
"""Return binary layers embedded in the file.

The returned `BinaryLayers` object is an immutable sequence of `BinaryLayer`
objects, one for each binary layer in the file. Each `BinaryLayer` object in
the sequence has a `name` attribute, and a `data` attribute which is list of
numpy arrays (or `None` if there was no binary mask for that frame). The length
of the list will be the same as the number of sequence frames in this file
(i.e. `self.attributes.sequenceCount`).
objects, one for each binary layer in the file (there will usually be a binary
layer associated with each channel in the dataset).

Each `BinaryLayer` object in the sequence has a `name` attribute, and a `data`
attribute which is list of numpy arrays (or `None` if there was no binary mask
for that frame). The length of the list will be the same as the number of
sequence frames in this file (i.e. `self.attributes.sequenceCount`).
`BinaryLayers` can be indexed directly with an integer corresponding to the
*frame* index.

Both the `BinaryLayers` and individual `BinaryLayer` objects can be cast to a
numpy array with `np.asarray()`, or by using the `.asarray()` method
Expand All @@ -1175,12 +1179,15 @@ def binary_data(self) -> BinaryLayers | None:
>>> f = ND2File("path/to/file.nd2")
>>> f.binary_data
<BinaryLayers with 4 layers>
>>> f.binary_data[0] # the first binary layer
>>> first_layer = f.binary_data[0] # the first binary layer
>>> first_layer
BinaryLayer(name='attached Widefield green (green color)',
comp_name='Widefield Green', comp_order=2, color=65280, color_mode=0,
state=524288, file_tag='RleZipBinarySequence_1_v1', layer_id=2)
>>> f.binary_data[0].data # list of arrays
>>> np.asarray(f.binary_data[0]) # just the first binary mask
>>> first_layer.data # list of arrays
# you can also index in to the BinaryLayers object itself
>>> first_layer[0] # get binary data for first frame (or None if missing)
>>> np.asarray(first_layer) # cast to array matching shape of full sequence
>>> np.asarray(f.binary_data).shape # cast all layers to array
(4, 3, 4, 5, 32, 32)
"""
Expand Down
15 changes: 14 additions & 1 deletion tests/test_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@

import nd2
import numpy as np
import numpy.testing as npt

DATA = Path(__file__).parent / "data"

# fmt: off
ROW0 = [0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,2,2,0,0,0,3,0,0,0,0,0,0,0]
# fmt: on


def test_binary():
with nd2.ND2File(DATA / "with_binary_and_rois.nd2") as f:
binlayers = f.binary_data
repr(binlayers)
repr(binlayers[0])
assert binlayers is not None
assert len(binlayers) == 4
assert binlayers[0].name == "attached Widefield green (green color)"
assert len(binlayers[0].data) == f.attributes.sequenceCount
assert len(binlayers[0]) == f.attributes.sequenceCount
# you can index into the data
npt.assert_array_equal(binlayers[0].data[2][0], ROW0)
# you can also index a BinaryLayer directly
assert isinstance(binlayers[0][2], np.ndarray)
assert binlayers[0][3] is None
npt.assert_array_equal(binlayers[0][2][0], ROW0)
ary = np.asarray(binlayers)
assert ary.shape == (4, 3, 4, 5, 32, 32)
assert ary.sum() == 172947