- add doc strings

This commit is contained in:
w.pomp
2026-08-07 17:00:28 +02:00
parent b950f4a39d
commit 5ad9a44ecd
16 changed files with 993 additions and 428 deletions
+366 -99
View File
@@ -47,9 +47,15 @@ class Imread:
TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var
""" """
@property @property
def reader_name(self) -> builtins.str: ... def reader_name(self) -> builtins.str:
r"""
the name of the reader used to open the file
"""
@property @property
def transform(self) -> None: ... def transform(self) -> None:
r"""
get the transformation matrix (not yet implemented)
"""
@property @property
def path(self) -> pathlib.Path: def path(self) -> pathlib.Path:
r""" r"""
@@ -71,7 +77,10 @@ class Imread:
the shape of the view the shape of the view
""" """
@property @property
def slice(self) -> builtins.list[builtins.str]: ... def slice(self) -> builtins.list[builtins.str]:
r"""
the current slice applied to the view
"""
@property @property
def size(self) -> builtins.int: def size(self) -> builtins.int:
r""" r"""
@@ -83,25 +92,40 @@ class Imread:
the number of dimensions in the view the number of dimensions in the view
""" """
@property @property
def T(self) -> Imread: ... def T(self) -> Imread:
r"""
transposed view (alias for transpose(None))
"""
@property @property
def dtype(self) -> numpy.dtype: ... def dtype(self) -> numpy.dtype:
r"""
the numpy dtype of the view
"""
@property @property
def z_stack(self) -> builtins.bool: ... def z_stack(self) -> builtins.bool:
r"""
whether the view contains a z-stack (more than one z slice)
"""
@property @property
def zstack(self) -> builtins.bool: def zstack(self) -> builtins.bool:
r""" r"""
backwards compatibility backwards compatibility
""" """
@property @property
def time_series(self) -> builtins.bool: ... def time_series(self) -> builtins.bool:
r"""
whether the view contains a time series (more than one time point)
"""
@property @property
def timeseries(self) -> builtins.bool: def timeseries(self) -> builtins.bool:
r""" r"""
backwards compatibility backwards compatibility
""" """
@property @property
def pixel_size(self) -> typing.Optional[builtins.float]: ... def pixel_size(self) -> typing.Optional[builtins.float]:
r"""
the pixel size in micrometers
"""
@property @property
def pxsize_um(self) -> typing.Optional[builtins.float]: def pxsize_um(self) -> typing.Optional[builtins.float]:
r""" r"""
@@ -113,9 +137,15 @@ class Imread:
backwards compatibility backwards compatibility
""" """
@property @property
def delta_z(self) -> typing.Optional[builtins.float]: ... def delta_z(self) -> typing.Optional[builtins.float]:
r"""
the z-step size in micrometers
"""
@property @property
def time_interval(self) -> typing.Optional[builtins.float]: ... def time_interval(self) -> typing.Optional[builtins.float]:
r"""
the time interval between frames in seconds
"""
@property @property
def timeinterval(self) -> typing.Optional[builtins.float]: def timeinterval(self) -> typing.Optional[builtins.float]:
r""" r"""
@@ -127,15 +157,24 @@ class Imread:
backwards compatibility backwards compatibility
""" """
@property @property
def objective_name(self) -> typing.Optional[builtins.str]: ... def objective_name(self) -> typing.Optional[builtins.str]:
r"""
the name of the objective
"""
@property @property
def magnification(self) -> typing.Optional[builtins.float]: ... def magnification(self) -> typing.Optional[builtins.float]:
r"""
the total magnification (objective × tube lens)
"""
@property @property
def tube_lens_name(self) -> typing.Optional[builtins.str]: ... def tube_lens_name(self) -> typing.Optional[builtins.str]:
r"""
the name of the tube lens
"""
def __new__( def __new__(
cls, cls,
path: str | pathlib.Path | Imread | bytes, path: str | pathlib.Path | Imread | bytes,
dtype: numpy.typing.DTypeLike = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None,
axes: builtins.str = "cztyx", axes: builtins.str = "cztyx",
reader: typing.Optional[builtins.str] = None, reader: typing.Optional[builtins.str] = None,
) -> Imread: ) -> Imread:
@@ -145,20 +184,29 @@ class Imread:
@staticmethod @staticmethod
def get_positions( def get_positions(
path: str | pathlib.Path | Imread | bytes, path: str | pathlib.Path | Imread | bytes,
) -> builtins.set[builtins.int]: ... ) -> builtins.set[builtins.int]:
r"""
get all available positions (series) in the file
"""
@staticmethod @staticmethod
def kill_vm() -> None: def kill_vm() -> None:
r""" r"""
only remains for backwards compatibility only remains for backwards compatibility
""" """
def reshape(self, order: builtins.str, copy: builtins.bool) -> typing.Any: ... def reshape(self, order: builtins.str, copy: builtins.bool) -> typing.Any:
r"""
reshape the view with a new axis order
"""
def with_transform( def with_transform(
self, self,
channels: builtins.bool = True, channels: builtins.bool = True,
drift: builtins.bool = False, drift: builtins.bool = False,
file: typing.Optional[typing.Any] = None, file: typing.Optional[typing.Any] = None,
bead_files: typing.Optional[typing.Any] = None, bead_files: typing.Optional[typing.Any] = None,
) -> Imread: ... ) -> Imread:
r"""
return a new view with transformations applied (channel alignment, drift correction)
"""
def squeeze(self) -> numpy.ndarray | int | float: ... def squeeze(self) -> numpy.ndarray | int | float: ...
def close(self) -> None: def close(self) -> None:
r""" r"""
@@ -178,69 +226,219 @@ class Imread:
""" """
def __array__( def __array__(
self, self,
dtype: numpy.typing.DTypeLike = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None,
copy: typing.Optional[builtins.bool] = None, copy: typing.Optional[builtins.bool] = None,
) -> typing.Any: ... ) -> typing.Any:
def __contains__(self, _item: typing.Any) -> builtins.bool: ... r"""
def __lt__(self, other: typing.Any) -> typing.Any: ... convert to a numpy array, optionally with a different dtype
def __le__(self, other: typing.Any) -> typing.Any: ... """
def __eq__(self, other: typing.Any) -> typing.Any: ... def __contains__(self, _item: typing.Any) -> builtins.bool:
def __ne__(self, other: typing.Any) -> typing.Any: ... r"""
def __gt__(self, other: typing.Any) -> typing.Any: ... check if an item is contained in the view (not implemented)
def __ge__(self, other: typing.Any) -> typing.Any: ... """
def __add__(self, other: typing.Any) -> typing.Any: ... def __lt__(self, other: typing.Any) -> typing.Any:
def __radd__(self, other: typing.Any) -> typing.Any: ... r"""
def __sub__(self, other: typing.Any) -> typing.Any: ... element-wise less than comparison
def __rsub__(self, other: typing.Any) -> typing.Any: ... """
def __mul__(self, other: typing.Any) -> typing.Any: ... def __le__(self, other: typing.Any) -> typing.Any:
def __rmul__(self, other: typing.Any) -> typing.Any: ... r"""
def __truediv__(self, other: typing.Any) -> typing.Any: ... element-wise less than or equal comparison
def __rtruediv__(self, other: typing.Any) -> typing.Any: ... """
def __floordiv__(self, other: typing.Any) -> typing.Any: ... def __eq__(self, other: typing.Any) -> typing.Any:
def __rfloordiv__(self, other: typing.Any) -> typing.Any: ... r"""
def __mod__(self, other: typing.Any) -> typing.Any: ... element-wise equality comparison
def __rmod__(self, other: typing.Any) -> typing.Any: ... """
def __matmul__(self, other: typing.Any) -> typing.Any: ... def __ne__(self, other: typing.Any) -> typing.Any:
def __rmatmul__(self, other: typing.Any) -> typing.Any: ... r"""
def __and__(self, other: typing.Any) -> typing.Any: ... element-wise not equal comparison
def __rand__(self, other: typing.Any) -> typing.Any: ... """
def __or__(self, other: typing.Any) -> typing.Any: ... def __gt__(self, other: typing.Any) -> typing.Any:
def __ror__(self, other: typing.Any) -> typing.Any: ... r"""
def __xor__(self, other: typing.Any) -> typing.Any: ... element-wise greater than comparison
def __rxor__(self, other: typing.Any) -> typing.Any: ... """
def __lshift__(self, other: typing.Any) -> typing.Any: ... def __ge__(self, other: typing.Any) -> typing.Any:
def __rlshift__(self, other: typing.Any) -> typing.Any: ... r"""
def __rshift__(self, other: typing.Any) -> typing.Any: ... element-wise greater than or equal comparison
def __rrshift__(self, other: typing.Any) -> typing.Any: ... """
def __neg__(self) -> typing.Any: ... def __add__(self, other: typing.Any) -> typing.Any:
def __pos__(self) -> typing.Any: ... r"""
def __abs__(self) -> typing.Any: ... element-wise addition
def __invert__(self) -> typing.Any: ... """
def __enter__(self) -> Imread: ... def __radd__(self, other: typing.Any) -> typing.Any:
r"""
element-wise addition (reflected)
"""
def __sub__(self, other: typing.Any) -> typing.Any:
r"""
element-wise subtraction
"""
def __rsub__(self, other: typing.Any) -> typing.Any:
r"""
element-wise subtraction (reflected)
"""
def __mul__(self, other: typing.Any) -> typing.Any:
r"""
element-wise multiplication
"""
def __rmul__(self, other: typing.Any) -> typing.Any:
r"""
element-wise multiplication (reflected)
"""
def __truediv__(self, other: typing.Any) -> typing.Any:
r"""
element-wise true division
"""
def __rtruediv__(self, other: typing.Any) -> typing.Any:
r"""
element-wise true division (reflected)
"""
def __floordiv__(self, other: typing.Any) -> typing.Any:
r"""
element-wise floor division
"""
def __rfloordiv__(self, other: typing.Any) -> typing.Any:
r"""
element-wise floor division (reflected)
"""
def __mod__(self, other: typing.Any) -> typing.Any:
r"""
element-wise modulo
"""
def __rmod__(self, other: typing.Any) -> typing.Any:
r"""
element-wise modulo (reflected)
"""
def __matmul__(self, other: typing.Any) -> typing.Any:
r"""
element-wise matrix multiplication
"""
def __rmatmul__(self, other: typing.Any) -> typing.Any:
r"""
element-wise matrix multiplication (reflected)
"""
def __and__(self, other: typing.Any) -> typing.Any:
r"""
element-wise bitwise AND
"""
def __rand__(self, other: typing.Any) -> typing.Any:
r"""
element-wise bitwise AND (reflected)
"""
def __or__(self, other: typing.Any) -> typing.Any:
r"""
element-wise bitwise OR
"""
def __ror__(self, other: typing.Any) -> typing.Any:
r"""
element-wise bitwise OR (reflected)
"""
def __xor__(self, other: typing.Any) -> typing.Any:
r"""
element-wise bitwise XOR
"""
def __rxor__(self, other: typing.Any) -> typing.Any:
r"""
element-wise bitwise XOR (reflected)
"""
def __lshift__(self, other: typing.Any) -> typing.Any:
r"""
element-wise left shift
"""
def __rlshift__(self, other: typing.Any) -> typing.Any:
r"""
element-wise left shift (reflected)
"""
def __rshift__(self, other: typing.Any) -> typing.Any:
r"""
element-wise right shift
"""
def __rrshift__(self, other: typing.Any) -> typing.Any:
r"""
element-wise right shift (reflected)
"""
def __neg__(self) -> typing.Any:
r"""
element-wise negation
"""
def __pos__(self) -> typing.Any:
r"""
element-wise positive
"""
def __abs__(self) -> typing.Any:
r"""
element-wise absolute value
"""
def __invert__(self) -> typing.Any:
r"""
element-wise bitwise inversion
"""
def __enter__(self) -> Imread:
r"""
context manager entry
"""
def __exit__( def __exit__(
self, self,
exc_type: typing.Optional[typing.Any] = None, exc_type: typing.Optional[typing.Any] = None,
exc_val: typing.Optional[typing.Any] = None, exc_val: typing.Optional[typing.Any] = None,
exc_tb: typing.Optional[typing.Any] = None, exc_tb: typing.Optional[typing.Any] = None,
) -> None: ... ) -> None:
def __getnewargs__(self) -> tuple[builtins.list[builtins.int]]: ... r"""
def __copy__(self) -> Imread: ... context manager exit
def __deepcopy__(self) -> Imread: ... """
def copy(self) -> Imread: ... def __getnewargs__(self) -> tuple[builtins.list[builtins.int]]:
def __iter__(self) -> Imread: ... r"""
def __next__(self) -> typing.Optional[typing.Any]: ... arguments for pickling
def __len__(self) -> builtins.int: ... """
def __repr__(self) -> builtins.str: ... def __copy__(self) -> Imread:
def __str__(self) -> builtins.str: ... r"""
shallow copy of the view
"""
def __deepcopy__(self) -> Imread:
r"""
deep copy of the view (same as shallow copy for this type)
"""
def copy(self) -> Imread:
r"""
create a copy of the view
"""
def __iter__(self) -> Imread:
r"""
iterate over the first axis
"""
def __next__(self) -> typing.Optional[typing.Any]:
r"""
get the next item in the iteration
"""
def __len__(self) -> builtins.int:
r"""
number of elements in the first axis
"""
def __repr__(self) -> builtins.str:
r"""
string representation with a summary of the image
"""
def __str__(self) -> builtins.str:
r"""
the file path as a string
"""
def get_frame( def get_frame(
self, c: builtins.int, z: builtins.int, t: builtins.int self, c: builtins.int, z: builtins.int, t: builtins.int
) -> typing.Any: ) -> typing.Any:
r""" r"""
retrieve a single frame at czt, sliced accordingly retrieve a single frame at czt, sliced accordingly
""" """
def flatten(self) -> typing.Any: ... def flatten(self) -> typing.Any:
def to_bytes(self) -> builtins.list[builtins.int]: ... r"""
def tobytes(self) -> builtins.list[builtins.int]: ... flatten the view into a 1D numpy array
"""
def to_bytes(self) -> builtins.list[builtins.int]:
r"""
convert the view to bytes
"""
def tobytes(self) -> builtins.list[builtins.int]:
r"""
convert the view to bytes (alias for to_bytes)
"""
def get_ax(self, axis: int | str) -> builtins.int: def get_ax(self, axis: int | str) -> builtins.int:
r""" r"""
find the position of an axis find the position of an axis
@@ -259,18 +457,32 @@ class Imread:
r""" r"""
collect data into a numpy array collect data into a numpy array
""" """
def exposure_time( def exposure_time(self, channel: builtins.int) -> typing.Optional[builtins.float]:
self, channel: builtins.int r"""
) -> typing.Optional[builtins.float]: ... the exposure time for a given channel in seconds
def binning(self, channel: builtins.int) -> typing.Optional[builtins.int]: ... """
def binning(self, channel: builtins.int) -> typing.Optional[builtins.int]:
r"""
the binning for a given channel
"""
def laser_wavelengths( def laser_wavelengths(
self, channel: builtins.int self, channel: builtins.int
) -> typing.Optional[builtins.float]: ... ) -> typing.Optional[builtins.float]:
def laser_power(self, channel: builtins.int) -> typing.Optional[builtins.float]: ... r"""
def filter_set_name( the laser wavelength for a given channel in nanometers
self, channel: builtins.int """
) -> typing.Optional[builtins.str]: ... def laser_power(self, channel: builtins.int) -> typing.Optional[builtins.float]:
def gain(self, channel: builtins.int) -> typing.Optional[builtins.float]: ... r"""
the laser power for a given channel as a fraction
"""
def filter_set_name(self, channel: builtins.int) -> typing.Optional[builtins.str]:
r"""
the name of the filter set for a given channel
"""
def gain(self, channel: builtins.int) -> typing.Optional[builtins.float]:
r"""
the detector gain for a given channel
"""
def summary(self) -> builtins.str: def summary(self) -> builtins.str:
r""" r"""
gives a helpful summary of the recorded experiment gives a helpful summary of the recorded experiment
@@ -297,7 +509,10 @@ class Imread:
colors: typing.Optional[typing.Sequence[builtins.str]] = None, colors: typing.Optional[typing.Sequence[builtins.str]] = None,
overwrite: builtins.bool = False, overwrite: builtins.bool = False,
bar: builtins.bool = True, bar: builtins.bool = True,
) -> None: ... ) -> None:
r"""
save the view as a TIFF file
"""
def save_as_movie( def save_as_movie(
self, self,
file: builtins.str | os.PathLike | pathlib.Path, file: builtins.str | os.PathLike | pathlib.Path,
@@ -308,7 +523,10 @@ class Imread:
overwrite: builtins.bool = False, overwrite: builtins.bool = False,
register: builtins.bool = False, register: builtins.bool = False,
no_scaling: builtins.bool = False, no_scaling: builtins.bool = False,
) -> None: ... ) -> None:
r"""
save the view as a movie file (MP4)
"""
def set_cache_size(self, size: builtins.int) -> None: def set_cache_size(self, size: builtins.int) -> None:
r""" r"""
backwards compatibility backwards compatibility
@@ -328,7 +546,7 @@ class Imread:
def max( def max(
self, self,
axis: int | str = None, axis: int | str = None,
dtype: numpy.typing.DTypeLike = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None,
out: typing.Any = None, out: typing.Any = None,
keepdims: bool = False, keepdims: bool = False,
initial: int | float = None, initial: int | float = None,
@@ -340,7 +558,7 @@ class Imread:
def min( def min(
self, self,
axis: int | str = None, axis: int | str = None,
dtype: numpy.typing.DTypeLike = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None,
out: typing.Any = None, out: typing.Any = None,
keepdims: bool = False, keepdims: bool = False,
initial: int | float = None, initial: int | float = None,
@@ -352,7 +570,7 @@ class Imread:
def mean( def mean(
self, self,
axis: int | str = None, axis: int | str = None,
dtype: numpy.typing.DTypeLike = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None,
out: typing.Any = None, out: typing.Any = None,
keepdims: bool = False, keepdims: bool = False,
) -> Imread | numpy.typing.NDArray | int | float: ) -> Imread | numpy.typing.NDArray | int | float:
@@ -362,7 +580,7 @@ class Imread:
def sum( def sum(
self, self,
axis: int | str = None, axis: int | str = None,
dtype: numpy.typing.DTypeLike = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None,
out: typing.Any = None, out: typing.Any = None,
keepdims: bool = False, keepdims: bool = False,
initial: int | float = None, initial: int | float = None,
@@ -373,18 +591,39 @@ class Imread:
""" """
class Shape: class Shape:
r"""
represents the shape of an image with named dimensions (c, z, t, y, x)
"""
@property @property
def c(self) -> builtins.int: ... def c(self) -> builtins.int:
r"""
the number of channels
"""
@property @property
def z(self) -> builtins.int: ... def z(self) -> builtins.int:
r"""
the number of z slices
"""
@property @property
def t(self) -> builtins.int: ... def t(self) -> builtins.int:
r"""
the number of time points
"""
@property @property
def y(self) -> builtins.int: ... def y(self) -> builtins.int:
r"""
the number of pixels along y
"""
@property @property
def x(self) -> builtins.int: ... def x(self) -> builtins.int:
r"""
the number of pixels along x
"""
@property @property
def axes(self) -> builtins.str: ... def axes(self) -> builtins.str:
r"""
the axis order as a string (e.g., "CZTYX")
"""
def __new__( def __new__(
cls, cls,
order: builtins.str, order: builtins.str,
@@ -393,9 +632,18 @@ class Shape:
t: builtins.int = 1, t: builtins.int = 1,
y: builtins.int = 1, y: builtins.int = 1,
x: builtins.int = 1, x: builtins.int = 1,
) -> Shape: ... ) -> Shape:
def __str__(self) -> builtins.str: ... r"""
def __repr__(self) -> builtins.str: ... create a new shape with the given dimensions
"""
def __str__(self) -> builtins.str:
r"""
string representation
"""
def __repr__(self) -> builtins.str:
r"""
detailed representation for debugging
"""
def __getnewargs__( def __getnewargs__(
self, self,
) -> tuple[ ) -> tuple[
@@ -405,12 +653,24 @@ class Shape:
builtins.int, builtins.int,
builtins.int, builtins.int,
builtins.int, builtins.int,
]: ... ]:
r"""
arguments for pickling
"""
def __getitem__( def __getitem__(
self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int] self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int]
) -> typing.Optional[int | list[int]]: ... ) -> typing.Optional[int | list[int]]:
def __len__(self) -> builtins.int: ... r"""
def to_list(self) -> builtins.list[builtins.int]: ... get dimension size by index or axis name
"""
def __len__(self) -> builtins.int:
r"""
number of dimensions in the shape
"""
def to_list(self) -> builtins.list[builtins.int]:
r"""
convert shape to a list of dimension sizes in order
"""
def batch_to_tiff( def batch_to_tiff(
files_in: typing.Sequence[builtins.str | os.PathLike | pathlib.Path], files_in: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
@@ -422,5 +682,12 @@ def batch_to_tiff(
overwrite: builtins.bool = False, overwrite: builtins.bool = False,
bar: builtins.bool = True, bar: builtins.bool = True,
message: typing.Optional[builtins.str] = None, message: typing.Optional[builtins.str] = None,
) -> None: ... ) -> None:
def main() -> None: ... r"""
batch convert multiple image files to TIFF format
"""
def main() -> None:
r"""
main entry point for the command-line interface
"""
+22
View File
@@ -32,12 +32,18 @@ pub trait Ax {
)] )]
#[strum(ascii_case_insensitive)] #[strum(ascii_case_insensitive)]
pub enum Axis { pub enum Axis {
/// channel
C, C,
/// z slice
Z, Z,
/// time
T, T,
/// y coordinate
Y, Y,
/// x coordinate
X, X,
#[strum(serialize = "N")] #[strum(serialize = "N")]
/// a new axis created by slicing, can be operated on but has no reader data
New, New,
} }
@@ -114,10 +120,15 @@ impl Ax for usize {
Clone, Debug, Serialize, Deserialize, EnumString, AsRefStr, Display, PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize, EnumString, AsRefStr, Display, PartialEq, Eq, Hash,
)] )]
#[strum(ascii_case_insensitive)] #[strum(ascii_case_insensitive)]
/// an operation to reduce an axis
pub enum Operation { pub enum Operation {
/// take the max along the axis
Max, Max,
/// take the min along the axis
Min, Min,
/// take the sum along the axis
Sum, Sum,
/// take the mean along the axis
Mean, Mean,
} }
@@ -229,6 +240,7 @@ impl IntoIterator for &Slice {
} }
} }
/// the size of a view along the axes it contains
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Shape { pub struct Shape {
/// size c (# channels) /// size c (# channels)
@@ -241,6 +253,7 @@ pub struct Shape {
pub y: usize, pub y: usize,
/// size x (horizontal) /// size x (horizontal)
pub x: usize, pub x: usize,
/// the axes that are present in the view, in order
pub order: Vec<Axis>, pub order: Vec<Axis>,
} }
@@ -330,6 +343,7 @@ impl From<Shape> for HashMap<Axis, usize> {
} }
} }
/// iterator over the sizes along the axes of a [`Shape`]
pub struct ShapeIter { pub struct ShapeIter {
shape: Shape, shape: Shape,
index: usize, index: usize,
@@ -344,6 +358,7 @@ impl Iterator for ShapeIter {
} }
} }
/// iterator over the sizes along the axes of a borrowed [`Shape`]
pub struct ShapeIterBorrow<'a> { pub struct ShapeIterBorrow<'a> {
shape: &'a Shape, shape: &'a Shape,
index: usize, index: usize,
@@ -371,6 +386,7 @@ impl IntoIterator for Shape {
} }
impl Shape { impl Shape {
/// create an empty shape with all sizes set to 1 and no axes
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
c: 1, c: 1,
@@ -382,6 +398,7 @@ impl Shape {
} }
} }
/// iterate over the sizes along the axes, in order
pub fn iter(&self) -> ShapeIterBorrow<'_> { pub fn iter(&self) -> ShapeIterBorrow<'_> {
ShapeIterBorrow { ShapeIterBorrow {
shape: self, shape: self,
@@ -389,18 +406,22 @@ impl Shape {
} }
} }
/// the number of axes in the shape
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.order.len() self.order.len()
} }
/// whether the shape has no axes
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.order.is_empty() self.order.is_empty()
} }
/// the sizes along the axes, in order
pub fn to_vec(&self) -> Vec<usize> { pub fn to_vec(&self) -> Vec<usize> {
self.order.iter().map(|axis| self[axis]).collect() self.order.iter().map(|axis| self[axis]).collect()
} }
/// the sizes along the axes as a map from axis to size
pub fn to_hashmap(&self) -> HashMap<Axis, usize> { pub fn to_hashmap(&self) -> HashMap<Axis, usize> {
let mut map = HashMap::new(); let mut map = HashMap::new();
for axis in self.order.iter() { for axis in self.order.iter() {
@@ -409,6 +430,7 @@ impl Shape {
map map
} }
/// set the size of an axis
pub fn set_axis(&mut self, axis: &Axis, value: usize) { pub fn set_axis(&mut self, axis: &Axis, value: usize) {
match axis { match axis {
Axis::C => self.c = value, Axis::C => self.c = value,
+322
View File
@@ -0,0 +1,322 @@
use crate::axes::{Axis, Operation};
use indexmap::{Equivalent, IndexMap};
use ndarray::{ArrayD, SliceInfoElem};
use std::any::Any;
use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use crate::readers::Frame;
/// maximum number of frames held in the cache
const DEFAULT_FRAME_CACHE_SIZE: usize = 128;
/// maximum number of materialized arrays held in the cache
const DEFAULT_ARRAY_CACHE_SIZE: usize = 32;
/// identity of the reader a frame was read from
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ReaderKey {
pub(crate) name: String,
pub(crate) path: PathBuf,
pub(crate) series: usize,
pub(crate) position: usize,
}
/// borrowed view of [`ReaderKey`] for cache lookups without allocation.
/// hashes byte-identically to [`ReaderKey`] (str/String and Path/PathBuf hash
/// the same), so it only matches the same reader identity.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ReaderKeyRef<'a> {
pub(crate) name: &'a str,
pub(crate) path: &'a Path,
pub(crate) series: usize,
pub(crate) position: usize,
}
impl Hash for ReaderKeyRef<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.path.hash(state);
self.series.hash(state);
self.position.hash(state);
}
}
impl Equivalent<ReaderKey> for ReaderKeyRef<'_> {
fn equivalent(&self, key: &ReaderKey) -> bool {
self.name == key.name
&& self.path == key.path.as_path()
&& self.series == key.series
&& self.position == key.position
}
}
impl ReaderKeyRef<'_> {
pub(crate) fn to_owned(&self) -> ReaderKey {
ReaderKey {
name: self.name.to_string(),
path: self.path.to_path_buf(),
series: self.series,
position: self.position,
}
}
}
pub(crate) type FrameKey = (ReaderKey, usize, usize, usize);
/// borrowed view of [`FrameKey`] for cache lookups without allocation.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct FrameKeyRef<'a> {
pub(crate) reader: ReaderKeyRef<'a>,
pub(crate) c: usize,
pub(crate) z: usize,
pub(crate) t: usize,
}
impl Hash for FrameKeyRef<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.reader.hash(state);
self.c.hash(state);
self.z.hash(state);
self.t.hash(state);
}
}
impl Equivalent<FrameKey> for FrameKeyRef<'_> {
fn equivalent(&self, key: &FrameKey) -> bool {
let (rk, c, z, t) = key;
self.reader.equivalent(rk) && self.c == *c && self.z == *z && self.t == *t
}
}
impl FrameKeyRef<'_> {
pub(crate) fn to_owned(&self) -> FrameKey {
(self.reader.to_owned(), self.c, self.z, self.t)
}
}
/// identity of a materialized array in the process-wide cache
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ArrayKey {
pub(crate) reader: ReaderKey,
pub(crate) dtype: &'static str,
pub(crate) slice: Vec<SliceInfoElem>,
pub(crate) axes: Vec<Axis>,
pub(crate) operations: Vec<(Axis, Operation)>,
}
/// borrowed view of [`ArrayKey`] for cache lookups without allocation.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ArrayKeyRef<'a> {
pub(crate) reader: ReaderKeyRef<'a>,
pub(crate) dtype: &'static str,
pub(crate) slice: &'a [SliceInfoElem],
pub(crate) axes: &'a [Axis],
pub(crate) operations: &'a IndexMap<Axis, Operation>,
}
impl Hash for ArrayKeyRef<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.reader.hash(state);
self.dtype.hash(state);
self.slice.hash(state);
self.axes.hash(state);
self.operations.len().hash(state);
for (ax, op) in self.operations.iter() {
ax.hash(state);
op.hash(state);
}
}
}
impl Equivalent<ArrayKey> for ArrayKeyRef<'_> {
fn equivalent(&self, key: &ArrayKey) -> bool {
self.reader.equivalent(&key.reader)
&& self.dtype == key.dtype
&& self.slice == key.slice
&& self.axes == key.axes
&& self.operations.len() == key.operations.len()
&& self
.operations
.iter()
.zip(&key.operations)
.all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op)
}
}
impl ArrayKeyRef<'_> {
pub(crate) fn to_owned(&self) -> ArrayKey {
ArrayKey {
reader: self.reader.to_owned(),
dtype: self.dtype,
slice: self.slice.to_vec(),
axes: self.axes.to_vec(),
operations: self
.operations
.iter()
.map(|(ax, op)| (*ax, op.clone()))
.collect(),
}
}
}
/// process-wide LRU cache of frames, shared between all views and threads
static GLOBAL_FRAME_CACHE: OnceLock<FrameCache> = OnceLock::new();
/// thread-safe LRU cache of frames read from the underlying reader
pub(crate) struct FrameCache {
inner: Mutex<FrameCacheInner>,
}
struct FrameCacheInner {
map: IndexMap<FrameKey, Arc<Frame>>,
capacity: usize,
}
impl Default for FrameCache {
fn default() -> Self {
Self::new(DEFAULT_FRAME_CACHE_SIZE)
}
}
impl FrameCache {
pub(crate) fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(FrameCacheInner {
map: IndexMap::with_capacity(capacity),
capacity,
}),
}
}
pub(crate) fn global() -> &'static FrameCache {
GLOBAL_FRAME_CACHE.get_or_init(FrameCache::default)
}
pub(crate) fn capacity(&self) -> usize {
self.inner.lock().unwrap().capacity
}
pub(crate) fn set_capacity(&self, capacity: usize) {
let mut inner = self.inner.lock().unwrap();
inner.capacity = capacity;
while inner.map.len() > inner.capacity {
inner.map.shift_remove_index(0);
}
}
pub(crate) fn get<Q>(&self, key: &Q) -> Option<Arc<Frame>>
where
Q: ?Sized + Hash + Equivalent<FrameKey>,
{
let mut inner = self.inner.lock().unwrap();
if let Some(idx) = inner.map.get_index_of(key) {
let (key, frame) = inner.map.shift_remove_index(idx).unwrap();
inner.map.insert(key, frame.clone());
Some(frame)
} else {
None
}
}
pub(crate) fn insert(&self, key: FrameKey, frame: Arc<Frame>) {
let mut inner = self.inner.lock().unwrap();
inner.map.insert(key, frame);
while inner.map.len() > inner.capacity {
inner.map.shift_remove_index(0);
}
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.inner.lock().unwrap().map.len()
}
}
impl Debug for FrameCache {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FrameCache").finish_non_exhaustive()
}
}
/// process-wide LRU cache of materialized arrays, shared between all views and threads
static GLOBAL_ARRAY_CACHE: OnceLock<ArrayCache> = OnceLock::new();
/// thread-safe LRU cache of materialized arrays produced by `as_array_dyn`
pub(crate) struct ArrayCache {
inner: Mutex<ArrayCacheInner>,
}
struct ArrayCacheInner {
map: IndexMap<ArrayKey, Arc<dyn Any + Send + Sync>>,
capacity: usize,
}
impl Default for ArrayCache {
fn default() -> Self {
Self::new(DEFAULT_ARRAY_CACHE_SIZE)
}
}
impl ArrayCache {
pub(crate) fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(ArrayCacheInner {
map: IndexMap::with_capacity(capacity),
capacity,
}),
}
}
pub(crate) fn global() -> &'static ArrayCache {
GLOBAL_ARRAY_CACHE.get_or_init(ArrayCache::default)
}
pub(crate) fn capacity(&self) -> usize {
self.inner.lock().unwrap().capacity
}
pub(crate) fn set_capacity(&self, capacity: usize) {
let mut inner = self.inner.lock().unwrap();
inner.capacity = capacity;
while inner.map.len() > inner.capacity {
inner.map.shift_remove_index(0);
}
}
pub(crate) fn get<T, Q>(&self, key: &Q) -> Option<Arc<ArrayD<T>>>
where
Q: ?Sized + Hash + Equivalent<ArrayKey>,
T: Any + Send + Sync,
{
let mut inner = self.inner.lock().unwrap();
if let Some(idx) = inner.map.get_index_of(key) {
let (key, array) = inner.map.shift_remove_index(idx).unwrap();
inner.map.insert(key, array.clone());
array.downcast::<ArrayD<T>>().ok()
} else {
None
}
}
pub(crate) fn insert<T: Any + Send + Sync>(&self, key: ArrayKey, array: ArrayD<T>) {
let mut inner = self.inner.lock().unwrap();
inner.map.insert(key, Arc::new(array));
while inner.map.len() > inner.capacity {
inner.map.shift_remove_index(0);
}
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.inner.lock().unwrap().map.len()
}
}
impl Debug for ArrayCache {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArrayCache").finish_non_exhaustive()
}
}
+3
View File
@@ -3,6 +3,7 @@ use phf::phf_map;
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
/// a static map from color names (like `"red"` or single letters like `"r"`) to hex strings
pub static COLORS: phf::Map<&'static str, &'static str> = phf_map! { pub static COLORS: phf::Map<&'static str, &'static str> = phf_map! {
"b" => "#0000FF", "b" => "#0000FF",
"g" => "#008000", "g" => "#008000",
@@ -162,6 +163,7 @@ pub static COLORS: phf::Map<&'static str, &'static str> = phf_map! {
"yellowgreen" => "#9ACD32", "yellowgreen" => "#9ACD32",
}; };
/// a color with red, green and blue components, parsed from a hex string (`#RRGGBB`) or a name in [`COLORS`]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Color { pub struct Color {
r: u8, r: u8,
@@ -196,6 +198,7 @@ impl Display for Color {
} }
impl Color { impl Color {
/// the color as an RGB vector `[r, g, b]`
pub fn to_rgb(&self) -> Vec<u8> { pub fn to_rgb(&self) -> Vec<u8> {
vec![self.r, self.g, self.b] vec![self.r, self.g, self.b]
} }
+50
View File
@@ -1,121 +1,171 @@
use strum::IntoStaticStr; use strum::IntoStaticStr;
use thiserror::Error; use thiserror::Error;
/// the error type used throughout the crate
#[derive(Debug, Error, IntoStaticStr)] #[derive(Debug, Error, IntoStaticStr)]
pub enum Error { pub enum Error {
/// an io error
#[error(transparent)] #[error(transparent)]
IO(#[from] std::io::Error), IO(#[from] std::io::Error),
/// an ndarray shape error
#[error(transparent)] #[error(transparent)]
Shape(#[from] ndarray::ShapeError), Shape(#[from] ndarray::ShapeError),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
/// an error from the j4rs java bridge
#[error(transparent)] #[error(transparent)]
J4rs(#[from] j4rs::errors::J4RsError), J4rs(#[from] j4rs::errors::J4RsError),
/// an infallible conversion
#[error(transparent)] #[error(transparent)]
Infallible(#[from] std::convert::Infallible), Infallible(#[from] std::convert::Infallible),
/// an integer parse error
#[error(transparent)] #[error(transparent)]
ParseIntError(#[from] std::num::ParseIntError), ParseIntError(#[from] std::num::ParseIntError),
/// an ome metadata error
#[error(transparent)] #[error(transparent)]
Ome(#[from] ome_metadata::error::Error), Ome(#[from] ome_metadata::error::Error),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
/// an error while downloading (e.g. the bioformats jar)
#[error(transparent)] #[error(transparent)]
Downloader(#[from] downloader::Error), Downloader(#[from] downloader::Error),
/// an error parsing an enum string with strum
#[error(transparent)] #[error(transparent)]
Strum(#[from] strum::ParseError), Strum(#[from] strum::ParseError),
#[cfg(feature = "tiffwrite")] #[cfg(feature = "tiffwrite")]
/// an indicatif progress bar template error
#[error(transparent)] #[error(transparent)]
TemplateError(#[from] indicatif::style::TemplateError), TemplateError(#[from] indicatif::style::TemplateError),
#[cfg(feature = "tiffwrite")] #[cfg(feature = "tiffwrite")]
/// an error from the tiffwrite crate
#[error(transparent)] #[error(transparent)]
TiffWrite(#[from] tiffwrite::error::Error), TiffWrite(#[from] tiffwrite::error::Error),
#[cfg(feature = "tiffseq")] #[cfg(feature = "tiffseq")]
/// a yaml (de)serialization error
#[error(transparent)] #[error(transparent)]
SerdeYaml(#[from] serde_yaml::Error), SerdeYaml(#[from] serde_yaml::Error),
#[cfg(any(feature = "tiffseq", feature = "tiff"))] #[cfg(any(feature = "tiffseq", feature = "tiff"))]
/// an error from the tiff crate
#[error(transparent)] #[error(transparent)]
Tiff(#[from] tiff::TiffError), Tiff(#[from] tiff::TiffError),
#[cfg(feature = "python")] #[cfg(feature = "python")]
/// a postcard (de)serialization error
#[error(transparent)] #[error(transparent)]
PostCard(#[from] postcard::Error), PostCard(#[from] postcard::Error),
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
/// an error from the libczi binding
#[error(transparent)] #[error(transparent)]
LibCzi(#[from] libczirw_sys::error::Error), LibCzi(#[from] libczirw_sys::error::Error),
/// a regex error
#[error(transparent)] #[error(transparent)]
RegexError(#[from] regex::Error), RegexError(#[from] regex::Error),
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
/// an xmltree error
#[error(transparent)] #[error(transparent)]
XmlTree(#[from] xmltree::Error), XmlTree(#[from] xmltree::Error),
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
/// an xmltree parse error
#[error(transparent)] #[error(transparent)]
XmlTreeParse(#[from] xmltree::ParseError), XmlTreeParse(#[from] xmltree::ParseError),
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
/// a czi-specific error
#[error(transparent)] #[error(transparent)]
Czi(#[from] crate::readers::czi::CziError), Czi(#[from] crate::readers::czi::CziError),
#[cfg(feature = "movie")] #[cfg(feature = "movie")]
/// an error joining a tokio task
#[error(transparent)] #[error(transparent)]
TokioJoin(#[from] tokio::task::JoinError), TokioJoin(#[from] tokio::task::JoinError),
#[cfg(feature = "bioformats_rust")] #[cfg(feature = "bioformats_rust")]
/// an error from the bioformats rust crate
#[error(transparent)] #[error(transparent)]
BioFormats(#[from] bioformats::error::BioFormatsError), BioFormats(#[from] bioformats::error::BioFormatsError),
/// the axis string could not be parsed
#[error("invalid axis: {0}")] #[error("invalid axis: {0}")]
InvalidAxis(String), InvalidAxis(String),
/// the axis was not found in the axes
#[error("axis {0} not found in axes {1}")] #[error("axis {0} not found in axes {1}")]
AxisNotFound(String, String), AxisNotFound(String, String),
/// a conversion error
#[error("conversion error: {0}")] #[error("conversion error: {0}")]
TryInto(String), TryInto(String),
/// the target file already exists
#[error("file already exists {0}")] #[error("file already exists {0}")]
FileAlreadyExists(String), FileAlreadyExists(String),
/// could not download ffmpeg
#[error("could not download ffmpeg: {0}")] #[error("could not download ffmpeg: {0}")]
FfmpegDownload(String), FfmpegDownload(String),
/// an ffmpeg error
#[error("FFmpeg error: {0}")] #[error("FFmpeg error: {0}")]
Ffmpeg(String), Ffmpeg(String),
/// the index is out of bounds
#[error("index {0} out of bounds {1}")] #[error("index {0} out of bounds {1}")]
OutOfBounds(isize, isize), OutOfBounds(isize, isize),
/// the axis was not included in the view
#[error("axis {0} has length {1}, but was not included")] #[error("axis {0} has length {1}, but was not included")]
OutOfBoundsAxis(String, usize), OutOfBoundsAxis(String, usize),
/// the dimensionality of the data does not match
#[error("dimensionality mismatch: {0} != {0}")] #[error("dimensionality mismatch: {0} != {0}")]
DimensionalityMismatch(usize, usize), DimensionalityMismatch(usize, usize),
/// the axis already has an operation
#[error("axis {0}: {1} is already operated on!")] #[error("axis {0}: {1} is already operated on!")]
AxisAlreadyOperated(usize, String), AxisAlreadyOperated(usize, String),
/// not enough free dimensions
#[error("not enough free dimensions")] #[error("not enough free dimensions")]
NotEnoughFreeDimensions, NotEnoughFreeDimensions,
/// cannot cast a pixel value to the requested type
#[error("cannot cast {0} to {1}")] #[error("cannot cast {0} to {1}")]
Cast(String, String), Cast(String, String),
/// the view is empty
#[error("empty view")] #[error("empty view")]
EmptyView, EmptyView,
/// the color string could not be parsed
#[error("invalid color: {0}")] #[error("invalid color: {0}")]
InvalidColor(String), InvalidColor(String),
/// no image or pixels found in the metadata
#[error("no image or pixels found")] #[error("no image or pixels found")]
NoImageOrPixels, NoImageOrPixels,
/// the attenuation value is invalid
#[error("invalid attenuation value: {0}")] #[error("invalid attenuation value: {0}")]
InvalidAttenuation(String), InvalidAttenuation(String),
/// the file name is invalid
#[error("not a valid file name")] #[error("not a valid file name")]
InvalidFileName, InvalidFileName,
/// the file has no parent directory
#[error("file has no parent")] #[error("file has no parent")]
NoParent, NoParent,
/// the pixel type is unknown
#[error("unknown pixel type {0}")] #[error("unknown pixel type {0}")]
UnknownPixelType(String), UnknownPixelType(String),
/// cannot compute the mean of an empty axis
#[error("no mean")] #[error("no mean")]
NoMean, NoMean,
/// the tiff file lock is poisoned
#[error("tiff is locked")] #[error("tiff is locked")]
TiffLock, TiffLock,
/// this feature is not implemented
#[error("not implemented: {0}")] #[error("not implemented: {0}")]
NotImplemented(String), NotImplemented(String),
/// a string could not be parsed
#[error("cannot parse: {0}")] #[error("cannot parse: {0}")]
Parse(String), Parse(String),
/// cannot convert the libczi pixel type
#[error("cannot convert libczi pixel type: {0}")] #[error("cannot convert libczi pixel type: {0}")]
Conversion(String), Conversion(String),
/// no reader could open the file
#[error("no reader found for {0}, tried: {1}")] #[error("no reader found for {0}, tried: {1}")]
NoReader(String, String), NoReader(String, String),
/// the reader cannot open the file
#[error("reader {0} cannot open file {1} because {2}")] #[error("reader {0} cannot open file {1} because {2}")]
InvalidReader(String, String, String), InvalidReader(String, String, String),
/// the file does not exist
#[error("file does not exist: {0}")] #[error("file does not exist: {0}")]
FileDoesNotExist(String), FileDoesNotExist(String),
/// cannot remove axes that have a size != 1
#[error("cannot remove axes {0}, size {1} != 1")] #[error("cannot remove axes {0}, size {1} != 1")]
SizeMismatch(String, usize), SizeMismatch(String, usize),
} }
impl Error { impl Error {
/// the name of the error variant as a static string
pub fn variant_name(&self) -> &'static str { pub fn variant_name(&self) -> &'static str {
self.into() self.into()
} }
+13
View File
@@ -43,22 +43,34 @@
//! # } //! # }
//! ``` //! ```
/// axis handling: axis enum, slicing and shape
pub mod axes; pub mod axes;
/// process-wide LRU caches for frames and materialized arrays
mod cache;
#[cfg(feature = "python")] #[cfg(feature = "python")]
mod py; mod py;
/// min/max/sum/mean operations along an axis
pub mod stats; pub mod stats;
/// the main data structure: an on-disk image that can be sliced without loading it fully
pub mod view; pub mod view;
/// named colors and color conversion
pub mod colors; pub mod colors;
/// the error type used throughout the crate
pub mod error; pub mod error;
/// ome metadata helpers
pub mod metadata; pub mod metadata;
#[cfg(feature = "movie")] #[cfg(feature = "movie")]
/// saving views as movies
pub mod movie; pub mod movie;
/// readers for the different supported image formats
pub mod readers; pub mod readers;
#[cfg(feature = "tiffwrite")] #[cfg(feature = "tiffwrite")]
/// saving views as tiff files
pub mod tiffwrite; pub mod tiffwrite;
mod utils; mod utils;
/// main entry point for the application
pub mod main { pub mod main {
#[cfg(feature = "tiffwrite")] #[cfg(feature = "tiffwrite")]
use crate::axes::{Axis, Operation}; use crate::axes::{Axis, Operation};
@@ -172,6 +184,7 @@ pub mod main {
} }
} }
/// the command line interface, run `ndbioimage --help` for an overview of the commands
pub fn main(args: Option<Vec<String>>) -> Result<(), Error> { pub fn main(args: Option<Vec<String>>) -> Result<(), Error> {
let cli = if let Some(args) = args { let cli = if let Some(args) = args {
Cli::parse_from(args) Cli::parse_from(args)
+17
View File
@@ -20,10 +20,14 @@ impl Metadata for Ome {
} }
} }
/// helper trait to extract useful information from ome metadata
pub trait Metadata { pub trait Metadata {
/// the instrument used to acquire the image
fn get_instrument(&self) -> Option<&Instrument>; fn get_instrument(&self) -> Option<&Instrument>;
/// the first image in the ome structure
fn get_image(&self) -> Option<&Image>; fn get_image(&self) -> Option<&Image>;
/// the pixels of the image
fn get_pixels(&self) -> Option<&Pixels> { fn get_pixels(&self) -> Option<&Pixels> {
if let Some(image) = self.get_image() { if let Some(image) = self.get_image() {
Some(&image.pixels) Some(&image.pixels)
@@ -32,6 +36,7 @@ pub trait Metadata {
} }
} }
/// the objective used to acquire the image
fn get_objective(&self) -> Option<&Objective> { fn get_objective(&self) -> Option<&Objective> {
let objective_id = self.get_image()?.objective_settings.as_ref()?.id.clone(); let objective_id = self.get_image()?.objective_settings.as_ref()?.id.clone();
self.get_instrument()? self.get_instrument()?
@@ -40,6 +45,7 @@ pub trait Metadata {
.find(|o| o.id == objective_id) .find(|o| o.id == objective_id)
} }
/// the tube lens used to acquire the image
fn get_tube_lens(&self) -> Option<&Objective> { fn get_tube_lens(&self) -> Option<&Objective> {
self.get_instrument()? self.get_instrument()?
.objective .objective
@@ -160,6 +166,7 @@ pub trait Metadata {
) )
} }
/// the binning of the detector for a channel (1, 2, 4 or 8)
fn binning(&self, channel: usize) -> Option<usize> { fn binning(&self, channel: usize) -> Option<usize> {
match self match self
.get_pixels()? .get_pixels()?
@@ -178,6 +185,7 @@ pub trait Metadata {
} }
} }
/// the excitation wavelength of the laser for a channel in nm
fn laser_wavelengths(&self, channel: usize) -> Result<Option<f64>, Error> { fn laser_wavelengths(&self, channel: usize) -> Result<Option<f64>, Error> {
Ok( Ok(
if let Some(pixels) = self.get_pixels() if let Some(pixels) = self.get_pixels()
@@ -195,6 +203,7 @@ pub trait Metadata {
) )
} }
/// the laser power (fraction of the maximum) for a channel
fn laser_powers(&self, channel: usize) -> Result<Option<f64>, Error> { fn laser_powers(&self, channel: usize) -> Result<Option<f64>, Error> {
if let Some(pixels) = self.get_pixels() if let Some(pixels) = self.get_pixels()
&& let Some(channel) = pixels.channel.get(channel) && let Some(channel) = pixels.channel.get(channel)
@@ -211,10 +220,12 @@ pub trait Metadata {
} }
} }
/// the name of the objective
fn objective_name(&self) -> Option<String> { fn objective_name(&self) -> Option<String> {
Some(self.get_objective()?.model.as_ref()?.clone()) Some(self.get_objective()?.model.as_ref()?.clone())
} }
/// the total magnification: objective magnification times tube lens magnification
fn magnification(&self) -> Option<f64> { fn magnification(&self) -> Option<f64> {
Some( Some(
(self.get_objective()?.nominal_magnification? as f64) (self.get_objective()?.nominal_magnification? as f64)
@@ -222,10 +233,12 @@ pub trait Metadata {
) )
} }
/// the name of the tube lens
fn tube_lens_name(&self) -> Option<String> { fn tube_lens_name(&self) -> Option<String> {
self.get_tube_lens()?.model.clone() self.get_tube_lens()?.model.clone()
} }
/// the name of the filter set for a channel
fn filter_set_name(&self, channel: usize) -> Option<String> { fn filter_set_name(&self, channel: usize) -> Option<String> {
let filter_set_id = self let filter_set_id = self
.get_pixels()? .get_pixels()?
@@ -244,6 +257,7 @@ pub trait Metadata {
.clone() .clone()
} }
/// the gain of the detector for a channel
fn gain(&self, channel: usize) -> Option<f64> { fn gain(&self, channel: usize) -> Option<f64> {
self.get_pixels() self.get_pixels()
.and_then(|p| p.channel.get(channel)) .and_then(|p| p.channel.get(channel))
@@ -257,18 +271,21 @@ pub trait Metadata {
}) })
} }
/// whether the image is a z-stack (more than one z slice)
fn is_zstack(&self) -> Result<bool, Error> { fn is_zstack(&self) -> Result<bool, Error> {
self.get_pixels() self.get_pixels()
.map(|p| Ok(p.size_z > 1)) .map(|p| Ok(p.size_z > 1))
.unwrap_or_else(|| Err(Error::NoImageOrPixels)) .unwrap_or_else(|| Err(Error::NoImageOrPixels))
} }
/// whether the image is a time lapse (more than one time point)
fn is_time_lapse(&self) -> Result<bool, Error> { fn is_time_lapse(&self) -> Result<bool, Error> {
self.get_pixels() self.get_pixels()
.map(|p| Ok(p.size_t > 1)) .map(|p| Ok(p.size_t > 1))
.unwrap_or_else(|| Err(Error::NoImageOrPixels)) .unwrap_or_else(|| Err(Error::NoImageOrPixels))
} }
/// a multi-line summary of the most relevant metadata, one field per line
fn summary(&self) -> Result<String, Error> { fn summary(&self) -> Result<String, Error> {
let size_c = if let Some(pixels) = self.get_pixels() { let size_c = if let Some(pixels) = self.get_pixels() {
pixels.channel.len() pixels.channel.len()
+8
View File
@@ -13,6 +13,7 @@ use ordered_float::OrderedFloat;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
/// options for creating a movie from a view
pub struct MovieOptions { pub struct MovieOptions {
velocity: f64, velocity: f64,
brightness: Vec<f64>, brightness: Vec<f64>,
@@ -38,6 +39,7 @@ impl Default for MovieOptions {
} }
impl MovieOptions { impl MovieOptions {
/// create movie options with the given parameters
pub fn new( pub fn new(
velocity: f64, velocity: f64,
brightness: Vec<f64>, brightness: Vec<f64>,
@@ -67,18 +69,22 @@ impl MovieOptions {
}) })
} }
/// set the frames per second of the movie
pub fn set_velocity(&mut self, velocity: f64) { pub fn set_velocity(&mut self, velocity: f64) {
self.velocity = velocity; self.velocity = velocity;
} }
/// set the brightness scale factors for each channel
pub fn set_brightness(&mut self, brightness: Vec<f64>) { pub fn set_brightness(&mut self, brightness: Vec<f64>) {
self.brightness = brightness; self.brightness = brightness;
} }
/// set the display scale (zoom) factor for the movie
pub fn set_scale(&mut self, scale: f64) { pub fn set_scale(&mut self, scale: f64) {
self.scale = scale; self.scale = scale;
} }
/// set the color lookup table for the movie
pub fn set_colors(&mut self, colors: &[String]) -> Result<(), Error> { pub fn set_colors(&mut self, colors: &[String]) -> Result<(), Error> {
let colors = colors let colors = colors
.iter() .iter()
@@ -88,6 +94,7 @@ impl MovieOptions {
Ok(()) Ok(())
} }
/// set whether an existing movie file should be overwritten
pub fn set_overwrite(&mut self, overwrite: bool) { pub fn set_overwrite(&mut self, overwrite: bool) {
self.overwrite = overwrite; self.overwrite = overwrite;
} }
@@ -142,6 +149,7 @@ where
R: Reader, R: Reader,
Self: 'static, Self: 'static,
{ {
/// save the view as a movie file with the given options
pub fn save_as_movie<P>(&self, path: P, options: &MovieOptions) -> Result<(), Error> pub fn save_as_movie<P>(&self, path: P, options: &MovieOptions) -> Result<(), Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
+98 -6
View File
@@ -179,7 +179,7 @@ impl PyView {
override_type(type_repr="str | pathlib.Path | Imread | bytes", imports=("pathlib")) override_type(type_repr="str | pathlib.Path | Imread | bytes", imports=("pathlib"))
)] )]
path: Bound<'py, PyAny>, path: Bound<'py, PyAny>,
#[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing") #[gen_stub(override_type(type_repr = "typing.Optional[numpy.typing.DTypeLike]", imports=("typing", "numpy", "numpy.typing")
))] ))]
dtype: Option<Bound<'py, PyAny>>, dtype: Option<Bound<'py, PyAny>>,
axes: &str, axes: &str,
@@ -229,6 +229,7 @@ impl PyView {
} }
} }
/// get all available positions (series) in the file
#[staticmethod] #[staticmethod]
fn get_positions<'py>( fn get_positions<'py>(
py: Python, py: Python,
@@ -244,16 +245,19 @@ impl PyView {
#[staticmethod] #[staticmethod]
fn kill_vm() {} fn kill_vm() {}
/// the name of the reader used to open the file
#[getter] #[getter]
fn reader_name(&self) -> String { fn reader_name(&self) -> String {
self.view.reader_name().to_string() self.view.reader_name().to_string()
} }
/// reshape the view with a new axis order
#[allow(unused_variables)] #[allow(unused_variables)]
fn reshape<'py>(&self, order: &str, copy: bool) -> PyResult<Bound<'py, PyAny>> { fn reshape<'py>(&self, order: &str, copy: bool) -> PyResult<Bound<'py, PyAny>> {
todo!() todo!()
} }
/// return a new view with transformations applied (channel alignment, drift correction)
#[allow(unused_variables)] #[allow(unused_variables)]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None))] #[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None))]
fn with_transform<'py>( fn with_transform<'py>(
@@ -266,6 +270,7 @@ impl PyView {
todo!() todo!()
} }
/// get the transformation matrix (not yet implemented)
#[getter] #[getter]
fn get_transform(&self) -> PyResult<()> { fn get_transform(&self) -> PyResult<()> {
todo!() todo!()
@@ -533,12 +538,13 @@ impl PyView {
} }
} }
/// convert to a numpy array, optionally with a different dtype
#[allow(unused_variables)] #[allow(unused_variables)]
#[pyo3(signature = (dtype = None, copy = None))] #[pyo3(signature = (dtype = None, copy = None))]
fn __array__<'py>( fn __array__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
#[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing") #[gen_stub(override_type(type_repr = "typing.Optional[numpy.typing.DTypeLike]", imports=("typing", "numpy", "numpy.typing")
))] ))]
dtype: Option<Bound<'py, PyAny>>, dtype: Option<Bound<'py, PyAny>>,
copy: Option<bool>, copy: Option<bool>,
@@ -550,10 +556,12 @@ impl PyView {
} }
} }
/// check if an item is contained in the view (not implemented)
fn __contains__(&self, _item: Bound<PyAny>) -> PyResult<bool> { fn __contains__(&self, _item: Bound<PyAny>) -> PyResult<bool> {
Err(PyNotImplementedError::new_err("contains not implemented")) Err(PyNotImplementedError::new_err("contains not implemented"))
} }
/// element-wise less than comparison
fn __lt__<'py>( fn __lt__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -565,6 +573,7 @@ impl PyView {
np.getattr("less")?.call1((&a, &b)) np.getattr("less")?.call1((&a, &b))
} }
/// element-wise less than or equal comparison
fn __le__<'py>( fn __le__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -576,6 +585,7 @@ impl PyView {
np.getattr("less_equal")?.call1((&a, &b)) np.getattr("less_equal")?.call1((&a, &b))
} }
/// element-wise equality comparison
fn __eq__<'py>( fn __eq__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -587,6 +597,7 @@ impl PyView {
np.getattr("equal")?.call1((&a, &b)) np.getattr("equal")?.call1((&a, &b))
} }
/// element-wise not equal comparison
fn __ne__<'py>( fn __ne__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -598,6 +609,7 @@ impl PyView {
np.getattr("not_equal")?.call1((&a, &b)) np.getattr("not_equal")?.call1((&a, &b))
} }
/// element-wise greater than comparison
fn __gt__<'py>( fn __gt__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -609,6 +621,7 @@ impl PyView {
np.getattr("greater")?.call1((&a, &b)) np.getattr("greater")?.call1((&a, &b))
} }
/// element-wise greater than or equal comparison
fn __ge__<'py>( fn __ge__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -620,6 +633,7 @@ impl PyView {
np.getattr("greater_equal")?.call1((&a, &b)) np.getattr("greater_equal")?.call1((&a, &b))
} }
/// element-wise addition
fn __add__<'py>( fn __add__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -631,6 +645,7 @@ impl PyView {
np.getattr("add")?.call1((&a, &b)) np.getattr("add")?.call1((&a, &b))
} }
/// element-wise addition (reflected)
fn __radd__<'py>( fn __radd__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -642,6 +657,7 @@ impl PyView {
np.getattr("add")?.call1((&a, &b)) np.getattr("add")?.call1((&a, &b))
} }
/// element-wise subtraction
fn __sub__<'py>( fn __sub__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -653,6 +669,7 @@ impl PyView {
np.getattr("subtract")?.call1((&a, &b)) np.getattr("subtract")?.call1((&a, &b))
} }
/// element-wise subtraction (reflected)
fn __rsub__<'py>( fn __rsub__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -664,6 +681,7 @@ impl PyView {
np.getattr("subtract")?.call1((&a, &b)) np.getattr("subtract")?.call1((&a, &b))
} }
/// element-wise multiplication
fn __mul__<'py>( fn __mul__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -675,6 +693,7 @@ impl PyView {
np.getattr("multiply")?.call1((&a, &b)) np.getattr("multiply")?.call1((&a, &b))
} }
/// element-wise multiplication (reflected)
fn __rmul__<'py>( fn __rmul__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -686,6 +705,7 @@ impl PyView {
np.getattr("multiply")?.call1((&a, &b)) np.getattr("multiply")?.call1((&a, &b))
} }
/// element-wise true division
fn __truediv__<'py>( fn __truediv__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -697,6 +717,7 @@ impl PyView {
np.getattr("true_divide")?.call1((&a, &b)) np.getattr("true_divide")?.call1((&a, &b))
} }
/// element-wise true division (reflected)
fn __rtruediv__<'py>( fn __rtruediv__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -708,6 +729,7 @@ impl PyView {
np.getattr("true_divide")?.call1((&a, &b)) np.getattr("true_divide")?.call1((&a, &b))
} }
/// element-wise floor division
fn __floordiv__<'py>( fn __floordiv__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -719,6 +741,7 @@ impl PyView {
np.getattr("floor_divide")?.call1((&a, &b)) np.getattr("floor_divide")?.call1((&a, &b))
} }
/// element-wise floor division (reflected)
fn __rfloordiv__<'py>( fn __rfloordiv__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -730,6 +753,7 @@ impl PyView {
np.getattr("floor_divide")?.call1((&a, &b)) np.getattr("floor_divide")?.call1((&a, &b))
} }
/// element-wise modulo
fn __mod__<'py>( fn __mod__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -741,6 +765,7 @@ impl PyView {
np.getattr("remainder")?.call1((&a, &b)) np.getattr("remainder")?.call1((&a, &b))
} }
/// element-wise modulo (reflected)
fn __rmod__<'py>( fn __rmod__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -788,6 +813,7 @@ impl PyView {
np.getattr("power")?.call1((&a, &b)) np.getattr("power")?.call1((&a, &b))
} }
/// element-wise matrix multiplication
fn __matmul__<'py>( fn __matmul__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -799,6 +825,7 @@ impl PyView {
np.getattr("matmul")?.call1((&a, &b)) np.getattr("matmul")?.call1((&a, &b))
} }
/// element-wise matrix multiplication (reflected)
fn __rmatmul__<'py>( fn __rmatmul__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -810,6 +837,7 @@ impl PyView {
np.getattr("matmul")?.call1((&a, &b)) np.getattr("matmul")?.call1((&a, &b))
} }
/// element-wise bitwise AND
fn __and__<'py>( fn __and__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -821,6 +849,7 @@ impl PyView {
np.getattr("bitwise_and")?.call1((&a, &b)) np.getattr("bitwise_and")?.call1((&a, &b))
} }
/// element-wise bitwise AND (reflected)
fn __rand__<'py>( fn __rand__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -832,6 +861,7 @@ impl PyView {
np.getattr("bitwise_and")?.call1((&a, &b)) np.getattr("bitwise_and")?.call1((&a, &b))
} }
/// element-wise bitwise OR
fn __or__<'py>( fn __or__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -843,6 +873,7 @@ impl PyView {
np.getattr("bitwise_or")?.call1((&a, &b)) np.getattr("bitwise_or")?.call1((&a, &b))
} }
/// element-wise bitwise OR (reflected)
fn __ror__<'py>( fn __ror__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -854,6 +885,7 @@ impl PyView {
np.getattr("bitwise_or")?.call1((&a, &b)) np.getattr("bitwise_or")?.call1((&a, &b))
} }
/// element-wise bitwise XOR
fn __xor__<'py>( fn __xor__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -865,6 +897,7 @@ impl PyView {
np.getattr("bitwise_xor")?.call1((&a, &b)) np.getattr("bitwise_xor")?.call1((&a, &b))
} }
/// element-wise bitwise XOR (reflected)
fn __rxor__<'py>( fn __rxor__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -876,6 +909,7 @@ impl PyView {
np.getattr("bitwise_xor")?.call1((&a, &b)) np.getattr("bitwise_xor")?.call1((&a, &b))
} }
/// element-wise left shift
fn __lshift__<'py>( fn __lshift__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -887,6 +921,7 @@ impl PyView {
np.getattr("left_shift")?.call1((&a, &b)) np.getattr("left_shift")?.call1((&a, &b))
} }
/// element-wise left shift (reflected)
fn __rlshift__<'py>( fn __rlshift__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -898,6 +933,7 @@ impl PyView {
np.getattr("left_shift")?.call1((&a, &b)) np.getattr("left_shift")?.call1((&a, &b))
} }
/// element-wise right shift
fn __rshift__<'py>( fn __rshift__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -909,6 +945,7 @@ impl PyView {
np.getattr("right_shift")?.call1((&a, &b)) np.getattr("right_shift")?.call1((&a, &b))
} }
/// element-wise right shift (reflected)
fn __rrshift__<'py>( fn __rrshift__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -920,34 +957,40 @@ impl PyView {
np.getattr("right_shift")?.call1((&a, &b)) np.getattr("right_shift")?.call1((&a, &b))
} }
/// element-wise negation
fn __neg__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { fn __neg__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let np = PyModule::import(py, "numpy")?; let np = PyModule::import(py, "numpy")?;
let a = self.as_array(py)?; let a = self.as_array(py)?;
np.getattr("negative")?.call1((&a,)) np.getattr("negative")?.call1((&a,))
} }
/// element-wise positive
fn __pos__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { fn __pos__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let np = PyModule::import(py, "numpy")?; let np = PyModule::import(py, "numpy")?;
let a = self.as_array(py)?; let a = self.as_array(py)?;
np.getattr("positive")?.call1((&a,)) np.getattr("positive")?.call1((&a,))
} }
/// element-wise absolute value
fn __abs__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { fn __abs__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let np = PyModule::import(py, "numpy")?; let np = PyModule::import(py, "numpy")?;
let a = self.as_array(py)?; let a = self.as_array(py)?;
np.getattr("absolute")?.call1((&a,)) np.getattr("absolute")?.call1((&a,))
} }
/// element-wise bitwise inversion
fn __invert__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { fn __invert__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let np = PyModule::import(py, "numpy")?; let np = PyModule::import(py, "numpy")?;
let a = self.as_array(py)?; let a = self.as_array(py)?;
np.getattr("invert")?.call1((&a,)) np.getattr("invert")?.call1((&a,))
} }
/// context manager entry
fn __enter__<'py>(slf: PyRef<'py, Self>) -> PyResult<PyRef<'py, Self>> { fn __enter__<'py>(slf: PyRef<'py, Self>) -> PyResult<PyRef<'py, Self>> {
Ok(slf) Ok(slf)
} }
/// context manager exit
#[allow(unused_variables)] #[allow(unused_variables)]
#[pyo3(signature = (exc_type=None, exc_val=None, exc_tb=None))] #[pyo3(signature = (exc_type=None, exc_val=None, exc_tb=None))]
fn __exit__( fn __exit__(
@@ -959,10 +1002,12 @@ impl PyView {
self.close() self.close()
} }
/// arguments for pickling
pub(crate) fn __getnewargs__(&self) -> PyResult<(Vec<u8>,)> { pub(crate) fn __getnewargs__(&self) -> PyResult<(Vec<u8>,)> {
Ok((to_stdvec(self).map_err(Error::from)?,)) Ok((to_stdvec(self).map_err(Error::from)?,))
} }
/// shallow copy of the view
fn __copy__(&self) -> Self { fn __copy__(&self) -> Self {
Self { Self {
view: self.view.clone(), view: self.view.clone(),
@@ -972,6 +1017,7 @@ impl PyView {
} }
} }
/// deep copy of the view (same as shallow copy for this type)
fn __deepcopy__(&self) -> Self { fn __deepcopy__(&self) -> Self {
Self { Self {
view: self.view.clone(), view: self.view.clone(),
@@ -981,6 +1027,7 @@ impl PyView {
} }
} }
/// create a copy of the view
fn copy(&self) -> Self { fn copy(&self) -> Self {
Self { Self {
view: self.view.clone(), view: self.view.clone(),
@@ -990,6 +1037,7 @@ impl PyView {
} }
} }
/// iterate over the first axis
fn __iter__(&self) -> Self { fn __iter__(&self) -> Self {
Self { Self {
view: self.view.clone(), view: self.view.clone(),
@@ -999,6 +1047,7 @@ impl PyView {
} }
} }
/// get the next item in the iteration
fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyAny>>> { fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyAny>>> {
let shape = self.view.shape(); let shape = self.view.shape();
if shape.is_empty() || (self.index == shape[0]) { if shape.is_empty() || (self.index == shape[0]) {
@@ -1030,14 +1079,17 @@ impl PyView {
} }
} }
/// number of elements in the first axis
fn __len__(&self) -> PyResult<usize> { fn __len__(&self) -> PyResult<usize> {
Ok(self.view.len()) Ok(self.view.len())
} }
/// string representation with a summary of the image
fn __repr__(&self) -> PyResult<String> { fn __repr__(&self) -> PyResult<String> {
Ok(self.view.summary()?) Ok(self.view.summary()?)
} }
/// the file path as a string
fn __str__(&self) -> PyResult<String> { fn __str__(&self) -> PyResult<String> {
Ok(self.view.path().display().to_string()) Ok(self.view.path().display().to_string())
} }
@@ -1119,6 +1171,7 @@ impl PyView {
}) })
} }
/// flatten the view into a 1D numpy array
fn flatten<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> { fn flatten<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
Ok(match self.dtype { Ok(match self.dtype {
PixelType::I8 => self.view.flatten::<i8>()?.into_pyarray(py).into_any(), PixelType::I8 => self.view.flatten::<i8>()?.into_pyarray(py).into_any(),
@@ -1137,6 +1190,7 @@ impl PyView {
}) })
} }
/// convert the view to bytes
fn to_bytes(&self) -> PyResult<Vec<u8>> { fn to_bytes(&self) -> PyResult<Vec<u8>> {
Ok(match self.dtype { Ok(match self.dtype {
PixelType::I8 => self.view.to_bytes::<i8>()?, PixelType::I8 => self.view.to_bytes::<i8>()?,
@@ -1155,6 +1209,7 @@ impl PyView {
}) })
} }
/// convert the view to bytes (alias for to_bytes)
fn tobytes(&self) -> PyResult<Vec<u8>> { fn tobytes(&self) -> PyResult<Vec<u8>> {
self.to_bytes() self.to_bytes()
} }
@@ -1192,6 +1247,7 @@ impl PyView {
} }
} }
/// the current slice applied to the view
#[getter] #[getter]
fn slice(&self) -> PyResult<Vec<String>> { fn slice(&self) -> PyResult<Vec<String>> {
Ok(self Ok(self
@@ -1283,6 +1339,7 @@ impl PyView {
}) })
} }
/// transposed view (alias for transpose(None))
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[getter] #[getter]
fn T(&self) -> PyResult<PyView> { fn T(&self) -> PyResult<PyView> {
@@ -1308,6 +1365,7 @@ impl PyView {
}) })
} }
/// the numpy dtype of the view
#[gen_stub(override_return_type(type_repr = "numpy.dtype", imports=("numpy")))] #[gen_stub(override_return_type(type_repr = "numpy.dtype", imports=("numpy")))]
#[getter] #[getter]
fn get_dtype<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArrayDescr>> { fn get_dtype<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArrayDescr>> {
@@ -1332,6 +1390,7 @@ impl PyView {
} }
} }
/// set the dtype of the view
#[gen_stub(skip)] #[gen_stub(skip)]
#[setter] #[setter]
fn set_dtype(&mut self, py: Python, dtype: Bound<'_, PyAny>) -> PyResult<()> { fn set_dtype(&mut self, py: Python, dtype: Bound<'_, PyAny>) -> PyResult<()> {
@@ -1451,6 +1510,7 @@ impl PyView {
} }
} }
/// get the mean overall or along a given axis
#[gen_stub(skip)] #[gen_stub(skip)]
#[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, *, r#where=true), text_signature = "axis: str | int" #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, *, r#where=true), text_signature = "axis: str | int"
)] )]
@@ -1553,6 +1613,7 @@ impl PyView {
} }
} }
/// whether the view contains a z-stack (more than one z slice)
#[getter] #[getter]
fn z_stack(&self) -> PyResult<bool> { fn z_stack(&self) -> PyResult<bool> {
if let Some(s) = self.view.size_ax(Axis::Z) { if let Some(s) = self.view.size_ax(Axis::Z) {
@@ -1572,6 +1633,7 @@ impl PyView {
} }
} }
/// whether the view contains a time series (more than one time point)
#[getter] #[getter]
fn time_series(&self) -> PyResult<bool> { fn time_series(&self) -> PyResult<bool> {
if let Some(s) = self.view.size_ax(Axis::T) { if let Some(s) = self.view.size_ax(Axis::T) {
@@ -1591,6 +1653,7 @@ impl PyView {
} }
} }
/// the pixel size in micrometers
#[getter] #[getter]
fn pixel_size(&self) -> PyResult<Option<f64>> { fn pixel_size(&self) -> PyResult<Option<f64>> {
Ok(self.ome.pixel_size()?) Ok(self.ome.pixel_size()?)
@@ -1608,11 +1671,13 @@ impl PyView {
Ok(self.ome.delta_z()?.map(|p| p / 1000.)) Ok(self.ome.delta_z()?.map(|p| p / 1000.))
} }
/// the z-step size in micrometers
#[getter] #[getter]
fn delta_z(&self) -> PyResult<Option<f64>> { fn delta_z(&self) -> PyResult<Option<f64>> {
Ok(self.ome.delta_z()?) Ok(self.ome.delta_z()?)
} }
/// the time interval between frames in seconds
#[getter] #[getter]
fn time_interval(&self) -> PyResult<Option<f64>> { fn time_interval(&self) -> PyResult<Option<f64>> {
Ok(self.ome.time_interval()?) Ok(self.ome.time_interval()?)
@@ -1624,6 +1689,7 @@ impl PyView {
Ok(self.ome.time_interval()?) Ok(self.ome.time_interval()?)
} }
/// the exposure time for a given channel in seconds
fn exposure_time(&self, channel: usize) -> PyResult<Option<f64>> { fn exposure_time(&self, channel: usize) -> PyResult<Option<f64>> {
Ok(self.ome.exposure_time(channel)?) Ok(self.ome.exposure_time(channel)?)
} }
@@ -1636,37 +1702,45 @@ impl PyView {
.collect::<Result<Vec<_>, Error>>()?) .collect::<Result<Vec<_>, Error>>()?)
} }
/// the binning for a given channel
fn binning(&self, channel: usize) -> Option<usize> { fn binning(&self, channel: usize) -> Option<usize> {
self.ome.binning(channel) self.ome.binning(channel)
} }
/// the laser wavelength for a given channel in nanometers
fn laser_wavelengths(&self, channel: usize) -> PyResult<Option<f64>> { fn laser_wavelengths(&self, channel: usize) -> PyResult<Option<f64>> {
Ok(self.ome.laser_wavelengths(channel)?) Ok(self.ome.laser_wavelengths(channel)?)
} }
/// the laser power for a given channel as a fraction
fn laser_power(&self, channel: usize) -> PyResult<Option<f64>> { fn laser_power(&self, channel: usize) -> PyResult<Option<f64>> {
Ok(self.ome.laser_powers(channel)?) Ok(self.ome.laser_powers(channel)?)
} }
/// the name of the objective
#[getter] #[getter]
fn objective_name(&self) -> Option<String> { fn objective_name(&self) -> Option<String> {
self.ome.objective_name() self.ome.objective_name()
} }
/// the total magnification (objective × tube lens)
#[getter] #[getter]
fn magnification(&self) -> Option<f64> { fn magnification(&self) -> Option<f64> {
self.ome.magnification() self.ome.magnification()
} }
/// the name of the tube lens
#[getter] #[getter]
fn tube_lens_name(&self) -> Option<String> { fn tube_lens_name(&self) -> Option<String> {
self.ome.tube_lens_name() self.ome.tube_lens_name()
} }
/// the name of the filter set for a given channel
fn filter_set_name(&self, channel: usize) -> Option<String> { fn filter_set_name(&self, channel: usize) -> Option<String> {
self.ome.filter_set_name(channel) self.ome.filter_set_name(channel)
} }
/// the detector gain for a given channel
fn gain(&self, channel: usize) -> Option<f64> { fn gain(&self, channel: usize) -> Option<f64> {
self.ome.gain(channel) self.ome.gain(channel)
} }
@@ -1730,6 +1804,7 @@ impl PyView {
)?) )?)
} }
/// save the view as a TIFF file
#[cfg(feature = "tiffwrite")] #[cfg(feature = "tiffwrite")]
#[pyo3(signature = (file, colors = None, overwrite = false, bar = true))] #[pyo3(signature = (file, colors = None, overwrite = false, bar = true))]
fn save_as_tiff( fn save_as_tiff(
@@ -1770,6 +1845,7 @@ impl PyView {
Ok(()) Ok(())
} }
/// save the view as a movie file (MP4)
#[cfg(feature = "movie")] #[cfg(feature = "movie")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[pyo3(signature = (file, speed = 1.0, brightness = None, scale = 1.0, colors = None, overwrite = false, register = false, no_scaling = false) #[pyo3(signature = (file, speed = 1.0, brightness = None, scale = 1.0, colors = None, overwrite = false, register = false, no_scaling = false)
@@ -1813,21 +1889,22 @@ submit! {
def __pow__(other: Imread | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ... def __pow__(other: Imread | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ...
def __rpow__(other: Imread | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ... def __rpow__(other: Imread | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ...
def max(axis: int | str = None, dtype: numpy.typing.DTypeLike = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: def max(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float:
""" Return the maximum along a given axis. Arguments beyond axis are not implemented """ """ Return the maximum along a given axis. Arguments beyond axis are not implemented """
def min(axis: int | str = None, dtype: numpy.typing.DTypeLike = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: def min(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float:
""" Return the minimum along a given axis. Arguments beyond axis are not implemented """ """ Return the minimum along a given axis. Arguments beyond axis are not implemented """
def mean(axis: int | str = None, dtype: numpy.typing.DTypeLike = None, out: typing.Any = None, keepdims: bool = False) -> Imread | numpy.typing.NDArray | int | float: def mean(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False) -> Imread | numpy.typing.NDArray | int | float:
""" Return the mean along a given axis. Arguments beyond axis are not implemented """ """ Return the mean along a given axis. Arguments beyond axis are not implemented """
def sum(axis: int | str = None, dtype: numpy.typing.DTypeLike = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float: def sum(axis: int | str = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None, out: typing.Any = None, keepdims: bool = False, initial: int | float = None, where: bool = True) -> Imread | numpy.typing.NDArray | int | float:
""" Return the sum along a given axis. Arguments beyond axis are not implemented """ """ Return the sum along a given axis. Arguments beyond axis are not implemented """
"# "#
} }
} }
/// batch convert multiple image files to TIFF format
#[cfg(feature = "tiffwrite")] #[cfg(feature = "tiffwrite")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")] #[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")]
@@ -1852,6 +1929,7 @@ fn batch_to_tiff(
Ok(()) Ok(())
} }
/// represents the shape of an image with named dimensions (c, z, t, y, x)
#[gen_stub_pyclass] #[gen_stub_pyclass]
#[pyclass( #[pyclass(
subclass, subclass,
@@ -1868,6 +1946,7 @@ struct PyShape {
#[gen_stub_pymethods] #[gen_stub_pymethods]
#[pymethods] #[pymethods]
impl PyShape { impl PyShape {
/// create a new shape with the given dimensions
#[new] #[new]
#[pyo3(signature = (order, c = 1, z = 1, t = 1, y = 1, x = 1))] #[pyo3(signature = (order, c = 1, z = 1, t = 1, y = 1, x = 1))]
fn new(order: String, c: usize, z: usize, t: usize, y: usize, x: usize) -> PyResult<Self> { fn new(order: String, c: usize, z: usize, t: usize, y: usize, x: usize) -> PyResult<Self> {
@@ -1887,35 +1966,42 @@ impl PyShape {
}) })
} }
/// the number of channels
#[getter] #[getter]
fn get_c(&self) -> usize { fn get_c(&self) -> usize {
self.inner.c self.inner.c
} }
/// the number of z slices
#[getter] #[getter]
fn get_z(&self) -> usize { fn get_z(&self) -> usize {
self.inner.z self.inner.z
} }
/// the number of time points
#[getter] #[getter]
fn get_t(&self) -> usize { fn get_t(&self) -> usize {
self.inner.t self.inner.t
} }
/// the number of pixels along y
#[getter] #[getter]
fn get_y(&self) -> usize { fn get_y(&self) -> usize {
self.inner.y self.inner.y
} }
/// the number of pixels along x
#[getter] #[getter]
fn get_x(&self) -> usize { fn get_x(&self) -> usize {
self.inner.x self.inner.x
} }
/// string representation
fn __str__(&self) -> String { fn __str__(&self) -> String {
format!("{}", self.inner) format!("{}", self.inner)
} }
/// detailed representation for debugging
fn __repr__(&self) -> String { fn __repr__(&self) -> String {
format!( format!(
"Shape({}, {}, {}, {}, {})", "Shape({}, {}, {}, {}, {})",
@@ -1923,6 +2009,7 @@ impl PyShape {
) )
} }
/// arguments for pickling
fn __getnewargs__(&self) -> (String, usize, usize, usize, usize, usize) { fn __getnewargs__(&self) -> (String, usize, usize, usize, usize, usize) {
( (
self.inner self.inner
@@ -1939,6 +2026,7 @@ impl PyShape {
) )
} }
/// get dimension size by index or axis name
#[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing") #[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing")
))] ))]
fn __getitem__<'py>( fn __getitem__<'py>(
@@ -2034,10 +2122,12 @@ impl PyShape {
} }
} }
/// number of dimensions in the shape
fn __len__(&self) -> usize { fn __len__(&self) -> usize {
self.inner.order.len() self.inner.order.len()
} }
/// convert shape to a list of dimension sizes in order
fn to_list(&self) -> Vec<usize> { fn to_list(&self) -> Vec<usize> {
vec![ vec![
self.inner.c, self.inner.c,
@@ -2048,6 +2138,7 @@ impl PyShape {
] ]
} }
/// the axis order as a string (e.g., "CZTYX")
#[getter] #[getter]
fn axes(&self) -> String { fn axes(&self) -> String {
self.inner self.inner
@@ -2081,6 +2172,7 @@ pub fn generate_stub(dest_path: String) -> PyResult<()> {
.generate()?) .generate()?)
} }
/// main entry point for the command-line interface
#[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")] #[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")]
#[pyfunction] #[pyfunction]
fn main() -> PyResult<()> { fn main() -> PyResult<()> {
+65 -4
View File
@@ -14,33 +14,45 @@ use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use std::sync::LazyLock; use std::sync::LazyLock;
/// czi file reader
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
pub mod czi; pub mod czi;
/// bioformats reader (pure rust)
#[cfg(feature = "bioformats_rust")] #[cfg(feature = "bioformats_rust")]
pub mod bioformats_rust; pub mod bioformats_rust;
/// bioformats reader (java bindings)
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
pub mod bioformats_java; pub mod bioformats_java;
/// tiff sequence reader
#[cfg(feature = "tiffseq")] #[cfg(feature = "tiffseq")]
pub mod tiffseq; pub mod tiffseq;
/// single tiff file reader
#[cfg(feature = "tiff")] #[cfg(feature = "tiff")]
pub mod tiff; pub mod tiff;
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([CZTSP])\D+(\d+)$").unwrap()); static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([CZTSP])\D+(\d+)$").unwrap());
/// dimensions of the image data
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct Dimensions { pub struct Dimensions {
/// number of channels
pub c: Option<usize>, pub c: Option<usize>,
/// number of z slices
pub z: Option<usize>, pub z: Option<usize>,
/// number of time points
pub t: Option<usize>, pub t: Option<usize>,
/// series index
pub s: Option<usize>, pub s: Option<usize>,
/// position index
pub p: Option<usize>, pub p: Option<usize>,
} }
impl Dimensions { impl Dimensions {
/// create dimensions with the given series and position
pub fn new(series: usize, position: usize) -> Self { pub fn new(series: usize, position: usize) -> Self {
Self { Self {
c: None, c: None,
@@ -51,6 +63,7 @@ impl Dimensions {
} }
} }
/// parse a path and extract dimensions from the directory structure
pub fn parse_path<P>(path: P) -> Result<(PathBuf, Self), Error> pub fn parse_path<P>(path: P) -> Result<(PathBuf, Self), Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
@@ -86,26 +99,40 @@ impl Dimensions {
} }
} }
/// Pixel types (u)int(8/16/32) or float(32/64), (u/i)(64/128) are not included in bioformats /// pixel type enum
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub enum PixelType { pub enum PixelType {
/// signed 8-bit integer
I8, I8,
/// unsigned 8-bit integer
U8, U8,
/// signed 16-bit integer
I16, I16,
/// unsigned 16-bit integer
U16, U16,
/// signed 32-bit integer
I32, I32,
/// unsigned 32-bit integer
U32, U32,
/// 32-bit float
F32, F32,
/// 64-bit float
F64, F64,
/// signed 64-bit integer
I64, I64,
/// unsigned 64-bit integer
U64, U64,
/// signed 128-bit integer
I128, I128,
/// unsigned 128-bit integer
U128, U128,
/// 128-bit float (emulated)
F128, F128,
} }
impl PixelType { impl PixelType {
/// number of bytes per pixel for this type
pub fn bytes_per_pixel(&self) -> usize { pub fn bytes_per_pixel(&self) -> usize {
match self { match self {
PixelType::I8 | PixelType::U8 => 1, PixelType::I8 | PixelType::U8 => 1,
@@ -117,37 +144,55 @@ impl PixelType {
} }
} }
/// Struct containing frame data in one of eight pixel types. Cast to `Array2<T>` using try_into. /// array data with a specific pixel type
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ArrayT<D: Dimension> { pub enum ArrayT<D: Dimension> {
/// signed 8-bit integer array
I8(Array<i8, D>), I8(Array<i8, D>),
/// unsigned 8-bit integer array
U8(Array<u8, D>), U8(Array<u8, D>),
/// signed 16-bit integer array
I16(Array<i16, D>), I16(Array<i16, D>),
/// unsigned 16-bit integer array
U16(Array<u16, D>), U16(Array<u16, D>),
/// signed 32-bit integer array
I32(Array<i32, D>), I32(Array<i32, D>),
/// unsigned 32-bit integer array
U32(Array<u32, D>), U32(Array<u32, D>),
/// 32-bit float array
F32(Array<f32, D>), F32(Array<f32, D>),
/// 64-bit float array
F64(Array<f64, D>), F64(Array<f64, D>),
/// signed 64-bit integer array
I64(Array<i64, D>), I64(Array<i64, D>),
/// unsigned 64-bit integer array
U64(Array<u64, D>), U64(Array<u64, D>),
/// signed 128-bit integer array
I128(Array<i128, D>), I128(Array<i128, D>),
/// unsigned 128-bit integer array
U128(Array<u128, D>), U128(Array<u128, D>),
F128(Array<f64, D>), // f128 is nightly /// 128-bit float array (emulated as f64)
F128(Array<f64, D>),
} }
/// type alias for a single frame (2D image)
pub type Frame = ArrayT<Ix2>; pub type Frame = ArrayT<Ix2>;
/// trait for reading image files
pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> { pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
/// create a new reader for the given path, series, and position
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error> fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
where where
P: AsRef<Path>; P: AsRef<Path>;
/// the name of the reader type
fn reader_name(&self) -> &'static str { fn reader_name(&self) -> &'static str {
type_name::<Self>() type_name::<Self>()
} }
// TODO: read from file if present // TODO: read from file if present
/// get the ome metadata for the image
fn metadata(&self) -> Result<Ome, Error>; fn metadata(&self) -> Result<Ome, Error>;
/// get a sliceable view on the image file /// get a sliceable view on the image file
@@ -161,18 +206,25 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
) )
} }
/// Retrieve fame at channel c, slize z and time t. /// retrieve frame at channel c, slice z and time t
#[allow(clippy::if_same_then_else)] #[allow(clippy::if_same_then_else)]
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error>; fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error>;
/// the path to the image file
fn path(&self) -> &Path; fn path(&self) -> &Path;
/// the series index
fn series(&self) -> usize; fn series(&self) -> usize;
/// the position index
fn position(&self) -> usize; fn position(&self) -> usize;
/// the shape of the image data
fn shape(&self) -> &Shape; fn shape(&self) -> &Shape;
/// the pixel type of the image data
fn pixel_type(&self) -> &PixelType; fn pixel_type(&self) -> &PixelType;
/// get all available positions for a given series
fn get_available_positions<P>(path: P, series: usize) -> Result<HashSet<usize>, Error> fn get_available_positions<P>(path: P, series: usize) -> Result<HashSet<usize>, Error>
where where
P: AsRef<Path>; P: AsRef<Path>;
/// get all available series
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error> fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
where where
P: AsRef<Path>; P: AsRef<Path>;
@@ -365,16 +417,22 @@ where
} }
} }
/// dynamic reader that can handle multiple file formats
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum DynReader { pub enum DynReader {
/// tiff file reader
#[cfg(feature = "tiff")] #[cfg(feature = "tiff")]
Tiff(tiff::TiffReader), Tiff(tiff::TiffReader),
/// tiff sequence reader
#[cfg(feature = "tiffseq")] #[cfg(feature = "tiffseq")]
TiffSeq(tiffseq::TiffSeqReader), TiffSeq(tiffseq::TiffSeqReader),
/// czi file reader
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
Czi(czi::CziReader), Czi(czi::CziReader),
/// bioformats reader (pure rust)
#[cfg(feature = "bioformats_rust")] #[cfg(feature = "bioformats_rust")]
BioFormatsRust(bioformats_rust::BioFormatsRustReader), BioFormatsRust(bioformats_rust::BioFormatsRustReader),
/// bioformats reader (java bindings)
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
BioFormatsJava(bioformats_java::BioFormatsJavaReader), BioFormatsJava(bioformats_java::BioFormatsJavaReader),
} }
@@ -626,6 +684,7 @@ impl Reader for DynReader {
} }
impl DynReader { impl DynReader {
/// create a dynreader by selecting the reader type explicitly
pub fn from_path_select_reader<P, R>(path: P, reader: R) -> Result<DynReader, Error> pub fn from_path_select_reader<P, R>(path: P, reader: R) -> Result<DynReader, Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
@@ -674,6 +733,7 @@ impl DynReader {
Ok(reader) Ok(reader)
} }
/// get all available positions, optionally selecting a specific reader type
pub fn get_available_positions_select_reader<P, R>( pub fn get_available_positions_select_reader<P, R>(
path: P, path: P,
series: usize, series: usize,
@@ -709,6 +769,7 @@ impl DynReader {
}) })
} }
/// get all available series, optionally selecting a specific reader type
pub fn get_available_series_select_reader<P, R>( pub fn get_available_series_select_reader<P, R>(
path: P, path: P,
reader: Option<R>, reader: Option<R>,
+3
View File
@@ -12,6 +12,7 @@ use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thread_local::ThreadLocal; use thread_local::ThreadLocal;
/// reader for czi (zeiss) image files
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct CziReader { pub struct CziReader {
#[serde(skip)] #[serde(skip)]
@@ -121,8 +122,10 @@ impl From<Option<String>> for Version {
} }
} }
/// errors specific to czi file reading
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum CziError { pub enum CziError {
/// czi file has no valid blocks
#[error("czi file has no valid blocks")] #[error("czi file has no valid blocks")]
NoValidBlocks, NoValidBlocks,
} }
+1
View File
@@ -12,6 +12,7 @@ use thread_local::ThreadLocal;
use tiff::decoder::{Decoder, DecodingResult}; use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag; use tiff::tags::Tag;
/// reader for single tiff image files
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct TiffReader { pub struct TiffReader {
#[serde(skip)] #[serde(skip)]
+1
View File
@@ -11,6 +11,7 @@ use std::str::FromStr;
use tiff::decoder::{Decoder, DecodingResult}; use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag; use tiff::tags::Tag;
/// reader for sequences of tiff files (one file per z/time plane)
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TiffSeqReader { pub struct TiffSeqReader {
path: PathBuf, path: PathBuf,
+5
View File
@@ -3,11 +3,16 @@ use ndarray::{Array, ArrayD, ArrayView, Axis, Dimension, RemoveAxis};
/// a trait to define the min, max, sum and mean operations along an axis /// a trait to define the min, max, sum and mean operations along an axis
pub trait MinMax { pub trait MinMax {
/// the type of the result after reducing one axis
type Output; type Output;
/// the max of the array along `axis`
fn max(self, axis: usize) -> Result<Self::Output, Error>; fn max(self, axis: usize) -> Result<Self::Output, Error>;
/// the min of the array along `axis`
fn min(self, axis: usize) -> Result<Self::Output, Error>; fn min(self, axis: usize) -> Result<Self::Output, Error>;
/// the sum of the array along `axis`
fn sum(self, axis: usize) -> Result<Self::Output, Error>; fn sum(self, axis: usize) -> Result<Self::Output, Error>;
/// the mean of the array along `axis`
fn mean(self, axis: usize) -> Result<Self::Output, Error>; fn mean(self, axis: usize) -> Result<Self::Output, Error>;
} }
+5
View File
@@ -14,6 +14,7 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};
use tiffwrite::{Bytes, Colors, Compression, IJTiffFile}; use tiffwrite::{Bytes, Colors, Compression, IJTiffFile};
/// options for saving tiff files
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TiffOptions { pub struct TiffOptions {
bar: Option<ProgressBar>, bar: Option<ProgressBar>,
@@ -34,6 +35,7 @@ impl Default for TiffOptions {
} }
impl TiffOptions { impl TiffOptions {
/// create tiff options with the given parameters
pub fn new( pub fn new(
bar: Option<ProgressBar>, bar: Option<ProgressBar>,
compression: Option<Compression>, compression: Option<Compression>,
@@ -77,6 +79,7 @@ impl TiffOptions {
self.compression = Compression::Deflate self.compression = Compression::Deflate
} }
/// set the color lookup table for the tiff
pub fn set_colors(&mut self, colors: &[String]) -> Result<(), Error> { pub fn set_colors(&mut self, colors: &[String]) -> Result<(), Error> {
let colors = colors let colors = colors
.iter() .iter()
@@ -86,6 +89,7 @@ impl TiffOptions {
Ok(()) Ok(())
} }
/// set whether an existing tiff file should be overwritten
pub fn set_overwrite(&mut self, overwrite: bool) { pub fn set_overwrite(&mut self, overwrite: bool) {
self.overwrite = overwrite; self.overwrite = overwrite;
} }
@@ -180,6 +184,7 @@ where
} }
} }
/// batch convert multiple files to tiff format
pub fn batch_to_tiff( pub fn batch_to_tiff(
files_in: &[PathBuf], files_in: &[PathBuf],
files_out: &[PathBuf], files_out: &[PathBuf],
+14 -319
View File
@@ -1,9 +1,10 @@
use crate::axes::{Ax, Axis, Operation, Shape, Slice, SliceInfoElemDef, slice_info}; use crate::axes::{Ax, Axis, Operation, Shape, Slice, SliceInfoElemDef, slice_info};
use crate::cache::{ArrayCache, ArrayKeyRef, FrameCache, FrameKeyRef, ReaderKeyRef};
use crate::error::Error; use crate::error::Error;
use crate::metadata::Metadata; use crate::metadata::Metadata;
use crate::readers::{Dimensions, DynReader, Frame, Reader}; use crate::readers::{Dimensions, DynReader, Frame, Reader};
use crate::stats::MinMax; use crate::stats::MinMax;
use indexmap::{Equivalent, IndexMap}; use indexmap::IndexMap;
use itertools::{Itertools, iproduct}; use itertools::{Itertools, iproduct};
use ndarray::{ use ndarray::{
Array, Array0, Array1, Array2, ArrayD, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix5, IxDyn, Array, Array0, Array1, Array2, ArrayD, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix5, IxDyn,
@@ -13,15 +14,15 @@ use num::traits::ToBytes;
use num::{Bounded, FromPrimitive, ToPrimitive, Zero}; use num::{Bounded, FromPrimitive, ToPrimitive, Zero};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
use std::any::{Any, type_name}; use std::any::type_name;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::iter::Sum; use std::iter::Sum;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{AddAssign, Deref, Div}; use std::ops::{AddAssign, Deref, Div};
use std::path::{Path, PathBuf}; use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock}; use std::sync::Arc;
fn idx_bnd(idx: isize, bnd: isize) -> Result<isize, Error> { fn idx_bnd(idx: isize, bnd: isize) -> Result<isize, Error> {
if idx < -bnd { if idx < -bnd {
@@ -47,6 +48,7 @@ fn slc_bnd(idx: isize, bnd: isize) -> Result<isize, Error> {
} }
} }
/// a trait for numeric types that can be used as pixel values
pub trait Number: pub trait Number:
'static 'static
+ Send + Send
@@ -74,318 +76,6 @@ impl<T> Number for T where
{ {
} }
/// maximum number of frames held in the cache
const DEFAULT_FRAME_CACHE_SIZE: usize = 128;
/// maximum number of materialized arrays held in the cache
const DEFAULT_ARRAY_CACHE_SIZE: usize = 2;
/// identity of the reader a frame was read from
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ReaderKey {
name: String,
path: PathBuf,
series: usize,
position: usize,
}
/// borrowed view of [`ReaderKey`] for cache lookups without allocation.
/// hashes byte-identically to [`ReaderKey`] (str/String and Path/PathBuf hash
/// the same), so it only matches the same reader identity.
#[derive(Debug, PartialEq, Eq)]
struct ReaderKeyRef<'a> {
name: &'a str,
path: &'a Path,
series: usize,
position: usize,
}
impl Hash for ReaderKeyRef<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.path.hash(state);
self.series.hash(state);
self.position.hash(state);
}
}
impl Equivalent<ReaderKey> for ReaderKeyRef<'_> {
fn equivalent(&self, key: &ReaderKey) -> bool {
self.name == key.name
&& self.path == key.path.as_path()
&& self.series == key.series
&& self.position == key.position
}
}
impl ReaderKeyRef<'_> {
fn to_owned(&self) -> ReaderKey {
ReaderKey {
name: self.name.to_string(),
path: self.path.to_path_buf(),
series: self.series,
position: self.position,
}
}
}
type FrameKey = (ReaderKey, usize, usize, usize);
/// borrowed view of [`FrameKey`] for cache lookups without allocation.
#[derive(Debug, PartialEq, Eq)]
struct FrameKeyRef<'a> {
reader: ReaderKeyRef<'a>,
c: usize,
z: usize,
t: usize,
}
impl Hash for FrameKeyRef<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.reader.hash(state);
self.c.hash(state);
self.z.hash(state);
self.t.hash(state);
}
}
impl Equivalent<FrameKey> for FrameKeyRef<'_> {
fn equivalent(&self, key: &FrameKey) -> bool {
let (rk, c, z, t) = key;
self.reader.equivalent(rk) && self.c == *c && self.z == *z && self.t == *t
}
}
impl FrameKeyRef<'_> {
fn to_owned(&self) -> FrameKey {
(self.reader.to_owned(), self.c, self.z, self.t)
}
}
/// identity of a materialized array in the process-wide cache
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ArrayKey {
reader: ReaderKey,
dtype: &'static str,
slice: Vec<SliceInfoElem>,
axes: Vec<Axis>,
operations: Vec<(Axis, Operation)>,
}
/// borrowed view of [`ArrayKey`] for cache lookups without allocation.
#[derive(Debug, PartialEq, Eq)]
struct ArrayKeyRef<'a> {
reader: ReaderKeyRef<'a>,
dtype: &'static str,
slice: &'a [SliceInfoElem],
axes: &'a [Axis],
operations: &'a IndexMap<Axis, Operation>,
}
impl Hash for ArrayKeyRef<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.reader.hash(state);
self.dtype.hash(state);
self.slice.hash(state);
self.axes.hash(state);
self.operations.len().hash(state);
for (ax, op) in self.operations.iter() {
ax.hash(state);
op.hash(state);
}
}
}
impl Equivalent<ArrayKey> for ArrayKeyRef<'_> {
fn equivalent(&self, key: &ArrayKey) -> bool {
self.reader.equivalent(&key.reader)
&& self.dtype == key.dtype
&& self.slice == key.slice
&& self.axes == key.axes
&& self.operations.len() == key.operations.len()
&& self
.operations
.iter()
.zip(&key.operations)
.all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op)
}
}
impl ArrayKeyRef<'_> {
fn to_owned(&self) -> ArrayKey {
ArrayKey {
reader: self.reader.to_owned(),
dtype: self.dtype,
slice: self.slice.to_vec(),
axes: self.axes.to_vec(),
operations: self
.operations
.iter()
.map(|(ax, op)| (*ax, op.clone()))
.collect(),
}
}
}
/// process-wide LRU cache of frames, shared between all views and threads
static GLOBAL_FRAME_CACHE: OnceLock<FrameCache> = OnceLock::new();
/// thread-safe LRU cache of frames read from the underlying reader
struct FrameCache {
inner: Mutex<FrameCacheInner>,
}
struct FrameCacheInner {
map: IndexMap<FrameKey, Arc<Frame>>,
capacity: usize,
}
impl Default for FrameCache {
fn default() -> Self {
Self::new(DEFAULT_FRAME_CACHE_SIZE)
}
}
impl FrameCache {
fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(FrameCacheInner {
map: IndexMap::with_capacity(capacity),
capacity,
}),
}
}
fn global() -> &'static FrameCache {
GLOBAL_FRAME_CACHE.get_or_init(FrameCache::default)
}
fn capacity(&self) -> usize {
self.inner.lock().unwrap().capacity
}
fn set_capacity(&self, capacity: usize) {
let mut inner = self.inner.lock().unwrap();
inner.capacity = capacity;
while inner.map.len() > inner.capacity {
inner.map.shift_remove_index(0);
}
}
fn get<Q>(&self, key: &Q) -> Option<Arc<Frame>>
where
Q: ?Sized + Hash + Equivalent<FrameKey>,
{
let mut inner = self.inner.lock().unwrap();
if let Some(idx) = inner.map.get_index_of(key) {
let (key, frame) = inner.map.shift_remove_index(idx).unwrap();
inner.map.insert(key, frame.clone());
Some(frame)
} else {
None
}
}
fn insert(&self, key: FrameKey, frame: Arc<Frame>) {
let mut inner = self.inner.lock().unwrap();
inner.map.insert(key, frame);
while inner.map.len() > inner.capacity {
inner.map.shift_remove_index(0);
}
}
#[cfg(test)]
fn len(&self) -> usize {
self.inner.lock().unwrap().map.len()
}
}
impl Debug for FrameCache {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FrameCache").finish_non_exhaustive()
}
}
/// process-wide LRU cache of materialized arrays, shared between all views and threads
static GLOBAL_ARRAY_CACHE: OnceLock<ArrayCache> = OnceLock::new();
/// thread-safe LRU cache of materialized arrays produced by `as_array_dyn`
struct ArrayCache {
inner: Mutex<ArrayCacheInner>,
}
struct ArrayCacheInner {
map: IndexMap<ArrayKey, Arc<dyn Any + Send + Sync>>,
capacity: usize,
}
impl Default for ArrayCache {
fn default() -> Self {
Self::new(DEFAULT_ARRAY_CACHE_SIZE)
}
}
impl ArrayCache {
fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(ArrayCacheInner {
map: IndexMap::with_capacity(capacity),
capacity,
}),
}
}
fn global() -> &'static ArrayCache {
GLOBAL_ARRAY_CACHE.get_or_init(ArrayCache::default)
}
fn capacity(&self) -> usize {
self.inner.lock().unwrap().capacity
}
fn set_capacity(&self, capacity: usize) {
let mut inner = self.inner.lock().unwrap();
inner.capacity = capacity;
while inner.map.len() > inner.capacity {
inner.map.shift_remove_index(0);
}
}
fn get<T, Q>(&self, key: &Q) -> Option<Arc<ArrayD<T>>>
where
Q: ?Sized + Hash + Equivalent<ArrayKey>,
T: Any + Send + Sync,
{
let mut inner = self.inner.lock().unwrap();
if let Some(idx) = inner.map.get_index_of(key) {
let (key, array) = inner.map.shift_remove_index(idx).unwrap();
inner.map.insert(key, array.clone());
array.downcast::<ArrayD<T>>().ok()
} else {
None
}
}
fn insert<T: Any + Send + Sync>(&self, key: ArrayKey, array: ArrayD<T>) {
let mut inner = self.inner.lock().unwrap();
inner.map.insert(key, Arc::new(array));
while inner.map.len() > inner.capacity {
inner.map.shift_remove_index(0);
}
}
#[cfg(test)]
fn len(&self) -> usize {
self.inner.lock().unwrap().map.len()
}
}
impl Debug for ArrayCache {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArrayCache").finish_non_exhaustive()
}
}
/// sliceable view on an image file /// sliceable view on an image file
#[serde_as] #[serde_as]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -634,6 +324,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
self.shape()[0] self.shape()[0]
} }
/// whether the view has no pixels in the first dimension
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.shape()[0] == 0 self.shape()[0] == 0
} }
@@ -643,6 +334,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
self.shape().into_iter().product() self.shape().into_iter().product()
} }
/// the size of the view along an axis, or `None` if the axis is not present
pub fn size_ax(&self, ax: Axis) -> Option<usize> { pub fn size_ax(&self, ax: Axis) -> Option<usize> {
self.axes() self.axes()
.iter() .iter()
@@ -745,6 +437,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
.with_operations(self.operations.clone())) .with_operations(self.operations.clone()))
} }
/// reduce an axis with an operation (max, min, sum or mean), returning a view of lower dimensionality
pub fn operate<A: Ax>( pub fn operate<A: Ax>(
&self, &self,
axis: A, axis: A,
@@ -1450,6 +1143,7 @@ where
/// trait to define a function to retrieve the only item in a 0d array /// trait to define a function to retrieve the only item in a 0d array
pub trait Item { pub trait Item {
/// the single item in the 0d array, cast to `T`
fn item<T>(&self) -> Result<T, Error> fn item<T>(&self) -> Result<T, Error>
where where
T: Number, T: Number,
@@ -1459,6 +1153,7 @@ pub trait Item {
} }
impl<R: Reader> View<Ix5, R> { impl<R: Reader> View<Ix5, R> {
/// create a view on the image at `path`, parsing series and position from the file name
pub fn from_path<P>(path: P) -> Result<Self, Error> pub fn from_path<P>(path: P) -> Result<Self, Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
@@ -1492,6 +1187,7 @@ impl<D: Dimension, R: Reader> Display for View<D, R> {
/// trait to convert numbers to bytes /// trait to convert numbers to bytes
pub trait ToBytesVec { pub trait ToBytesVec {
/// the number as a vector of bytes in native endianness
fn to_bytes_vec(&self) -> Vec<u8>; fn to_bytes_vec(&self) -> Vec<u8>;
} }
@@ -1516,12 +1212,11 @@ to_bytes_vec_impl!(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::axes::{Axis, Operation}; use crate::axes::{Axis, Operation};
use crate::cache::{ArrayCache, ArrayKey, ArrayKeyRef, FrameCache, ReaderKey, ReaderKeyRef};
use crate::error::Error; use crate::error::Error;
use crate::readers::{DynReader, Frame, Reader}; use crate::readers::{DynReader, Frame, Reader};
use crate::stats::MinMax; use crate::stats::MinMax;
use crate::view::{ use crate::view::Item;
ArrayCache, ArrayKey, ArrayKeyRef, FrameCache, Item, ReaderKey, ReaderKeyRef,
};
use indexmap::IndexMap; use indexmap::IndexMap;
use ndarray::{Array, Array4, Array5, NewAxis}; use ndarray::{Array, Array4, Array5, NewAxis};
use ndarray::{Array2, ArrayD, IxDyn, SliceInfoElem, s}; use ndarray::{Array2, ArrayD, IxDyn, SliceInfoElem, s};