- improve Imread slicing

- can_open function for Reader trait
This commit is contained in:
w.pomp committed 2026-10-22 19:50:20 +02:00
1 parent f0404d0419
commit 3e33fe18c1
13 files changed
+735 -191

No files matched your search

@@ -52,6 +52,8 @@ class Flags:
def __new__(cls) -> Flags: ...
def __getnewargs__(self) -> None: ...
def __getitem__(self, item: builtins.str) -> builtins.bool: ...
def __str__(self) -> builtins.str: ...
def __repr__(self) -> builtins.str: ...
class Imread:
r"""
@@ -211,6 +213,16 @@ class Imread:
the name of the tube lens
"""
@property
def channel_names(self) -> builtins.dict[builtins.int, builtins.str]:
r"""
names of the channels in order
"""
@property
def summary(self) -> builtins.str:
r"""
gives a helpful summary of the recorded experiment
"""
@property
def interpolation_order(self) -> typing.Optional[builtins.int]:
r"""
the bspline order used for interpolation when using transforms
@@ -293,7 +305,9 @@ class Imread:
r"""
reshape the view with a new axis order
"""
def squeeze(self) -> numpy.ndarray | int | float: ...
def squeeze(
self, axes: str | typing.Sequence[int | str] = None
) -> Imread | float | int: ...
def close(self) -> None:
r"""
close the file: does nothing as this is handled automatically
@@ -306,7 +320,19 @@ class Imread:
r"""
change the data type of the view: (u)int(8/16/32) or float(32/64)
"""
def __getitem__(self, n: typing.Any) -> typing.Any:
def __call__(
self,
c: typing.Optional[int | slice | numpy.ArrayLike] = None,
z: typing.Optional[int | slice | numpy.ArrayLike] = None,
t: typing.Optional[int | slice | numpy.ArrayLike] = None,
y: typing.Optional[int | slice | numpy.ArrayLike] = None,
x: typing.Optional[int | slice | numpy.ArrayLike] = None,
) -> typing.Optional[int | Imread]:
r"""
same as im[] but allowing keyword axes, but slices need to made with slice()
also, no ellipsis or new axes
"""
def __getitem__(self, n: typing.Any) -> typing.Optional[int | Imread]:
r"""
slice the view and return a new view or a single number
"""
@@ -314,7 +340,7 @@ class Imread:
self,
dtype: typing.Optional[numpy.typing.DTypeLike] = None,
copy: typing.Optional[builtins.bool] = None,
) -> typing.Any:
) -> numpy.typing.NDArray:
r"""
convert to a numpy array, optionally with a different dtype
"""
@@ -322,139 +348,199 @@ class Imread:
r"""
check if an item is contained in the view (not implemented)
"""
def __lt__(self, other: typing.Any) -> typing.Any:
def __lt__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise less than comparison
"""
def __le__(self, other: typing.Any) -> typing.Any:
def __le__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise less than or equal comparison
"""
def __eq__(self, other: typing.Any) -> typing.Any:
def __eq__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise equality comparison
"""
def __ne__(self, other: typing.Any) -> typing.Any:
def __ne__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise not equal comparison
"""
def __gt__(self, other: typing.Any) -> typing.Any:
def __gt__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise greater than comparison
"""
def __ge__(self, other: typing.Any) -> typing.Any:
def __ge__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise greater than or equal comparison
"""
def __add__(self, other: typing.Any) -> typing.Any:
def __add__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise addition
"""
def __radd__(self, other: typing.Any) -> typing.Any:
def __radd__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise addition (reflected)
"""
def __sub__(self, other: typing.Any) -> typing.Any:
def __sub__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise subtraction
"""
def __rsub__(self, other: typing.Any) -> typing.Any:
def __rsub__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise subtraction (reflected)
"""
def __mul__(self, other: typing.Any) -> typing.Any:
def __mul__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise multiplication
"""
def __rmul__(self, other: typing.Any) -> typing.Any:
def __rmul__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise multiplication (reflected)
"""
def __truediv__(self, other: typing.Any) -> typing.Any:
def __truediv__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise true division
"""
def __rtruediv__(self, other: typing.Any) -> typing.Any:
def __rtruediv__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise true division (reflected)
"""
def __floordiv__(self, other: typing.Any) -> typing.Any:
def __floordiv__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise floor division
"""
def __rfloordiv__(self, other: typing.Any) -> typing.Any:
def __rfloordiv__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise floor division (reflected)
"""
def __mod__(self, other: typing.Any) -> typing.Any:
def __mod__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise modulo
"""
def __rmod__(self, other: typing.Any) -> typing.Any:
def __rmod__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise modulo (reflected)
"""
def __matmul__(self, other: typing.Any) -> typing.Any:
def __matmul__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise matrix multiplication
"""
def __rmatmul__(self, other: typing.Any) -> typing.Any:
def __rmatmul__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise matrix multiplication (reflected)
"""
def __and__(self, other: typing.Any) -> typing.Any:
def __and__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise bitwise AND
"""
def __rand__(self, other: typing.Any) -> typing.Any:
def __rand__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise bitwise AND (reflected)
"""
def __or__(self, other: typing.Any) -> typing.Any:
def __or__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise bitwise OR
"""
def __ror__(self, other: typing.Any) -> typing.Any:
def __ror__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise bitwise OR (reflected)
"""
def __xor__(self, other: typing.Any) -> typing.Any:
def __xor__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise bitwise XOR
"""
def __rxor__(self, other: typing.Any) -> typing.Any:
def __rxor__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r"""
element-wise bitwise XOR (reflected)
"""
def __lshift__(self, other: typing.Any) -> typing.Any:
def __lshift__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise left shift
"""
def __rlshift__(self, other: typing.Any) -> typing.Any:
def __rlshift__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise left shift (reflected)
"""
def __rshift__(self, other: typing.Any) -> typing.Any:
def __rshift__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise right shift
"""
def __rrshift__(self, other: typing.Any) -> typing.Any:
def __rrshift__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r"""
element-wise right shift (reflected)
"""
def __neg__(self) -> typing.Any:
def __neg__(self) -> numpy.typing.NDArray | float:
r"""
element-wise negation
"""
def __pos__(self) -> typing.Any:
def __pos__(self) -> numpy.typing.NDArray | float:
r"""
element-wise positive
"""
def __abs__(self) -> typing.Any:
def __abs__(self) -> numpy.typing.NDArray | float:
r"""
element-wise absolute value
"""
def __invert__(self) -> typing.Any:
def __invert__(self) -> numpy.typing.NDArray | float:
r"""
element-wise bitwise inversion
"""
@@ -507,13 +593,19 @@ class Imread:
r"""
the file path as a string
"""
def frame(
self, c: builtins.int, z: builtins.int, t: builtins.int
) -> numpy.typing.NDArray | float:
r"""
retrieve a single frame at czt, sliced accordingly, alias of get_frame
"""
def get_frame(
self, c: builtins.int, z: builtins.int, t: builtins.int
) -> typing.Any:
) -> numpy.typing.NDArray | float:
r"""
retrieve a single frame at czt, sliced accordingly
"""
def flatten(self) -> typing.Any:
def flatten(self) -> numpy.typing.NDArray | float:
r"""
flatten the view into a 1D numpy array
"""
@@ -529,6 +621,10 @@ class Imread:
r"""
find the position of an axis
"""
def swapaxes(self, ax0: int | str, ax1: int | str) -> Imread:
r"""
swap two axes
"""
def swap_axes(self, ax0: int | str, ax1: int | str) -> Imread:
r"""
swap two axes
@@ -539,7 +635,7 @@ class Imread:
r"""
permute the order of the axes
"""
def as_array(self) -> typing.Any:
def as_array(self) -> numpy.typing.NDArray | float:
r"""
collect data into a numpy array
"""
@@ -573,10 +669,6 @@ class Imread:
r"""
the name of a particular channel
"""
def summary(self) -> builtins.str:
r"""
gives a helpful summary of the recorded experiment
"""
@staticmethod
def get_available_series(
path: str | pathlib.Path | Imread, reader: typing.Optional[builtins.str] = None
@@ -793,7 +885,7 @@ class Shape:
arguments for pickling
"""
def __getitem__(
self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int]
self, idx: str | int | None | Ellipsis | slice | typing.Sequence[int]
) -> typing.Optional[int | tuple[int]]:
r"""
get dimension size by index or axis name
+1 -1
View File
@@ -35,7 +35,7 @@ ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub"
[tool.maturin]
python-source = "py"
features = ["python", "bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"]
features = ["python", "bioformats_java", "gpl-formats", "bioformats_rust", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"]
no-default-features = true
module-name = "ndbioimage.ndbioimage_rs"
include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"]
+13 -4
View File
@@ -314,16 +314,16 @@ impl Index<&Axis> for Shape {
impl Index<usize> for Shape {
type Output = usize;
fn index(&self, dim: usize) -> &Self::Output {
&self[self.order[dim % self.order.len()]]
fn index(&self, index: usize) -> &Self::Output {
&self[self.order[index]]
}
}
impl Index<&usize> for Shape {
type Output = usize;
fn index(&self, dim: &usize) -> &Self::Output {
&self[self.order[dim % self.order.len()]]
fn index(&self, index: &usize) -> &Self::Output {
&self[self.order[*index]]
}
}
@@ -406,6 +406,10 @@ impl Shape {
}
}
pub fn get(&self, index: &usize) -> Option<&usize> {
self.order.get(*index).map(|axis| &self[axis])
}
/// the number of axes in the shape
pub fn len(&self) -> usize {
self.order.len()
@@ -421,6 +425,11 @@ impl Shape {
self.order.iter().map(|axis| self[axis]).collect()
}
/// the sizes in cztyx order
pub fn cztyx(&self) -> [usize; 5] {
[self.c, self.z, self.t, self.y, self.x]
}
/// 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();
File diff suppressed because it is too large. Load diff
+34
View File
@@ -188,6 +188,12 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
where
P: AsRef<Path>;
/// a simple fast test to see if the reader can open a path without parsing the whole file
/// not a guarantee of success, but rather an early failure
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>;
/// the name of the reader type
fn reader_name(&self) -> &'static str {
type_name::<Self>()
@@ -478,6 +484,34 @@ impl Reader for DynReader {
))
}
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
let mut can_open = false;
#[cfg(feature = "tiff")]
{
can_open |= tiff::TiffReader::can_open(&path)?;
}
#[cfg(feature = "tiffseq")]
{
can_open |= tiffseq::TiffSeqReader::can_open(&path)?;
}
#[cfg(feature = "czi")]
{
can_open |= czi::CziReader::can_open(&path)?;
}
#[cfg(feature = "bioformats_rust")]
{
can_open |= bioformats_rust::BioFormatsRustReader::can_open(&path)?;
}
#[cfg(feature = "bioformats_java")]
{
can_open |= bioformats_java::BioFormatsJavaReader::can_open(&path)?;
}
Ok(can_open)
}
fn reader_name(&self) -> &'static str {
match self {
#[cfg(feature = "tiff")]
@@ -666,6 +666,13 @@ impl Reader for BioFormatsJavaReader {
Ok(new)
}
fn can_open<P>(_path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
Ok(true)
}
fn metadata(&self) -> Result<Ome, Error> {
self.get_ome()
}
@@ -274,6 +274,13 @@ impl Reader for BioFormatsRustReader {
Ok(new)
}
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
Ok(path.as_ref().extension() != Some(std::ffi::OsStr::new("czi")))
}
fn metadata(&self) -> Result<Ome, Error> {
let reader = self.get_reader()?;
let metadata = reader.metadata();
+7
View File
@@ -215,6 +215,13 @@ impl Reader for CziReader {
Ok(new)
}
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
Ok(path.as_ref().extension() == Some(std::ffi::OsStr::new("czi")))
}
fn metadata(&self) -> Result<Ome, Error> {
let mut ome = Ome::default();
let xml = xmltree::Element::parse(self.metadata_xml()?.as_bytes())?;
+38 -2
View File
@@ -232,6 +232,24 @@ impl Reader for TiffReader {
Ok(new)
}
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let mut file = File::open(path)?;
let mut sig = [0; 2];
file.read_exact(&mut sig)?;
if sig == [73, 73] {
file.read_exact(&mut sig)?;
Ok((sig[0] == 42) || (sig[0] == 43))
} else if sig == [77, 77] {
Ok((sig[1] == 42) || (sig[1] == 43))
} else {
Ok(false)
}
}
fn metadata(&self) -> Result<Ome, Error> {
let mut reader = self.get_reader()?;
let image_description = reader.current_ifd().get_tag(Tag::ImageDescription);
@@ -466,18 +484,36 @@ impl Reader for TiffReader {
&self.pixel_type
}
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
P: AsRef<Path>,
{
let path = path.as_ref();
if Self::can_open(path)? {
Ok(HashSet::from([0]))
} else {
Err(Error::InvalidReader(
"TiffReader".to_string(),
path.to_string_lossy().to_string(),
"it is not a tiff".to_string(),
))
}
}
fn get_available_series<P>(_path: P) -> Result<HashSet<usize>, Error>
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
where
P: AsRef<Path>,
{
let path = path.as_ref();
if Self::can_open(path)? {
Ok(HashSet::from([0]))
} else {
Err(Error::InvalidReader(
"TiffReader".to_string(),
path.to_string_lossy().to_string(),
"it is not a tiff".to_string(),
))
}
}
}
+17 -1
View File
@@ -239,6 +239,13 @@ impl Reader for TiffSeqReader {
})
}
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
Ok(path.as_ref().is_dir())
}
fn metadata(&self) -> Result<Ome, Error> {
let mut ome = Ome::default();
let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping());
@@ -580,11 +587,20 @@ impl Reader for TiffSeqReader {
&self.pixel_type
}
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
P: AsRef<Path>,
{
let path = path.as_ref();
if Self::can_open(path)? {
Ok(HashSet::from([0]))
} else {
Err(Error::InvalidReader(
"TiffSeqReader".to_string(),
path.to_string_lossy().to_string(),
"not a tiff sequence".to_string(),
))
}
}
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
+37
View File
@@ -284,6 +284,21 @@ impl<D: Dimension, R: Reader> View<D, R> {
.ok_or(Error::NoChannel(channel))
}
/// names of the channels in order
pub fn channel_names(&self) -> Result<HashMap<usize, String>, Error> {
if self.channel_names.get().is_none() {
let names = self.metadata()?.image[0]
.pixels
.channel
.iter()
.enumerate()
.map(|(i, c)| (i, c.name.clone().unwrap_or_else(|| format!("{}", i))))
.collect::<HashMap<_, _>>();
let _ = self.channel_names.set(names);
}
Ok(self.channel_names.get().cloned().unwrap_or_default())
}
/// set the maximum number of frames the process-wide cache may hold,
/// evicting the least recently used frames
pub fn with_cache_capacity(self, capacity: usize) -> Self {
@@ -404,6 +419,28 @@ impl<D: Dimension, R: Reader> View<D, R> {
view.slice(slice.as_slice())
}
/// remove axes of size 1 in axes
pub fn squeeze_axes(&self, axes: HashSet<Axis>) -> Result<View<IxDyn, R>, Error> {
let view = self.clone().into_dyn();
let slice: Vec<_> = self
.shape()
.into_iter()
.zip(self.axes().iter())
.map(|(s, a)| {
if s == 1 && axes.contains(a) {
SliceInfoElem::Index(0)
} else {
SliceInfoElem::Slice {
start: 0,
end: None,
step: 1,
}
}
})
.collect();
view.slice(slice.as_slice())
}
pub(crate) fn op_axes(&self) -> Vec<Axis> {
self.operations.keys().cloned().collect()
}
+2
View File
@@ -26,8 +26,10 @@ def image(array):
@pytest.mark.parametrize(
"s",
list(
combinations_with_replacement(
(0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5
)
),
)
def test_slicing(s, image, array):
+2
View File
@@ -26,6 +26,7 @@ def image(array):
@pytest.mark.parametrize(
"fun_and_axis",
list(
product(
(
np.sum,
@@ -44,6 +45,7 @@ def image(array):
np.nanstd,
),
(None, 0, 1, 2, 3, 4),
)
),
)
def test_ufuncs(fun_and_axis, image, array):