- open tiff seq folders with bioformats_java by finding the first tiff recursively

- make PyView serializable by skipping the ome field
- fix python tests
This commit is contained in:
w.pomp
2026-07-13 15:16:10 +02:00
parent 0ce1187a43
commit 916d301f1b
7 changed files with 76 additions and 53 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub"
[tool.maturin] [tool.maturin]
python-source = "py" python-source = "py"
features = ["python", "bioformats_java", "tiffwrite", "czi"] features = ["python", "bioformats_java", "tiffwrite"]
module-name = "ndbioimage.ndbioimage_rs" module-name = "ndbioimage.ndbioimage_rs"
include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"] include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"]
strip = true strip = true
+38 -39
View File
@@ -70,6 +70,7 @@ impl From<crate::error::Error> for PyErr {
struct PyView { struct PyView {
view: View<IxDyn, DynReader>, view: View<IxDyn, DynReader>,
dtype: PixelType, dtype: PixelType,
#[serde(skip)]
ome: Ome, ome: Ome,
index: usize, index: usize,
} }
@@ -157,9 +158,12 @@ impl PyView {
#[pyo3(signature = (path, dtype = None, axes = "cztyx", reader = None))] #[pyo3(signature = (path, dtype = None, axes = "cztyx", reader = None))]
fn new<'py>( fn new<'py>(
py: Python<'py>, py: Python<'py>,
#[gen_stub(override_type(type_repr="str | pathlib.Path | View | bytes", imports=("pathlib")))] #[gen_stub(
override_type(type_repr="str | pathlib.Path | View | bytes", imports=("pathlib"))
)]
path: Bound<'py, PyAny>, path: Bound<'py, PyAny>,
#[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")))] #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")
))]
dtype: Option<Bound<'py, PyAny>>, dtype: Option<Bound<'py, PyAny>>,
axes: &str, axes: &str,
reader: Option<&str>, reader: Option<&str>,
@@ -167,7 +171,9 @@ impl PyView {
if path.is_instance_of::<Self>() { if path.is_instance_of::<Self>() {
Ok(path.cast_into::<Self>()?.extract::<Self>()?) Ok(path.cast_into::<Self>()?.extract::<Self>()?)
} else if path.is_instance_of::<PyBytes>() { } else if path.is_instance_of::<PyBytes>() {
Ok(from_bytes(&path.extract::<Vec<u8>>()?).map_err(Error::from)?) let mut pyview: Self = from_bytes(&path.extract::<Vec<u8>>()?).map_err(Error::from)?;
pyview.ome = pyview.view.metadata()?;
Ok(pyview)
} else { } else {
let builtins = PyModule::import(py, "builtins")?; let builtins = PyModule::import(py, "builtins")?;
let path = PathBuf::from( let path = PathBuf::from(
@@ -209,7 +215,9 @@ impl PyView {
#[staticmethod] #[staticmethod]
fn get_positions<'py>( fn get_positions<'py>(
py: Python, py: Python,
#[gen_stub(override_type(type_repr="str | pathlib.Path | View | bytes", imports=("pathlib")))] #[gen_stub(
override_type(type_repr="str | pathlib.Path | View | bytes", imports=("pathlib"))
)]
path: Bound<'py, PyAny>, path: Bound<'py, PyAny>,
) -> PyResult<HashSet<usize>> { ) -> PyResult<HashSet<usize>> {
Self::get_available_series(py, path, None) Self::get_available_series(py, path, None)
@@ -337,7 +345,8 @@ impl PyView {
fn as_type( fn as_type(
&self, &self,
py: Python<'_>, py: Python<'_>,
#[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")))] #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")
))]
dtype: Bound<'_, PyAny>, dtype: Bound<'_, PyAny>,
) -> PyResult<Self> { ) -> PyResult<Self> {
let np = PyModule::import(py, "numpy")?; let np = PyModule::import(py, "numpy")?;
@@ -356,7 +365,8 @@ impl PyView {
fn astype( fn astype(
&self, &self,
py: Python<'_>, py: Python<'_>,
#[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")))] #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")
))]
dtype: Bound<'_, PyAny>, dtype: Bound<'_, PyAny>,
) -> PyResult<Self> { ) -> PyResult<Self> {
self.as_type(py, dtype) self.as_type(py, dtype)
@@ -510,7 +520,8 @@ impl PyView {
fn __array__<'py>( fn __array__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
#[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")))] #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")
))]
dtype: Option<Bound<'py, PyAny>>, dtype: Option<Bound<'py, PyAny>>,
copy: Option<bool>, copy: Option<bool>,
) -> PyResult<Bound<'py, PyAny>> { ) -> PyResult<Bound<'py, PyAny>> {
@@ -1233,7 +1244,8 @@ impl PyView {
#[pyo3(signature = (axes = None))] #[pyo3(signature = (axes = None))]
fn transpose( fn transpose(
&self, &self,
#[gen_stub(override_type(type_repr="typing.Optional[typing.Sequence[int | str]]", imports=("typing")))] #[gen_stub(override_type(type_repr="typing.Optional[typing.Sequence[int | str]]", imports=("typing")
))]
axes: Option<Vec<Bound<PyAny>>>, axes: Option<Vec<Bound<PyAny>>>,
) -> PyResult<Self> { ) -> PyResult<Self> {
let view = if let Some(axes) = axes { let view = if let Some(axes) = axes {
@@ -1316,7 +1328,8 @@ impl PyView {
/// get the maximum overall or along a given axis /// get the maximum overall or along a given axis
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[gen_stub(skip)] #[gen_stub(skip)]
#[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=None, r#where=true), text_signature = "axis: str | int")] #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=None, r#where=true), text_signature = "axis: str | int"
)]
fn max<'py>( fn max<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -1369,7 +1382,8 @@ impl PyView {
/// get the minimum overall or along a given axis /// get the minimum overall or along a given axis
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[gen_stub(skip)] #[gen_stub(skip)]
#[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int")] #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int"
)]
fn min<'py>( fn min<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -1420,7 +1434,8 @@ impl PyView {
} }
#[gen_stub(skip)] #[gen_stub(skip)]
#[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, *, r#where=true), text_signature = "axis: str | int")] #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, *, r#where=true), text_signature = "axis: str | int"
)]
fn mean<'py>( fn mean<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -1459,7 +1474,8 @@ impl PyView {
/// get the sum overall or along a given axis /// get the sum overall or along a given axis
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[gen_stub(skip)] #[gen_stub(skip)]
#[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int")] #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int"
)]
fn sum<'py>( fn sum<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
@@ -1656,23 +1672,13 @@ impl PyView {
py_view.view.path().to_owned() py_view.view.path().to_owned()
} else { } else {
let builtins = PyModule::import(py, "builtins")?; let builtins = PyModule::import(py, "builtins")?;
let mut path = PathBuf::from( PathBuf::from(
builtins builtins
.getattr("str")? .getattr("str")?
.call1((path,))? .call1((path,))?
.cast_into::<PyString>()? .cast_into::<PyString>()?
.extract::<String>()?, .extract::<String>()?,
); )
if path.is_dir() {
for file in path.read_dir()?.flatten().sorted_by_key(|i| i.file_name()) {
let p = file.path();
if file.path().is_file() && (p.extension() == Some("tif".as_ref())) {
path = p;
break;
}
}
}
path
}; };
Ok(DynReader::get_available_series_select_reader(path, reader)?) Ok(DynReader::get_available_series_select_reader(path, reader)?)
@@ -1693,23 +1699,13 @@ impl PyView {
py_view.view.path().to_owned() py_view.view.path().to_owned()
} else { } else {
let builtins = PyModule::import(py, "builtins")?; let builtins = PyModule::import(py, "builtins")?;
let mut path = PathBuf::from( PathBuf::from(
builtins builtins
.getattr("str")? .getattr("str")?
.call1((path,))? .call1((path,))?
.cast_into::<PyString>()? .cast_into::<PyString>()?
.extract::<String>()?, .extract::<String>()?,
); )
if path.is_dir() {
for file in path.read_dir()?.flatten().sorted_by_key(|i| i.file_name()) {
let p = file.path();
if file.path().is_file() && (p.extension() == Some("tif".as_ref())) {
path = p;
break;
}
}
}
path
}; };
Ok(DynReader::get_available_positions_select_reader( Ok(DynReader::get_available_positions_select_reader(
path, series, reader, path, series, reader,
@@ -1758,7 +1754,8 @@ impl PyView {
#[cfg(feature = "movie")] #[cfg(feature = "movie")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[pyo3(signature = (file, speed = 1.0, brightness = None, scale = 1.0, colors = None, overwrite = false, register = false, no_scaling = false))] #[pyo3(signature = (file, speed = 1.0, brightness = None, scale = 1.0, colors = None, overwrite = false, register = false, no_scaling = false)
)]
fn save_as_movie( fn save_as_movie(
&self, &self,
file: PathBuf, file: PathBuf,
@@ -1813,7 +1810,8 @@ submit! {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")] #[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")]
#[pyfunction] #[pyfunction]
#[pyo3(signature = (files_in, files_out, operations = None, colors = None, overwrite = false, bar = true, message = None))] #[pyo3(signature = (files_in, files_out, operations = None, colors = None, overwrite = false, bar = true, message = None)
)]
fn batch_to_tiff( fn batch_to_tiff(
py: Python, py: Python,
files_in: Vec<PathBuf>, files_in: Vec<PathBuf>,
@@ -1919,7 +1917,8 @@ impl PyShape {
) )
} }
#[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing")))] #[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing")
))]
fn __getitem__<'py>( fn __getitem__<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
+31 -9
View File
@@ -7,6 +7,7 @@ use std::path::{Path, PathBuf};
pub use crate::readers::{ArrayT, PixelType, Reader}; pub use crate::readers::{ArrayT, PixelType, Reader};
use crate::readers::{DynReader, Frame, Shape}; use crate::readers::{DynReader, Frame, Shape};
use itertools::Itertools;
use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder}; use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder};
use std::cell::OnceCell; use std::cell::OnceCell;
use std::collections::HashSet; use std::collections::HashSet;
@@ -461,6 +462,22 @@ impl Drop for BioFormatsJavaReader {
} }
} }
fn find_tiff(path: PathBuf) -> Result<Option<PathBuf>, Error> {
if let Some(ext) = path.extension()
&& path.is_file()
&& (["tif", "tiff"].contains(&ext.to_string_lossy().to_lowercase().as_str()))
{
return Ok(Some(path));
} else if path.is_dir() {
for file in path.read_dir()?.flatten().sorted_by_key(|i| i.file_name()) {
if let Ok(Some(file)) = find_tiff(file.path()) {
return Ok(Some(file));
}
}
}
Ok(None)
}
impl Reader for BioFormatsJavaReader { impl Reader for BioFormatsJavaReader {
/// Create a new reader for the image file at a path, and open series #. /// Create a new reader for the image file at a path, and open series #.
fn new<P>(path: P, series: usize, _position: usize) -> Result<Self, Error> fn new<P>(path: P, series: usize, _position: usize) -> Result<Self, Error>
@@ -470,13 +487,10 @@ impl Reader for BioFormatsJavaReader {
DebugTools::set_root_level("ERROR")?; DebugTools::set_root_level("ERROR")?;
let mut path = path.as_ref().to_path_buf(); let mut path = path.as_ref().to_path_buf();
if path.is_dir() { if path.is_dir() {
for file in path.read_dir()?.flatten() { let orig = path.clone();
let p = file.path(); path = find_tiff(path)?.ok_or_else(|| {
if file.path().is_file() && (p.extension() == Some("tif".as_ref())) { Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string())
path = p; })?;
break;
}
}
} }
let mut new = BioFormatsJavaReader { let mut new = BioFormatsJavaReader {
reader: ThreadLocal::default(), reader: ThreadLocal::default(),
@@ -549,9 +563,16 @@ impl Reader for BioFormatsJavaReader {
P: AsRef<Path>, P: AsRef<Path>,
{ {
DebugTools::set_root_level("ERROR")?; DebugTools::set_root_level("ERROR")?;
let mut path = path.as_ref().to_path_buf();
if path.is_dir() {
let orig = path.clone();
path = find_tiff(path)?.ok_or_else(|| {
Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string())
})?;
}
let new = BioFormatsJavaReader { let new = BioFormatsJavaReader {
reader: ThreadLocal::default(), reader: ThreadLocal::default(),
path: path.as_ref().to_path_buf(), path,
series: 0, series: 0,
shape: Shape::default(), shape: Shape::default(),
pixel_type: PixelType::I8, pixel_type: PixelType::I8,
@@ -594,7 +615,8 @@ mod tests {
metadata_e: "czi/YTL1849A131_2023_05_04__13_36_36.czi", metadata_e: "czi/YTL1849A131_2023_05_04__13_36_36.czi",
metadata_f: "czi/EU_UV_t=1-01.czi", metadata_f: "czi/EU_UV_t=1-01.czi",
metadata_g: "tiffseq/4-Pos_001_002/img_000000000_Cy3-Cy3_filter_000.tif", metadata_g: "tiffseq/4-Pos_001_002/img_000000000_Cy3-Cy3_filter_000.tif",
metadata_h: "tiffseq/20-Pos_005_005/img_000000000_Cy3-Cy3_filter_000.tif" metadata_h: "tiffseq/20-Pos_005_005/img_000000000_Cy3-Cy3_filter_000.tif",
metadata_i: "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1",
} }
#[test] #[test]
+1 -1
View File
@@ -835,7 +835,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
.collect(); .collect();
for (ax, op) in self.operations.iter().skip(op_xy.len() + op_czt.len()) { for (ax, op) in self.operations.iter().skip(op_xy.len() + op_czt.len()) {
if let Some(idx) = ax_out.remove(ax) { if let Some(idx) = ax_out.remove(ax) {
for (_, i) in ax_out.iter_mut() { for i in ax_out.values_mut() {
if *i > idx { if *i > idx {
*i -= 1; *i -= 1;
} }
+3 -1
View File
@@ -10,7 +10,9 @@ from ndbioimage import Imread
"file", "file",
[ [
file file
for file in (Path(__file__).parent / "files").iterdir() for path in (Path(__file__).parent / "files").iterdir()
if path.is_dir() and path.name != "czi_xml"
for file in path.iterdir()
if not file.suffix == ".pzl" if not file.suffix == ".pzl"
], ],
) )
+1 -1
View File
@@ -18,7 +18,7 @@ def array():
@pytest.fixture() @pytest.fixture()
def image(array): def image(array):
with tempfile.TemporaryDirectory() as folder: with tempfile.TemporaryDirectory() as folder:
file = Path(folder) / "test.tif" file = Path(folder) / "tiff" / "test.tif"
tiffwrite(file, array, "yxczt") tiffwrite(file, array, "yxczt")
with Imread(file, axes="yxczt") as im: with Imread(file, axes="yxczt") as im:
yield im yield im
+1 -1
View File
@@ -17,7 +17,7 @@ def array():
@pytest.fixture() @pytest.fixture()
def image(array): def image(array):
with tempfile.TemporaryDirectory() as folder: with tempfile.TemporaryDirectory() as folder:
file = Path(folder) / "test.tif" file = Path(folder) / "tiff" / "test.tif"
tiffwrite(file, array, "yxczt") tiffwrite(file, array, "yxczt")
with Imread(file, axes="yxczt") as im: with Imread(file, axes="yxczt") as im:
yield im yield im