- add numpy flags

- all python objects pickleble
- transforms in python submodule
- fix tiffreader pxsize
- expose more of transforms to python
- loading transforms from yaml
This commit is contained in:
Wim Pomp committed 2026-09-30 20:48:42 +02:00
1 parent 6041cda475
commit 41aa841b14
13 files changed
+1202 -283

No files matched your search

+4 -1
View File
@@ -19,7 +19,7 @@ name = "ndbioimage"
crate-type = ["cdylib", "rlib"]
[dependencies]
bioformats = { version = "0.1", optional = true }
bioformats = { version = "0.1", optional = true, default-features = false, features = ["zarr", "tissuefaxs"] }
clap = { version = "4", features = ["derive"] }
console = { version = "0.16", optional = true }
downloader = { version = "0.2", optional = true, default-features = false, features = ["rustls-tls"] }
@@ -83,5 +83,8 @@ features = ["bioformats_java", "bioformats_rust", "czi", "movie", "tiffseq", "ti
[profile.test]
inherits = "release"
[profile.release]
debug = true
[profile.dev.package.backtrace]
opt-level = 3
+33 -10
View File
@@ -13,8 +13,14 @@ from numpy.typing import ArrayLike
os.environ["RUST_BACKTRACE"] = "full"
os.environ["COLORBT_SHOW_HIDDEN"] = "1"
from . import ndbioimage_rs as rs
from .ndbioimage_rs import Imread, Transform, Transforms
from .ndbioimage_rs import Imread, generate_stub
try:
from .ndbioimage_rs import transforms
sys.modules["ndbioimage.transforms"] = transforms
except (ImportError, ModuleNotFoundError):
pass
try:
from .ndbioimage_rs import batch_to_tiff
@@ -23,8 +29,12 @@ except ImportError:
try:
import ome_metadata # noqa
except ModuleNotFoundError:
sys.modules["ome_metadata"] = rs.ome_metadata # noqa
except (ImportError, ModuleNotFoundError):
from .ndbioimage_rs import ome_metadata # noqa
sys.modules["ome_metadata"] = ome_metadata
import ome_metadata # noqa
try:
__version__ = version(Path(__file__).parent.name)
@@ -58,13 +68,26 @@ def ndbioimage_generate_stub():
path = Path(sys.argv[1]).resolve()
else:
path = Path.cwd().resolve()
if (path / "py" / "ndbioimage" / "__init__.py").exists():
rs.generate_stub(str(path)) # noqa
nd = path / "py" / "ndbioimage"
if (nd / "__init__.pyi").exists():
(nd / "__init__.pyi").rename(nd / "ndbioimage.pyi")
try:
if (nd / "__init__.py").exists():
generate_stub(str(path)) # noqa
else:
raise ModuleNotFoundError(str(path / "py" / "ndbioimage" / "__init__.py"))
(path / "py" / "ndbioimage" / "__init__.pyi").rename(
path / "py" / "ndbioimage" / "ndbioimage_rs.pyi"
)
raise ModuleNotFoundError(str(nd / "__init__.py"))
rs = nd / "ndbioimage_rs"
rs.mkdir(parents=True, exist_ok=True)
(nd / "__init__.pyi").rename(rs / "__init__.pyi")
for stub in nd.glob("**/__init__.pyi"):
if stub.parent != rs:
stub.rename((rs / stub.parent.name).with_suffix(".pyi"))
stub.parent.rmdir()
except Exception as e:
raise e
finally:
if (nd / "ndbioimage.pyi").exists():
(nd / "ndbioimage.pyi").rename(nd / "__init__.pyi")
R = TypeVar("R")
+3
View File
@@ -0,0 +1,3 @@
from .ndbioimage_rs import Imread, Shape
__all__ = ["Imread", "Shape"]
@@ -9,15 +9,50 @@ import typing
import numpy
import numpy.typing
from . import transforms
__all__ = [
"Flags",
"Imread",
"Shape",
"Transform",
"Transforms",
"batch_to_tiff",
"generate_stub",
"main",
"transforms",
]
class Flags:
r"""
https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html
"""
@property
def c_contiguous(self) -> builtins.bool: ...
@property
def f_contiguous(self) -> builtins.bool: ...
@property
def owndata(self) -> builtins.bool: ...
@property
def writable(self) -> builtins.bool: ...
@property
def aligned(self) -> builtins.bool: ...
@property
def writebackifcopy(self) -> builtins.bool: ...
@property
def fnc(self) -> builtins.bool: ...
@property
def forc(self) -> builtins.bool: ...
@property
def behaved(self) -> builtins.bool: ...
@property
def carray(self) -> builtins.bool: ...
@property
def farray(self) -> builtins.bool: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __hash__(self) -> builtins.int: ...
def __new__(cls) -> Flags: ...
def __getnewargs__(self) -> None: ...
def __getitem__(self, item: builtins.str) -> builtins.bool: ...
class Imread:
r"""
class to read image files, while taking good care of important metadata,
@@ -48,16 +83,28 @@ class Imread:
TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var
"""
available_readers: builtins.list[builtins.str]
@property
def flags(self) -> Flags:
r"""
https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html
"""
@property
def reader_name(self) -> builtins.str:
r"""
the name of the reader used to open the file
"""
@property
def transform(self) -> Transforms:
def transforms(self) -> transforms.Transforms:
r"""
get the transformation
"""
@transforms.setter
def transforms(self, value: transforms.Transforms) -> None:
r"""
set the transformation
"""
@property
def path(self) -> pathlib.Path:
r"""
@@ -183,6 +230,15 @@ class Imread:
r"""
new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64)
"""
def set_flags(
self,
write: typing.Optional[builtins.bool] = None,
align: typing.Optional[builtins.bool] = None,
uic: typing.Optional[builtins.bool] = None,
) -> None:
r"""
https://numpy.org/doc/stable/reference/generated/numpy.ndarray.setflags.html#numpy.ndarray.setflags
"""
@staticmethod
def get_positions(
path: str | pathlib.Path | Imread | bytes,
@@ -210,22 +266,31 @@ class Imread:
) -> Imread:
r"""
return a new view with transformations applied (channel alignment, drift correction)
backward compatibility
"""
def set_transform(self, transform: Transforms) -> None:
def with_transforms(
self,
channels: builtins.bool = True,
drift: builtins.bool = False,
file: typing.Optional[typing.Any] = None,
bead_files: typing.Optional[typing.Any] = None,
main_channel: typing.Optional[builtins.int] = None,
default_transform: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Imread:
r"""
set the transformation
return a new view with transformations applied (channel alignment, drift correction)
"""
def load_transform_from_yaml(
def load_transforms_from_yaml(
self, path: builtins.str | os.PathLike | pathlib.Path
) -> None: ...
def calculate_channel_transforms_2d(
self, main_channel: builtins.int
) -> builtins.list[Transform]: ...
) -> builtins.dict[builtins.str, transforms.Transform]: ...
def calculate_channel_transforms_3d(
self, main_channel: builtins.int
) -> builtins.list[Transform]: ...
def calculate_drift_transform_2d(self) -> builtins.list[Transform]: ...
def calculate_drift_transform_3d(self) -> builtins.list[Transform]: ...
) -> builtins.dict[builtins.str, transforms.Transform]: ...
def calculate_drift_transforms_2d(self) -> builtins.list[transforms.Transform]: ...
def calculate_drift_transforms_3d(self) -> builtins.list[transforms.Transform]: ...
def squeeze(self) -> numpy.ndarray | int | float: ...
def close(self) -> None:
r"""
@@ -678,7 +743,7 @@ class Shape:
"""
def __getitem__(
self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int]
) -> typing.Optional[int | list[int]]:
) -> typing.Optional[int | tuple[int]]:
r"""
get dimension size by index or axis name
"""
@@ -691,127 +756,6 @@ class Shape:
convert shape to a list of dimension sizes in order
"""
class Transform:
@property
def parameters(self) -> builtins.list[builtins.float]: ...
@parameters.setter
def parameters(self, value: typing.Sequence[builtins.float]) -> None: ...
@property
def dparameters(self) -> builtins.list[builtins.float]: ...
@dparameters.setter
def dparameters(self, value: typing.Sequence[builtins.float]) -> None: ...
@property
def center(self) -> builtins.list[builtins.float]: ...
@center.setter
def center(self, value: typing.Sequence[builtins.float]) -> None: ...
@property
def shape(self) -> builtins.list[builtins.int]: ...
@shape.setter
def shape(self, value: typing.Sequence[builtins.int]) -> None: ...
@property
def ndim(self) -> builtins.int: ...
@property
def matrix(self) -> numpy.typing.NDArray[numpy.float64]: ...
@matrix.setter
def matrix(self, value: numpy.typing.ArrayLike) -> None: ...
@property
def dmatrix(self) -> numpy.typing.NDArray[numpy.float64]: ...
@dmatrix.setter
def dmatrix(self, value: numpy.typing.ArrayLike) -> None: ...
@property
def inverse(self) -> Transform: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
parameters: typing.Sequence[builtins.float],
shape: typing.Sequence[builtins.int],
center: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Transform: ...
def __getnewargs__(
self,
) -> tuple[
builtins.list[builtins.float],
builtins.list[builtins.int],
typing.Optional[builtins.list[builtins.float]],
]: ...
def __getstate__(self) -> builtins.list[builtins.float]: ...
def __setstate__(self, state: typing.Sequence[builtins.float]) -> None: ...
def __add__(self, other: Transform) -> Transform: ...
def __radd__(self, other: Transform) -> Transform: ...
def __sub__(self, other: Transform) -> Transform: ...
def __rsub__(self, other: Transform) -> Transform: ...
def __mul__(self, other: Transform | float) -> Transform: ...
def __rmul__(self, other: Transform | float) -> Transform: ...
def __truediv__(self, other: builtins.float) -> Transform: ...
def adapt(
self,
center: typing.Sequence[builtins.float],
shape: typing.Sequence[builtins.int],
) -> None: ...
@staticmethod
def from_scaling(scaling: typing.Sequence[builtins.float]) -> Transform: ...
@staticmethod
def from_translation(translation: typing.Sequence[builtins.float]) -> Transform: ...
@staticmethod
def from_rotation(
theta: builtins.float, center: typing.Sequence[builtins.float]
) -> Transform: ...
def with_scaling(self, scaling: typing.Sequence[builtins.float]) -> Transform: ...
def with_translation(
self, translation: typing.Sequence[builtins.float]
) -> Transform: ...
def with_rotation(
self, theta: builtins.float, center: typing.Sequence[builtins.float]
) -> Transform: ...
def interpolate(
self, order: builtins.int, image: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
def interpolate_par(
self, order: builtins.int, image: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
def is_unity(self) -> builtins.bool: ...
def transform_point(
self, point: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
def transform_points(
self, points: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
@staticmethod
def register(
fixed: numpy.typing.ArrayLike,
moving: numpy.typing.ArrayLike,
fixed_mu: typing.Sequence[typing.Optional[builtins.float]],
initial_guess: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Transform: ...
@staticmethod
def register_affine(
fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike
) -> Transform: ...
@staticmethod
def register_translation(
fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike
) -> Transform: ...
class Transforms:
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __getstate__(self) -> builtins.list[builtins.int]: ...
def __setstate__(self, state: typing.Sequence[builtins.int]) -> None: ...
@staticmethod
def load(path: builtins.str | os.PathLike | pathlib.Path) -> Transforms: ...
def save(self, path: builtins.str | os.PathLike | pathlib.Path) -> None: ...
@staticmethod
def calculate_channel_transforms_2d(
bead_files: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
main_channel: builtins.int,
default_transform: typing.Optional[Transform],
) -> builtins.list[Transform]: ...
@staticmethod
def calculate_channel_transforms_3d(
bead_files: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
main_channel: builtins.int,
default_transform: typing.Optional[Transform],
) -> builtins.list[Transform]: ...
def batch_to_tiff(
files_in: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
files_out: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
@@ -827,6 +771,11 @@ def batch_to_tiff(
batch convert multiple image files to TIFF format
"""
def generate_stub(dest_path: builtins.str) -> None:
r"""
generates ndbioimage/__init__.pyi
"""
def main() -> None:
r"""
main entry point for the command-line interface
+244
View File
@@ -0,0 +1,244 @@
# This file is automatically generated by pyo3_stub_gen
# ruff: noqa: E501, F401, F403, F405
import builtins
import os
import pathlib
import typing
import numpy
import numpy.typing
__all__ = [
"Optimizer",
"RegistrationStep",
"SamplingArg",
"Sigma",
"Transform",
"Transforms",
]
class Optimizer:
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(cls, optimizer: builtins.str) -> Optimizer: ...
def __getnewargs__(self) -> tuple[builtins.str]: ...
@staticmethod
def lbfgs() -> Optimizer: ...
@staticmethod
def asgd() -> Optimizer: ...
class RegistrationStep:
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
sigma: Sigma,
sample: SamplingArg,
n_bins: builtins.int,
tolerance: builtins.float,
edge: builtins.float,
max_iterations: builtins.int,
learning_rate: builtins.float,
optimizer: Optimizer,
) -> RegistrationStep: ...
def __getnewargs__(
self,
) -> tuple[
Sigma,
SamplingArg,
builtins.int,
builtins.float,
builtins.float,
builtins.int,
builtins.float,
Optimizer,
]: ...
@staticmethod
def default_steps(
ndim: builtins.int, n: builtins.int
) -> builtins.list[RegistrationStep]: ...
class SamplingArg:
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
fixed: typing.Optional[builtins.int] = None,
fixed_at: typing.Optional[
typing.Sequence[typing.Sequence[builtins.float]]
] = None,
random: typing.Optional[builtins.int] = None,
) -> SamplingArg: ...
def __getnewargs__(
self,
) -> tuple[
typing.Optional[builtins.int],
typing.Optional[builtins.list[builtins.list[builtins.float]]],
typing.Optional[builtins.int],
]: ...
@staticmethod
def fixed(size: builtins.int) -> SamplingArg: ...
@staticmethod
def fixed_at(
f: typing.Sequence[typing.Sequence[builtins.float]],
) -> SamplingArg: ...
@staticmethod
def random(size: builtins.int) -> SamplingArg: ...
class Sigma:
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
absolute: typing.Optional[typing.Sequence[builtins.float]] = None,
relative: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Sigma: ...
def __getnewargs__(
self,
) -> tuple[builtins.str, typing.Optional[builtins.list[builtins.float]]]: ...
@staticmethod
def none() -> Sigma: ...
@staticmethod
def absolute(sigma: typing.Sequence[builtins.float]) -> Sigma: ...
@staticmethod
def relative(sigma: typing.Sequence[builtins.float]) -> Sigma: ...
def smooth(
self, array: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
def sigma(
self, shape: typing.Sequence[builtins.int]
) -> typing.Optional[builtins.list[builtins.float]]: ...
class Transform:
@property
def parameters(self) -> builtins.list[builtins.float]: ...
@parameters.setter
def parameters(self, value: typing.Sequence[builtins.float]) -> None: ...
@property
def dparameters(self) -> builtins.list[builtins.float]: ...
@dparameters.setter
def dparameters(self, value: typing.Sequence[builtins.float]) -> None: ...
@property
def center(self) -> builtins.list[builtins.float]: ...
@center.setter
def center(self, value: typing.Sequence[builtins.float]) -> None: ...
@property
def shape(self) -> builtins.list[builtins.int]: ...
@shape.setter
def shape(self, value: typing.Sequence[builtins.int]) -> None: ...
@property
def ndim(self) -> builtins.int: ...
@property
def matrix(self) -> numpy.typing.NDArray[numpy.float64]: ...
@matrix.setter
def matrix(self, value: numpy.typing.ArrayLike) -> None: ...
@property
def dmatrix(self) -> numpy.typing.NDArray[numpy.float64]: ...
@dmatrix.setter
def dmatrix(self, value: numpy.typing.ArrayLike) -> None: ...
@property
def inverse(self) -> Transform: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
parameters: typing.Optional[typing.Sequence[builtins.float]] = None,
shape: typing.Optional[typing.Sequence[builtins.int]] = None,
center: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Transform: ...
def __getnewargs__(
self,
) -> tuple[
builtins.list[builtins.float],
builtins.list[builtins.int],
typing.Optional[builtins.list[builtins.float]],
]: ...
def __getstate__(self) -> builtins.list[builtins.float]: ...
def __setstate__(self, state: typing.Sequence[builtins.float]) -> None: ...
def __add__(self, other: Transform) -> Transform: ...
def __radd__(self, other: Transform) -> Transform: ...
def __sub__(self, other: Transform) -> Transform: ...
def __rsub__(self, other: Transform) -> Transform: ...
def __mul__(self, other: Transform | float) -> Transform: ...
def __rmul__(self, other: Transform | float) -> Transform: ...
def __truediv__(self, other: builtins.float) -> Transform: ...
def adapt(
self,
center: typing.Sequence[builtins.float],
shape: typing.Sequence[builtins.int],
) -> None: ...
@staticmethod
def from_scaling(scaling: typing.Sequence[builtins.float]) -> Transform: ...
@staticmethod
def from_translation(translation: typing.Sequence[builtins.float]) -> Transform: ...
@staticmethod
def from_rotation(
theta: builtins.float, center: typing.Sequence[builtins.float]
) -> Transform: ...
def with_scaling(self, scaling: typing.Sequence[builtins.float]) -> Transform: ...
def with_translation(
self, translation: typing.Sequence[builtins.float]
) -> Transform: ...
def with_rotation(
self, theta: builtins.float, center: typing.Sequence[builtins.float]
) -> Transform: ...
def interpolate(
self, order: builtins.int, image: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
def interpolate_par(
self, order: builtins.int, image: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
def is_unity(self) -> builtins.bool: ...
def transform_point(
self, point: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
def transform_points(
self, points: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ...
@staticmethod
def register(
fixed: numpy.typing.ArrayLike,
moving: numpy.typing.ArrayLike,
fixed_mu: typing.Sequence[typing.Optional[builtins.float]],
initial_guess: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Transform: ...
@staticmethod
def register_affine(
fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike
) -> Transform: ...
@staticmethod
def register_translation(
fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike
) -> Transform: ...
class Transforms:
@property
def channel_transforms(self) -> builtins.dict[builtins.str, Transform]: ...
@channel_transforms.setter
def channel_transforms(
self, value: typing.Mapping[builtins.str, Transform]
) -> None: ...
@property
def drift_transforms(self) -> builtins.list[Transform]: ...
@drift_transforms.setter
def drift_transforms(self, value: typing.Sequence[Transform]) -> None: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
channel: typing.Mapping[builtins.str, Transform] = {},
drift: typing.Sequence[Transform] = [],
) -> Transforms: ...
def __getnewargs__(
self,
) -> tuple[builtins.dict[builtins.str, Transform], builtins.list[Transform]]: ...
@staticmethod
def load(path: builtins.str | os.PathLike | pathlib.Path) -> Transforms: ...
def save(self, path: builtins.str | os.PathLike | pathlib.Path) -> None: ...
@staticmethod
def calculate_channel_transforms_2d(
bead_files: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
main_channel: builtins.int,
default_transform: typing.Optional[Transform],
) -> builtins.dict[builtins.str, Transform]: ...
@staticmethod
def calculate_channel_transforms_3d(
bead_files: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
main_channel: builtins.int,
default_transform: typing.Optional[Transform],
) -> builtins.dict[builtins.str, Transform]: ...
+17
View File
@@ -0,0 +1,17 @@
from .ndbioimage_rs.transforms import (
Optimizer,
RegistrationStep,
SamplingArg,
Sigma,
Transform,
Transforms,
)
__all__ = [
"Optimizer",
"RegistrationStep",
"SamplingArg",
"Sigma",
"Transform",
"Transforms",
]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ndbioimage"
version = "2027.0.4"
version = "2027.0.5"
requires-python = ">=3.10"
classifiers = [
"License :: OSI Approved :: MIT License",
+21 -3
View File
@@ -41,6 +41,7 @@ pub(crate) fn ndbioimage_file() -> PathBuf {
}
/// generates ndbioimage/__init__.pyi
#[gen_stub_pyfunction]
#[pyfunction]
pub fn generate_stub(dest_path: String) -> PyResult<()> {
Ok(StubInfo::from_project_root(
@@ -84,14 +85,31 @@ mod ndbioimage_rs {
#[pymodule_export]
use super::imread::PyShape;
#[pymodule_export]
use super::imread::PyFlags;
#[pymodule_export]
use ome_metadata::py::ome_metadata;
#[cfg(feature = "transforms")]
#[pymodule]
mod transforms {
#[pymodule_export]
use super::transforms::PyTransform;
use super::super::transforms::PyTransform;
#[cfg(feature = "transforms")]
#[pymodule_export]
use super::transforms::PyTransforms;
use super::super::transforms::PyTransforms;
#[pymodule_export]
use super::super::transforms::PyRegistrationStep;
#[pymodule_export]
use super::super::transforms::PySigma;
#[pymodule_export]
use super::super::transforms::PySamplingArg;
#[pymodule_export]
use super::super::transforms::PyOptimizer;
}
}
+178 -19
View File
@@ -15,7 +15,9 @@ use numpy::{
};
use ome_metadata::Ome;
use postcard::{from_bytes, to_stdvec};
use pyo3::exceptions::{PyIndexError, PyNotImplementedError, PyTypeError, PyValueError};
use pyo3::exceptions::{
PyIndexError, PyKeyError, PyNotImplementedError, PyTypeError, PyValueError,
};
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyEllipsis, PyInt, PyList, PyNone, PySlice, PyString, PyTuple};
pub(crate) use pyo3::{
@@ -25,7 +27,7 @@ pub(crate) use pyo3::{
use pyo3_stub_gen::derive::*;
use pyo3_stub_gen::inventory::submit;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
/// class to read image files, while taking good care of important metadata,
@@ -204,6 +206,42 @@ impl PyView {
}
}
/// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html
#[getter]
fn flags(&self) -> PyFlags {
PyFlags::new()
}
/// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.setflags.html#numpy.ndarray.setflags
#[pyo3(signature = (write = None, align = None, uic = None))]
fn set_flags(
&self,
write: Option<bool>,
align: Option<bool>,
uic: Option<bool>,
) -> PyResult<()> {
if let Some(write) = write
&& write
{
Err(PyValueError::new_err("write cannot be set True"))
} else if let Some(align) = align
&& align
{
Err(PyValueError::new_err("align cannot be set True"))
} else if let Some(uic) = uic
&& uic
{
Err(PyValueError::new_err("uic cannot be set True"))
} else {
Ok(())
}
}
#[classattr]
fn available_readers() -> Vec<String> {
DynReader::available_readers()
}
/// get all available positions (series) in the file
#[staticmethod]
fn get_positions<'py>(
@@ -232,11 +270,34 @@ impl PyView {
todo!()
}
/// return a new view with transformations applied (channel alignment, drift correction)
/// backward compatibility
#[cfg(feature = "transforms")]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))]
fn with_transform<'py>(
&self,
channels: bool,
drift: bool,
file: Option<Bound<'py, PyAny>>,
bead_files: Option<Bound<'py, PyAny>>,
main_channel: Option<usize>,
default_transform: Option<Vec<f64>>,
) -> PyResult<PyView> {
self.with_transforms(
channels,
drift,
file,
bead_files,
main_channel,
default_transform,
)
}
/// return a new view with transformations applied (channel alignment, drift correction)
#[cfg(feature = "transforms")]
#[expect(unused_variables)]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))]
fn with_transform<'py>(
fn with_transforms<'py>(
&self,
channels: bool,
drift: bool,
@@ -251,21 +312,22 @@ impl PyView {
/// get the transformation
#[cfg(feature = "transforms")]
#[getter]
fn get_transform(&self) -> PyResult<PyTransforms> {
fn get_transforms(&self) -> PyResult<PyTransforms> {
Ok(PyTransforms {
inner: self.view.transforms.clone(),
inner: self.view.get_transforms().clone(),
})
}
/// set the transformation
#[cfg(feature = "transforms")]
fn set_transform(&mut self, transform: PyTransforms) {
self.view.transforms = transform.inner;
#[setter]
fn set_transforms(&mut self, transform: PyTransforms) {
self.view.set_transforms(transform.inner);
}
#[cfg(feature = "transforms")]
fn load_transform_from_yaml(&mut self, path: PathBuf) -> PyResult<()> {
self.view.load_transform_from_yaml(path.as_path())?;
fn load_transforms_from_yaml(&mut self, path: PathBuf) -> PyResult<()> {
self.view.load_transforms_from_yaml(path.as_path())?;
Ok(())
}
@@ -274,14 +336,19 @@ impl PyView {
&self,
py: Python,
main_channel: usize,
) -> PyResult<Vec<PyTransform>> {
) -> PyResult<HashMap<String, PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_channel_transforms_2d(main_channel)?
.into_iter()
.map(|t| PyTransform {
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
})
@@ -292,25 +359,30 @@ impl PyView {
&self,
py: Python,
main_channel: usize,
) -> PyResult<Vec<PyTransform>> {
) -> PyResult<HashMap<String, PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_channel_transforms_3d(main_channel)?
.into_iter()
.map(|t| PyTransform {
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
})
}
#[cfg(feature = "transforms")]
fn calculate_drift_transform_2d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
fn calculate_drift_transforms_2d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_drift_transform_2d()?
.calculate_drift_transforms_2d()?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
@@ -320,11 +392,11 @@ impl PyView {
}
#[cfg(feature = "transforms")]
fn calculate_drift_transform_3d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
fn calculate_drift_transforms_3d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_drift_transform_3d()?
.calculate_drift_transforms_3d()?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
@@ -2094,7 +2166,7 @@ impl PyShape {
}
/// get dimension size by index or axis name
#[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing")
#[gen_stub(override_return_type(type_repr="typing.Optional[int | tuple[int]]", imports=("typing")
))]
fn __getitem__<'py>(
&self,
@@ -2185,7 +2257,7 @@ impl PyShape {
} else if shape.len() == 1 {
Ok(shape[0].into_bound_py_any(py)?)
} else {
Ok(shape.into_bound_py_any(py)?)
Ok(PyTuple::new(py, shape)?.into_bound_py_any(py)?)
}
}
@@ -2215,3 +2287,90 @@ impl PyShape {
.collect::<String>()
}
}
/// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html
#[gen_stub_pyclass]
#[pyclass(
subclass,
from_py_object,
frozen,
eq,
hash,
name = "Flags",
module = "ndbioimage"
)]
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub(crate) struct PyFlags {
#[pyo3(get)]
c_contiguous: bool,
#[pyo3(get)]
f_contiguous: bool,
#[pyo3(get)]
owndata: bool,
#[pyo3(get)]
writable: bool,
#[pyo3(get)]
aligned: bool,
#[pyo3(get)]
writebackifcopy: bool,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyFlags {
#[new]
fn new() -> Self {
Self {
c_contiguous: false,
f_contiguous: false,
owndata: false,
writable: false,
aligned: false,
writebackifcopy: false,
}
}
fn __getnewargs__(&self) {}
fn __getitem__(&self, item: String) -> PyResult<bool> {
match item.to_lowercase().as_str() {
"c_contiguous" | "c" => Ok(self.c_contiguous),
"f_contiguous" | "f" => Ok(self.f_contiguous),
"owndata" | "o" => Ok(self.owndata),
"writable" | "w" => Ok(self.writable),
"aligned" | "a" => Ok(self.aligned),
"writebackifcopy" | "x" => Ok(self.writebackifcopy),
"fnc" => Ok(self.fnc()),
"forc" => Ok(self.forc()),
"behaved" | "b" => Ok(self.behaved()),
"carray" | "ca" => Ok(self.carray()),
"farray" | "fa" => Ok(self.farray()),
_ => Err(PyKeyError::new_err(format!("no such key: {}", item))),
}
}
#[getter]
fn fnc(&self) -> bool {
self.c_contiguous && !self.f_contiguous
}
#[getter]
fn forc(&self) -> bool {
self.c_contiguous || self.f_contiguous
}
#[getter]
fn behaved(&self) -> bool {
self.aligned && self.writable
}
#[getter]
fn carray(&self) -> bool {
self.behaved() && self.c_contiguous
}
#[getter]
fn farray(&self) -> bool {
self.behaved() && self.f_contiguous && !self.c_contiguous
}
}
+354 -13
View File
@@ -1,16 +1,18 @@
use crate::error::Error;
use crate::transforms::Transforms;
use image_registration::metric::{SamplingArg, Sigma};
use image_registration::register::{Optimizer, RegistrationStep};
use image_registration::transform::Transform;
use ndarray::{Ix0, Ix1, Ix2, Ix3, Ix4, Ix5, IxDyn};
use numpy::{
AllowTypeChange, IntoPyArray, PyArray, PyArray1, PyArray2, PyArrayDyn, PyArrayLike1,
PyArrayLike2, PyArrayLikeDyn,
};
use postcard::{from_bytes, to_stdvec};
use pyo3::exceptions::{PyNotImplementedError, PyValueError};
use pyo3::prelude::*;
use pyo3_stub_gen::derive::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[gen_stub_pyclass]
@@ -19,7 +21,7 @@ use std::path::PathBuf;
from_py_object,
eq,
name = "Transform",
module = "ndbioimage"
module = "ndbioimage.transforms"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PyTransform {
@@ -30,8 +32,14 @@ pub(crate) struct PyTransform {
#[pymethods]
impl PyTransform {
#[new]
#[pyo3(signature = (parameters, shape, center = None))]
fn new(parameters: Vec<f64>, shape: Vec<usize>, center: Option<Vec<f64>>) -> Self {
#[pyo3(signature = (parameters = None, shape = None, center = None))]
fn new(
parameters: Option<Vec<f64>>,
shape: Option<Vec<usize>>,
center: Option<Vec<f64>>,
) -> Self {
let parameters = parameters.unwrap_or_else(|| vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
let shape = shape.unwrap_or_else(|| vec![512, 512]);
if let Some(center) = center {
Self {
inner: Transform::new_with_center(parameters, center, shape),
@@ -598,7 +606,7 @@ impl PyTransform {
from_py_object,
eq,
name = "Transforms",
module = "ndbioimage"
module = "ndbioimage.transforms"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PyTransforms {
@@ -608,12 +616,51 @@ pub(crate) struct PyTransforms {
#[gen_stub_pymethods]
#[pymethods]
impl PyTransforms {
pub(crate) fn __getstate__(&self) -> PyResult<Vec<u8>> {
Ok(to_stdvec(self).map_err(Error::from)?)
#[new]
#[pyo3(signature = (channel = HashMap::new(), drift = Vec::new()))]
fn new(
channel: HashMap<String, PyTransform>,
drift: Vec<PyTransform>,
) -> PyResult<PyTransforms> {
let channel = channel.into_iter().map(|(n, t)| (n, t.inner)).collect();
let drift = drift.into_iter().map(|t| t.inner).collect();
Ok(PyTransforms {
inner: Transforms { channel, drift },
})
}
pub(crate) fn __setstate__(&mut self, state: Vec<u8>) -> PyResult<()> {
Ok(from_bytes(&state).map_err(Error::from)?)
#[getter]
fn get_channel_transforms(&self) -> HashMap<String, PyTransform> {
self.inner
.channel
.iter()
.map(|(n, t)| (n.clone(), PyTransform { inner: t.clone() }))
.collect()
}
#[setter]
fn set_channel_transforms(&mut self, transforms: HashMap<String, PyTransform>) -> PyResult<()> {
self.inner.channel = transforms.into_iter().map(|(n, t)| (n, t.inner)).collect();
Ok(())
}
#[getter]
fn get_drift_transforms(&self) -> Vec<PyTransform> {
self.inner
.drift
.iter()
.map(|t| PyTransform { inner: t.clone() })
.collect()
}
#[setter]
fn set_drift_transforms(&mut self, transforms: Vec<PyTransform>) -> PyResult<()> {
self.inner.drift = transforms.into_iter().map(|t| t.inner).collect();
Ok(())
}
fn __getnewargs__(&self) -> (HashMap<String, PyTransform>, Vec<PyTransform>) {
(self.get_channel_transforms(), self.get_drift_transforms())
}
#[staticmethod]
@@ -632,7 +679,7 @@ impl PyTransforms {
bead_files: Vec<PathBuf>,
main_channel: usize,
default_transform: Option<PyTransform>,
) -> PyResult<Vec<PyTransform>> {
) -> PyResult<HashMap<String, PyTransform>> {
Ok(Transforms::calculate_channel_transforms_2d(
&bead_files
.iter()
@@ -644,8 +691,13 @@ impl PyTransforms {
.transpose()?,
)?
.into_iter()
.map(|t| PyTransform {
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
}
@@ -655,7 +707,7 @@ impl PyTransforms {
bead_files: Vec<PathBuf>,
main_channel: usize,
default_transform: Option<PyTransform>,
) -> PyResult<Vec<PyTransform>> {
) -> PyResult<HashMap<String, PyTransform>> {
Ok(Transforms::calculate_channel_transforms_3d(
&bead_files
.iter()
@@ -667,9 +719,298 @@ impl PyTransforms {
.transpose()?,
)?
.into_iter()
.map(|t| PyTransform {
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
}
}
#[gen_stub_pyclass]
#[pyclass(
subclass,
from_py_object,
eq,
name = "RegistrationStep",
module = "ndbioimage.transforms"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PyRegistrationStep {
pub(crate) inner: RegistrationStep,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyRegistrationStep {
#[new]
#[expect(clippy::too_many_arguments)]
fn new(
sigma: PySigma,
sample: PySamplingArg,
n_bins: usize,
tolerance: f64,
edge: f64,
max_iterations: usize,
learning_rate: f64,
optimizer: PyOptimizer,
) -> PyResult<PyRegistrationStep> {
Ok(PyRegistrationStep {
inner: RegistrationStep::new(
sigma.inner,
sample.inner,
n_bins,
tolerance,
edge,
max_iterations,
learning_rate,
optimizer.inner,
),
})
}
fn __getnewargs__(
&self,
) -> (
PySigma,
PySamplingArg,
usize,
f64,
f64,
usize,
f64,
PyOptimizer,
) {
(
PySigma {
inner: self.inner.sigma.clone(),
},
PySamplingArg {
inner: self.inner.samples.clone(),
},
self.inner.n_bins,
self.inner.tolerance,
self.inner.edge,
self.inner.max_iterations,
self.inner.learning_rate,
PyOptimizer {
inner: self.inner.optimizer.clone(),
},
)
}
#[staticmethod]
fn default_steps(ndim: usize, n: usize) -> Vec<PyRegistrationStep> {
RegistrationStep::default_steps(ndim, n)
.into_iter()
.map(|s| PyRegistrationStep { inner: s })
.collect()
}
}
#[gen_stub_pyclass]
#[pyclass(
subclass,
from_py_object,
eq,
name = "Sigma",
module = "ndbioimage.transforms"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PySigma {
pub(crate) inner: Sigma,
}
#[gen_stub_pymethods]
#[pymethods]
impl PySigma {
#[new]
#[pyo3(signature = (absolute = None, relative = None))]
fn new(absolute: Option<Vec<f64>>, relative: Option<Vec<f64>>) -> Self {
if let Some(absolute) = absolute {
Self {
inner: Sigma::Absolute(absolute),
}
} else if let Some(relative) = relative {
Self {
inner: Sigma::Relative(relative),
}
} else {
Self { inner: Sigma::None }
}
}
fn __getnewargs__(&self) -> (String, Option<Vec<f64>>) {
match &self.inner {
Sigma::None => ("none".to_string(), None),
Sigma::Absolute(sigma) => ("absolute".to_string(), Some(sigma.clone())),
Sigma::Relative(sigma) => ("relative".to_string(), Some(sigma.clone())),
}
}
#[staticmethod]
fn none() -> PySigma {
PySigma { inner: Sigma::None }
}
#[staticmethod]
fn absolute(sigma: Vec<f64>) -> PySigma {
PySigma {
inner: Sigma::Absolute(sigma),
}
}
#[staticmethod]
fn relative(sigma: Vec<f64>) -> PySigma {
PySigma {
inner: Sigma::Relative(sigma),
}
}
fn smooth<'py>(
&self,
py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
array: PyArrayLikeDyn<f64, AllowTypeChange>,
) -> PyResult<Bound<'py, PyArrayDyn<f64>>> {
let array = array.as_array();
Ok(py
.detach(|| self.inner.smooth(array))
.map_err(Error::from)?
.into_pyarray(py))
}
fn sigma(&self, shape: Vec<usize>) -> Option<Vec<f64>> {
self.inner.sigma(shape.as_slice())
}
}
#[gen_stub_pyclass]
#[pyclass(
subclass,
from_py_object,
eq,
name = "SamplingArg",
module = "ndbioimage.transforms"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PySamplingArg {
pub(crate) inner: SamplingArg,
}
#[gen_stub_pymethods]
#[pymethods]
impl PySamplingArg {
#[new]
#[pyo3(signature = (fixed = None, fixed_at = None, random = None))]
fn new(
fixed: Option<usize>,
fixed_at: Option<Vec<Vec<f64>>>,
random: Option<usize>,
) -> PyResult<Self> {
if let Some(fixed) = fixed {
Ok(Self {
inner: SamplingArg::Fixed(fixed),
})
} else if let Some(fixed_at) = fixed_at {
Ok(Self {
inner: SamplingArg::FixedAt(fixed_at),
})
} else if let Some(random) = random {
Ok(Self {
inner: SamplingArg::Random(random),
})
} else {
Err(PyValueError::new_err(
"missing argument 'fixed', 'fixed_at' or 'random'",
))
}
}
fn __getnewargs__(&self) -> (Option<usize>, Option<Vec<Vec<f64>>>, Option<usize>) {
match &self.inner {
SamplingArg::Fixed(fixed) => (Some(*fixed), None, None),
SamplingArg::FixedAt(fixed_at) => (None, Some(fixed_at.clone()), None),
SamplingArg::Random(usize) => (None, None, Some(*usize)),
}
}
#[staticmethod]
fn fixed(size: usize) -> PySamplingArg {
PySamplingArg {
inner: SamplingArg::Fixed(size),
}
}
#[staticmethod]
fn fixed_at(f: Vec<Vec<f64>>) -> PySamplingArg {
PySamplingArg {
inner: SamplingArg::FixedAt(f),
}
}
#[staticmethod]
fn random(size: usize) -> PySamplingArg {
PySamplingArg {
inner: SamplingArg::Random(size),
}
}
}
#[gen_stub_pyclass]
#[pyclass(
subclass,
from_py_object,
eq,
name = "Optimizer",
module = "ndbioimage.transforms"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PyOptimizer {
pub(crate) inner: Optimizer,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyOptimizer {
#[new]
fn new(optimizer: String) -> PyResult<Self> {
match optimizer.to_lowercase().as_str() {
"lbfgs" => Ok(Self {
inner: Optimizer::LBFGS,
}),
"asgd" => Ok(Self {
inner: Optimizer::ASGD,
}),
_ => Err(PyValueError::new_err(format!(
"invalid optimizer: {}",
optimizer
))),
}
}
fn __getnewargs__(&self) -> (String,) {
(match &self.inner {
Optimizer::LBFGS => "lbfgs".to_string(),
Optimizer::ASGD => "asgd".to_string(),
},)
}
#[staticmethod]
fn lbfgs() -> PyOptimizer {
PyOptimizer {
inner: Optimizer::LBFGS,
}
}
#[staticmethod]
fn asgd() -> PyOptimizer {
PyOptimizer {
inner: Optimizer::ASGD,
}
}
}
+21 -3
View File
@@ -229,6 +229,7 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
P: AsRef<Path>;
}
/// Bioformats numbering
impl TryFrom<i32> for PixelType {
type Error = Error;
@@ -257,6 +258,7 @@ impl FromStr for PixelType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"bool" | "bit" => Ok(PixelType::Bool),
"int8" | "i8" => Ok(PixelType::I8),
"uint8" | "u8" => Ok(PixelType::U8),
"int16" | "i16" => Ok(PixelType::I16),
@@ -683,6 +685,22 @@ impl Reader for DynReader {
}
impl DynReader {
/// list of enabled readers
pub fn available_readers() -> Vec<String> {
vec![
#[cfg(feature = "tiff")]
"tiff".to_string(),
#[cfg(feature = "tiffseq")]
"tiffseq".to_string(),
#[cfg(feature = "czi")]
"czi".to_string(),
#[cfg(feature = "bioformats_rust")]
"bioformats_rust".to_string(),
#[cfg(feature = "bioformats_java")]
"bioformats_java".to_string(),
]
}
/// 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
@@ -694,7 +712,7 @@ impl DynReader {
let reader = reader.as_ref();
let reader = match reader.to_lowercase().as_str() {
#[cfg(feature = "tiff")]
"tif" => Ok(DynReader::Tiff(tiff::TiffReader::new(
"tiff" => Ok(DynReader::Tiff(tiff::TiffReader::new(
path,
dimensions.s.unwrap_or(0),
dimensions.p.unwrap_or(0),
@@ -746,7 +764,7 @@ impl DynReader {
let reader = reader.as_ref();
match reader.to_lowercase().as_str() {
#[cfg(feature = "tiff")]
"tif" => Ok(tiff::TiffReader::get_available_positions(path, series)?),
"tiff" => Ok(tiff::TiffReader::get_available_positions(path, series)?),
#[cfg(feature = "tiffseq")]
"tiffseq" => Ok(tiffseq::TiffSeqReader::get_available_positions(
path, series,
@@ -781,7 +799,7 @@ impl DynReader {
let reader = reader.as_ref();
match reader.to_lowercase().as_str() {
#[cfg(feature = "tiff")]
"tif" => Ok(tiff::TiffReader::get_available_series(path)?),
"tiff" => Ok(tiff::TiffReader::get_available_series(path)?),
#[cfg(feature = "tiffseq")]
"tiffseq" => Ok(tiffseq::TiffSeqReader::get_available_series(path)?),
#[cfg(feature = "czi")]
+54 -41
View File
@@ -9,6 +9,7 @@ use std::fs::File;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use thread_local::ThreadLocal;
use tiff::decoder::ifd::Value;
use tiff::decoder::{Decoder, DecodingResult};
use tiff::tags::Tag;
@@ -25,8 +26,6 @@ pub struct TiffReader {
n_samples: usize,
p_ndim: u8,
planar: bool,
interval_t: f64,
pixel_size: Option<f64>,
}
impl From<TiffReader> for DynReader {
@@ -55,8 +54,6 @@ impl Clone for TiffReader {
n_samples: self.n_samples,
p_ndim: self.p_ndim,
planar: self.planar,
interval_t: self.interval_t,
pixel_size: self.pixel_size,
}
}
}
@@ -128,8 +125,6 @@ impl Reader for TiffReader {
n_samples: 0,
p_ndim: 0,
planar: true,
interval_t: 0.0,
pixel_size: None,
};
let mut reader = new.get_reader()?;
@@ -157,7 +152,7 @@ impl Reader for TiffReader {
.get_tag(Tag::Unknown(50839))
.or_else(|_| reader.get_tag(Tag::ImageDescription))?;
let imagej_bytes = match imagej_bytes {
tiff::decoder::ifd::Value::Ascii(s) => s.into_bytes(),
Value::Ascii(s) => s.into_bytes(),
other => other.into_u8_vec()?,
};
let metadata_str = String::from_utf8_lossy(&imagej_bytes);
@@ -179,30 +174,6 @@ impl Reader for TiffReader {
.get("frames")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(1);
let interval_t = metadata_map
.get("interval")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let ru = reader
.get_tag(Tag::ResolutionUnit)
.ok()
.and_then(|i| i.into_u16().ok())
.unwrap_or(2);
let pixel_size = reader
.get_tag(Tag::XResolution)
.ok()
.and_then(|i| i.into_f64().ok())
.and_then(|i| {
if i <= 0.0 {
None
} else if ru == 3 {
Some(1e4 / i)
} else if ru == 2 {
Some(25400.0 / i)
} else {
Some(1.0 / i)
}
});
let mult = if p_ndim == 3 { n_samples } else { 1 };
let expected = size_c * size_z * size_t / mult;
@@ -231,8 +202,6 @@ impl Reader for TiffReader {
new.pixel_type = pixtype;
new.n_samples = n_samples;
new.p_ndim = p_ndim;
new.interval_t = interval_t;
new.pixel_size = pixel_size;
new.planar = planar;
// test reading first frame, if error another reader (bioformats) can take over
@@ -241,6 +210,48 @@ impl Reader for TiffReader {
}
fn metadata(&self) -> Result<Ome, Error> {
let mut reader = self.get_reader()?;
let imagej_bytes = reader
.get_tag(Tag::Unknown(50839))
.or_else(|_| reader.get_tag(Tag::ImageDescription))?;
let imagej_bytes = match imagej_bytes {
Value::Ascii(s) => s.into_bytes(),
other => other.into_u8_vec()?,
};
let metadata_str = String::from_utf8_lossy(&imagej_bytes);
let metadata_map = parse_imagej_metadata(&metadata_str);
let interval_t = metadata_map
.get("interval")
.and_then(|v| v.parse::<f64>().ok());
let ru = reader
.get_tag(Tag::ResolutionUnit)
.ok()
.and_then(|i| i.into_u16().ok())
.unwrap_or(2);
let pixel_size = reader
.get_tag(Tag::XResolution)
.ok()
.and_then(|i| i.into_u64_vec().ok())
.and_then(|i| {
i.first()
.and_then(|&n| i.get(1).map(|&d| (n as f64) / (d as f64)))
})
.and_then(|i| {
if i <= 0.0 {
None
} else if ru == 3 {
Some(1e4 / i)
} else if ru == 2 {
Some(25400.0 / i)
} else {
Some(1.0 / i)
}
});
let interval_z = metadata_map
.get("spacing")
.and_then(|v| v.parse::<f64>().ok());
let ome_pixel_type = match self.pixel_type {
PixelType::I8 => ome::PixelType::Int8,
PixelType::U8 => ome::PixelType::Uint8,
@@ -262,13 +273,13 @@ impl Reader for TiffReader {
size_t: self.shape.t as i32,
dimension_order: ome::PixelsDimensionOrderType::Xyczt,
r#type: ome_pixel_type,
physical_size_x: self.pixel_size.map(|v| v as f32),
physical_size_x: pixel_size.map(|v| v as f32),
physical_size_x_unit: ome::UnitsLength::um,
physical_size_y: self.pixel_size.map(|v| v as f32),
physical_size_y: pixel_size.map(|v| v as f32),
physical_size_y_unit: ome::UnitsLength::um,
physical_size_z: None,
physical_size_z: interval_z.map(|v| v as f32),
physical_size_z_unit: ome::UnitsLength::um,
time_increment: None,
time_increment: interval_t.map(|v| v as f32),
time_increment_unit: ome::UnitsTime::s,
significant_bits: None,
interleaved: None,
@@ -295,11 +306,13 @@ impl Reader for TiffReader {
the_c: Some(c as i32),
the_z: Some(z as i32),
the_t: Some(t as i32),
delta_t: if self.interval_t > 0.0 {
Some(t as f32 * self.interval_t as f32)
delta_t: interval_t.and_then(|v| {
if v > 0.0 {
Some((t as f64 * v) as f32)
} else {
None
},
}
}),
..Default::default()
});
}
@@ -341,7 +354,7 @@ impl Reader for TiffReader {
reader.seek_to_image(page_idx)?;
let data: DecodingResult = if self.planar {
let mut result = DecodingResult::U16(vec![]);
let mut result = DecodingResult::U16(Vec::new());
reader.read_image_to_buffer(&mut result)?;
result
} else {
+191 -60
View File
@@ -3,30 +3,73 @@ use crate::error::Error;
use crate::readers::{DynReader, Reader};
use crate::view::View;
pub use image_registration::transform::Transform;
use ndarray::{Dimension, Ix2, Ix3, s};
use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, s};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::io::Seek;
use std::path::Path;
#[expect(clippy::upper_case_acronyms)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum TransformD {
YX(Transform<Ix2>),
ZYX(Transform<Ix3>),
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
struct TransformOld {
#[serde(alias = "CenterOfRotationPoint")]
center_of_rotation_point: Vec<f64>,
#[serde(alias = "Size")]
size: Vec<f64>,
#[serde(alias = "TransformParameters")]
transform_parameters: Vec<f64>,
#[serde(alias = "dTransformParameters")]
dtransform_parameters: Vec<f64>,
}
impl TryFrom<HashMap<String, TransformOld>> for Transforms {
type Error = Error;
fn try_from(t: HashMap<String, TransformOld>) -> Result<Self, Error> {
Ok(Self {
channel: t
.into_iter()
.map(|(n, i)| {
Ok((
n,
Transform::new_with_center(
i.transform_parameters,
i.center_of_rotation_point,
i.size.into_iter().map(|j| j as usize).collect(),
)
.with_dparameters(i.dtransform_parameters)
.inverse()?,
))
})
.collect::<Result<_, Error>>()?,
drift: Vec::new(),
})
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Transforms {
pub channel: Vec<TransformD>,
pub drift: Vec<TransformD>,
#[serde(skip_serializing_if = "HashMap::is_empty", default = "HashMap::new")]
pub channel: HashMap<String, Transform<IxDyn>>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
pub drift: Vec<Transform<IxDyn>>,
}
impl Transforms {
pub fn load(path: &Path) -> Result<Self, Error> {
let file = std::fs::File::open(path)?;
Ok(yaml_serde::from_reader(file)?)
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let mut file = std::fs::File::open(&path)?;
Ok(
if let Ok(t) = yaml_serde::from_reader::<_, Self>(&file)
&& !(t.channel.is_empty() && t.drift.is_empty())
{
t
} else {
file.seek(std::io::SeekFrom::Start(0))?;
yaml_serde::from_reader::<_, HashMap<String, TransformOld>>(&file)?.try_into()?
},
)
}
pub fn save(&self, path: &Path) -> Result<(), Error> {
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
@@ -39,30 +82,43 @@ impl Transforms {
bead_files: &[&Path],
main_channel: usize,
default_transform: Option<Transform<Ix2>>,
) -> Result<Vec<Transform<Ix2>>, Error> {
) -> Result<HashMap<String, Transform<Ix2>>, Error> {
let mut transforms = Vec::new();
let default_transform = default_transform.unwrap_or_default();
for file in bead_files {
let view = View::<_, DynReader>::from_path(file)?;
transforms.push(view.calculate_channel_transforms_2d(main_channel)?)
}
let n_channels = transforms.iter().map(|t| t.len()).max().unwrap();
let mut average_transforms = Vec::new();
for channel in 0..n_channels {
let matrix = transforms
let channels = transforms
.iter()
.map(|t| (&t[channel] * &default_transform).matrix())
.reduce(|a, b| a + b)
.unwrap()
/ n_channels as f64;
let dmatrix = transforms
.flat_map(|t| t.keys())
.collect::<HashSet<_>>();
let mut average_transforms = HashMap::new();
for channel in channels {
let (s, n) = transforms
.iter()
.map(|t| ((&t[channel] * &default_transform).matrix() - &matrix).powi(2))
.reduce(|a, b| a + b)
.unwrap()
.sqrt()
/ (n_channels as f64).sqrt();
average_transforms.push(
.filter_map(|t| {
if t.contains_key(channel) {
Some((&t[channel] * &default_transform).matrix())
} else {
None
}
})
.fold((Array2::zeros((3, 3)), 0), |(s, n), t| (s + t, n + 1));
let matrix = s / (n as f64);
let (s, n) = transforms
.iter()
.filter_map(|t| {
if t.contains_key(channel) {
Some(((&t[channel] * &default_transform).matrix() - &matrix).powi(2))
} else {
None
}
})
.fold((Array2::zeros((3, 3)), 0), |(s, n), t| (s + t, n + 1));
let dmatrix = (s / n as f64).sqrt();
average_transforms.insert(
channel.clone(),
Transform::default()
.with_matrix(matrix.view())
.with_dmatrix(dmatrix.view()),
@@ -75,30 +131,43 @@ impl Transforms {
bead_files: &[&Path],
main_channel: usize,
default_transform: Option<Transform<Ix3>>,
) -> Result<Vec<Transform<Ix3>>, Error> {
) -> Result<HashMap<String, Transform<Ix3>>, Error> {
let mut transforms = Vec::new();
let default_transform = default_transform.unwrap_or_default();
for file in bead_files {
let view = View::<_, DynReader>::from_path(file)?;
transforms.push(view.calculate_channel_transforms_3d(main_channel)?)
}
let n_channels = transforms.iter().map(|t| t.len()).max().unwrap();
let mut average_transforms = Vec::new();
for channel in 0..n_channels {
let matrix = transforms
let channels = transforms
.iter()
.map(|t| (&t[channel] * &default_transform).matrix())
.reduce(|a, b| a + b)
.unwrap()
/ n_channels as f64;
let dmatrix = transforms
.flat_map(|t| t.keys())
.collect::<HashSet<_>>();
let mut average_transforms = HashMap::new();
for channel in channels {
let (s, n) = transforms
.iter()
.map(|t| ((&t[channel] * &default_transform).matrix() - &matrix).powi(2))
.reduce(|a, b| a + b)
.unwrap()
.sqrt()
/ (n_channels as f64).sqrt();
average_transforms.push(
.filter_map(|t| {
if t.contains_key(channel) {
Some((&t[channel] * &default_transform).matrix())
} else {
None
}
})
.fold((Array2::zeros((4, 4)), 0), |(s, n), t| (s + t, n + 1));
let matrix = s / (n as f64);
let (s, n) = transforms
.iter()
.filter_map(|t| {
if t.contains_key(channel) {
Some(((&t[channel] * &default_transform).matrix() - &matrix).powi(2))
} else {
None
}
})
.fold((Array2::zeros((4, 4)), 0), |(s, n), t| (s + t, n + 1));
let dmatrix = (s / n as f64).sqrt();
average_transforms.insert(
channel.clone(),
Transform::default()
.with_matrix(matrix.view())
.with_dmatrix(dmatrix.view()),
@@ -109,34 +178,61 @@ impl Transforms {
}
impl<D: Dimension, R: Reader> View<D, R> {
pub fn with_transform_from_yaml(mut self, path: &Path) -> Result<Self, Error> {
self.transforms = Transforms::load(path)?;
#[cfg(feature = "transforms")]
pub(crate) fn get_transforms(&self) -> &Transforms {
&self.transforms
}
#[cfg(feature = "transforms")]
pub(crate) fn set_transforms(&mut self, transforms: Transforms) {
self.transforms = transforms;
}
pub fn with_transforms_from_yaml(mut self, path: &Path) -> Result<Self, Error> {
self.set_transforms(Transforms::load(path)?);
Ok(self)
}
pub fn load_transform_from_yaml(&mut self, path: &Path) -> Result<(), Error> {
self.transforms = Transforms::load(path)?;
pub fn load_transforms_from_yaml(&mut self, path: &Path) -> Result<(), Error> {
self.set_transforms(Transforms::load(path)?);
Ok(())
}
pub fn calculate_channel_transforms_2d(
&self,
main_channel: usize,
) -> Result<Vec<Transform<Ix2>>, Error> {
) -> Result<HashMap<String, Transform<Ix2>>, Error> {
let main_max = self
.slice_cztyx(s![main_channel, .., 0, .., ..])?
.max_proj(Axis::Z)?
.as_array::<f64>()?;
let mut transforms = Vec::new();
for channel in 0..self.shape().c {
let mut transforms = HashMap::new();
let shape = vec![self.shape().y, self.shape().x];
let center = shape
.iter()
.map(|&i| (i as f64 - 1.0) / 2.0)
.collect::<Vec<_>>();
for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() {
let name = metadata
.name
.clone()
.unwrap_or_else(|| format!("channel{}", channel));
if channel == main_channel {
transforms.push(Transform::default());
transforms.insert(
name,
Transform::default()
.with_shape(shape.clone())
.with_center(center.clone()),
);
} else {
let max = self
.slice_cztyx(s![channel, .., 0, .., ..])?
.max_proj(Axis::Z)?
.as_array::<f64>()?;
transforms.push(Transform::register_affine(main_max.view(), max.view())?);
transforms.insert(
name,
Transform::register_affine(main_max.view(), max.view())?,
);
}
}
Ok(transforms)
@@ -145,29 +241,46 @@ impl<D: Dimension, R: Reader> View<D, R> {
pub fn calculate_channel_transforms_3d(
&self,
main_channel: usize,
) -> Result<Vec<Transform<Ix3>>, Error> {
) -> Result<HashMap<String, Transform<Ix3>>, Error> {
let main_max = self
.slice_cztyx(s![main_channel, .., 0, .., ..])?
.as_array::<f64>()?;
let mut transforms = Vec::new();
for channel in 0..self.shape().c {
let mut transforms = HashMap::new();
let shape = vec![self.shape().z, self.shape().y, self.shape().x];
let center = shape
.iter()
.map(|&i| (i as f64 - 1.0) / 2.0)
.collect::<Vec<_>>();
for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() {
let name = metadata
.name
.clone()
.unwrap_or_else(|| format!("channel{}", channel));
if channel == main_channel {
transforms.push(Transform::default());
transforms.insert(
name,
Transform::default()
.with_shape(shape.clone())
.with_center(center.clone()),
);
} else {
let max = self
.slice_cztyx(s![channel, .., 0, .., ..])?
.as_array::<f64>()?;
transforms.push(Transform::register_affine(main_max.view(), max.view())?);
transforms.insert(
name,
Transform::register_affine(main_max.view(), max.view())?,
);
}
}
Ok(transforms)
}
pub fn calculate_drift_transform_2d(&self) -> Result<Vec<Transform<Ix2>>, Error> {
pub fn calculate_drift_transforms_2d(&self) -> Result<Vec<Transform<Ix2>>, Error> {
todo!()
}
pub fn calculate_drift_transform_3d(&self) -> Result<Vec<Transform<Ix3>>, Error> {
pub fn calculate_drift_transforms_3d(&self) -> Result<Vec<Transform<Ix3>>, Error> {
todo!()
}
}
@@ -182,4 +295,22 @@ mod tests {
Ok(())
}
#[test]
fn load_transforms() -> Result<(), Box<dyn std::error::Error>> {
let t = Transforms::load("tests/files/transforms.yml")?;
println!("t: {:?}", t);
assert_eq!(t.channel["TV1"].ndim(), 2);
assert_eq!(t.channel["TV2"].ndim(), 2);
Ok(())
}
#[test]
fn load_old_transforms() -> Result<(), Box<dyn std::error::Error>> {
let t = Transforms::load("tests/files/transform_red.yml")?;
println!("t: {:?}", t);
assert_eq!(t.channel["TV1"].ndim(), 2);
assert_eq!(t.channel["TV2"].ndim(), 2);
Ok(())
}
}