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