- more transforms

This commit is contained in:
w.pomp
2026-09-15 16:13:51 +02:00
parent 4074770fd3
commit 6041cda475
18 changed files with 3383 additions and 2794 deletions
+5 -6
View File
@@ -21,7 +21,6 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
bioformats = { version = "0.1", optional = true } bioformats = { version = "0.1", optional = true }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
color-eyre = { version = "0.6", optional = true }
console = { version = "0.16", optional = true } console = { version = "0.16", optional = true }
downloader = { version = "0.2", optional = true, default-features = false, features = ["rustls-tls"] } downloader = { version = "0.2", optional = true, default-features = false, features = ["rustls-tls"] }
ffmpeg-sidecar = { version = "2", optional = true } ffmpeg-sidecar = { version = "2", optional = true }
@@ -38,12 +37,11 @@ ome-metadata = "0.5"
ordered-float = { version = "5", optional = true } ordered-float = { version = "5", optional = true }
phf = { version = "0.14", features = ["macros"] } phf = { version = "0.14", features = ["macros"] }
postcard = { version = "1", features = ["use-std"], optional = true } postcard = { version = "1", features = ["use-std"], optional = true }
pyo3 = { version = "0.29", features = ["abi3-py310", "eyre", "anyhow", "generate-import-lib"], optional = true } pyo3 = { version = "0.29", features = ["abi3-py310", "anyhow", "generate-import-lib"], optional = true }
pyo3-stub-gen = { version = "0.23", optional = true } pyo3-stub-gen = { version = "0.23", optional = true }
rayon = { version = "1", optional = true } rayon = { version = "1", optional = true }
regex = "1" regex = "1"
serde = { version = "1", features = ["rc", "derive"] } serde = { version = "1", features = ["rc", "derive"] }
serde_yaml = { version = "0.9", optional = true }
serde_with = "3" serde_with = "3"
strum = { version = "0.28", features = ["derive"] } strum = { version = "0.28", features = ["derive"] }
thiserror = "2" thiserror = "2"
@@ -52,6 +50,7 @@ tiffwrite = { version = "2026.6.0", optional = true }
tokio = { version = "1", features = ["rt", "rt-multi-thread"], optional = true } tokio = { version = "1", features = ["rt", "rt-multi-thread"], optional = true }
thread_local = { version = "1", optional = true } thread_local = { version = "1", optional = true }
xmltree = { version = "0.12", optional = true } xmltree = { version = "0.12", optional = true }
yaml_serde = { version = "0.10", optional = true }
[dev-dependencies] [dev-dependencies]
rayon = "1" rayon = "1"
@@ -67,15 +66,15 @@ toml = "1"
default = ["bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "movie", "tiffwrite"] default = ["bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "movie", "tiffwrite"]
all = ["bioformats_java", "bioformats_rust", "czi", "gpl-formats", "movie", "tiffseq", "tiffwrite", "tiff", "transforms"] all = ["bioformats_java", "bioformats_rust", "czi", "gpl-formats", "movie", "tiffseq", "tiffwrite", "tiff", "transforms"]
gpl-formats = [] gpl-formats = []
python = ["dep:pyo3", "dep:numpy", "dep:color-eyre", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"] python = ["dep:pyo3", "dep:numpy", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"]
czi = ["dep:libczirw-sys", "dep:xmltree", "dep:thread_local"] czi = ["dep:libczirw-sys", "dep:xmltree", "dep:thread_local"]
bioformats_rust = ["dep:bioformats", "dep:thread_local"] bioformats_rust = ["dep:bioformats", "dep:thread_local"]
bioformats_java = ["dep:j4rs", "dep:thread_local", "dep:downloader"] bioformats_java = ["dep:j4rs", "dep:thread_local", "dep:downloader"]
tiffwrite = ["dep:tiffwrite", "dep:indicatif", "dep:console", "dep:rayon"] tiffwrite = ["dep:tiffwrite", "dep:indicatif", "dep:console", "dep:rayon"]
tiffseq = ["dep:tiff", "dep:serde_yaml"] tiffseq = ["dep:tiff", "dep:yaml_serde"]
tiff = ["dep:tiff", "dep:thread_local"] tiff = ["dep:tiff", "dep:thread_local"]
movie = ["dep:ffmpeg-sidecar", "dep:tokio", "dep:ordered-float", "dep:indicatif", "dep:console"] movie = ["dep:ffmpeg-sidecar", "dep:tokio", "dep:ordered-float", "dep:indicatif", "dep:console"]
transforms = ["dep:image-registration"] transforms = ["dep:image-registration", "dep:yaml_serde"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
no-default-features = true no-default-features = true
+3 -6
View File
@@ -13,9 +13,8 @@ from numpy.typing import ArrayLike
os.environ["RUST_BACKTRACE"] = "full" os.environ["RUST_BACKTRACE"] = "full"
os.environ["COLORBT_SHOW_HIDDEN"] = "1" os.environ["COLORBT_SHOW_HIDDEN"] = "1"
from . import ndbioimage_rs as rs # noqa from . import ndbioimage_rs as rs
from .ndbioimage_rs import Imread from .ndbioimage_rs import Imread, Transform, Transforms
from .transforms import Transform, Transforms # noqa: F401
try: try:
from .ndbioimage_rs import batch_to_tiff from .ndbioimage_rs import batch_to_tiff
@@ -63,11 +62,9 @@ def ndbioimage_generate_stub():
rs.generate_stub(str(path)) # noqa rs.generate_stub(str(path)) # noqa
else: else:
raise ModuleNotFoundError(str(path / "py" / "ndbioimage" / "__init__.py")) raise ModuleNotFoundError(str(path / "py" / "ndbioimage" / "__init__.py"))
(path / "py" / "ndbioimage" / "__init__.pyi").unlink(missing_ok=True) (path / "py" / "ndbioimage" / "__init__.pyi").rename(
(path / "py" / "ndbioimage" / "ndbioimage_rs" / "__init__.pyi").rename(
path / "py" / "ndbioimage" / "ndbioimage_rs.pyi" path / "py" / "ndbioimage" / "ndbioimage_rs.pyi"
) )
(path / "py" / "ndbioimage" / "ndbioimage_rs").rmdir()
R = TypeVar("R") R = TypeVar("R")
+142 -2
View File
@@ -12,6 +12,8 @@ import numpy.typing
__all__ = [ __all__ = [
"Imread", "Imread",
"Shape", "Shape",
"Transform",
"Transforms",
"batch_to_tiff", "batch_to_tiff",
"main", "main",
] ]
@@ -52,9 +54,9 @@ class Imread:
the name of the reader used to open the file the name of the reader used to open the file
""" """
@property @property
def transform(self) -> None: def transform(self) -> Transforms:
r""" r"""
get the transformation matrix (not yet implemented) get the transformation
""" """
@property @property
def path(self) -> pathlib.Path: def path(self) -> pathlib.Path:
@@ -203,10 +205,27 @@ class Imread:
drift: builtins.bool = False, drift: builtins.bool = False,
file: typing.Optional[typing.Any] = None, file: typing.Optional[typing.Any] = None,
bead_files: typing.Optional[typing.Any] = None, bead_files: typing.Optional[typing.Any] = None,
main_channel: typing.Optional[builtins.int] = None,
default_transform: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Imread: ) -> Imread:
r""" r"""
return a new view with transformations applied (channel alignment, drift correction) return a new view with transformations applied (channel alignment, drift correction)
""" """
def set_transform(self, transform: Transforms) -> None:
r"""
set the transformation
"""
def load_transform_from_yaml(
self, path: builtins.str | os.PathLike | pathlib.Path
) -> None: ...
def calculate_channel_transforms_2d(
self, main_channel: builtins.int
) -> builtins.list[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]: ...
def squeeze(self) -> numpy.ndarray | int | float: ... def squeeze(self) -> numpy.ndarray | int | float: ...
def close(self) -> None: def close(self) -> None:
r""" r"""
@@ -672,6 +691,127 @@ class Shape:
convert shape to a list of dimension sizes in order 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( def batch_to_tiff(
files_in: typing.Sequence[builtins.str | os.PathLike | pathlib.Path], files_in: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
files_out: typing.Sequence[builtins.str | os.PathLike | pathlib.Path], files_out: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
-7
View File
@@ -1,7 +0,0 @@
#Insight Transform File V1.0
#Transform 0
Transform: CompositeTransform_double_2_2
#Transform 1
Transform: AffineTransform_double_2_2
Parameters: 1 0 0 1 0 0
FixedParameters: 255.5 255.5
-572
View File
@@ -1,572 +0,0 @@
import warnings
from copy import deepcopy
from pathlib import Path
import numpy as np
import yaml
from parfor import Chunks, pmap
from skimage import filters
from tiffwrite import IJTiffFile
from tqdm.auto import tqdm
try:
# best if SimpleElastix is installed: https://simpleelastix.readthedocs.io/GettingStarted.html
import SimpleITK as sitk # noqa
except ImportError:
sitk = None
try:
from pandas import DataFrame, Series, concat
except ImportError:
DataFrame, Series, concat = None, None, None
if hasattr(yaml, "full_load"):
yamlload = yaml.full_load
else:
yamlload = yaml.load
class Transforms(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.default = Transform()
@classmethod
def from_file(cls, file, C=True, T=True):
with open(Path(file).with_suffix(".yml")) as f:
return cls.from_dict(yamlload(f), C, T)
@classmethod
def from_dict(cls, d, C=True, T=True):
new = cls()
for key, value in d.items():
if isinstance(key, str) and C:
new[key.replace(r"\:", ":").replace("\\\\", "\\")] = (
Transform.from_dict(value)
)
elif T:
new[key] = Transform.from_dict(value)
return new
@classmethod
def from_shifts(cls, shifts):
new = cls()
for key, shift in shifts.items():
new[key] = Transform.from_shift(shift)
return new
def __mul__(self, other):
new = Transforms()
if isinstance(other, Transforms):
for key0, value0 in self.items():
for key1, value1 in other.items():
new[key0 + key1] = value0 * value1
return new
elif other is None:
return self
else:
for key in self.keys():
new[key] = self[key] * other
return new
def asdict(self):
return {
key.replace("\\", "\\\\").replace(":", r"\:")
if isinstance(key, str)
else key: value.asdict()
for key, value in self.items()
}
def __getitem__(self, item):
return (
np.prod([self[i] for i in item[::-1]])
if isinstance(item, tuple)
else super().__getitem__(item)
)
def __missing__(self, key):
return self.default
def __getstate__(self):
return self.__dict__
def __setstate__(self, state):
self.__dict__.update(state)
def __hash__(self):
return hash(frozenset((*self.__dict__.items(), *self.items())))
def save(self, file):
with open(Path(file).with_suffix(".yml"), "w") as f:
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
def copy(self):
return deepcopy(self)
def adapt(self, origin, shape, channel_names):
def key_map(a, b):
def fun(b, key_a):
for key_b in b:
if key_b in key_a or key_a in key_b:
return key_a, key_b
return {n[0]: n[1] for key_a in a if (n := fun(b, key_a))}
for value in self.values():
value.adapt(origin, shape)
self.default.adapt(origin, shape)
transform_channels = {key for key in self.keys() if isinstance(key, str)}
if set(channel_names) - transform_channels:
mapping = key_map(channel_names, transform_channels)
warnings.warn(
f"The image file and the transform do not have the same channels,"
f" creating a mapping: {mapping}"
)
for key_im, key_t in mapping.items():
self[key_im] = self[key_t]
@property
def inverse(self):
# TODO: check for C@T
inverse = self.copy()
for key, value in self.items():
inverse[key] = value.inverse
return inverse
def coords_pandas(self, array, channel_names, columns=None):
if isinstance(array, DataFrame):
return concat(
[
self.coords_pandas(row, channel_names, columns)
for _, row in array.iterrows()
],
axis=1,
).T
elif isinstance(array, Series):
key = []
if "C" in array:
key.append(channel_names[int(array["C"])])
if "T" in array:
key.append(int(array["T"]))
return self[tuple(key)].coords(array, columns)
else:
raise TypeError("Not a pandas DataFrame or Series.")
def with_beads(self, cyllens, bead_files):
assert len(bead_files) > 0, (
"At least one file is needed to calculate the registration."
)
transforms = [
self.calculate_channel_transforms(file, cyllens) for file in bead_files
]
for key in {key for transform in transforms for key in transform.keys()}:
new_transforms = [
transform[key] for transform in transforms if key in transform
]
if len(new_transforms) == 1:
self[key] = new_transforms[0]
else:
self[key] = Transform()
self[key].parameters = np.mean(
[t.parameters for t in new_transforms], 0
)
self[key].dparameters = (
np.std([t.parameters for t in new_transforms], 0)
/ np.sqrt(len(new_transforms))
).tolist()
return self
@staticmethod
def get_bead_files(path):
from . import Imread
files = []
for file in path.iterdir():
if file.name.lower().startswith("beads"):
try:
with Imread(file):
files.append(file)
except Exception:
pass
files = sorted(files)
if not files:
raise Exception("No bead file found!")
checked_files = []
for file in files:
try:
if file.is_dir():
file /= "Pos0"
with Imread(file): # check for errors opening the file
checked_files.append(file)
except (Exception,):
continue
if not checked_files:
raise Exception("No bead file found!")
return checked_files
@staticmethod
def calculate_channel_transforms(bead_file, cyllens):
"""When no channel is not transformed by a cylindrical lens, assume that the image is scaled by a factor 1.162
in the horizontal direction"""
from . import Imread
with Imread(bead_file, axes="zcyx") as im: # noqa
max_ims = im.max("z")
goodch = [c for c, max_im in enumerate(max_ims) if not im.is_noise(max_im)]
if not goodch:
goodch = list(range(len(max_ims)))
untransformed = [
c
for c in range(im.shape["c"])
if cyllens[im.detector[c]].lower() == "none"
]
good_and_untrans = sorted(set(goodch) & set(untransformed))
if good_and_untrans:
masterch = good_and_untrans[0]
else:
masterch = goodch[0]
transform = Transform()
if not good_and_untrans:
matrix = transform.matrix
matrix[0, 0] = 0.86
transform.matrix = matrix
transforms = Transforms()
for c in tqdm(goodch, desc="Calculating channel transforms"): # noqa
if c == masterch:
transforms[im.channel_names[c]] = transform
else:
transforms[im.channel_names[c]] = (
Transform.register(max_ims[masterch], max_ims[c]) * transform
)
return transforms
@staticmethod
def save_channel_transform_tiff(bead_files, tiffile):
from . import Imread
n_channels = 0
for file in bead_files:
with Imread(file) as im:
n_channels = max(n_channels, im.shape["c"])
with IJTiffFile(tiffile) as tif:
for t, file in enumerate(bead_files):
with Imread(file) as im:
with Imread(file).with_transform() as jm:
for c in range(im.shape["c"]):
tif.save(
np.hstack(
(im(c=c, t=0).max("z"), jm(c=c, t=0).max("z"))
),
c,
0,
t,
)
def with_drift(self, im):
"""Calculate shifts relative to the first frame
divide the sequence into groups,
compare each frame to the frame in the middle of the group and compare these middle frames to each other
"""
im = im.transpose("tzycx")
t_groups = [
list(chunk)
for chunk in Chunks(
range(im.shape["t"]), size=round(np.sqrt(im.shape["t"]))
)
]
t_keys = [int(np.round(np.mean(t_group))) for t_group in t_groups]
t_pairs = [
(int(np.round(np.mean(t_group))), frame)
for t_group in t_groups
for frame in t_group
]
t_pairs.extend(zip(t_keys, t_keys[1:]))
fmaxz_keys = {
t_key: filters.gaussian(im[t_key].max("z"), 5) for t_key in t_keys
}
def fun(t_key_t, im, fmaxz_keys):
t_key, t = t_key_t
if t_key == t:
return 0, 0
else:
fmaxz = filters.gaussian(im[t].max("z"), 5)
return Transform.register(
fmaxz_keys[t_key], fmaxz, "translation"
).parameters[4:]
shifts = np.array(
pmap(fun, t_pairs, (im, fmaxz_keys), desc="Calculating image shifts.")
)
shift_keys_cum = np.zeros(2)
for shift_keys, t_group in zip(
np.vstack((-shifts[0], shifts[im.shape["t"] :])), t_groups
):
shift_keys_cum += shift_keys
shifts[t_group] += shift_keys_cum
for i, shift in enumerate(shifts[: im.shape["t"]]):
self[i] = Transform.from_shift(shift)
return self
class Transform:
def __init__(self):
if sitk is None:
self.transform = None
else:
self.transform = sitk.ReadTransform(
str(Path(__file__).parent / "transform.txt")
)
self.dparameters = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
self.shape = [512.0, 512.0]
self.origin = [255.5, 255.5]
self._last, self._inverse = None, None
def __reduce__(self):
return self.from_dict, (self.asdict(),)
def __repr__(self):
return self.asdict().__repr__()
def __str__(self):
return self.asdict().__str__()
@classmethod
def register(cls, fix, mov, kind=None):
"""kind: 'affine', 'translation', 'rigid'"""
if sitk is None:
raise ImportError(
"SimpleElastix is not installed: "
"https://simpleelastix.readthedocs.io/GettingStarted.html"
)
new = cls()
kind = kind or "affine"
new.shape = fix.shape
fix, mov = new.cast_image(fix), new.cast_image(mov)
# TODO: implement RigidTransform
tfilter = sitk.ElastixImageFilter()
tfilter.LogToConsoleOff()
tfilter.SetFixedImage(fix)
tfilter.SetMovingImage(mov)
tfilter.SetParameterMap(sitk.GetDefaultParameterMap(kind))
tfilter.Execute()
transform = tfilter.GetTransformParameterMap()[0]
if kind == "affine":
new.parameters = [float(t) for t in transform["TransformParameters"]]
new.shape = [float(t) for t in transform["Size"]]
new.origin = [float(t) for t in transform["CenterOfRotationPoint"]]
elif kind == "translation":
new.parameters = [1.0, 0.0, 0.0, 1.0] + [
float(t) for t in transform["TransformParameters"]
]
new.shape = [float(t) for t in transform["Size"]]
new.origin = [(t - 1) / 2 for t in new.shape]
else:
raise NotImplementedError(f"{kind} tranforms not implemented (yet)")
new.dparameters = 6 * [np.nan]
return new
@classmethod
def from_shift(cls, shift):
return cls.from_array(np.array(((1, 0, shift[0]), (0, 1, shift[1]), (0, 0, 1))))
@classmethod
def from_array(cls, array):
new = cls()
new.matrix = array
return new
@classmethod
def from_file(cls, file):
with open(Path(file).with_suffix(".yml")) as f:
return cls.from_dict(yamlload(f))
@classmethod
def from_dict(cls, d):
new = cls()
new.origin = (
None
if d["CenterOfRotationPoint"] is None
else [float(i) for i in d["CenterOfRotationPoint"]]
)
new.parameters = (
(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
if d["TransformParameters"] is None
else [float(i) for i in d["TransformParameters"]]
)
new.dparameters = (
[
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0) if i is None else float(i)
for i in d["dTransformParameters"]
]
if "dTransformParameters" in d
else 6 * [np.nan] and d["dTransformParameters"] is not None
)
new.shape = (
None
if d["Size"] is None
else [None if i is None else float(i) for i in d["Size"]]
)
return new
def __mul__(self, other): # TODO: take care of dmatrix
result = self.copy()
if isinstance(other, Transform):
result.matrix = self.matrix @ other.matrix
result.dmatrix = self.dmatrix @ other.matrix + self.matrix @ other.dmatrix
else:
result.matrix = self.matrix @ other
result.dmatrix = self.dmatrix @ other
return result
def is_unity(self):
return self.parameters == [1, 0, 0, 1, 0, 0]
def copy(self):
return deepcopy(self)
@staticmethod
def cast_image(im):
if not isinstance(im, sitk.Image):
im = sitk.GetImageFromArray(np.asarray(im))
return im
@staticmethod
def cast_array(im):
if isinstance(im, sitk.Image):
im = sitk.GetArrayFromImage(im)
return im
@property
def matrix(self):
return np.array(
(
(*self.parameters[:2], self.parameters[4]),
(*self.parameters[2:4], self.parameters[5]),
(0, 0, 1),
)
)
@matrix.setter
def matrix(self, value):
value = np.asarray(value)
self.parameters = [*value[0, :2], *value[1, :2], *value[:2, 2]]
@property
def dmatrix(self):
return np.array(
(
(*self.dparameters[:2], self.dparameters[4]),
(*self.dparameters[2:4], self.dparameters[5]),
(0, 0, 0),
)
)
@dmatrix.setter
def dmatrix(self, value):
value = np.asarray(value)
self.dparameters = [*value[0, :2], *value[1, :2], *value[:2, 2]]
@property
def parameters(self):
if self.transform is not None:
return list(self.transform.GetParameters())
else:
return [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
@parameters.setter
def parameters(self, value):
if self.transform is not None:
value = np.asarray(value)
self.transform.SetParameters(value.tolist())
@property
def origin(self):
if self.transform is not None:
return self.transform.GetFixedParameters()
@origin.setter
def origin(self, value):
if self.transform is not None:
value = np.asarray(value)
self.transform.SetFixedParameters(value.tolist())
@property
def inverse(self):
if self.is_unity():
return self
if self._last is None or self._last != self.asdict():
self._last = self.asdict()
self._inverse = Transform.from_dict(self.asdict())
self._inverse.transform = self._inverse.transform.GetInverse()
self._inverse._last = self._inverse.asdict()
self._inverse._inverse = self
return self._inverse
def adapt(self, origin, shape):
self.origin -= np.array(origin) + (self.shape - np.array(shape)[:2]) / 2
self.shape = shape[:2]
def asdict(self):
return {
"CenterOfRotationPoint": self.origin,
"Size": self.shape,
"TransformParameters": self.parameters,
"dTransformParameters": np.nan_to_num(self.dparameters, nan=1e99).tolist(),
}
def frame(self, im, default=0):
if self.is_unity():
return im
else:
if sitk is None:
raise ImportError(
"SimpleElastix is not installed: "
"https://simpleelastix.readthedocs.io/GettingStarted.html"
)
dtype = im.dtype
im = im.astype("float")
intp = (
sitk.sitkBSpline
if np.issubdtype(dtype, np.floating)
else sitk.sitkNearestNeighbor
)
return self.cast_array(
sitk.Resample(self.cast_image(im), self.transform, intp, default)
).astype(dtype)
def coords(self, array, columns=None):
"""Transform coordinates in 2 column numpy array,
or in pandas DataFrame or Series objects in columns ['x', 'y']
"""
if self.is_unity():
return array.copy()
elif DataFrame is not None and isinstance(array, (DataFrame, Series)):
columns = columns or ["x", "y"]
array = array.copy()
if isinstance(array, DataFrame):
array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy()))
elif isinstance(array, Series):
array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy()))[
0
]
return array
else: # somehow we need to use the inverse here to get the same effect as when using self.frame
return np.array(
[
self.inverse.transform.TransformPoint(i.tolist())
for i in np.asarray(array)
]
)
def save(self, file):
"""save the parameters of the transform calculated
with affine_registration to a yaml file
"""
if not file[-3:] == "yml":
file += ".yml"
with open(file, "w") as f:
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project] [project]
name = "ndbioimage" name = "ndbioimage"
version = "2027.0.3" version = "2027.0.4"
requires-python = ">=3.10" requires-python = ">=3.10"
classifiers = [ classifiers = [
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
@@ -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", "gpl-formats", "czi", "tiff", "tiffseq", "tiffwrite", "movie"] features = ["python", "bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"]
no-default-features = true no-default-features = true
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*"]
+24 -14
View File
@@ -10,8 +10,8 @@ pub enum Error {
/// an ndarray shape error /// an ndarray shape error
#[error(transparent)] #[error(transparent)]
Shape(#[from] ndarray::ShapeError), Shape(#[from] ndarray::ShapeError),
#[cfg(feature = "bioformats_java")]
/// an error from the j4rs java bridge /// an error from the j4rs java bridge
#[cfg(feature = "bioformats_java")]
#[error(transparent)] #[error(transparent)]
J4rs(#[from] j4rs::errors::J4RsError), J4rs(#[from] j4rs::errors::J4RsError),
/// an infallible conversion /// an infallible conversion
@@ -23,60 +23,64 @@ pub enum Error {
/// an ome metadata error /// an ome metadata error
#[error(transparent)] #[error(transparent)]
Ome(#[from] ome_metadata::error::Error), Ome(#[from] ome_metadata::error::Error),
#[cfg(feature = "bioformats_java")]
/// an error while downloading (e.g. the bioformats jar) /// an error while downloading (e.g. the bioformats jar)
#[cfg(feature = "bioformats_java")]
#[error(transparent)] #[error(transparent)]
Downloader(#[from] downloader::Error), Downloader(#[from] downloader::Error),
/// an error parsing an enum string with strum /// an error parsing an enum string with strum
#[error(transparent)] #[error(transparent)]
Strum(#[from] strum::ParseError), Strum(#[from] strum::ParseError),
#[cfg(feature = "tiffwrite")]
/// an indicatif progress bar template error /// an indicatif progress bar template error
#[cfg(feature = "tiffwrite")]
#[error(transparent)] #[error(transparent)]
TemplateError(#[from] indicatif::style::TemplateError), TemplateError(#[from] indicatif::style::TemplateError),
#[cfg(feature = "tiffwrite")]
/// an error from the tiffwrite crate /// an error from the tiffwrite crate
#[cfg(feature = "tiffwrite")]
#[error(transparent)] #[error(transparent)]
TiffWrite(#[from] tiffwrite::error::Error), TiffWrite(#[from] tiffwrite::error::Error),
#[cfg(feature = "tiffseq")]
/// a yaml (de)serialization error /// a yaml (de)serialization error
#[cfg(feature = "tiffseq")]
#[error(transparent)] #[error(transparent)]
SerdeYaml(#[from] serde_yaml::Error), SerdeYaml(#[from] yaml_serde::Error),
#[cfg(any(feature = "tiffseq", feature = "tiff"))]
/// an error from the tiff crate /// an error from the tiff crate
#[cfg(any(feature = "tiffseq", feature = "tiff"))]
#[error(transparent)] #[error(transparent)]
Tiff(#[from] tiff::TiffError), Tiff(#[from] tiff::TiffError),
#[cfg(feature = "python")]
/// a postcard (de)serialization error /// a postcard (de)serialization error
#[cfg(feature = "python")]
#[error(transparent)] #[error(transparent)]
PostCard(#[from] postcard::Error), PostCard(#[from] postcard::Error),
#[cfg(feature = "czi")]
/// an error from the libczi binding /// an error from the libczi binding
#[cfg(feature = "czi")]
#[error(transparent)] #[error(transparent)]
LibCzi(#[from] libczirw_sys::error::Error), LibCzi(#[from] libczirw_sys::error::Error),
/// a regex error /// a regex error
#[error(transparent)] #[error(transparent)]
RegexError(#[from] regex::Error), RegexError(#[from] regex::Error),
#[cfg(feature = "czi")]
/// an xmltree error /// an xmltree error
#[cfg(feature = "czi")]
#[error(transparent)] #[error(transparent)]
XmlTree(#[from] xmltree::Error), XmlTree(#[from] xmltree::Error),
#[cfg(feature = "czi")]
/// an xmltree parse error /// an xmltree parse error
#[cfg(feature = "czi")]
#[error(transparent)] #[error(transparent)]
XmlTreeParse(#[from] xmltree::ParseError), XmlTreeParse(#[from] xmltree::ParseError),
#[cfg(feature = "czi")]
/// a czi-specific error /// a czi-specific error
#[cfg(feature = "czi")]
#[error(transparent)] #[error(transparent)]
Czi(#[from] crate::readers::czi::CziError), Czi(#[from] crate::readers::czi::CziError),
#[cfg(feature = "movie")]
/// an error joining a tokio task /// an error joining a tokio task
#[cfg(feature = "movie")]
#[error(transparent)] #[error(transparent)]
TokioJoin(#[from] tokio::task::JoinError), TokioJoin(#[from] tokio::task::JoinError),
#[cfg(feature = "bioformats_rust")]
/// an error from the bioformats rust crate /// an error from the bioformats rust crate
#[cfg(feature = "bioformats_rust")]
#[error(transparent)] #[error(transparent)]
BioFormats(#[from] bioformats::error::BioFormatsError), BioFormats(#[from] bioformats::error::BioFormatsError),
/// an image registration / transforms error
#[cfg(feature = "transforms")]
#[error(transparent)]
ImageRegistration(#[from] image_registration::error::Error),
/// the axis string could not be parsed /// the axis string could not be parsed
#[error("invalid axis: {0}")] #[error("invalid axis: {0}")]
@@ -162,6 +166,12 @@ pub enum Error {
/// cannot remove axes that have a size != 1 /// cannot remove axes that have a size != 1
#[error("cannot remove axes {0}, size {1} != 1")] #[error("cannot remove axes {0}, size {1} != 1")]
SizeMismatch(String, usize), SizeMismatch(String, usize),
/// shape mismatch
#[error("shape mismatch: {0:?}, {1:?}")]
ShapeMismatch(Vec<usize>, Vec<usize>),
/// file mismatch
#[error("{0} do not match in {1} and {2}")]
FileMismatch(String, String, String),
} }
impl Error { impl Error {
+19 -2137
View File
File diff suppressed because it is too large Load Diff
+2217
View File
File diff suppressed because it is too large Load Diff
+675
View File
@@ -0,0 +1,675 @@
use crate::error::Error;
use crate::transforms::Transforms;
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::path::PathBuf;
#[gen_stub_pyclass]
#[pyclass(
subclass,
from_py_object,
eq,
name = "Transform",
module = "ndbioimage"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PyTransform {
pub(crate) inner: Transform<IxDyn>,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyTransform {
#[new]
#[pyo3(signature = (parameters, shape, center = None))]
fn new(parameters: Vec<f64>, shape: Vec<usize>, center: Option<Vec<f64>>) -> Self {
if let Some(center) = center {
Self {
inner: Transform::new_with_center(parameters, center, shape),
}
} else {
Self {
inner: Transform::new(parameters, shape),
}
}
}
fn __getnewargs__(&self) -> (Vec<f64>, Vec<usize>, Option<Vec<f64>>) {
(
self.inner.parameters.clone(),
self.inner.shape.clone(),
Some(self.inner.center.clone()),
)
}
fn __getstate__(&self) -> Vec<f64> {
self.inner.dparameters.clone()
}
fn __setstate__(&mut self, state: Vec<f64>) {
self.inner.dparameters = state;
}
fn __add__(&self, other: &PyTransform) -> PyTransform {
PyTransform {
inner: &self.inner + &other.inner,
}
}
fn __radd__(&self, other: &PyTransform) -> PyTransform {
PyTransform {
inner: &other.inner + &self.inner,
}
}
fn __sub__(&self, other: &PyTransform) -> PyTransform {
PyTransform {
inner: &self.inner - &other.inner,
}
}
fn __rsub__(&self, other: &PyTransform) -> PyTransform {
PyTransform {
inner: &other.inner - &self.inner,
}
}
fn __mul__(
&self,
py: Python,
#[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound<PyAny>,
) -> PyResult<PyTransform> {
if other.is_instance_of::<PyTransform>() {
Ok(PyTransform {
inner: &self.inner * &other.extract::<PyTransform>()?.inner,
})
} else {
let builtins = PyModule::import(py, "builtins")?;
let other = builtins.getattr("float")?.call1((&other,))?;
Ok(PyTransform {
inner: &self.inner * other.extract::<f64>()?,
})
}
}
fn __rmul__(
&self,
py: Python,
#[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound<PyAny>,
) -> PyResult<PyTransform> {
if other.is_instance_of::<PyTransform>() {
Ok(PyTransform {
inner: &other.extract::<PyTransform>()?.inner * &self.inner,
})
} else {
let builtins = PyModule::import(py, "builtins")?;
let other = builtins.getattr("float")?.call1((&other,))?;
Ok(PyTransform {
inner: other.extract::<f64>()? * &self.inner,
})
}
}
fn __truediv__(&self, py: Python, other: f64) -> PyResult<PyTransform> {
let builtins = PyModule::import(py, "builtins")?;
let other = builtins.getattr("float")?.call1((&other,))?;
Ok(PyTransform {
inner: &self.inner / other.extract::<f64>()?,
})
}
#[getter]
fn get_parameters(&self) -> Vec<f64> {
self.inner.parameters.clone()
}
#[setter]
fn set_parameters(&mut self, parameters: Vec<f64>) {
self.inner.parameters = parameters;
}
#[getter]
fn get_dparameters(&self) -> Vec<f64> {
self.inner.dparameters.clone()
}
#[setter]
fn set_dparameters(&mut self, dparameters: Vec<f64>) {
self.inner.dparameters = dparameters;
}
#[getter]
fn get_center(&self) -> Vec<f64> {
self.inner.center.clone()
}
#[setter]
fn set_center(&mut self, center: Vec<f64>) {
self.inner.center = center;
}
#[getter]
fn get_shape(&self) -> Vec<usize> {
self.inner.shape.clone()
}
#[setter]
fn set_shape(&mut self, shape: Vec<usize>) {
self.inner.shape = shape;
}
#[getter]
fn get_ndim(&self) -> usize {
self.inner.ndim()
}
#[getter]
fn get_matrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray<f64, Ix2>> {
self.inner.matrix().into_pyarray(py)
}
#[setter]
fn set_matrix(
&mut self,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
matrix: PyArrayLike2<f64, AllowTypeChange>,
) {
let matrix = matrix.as_array();
self.inner.set_matrix(matrix);
}
#[getter]
fn get_dmatrix<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray<f64, Ix2>> {
self.inner.dmatrix().into_pyarray(py)
}
#[setter]
fn set_dmatrix(
&mut self,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
dmatrix: PyArrayLike2<f64, AllowTypeChange>,
) {
let dmatrix = dmatrix.as_array();
self.inner.set_dmatrix(dmatrix);
}
#[getter]
fn inverse(&self) -> PyResult<PyTransform> {
Ok(PyTransform {
inner: self.inner.inverse().map_err(Error::from)?,
})
}
fn adapt(&mut self, center: Vec<f64>, shape: Vec<usize>) {
self.inner.adapt(center.as_slice(), shape.as_slice());
}
#[staticmethod]
fn from_scaling(scaling: Vec<f64>) -> PyTransform {
PyTransform {
inner: Transform::from_scaling(scaling.as_slice()),
}
}
#[staticmethod]
fn from_translation(translation: Vec<f64>) -> PyTransform {
PyTransform {
inner: Transform::from_translation(translation.as_slice()),
}
}
#[staticmethod]
fn from_rotation(theta: f64, center: Vec<f64>) -> PyTransform {
PyTransform {
inner: Transform::from_rotation(theta, center.as_slice()).into_dyn(),
}
}
fn with_scaling(&self, scaling: Vec<f64>) -> PyTransform {
PyTransform {
inner: Transform::from_scaling(scaling.as_slice()) * self.inner.clone(),
}
}
fn with_translation(&self, translation: Vec<f64>) -> PyTransform {
PyTransform {
inner: Transform::from_translation(translation.as_slice()) * self.inner.clone(),
}
}
fn with_rotation(&self, theta: f64, center: Vec<f64>) -> PyTransform {
PyTransform {
inner: Transform::from_rotation(theta, center.as_slice()).into_dyn()
* self.inner.clone(),
}
}
fn interpolate<'py>(
&self,
py: Python<'py>,
order: usize,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
image: PyArrayLikeDyn<f64, AllowTypeChange>,
) -> PyResult<Bound<'py, PyArrayDyn<f64>>> {
let image = image.as_array();
Ok(py
.detach(|| match order {
0 => Ok(self
.inner
.interpolate::<0, _, _>(image.into_dimensionality().map_err(Error::from)?)),
1 => Ok(self
.inner
.interpolate::<1, _, _>(image.into_dimensionality().map_err(Error::from)?)),
2 => Ok(self
.inner
.interpolate::<2, _, _>(image.into_dimensionality().map_err(Error::from)?)),
3 => Ok(self
.inner
.interpolate::<3, _, _>(image.into_dimensionality().map_err(Error::from)?)),
4 => Ok(self
.inner
.interpolate::<4, _, _>(image.into_dimensionality().map_err(Error::from)?)),
5 => Ok(self
.inner
.interpolate::<5, _, _>(image.into_dimensionality().map_err(Error::from)?)),
_ => Err(PyValueError::new_err("order must be 0 <= order < 6")),
})?
.map_err(Error::from)?
.into_pyarray(py))
}
fn interpolate_par<'py>(
&self,
py: Python<'py>,
order: usize,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
image: PyArrayLikeDyn<f64, AllowTypeChange>,
) -> PyResult<Bound<'py, PyArrayDyn<f64>>> {
let image = image.as_array();
Ok(py
.detach(|| match order {
0 => Ok(self
.inner
.interpolate_par::<0, _, _>(image.into_dimensionality().map_err(Error::from)?)),
1 => Ok(self
.inner
.interpolate_par::<1, _, _>(image.into_dimensionality().map_err(Error::from)?)),
2 => Ok(self
.inner
.interpolate_par::<2, _, _>(image.into_dimensionality().map_err(Error::from)?)),
3 => Ok(self
.inner
.interpolate_par::<3, _, _>(image.into_dimensionality().map_err(Error::from)?)),
4 => Ok(self
.inner
.interpolate_par::<4, _, _>(image.into_dimensionality().map_err(Error::from)?)),
5 => Ok(self
.inner
.interpolate_par::<5, _, _>(image.into_dimensionality().map_err(Error::from)?)),
_ => Err(PyValueError::new_err("order must be 0 <= order < 6")),
})?
.map_err(Error::from)?
.into_pyarray(py))
}
fn is_unity(&self) -> bool {
self.inner.is_unity()
}
fn transform_point<'py>(
&self,
py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
point: PyArrayLike1<f64, AllowTypeChange>,
) -> Bound<'py, PyArray1<f64>> {
let point = point.as_array();
if let Some(slice) = point.as_slice() {
self.inner.transform_point(slice)
} else {
let point = point.to_vec();
self.inner.transform_point(&point)
}
.into_pyarray(py)
}
fn transform_points<'py>(
&self,
py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
points: PyArrayLike2<f64, AllowTypeChange>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let points = points.as_array();
Ok(py
.detach(|| self.inner.transform_points(points))
.map_err(Error::from)?
.into_pyarray(py))
}
#[staticmethod]
#[pyo3(signature = (fixed, moving, fixed_mu, initial_guess = None))]
fn register<'py>(
py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
fixed: PyArrayLikeDyn<f64, AllowTypeChange>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
moving: PyArrayLikeDyn<f64, AllowTypeChange>,
fixed_mu: Vec<Option<f64>>,
initial_guess: Option<Vec<f64>>,
) -> PyResult<PyTransform> {
let fixed = fixed.as_array();
let moving = moving.as_array();
if fixed.shape() != moving.shape() {
return Err(PyErr::from(Error::ShapeMismatch(
fixed.shape().to_vec(),
moving.shape().to_vec(),
)));
}
py.detach(|| match fixed.ndim() {
0 => Ok(PyTransform {
inner: Transform::register(
fixed.into_dimensionality::<Ix0>().map_err(Error::from)?,
moving.into_dimensionality::<Ix0>().map_err(Error::from)?,
fixed_mu,
None,
initial_guess,
)
.map_err(Error::from)?
.into_dyn(),
}),
1 => Ok(PyTransform {
inner: Transform::register(
fixed.into_dimensionality::<Ix1>().map_err(Error::from)?,
moving.into_dimensionality::<Ix1>().map_err(Error::from)?,
fixed_mu,
None,
initial_guess,
)
.map_err(Error::from)?
.into_dyn(),
}),
2 => Ok(PyTransform {
inner: Transform::register(
fixed.into_dimensionality::<Ix2>().map_err(Error::from)?,
moving.into_dimensionality::<Ix2>().map_err(Error::from)?,
fixed_mu,
None,
initial_guess,
)
.map_err(Error::from)?
.into_dyn(),
}),
3 => Ok(PyTransform {
inner: Transform::register(
fixed.into_dimensionality::<Ix3>().map_err(Error::from)?,
moving.into_dimensionality::<Ix3>().map_err(Error::from)?,
fixed_mu,
None,
initial_guess,
)
.map_err(Error::from)?
.into_dyn(),
}),
4 => Ok(PyTransform {
inner: Transform::register(
fixed.into_dimensionality::<Ix4>().map_err(Error::from)?,
moving.into_dimensionality::<Ix4>().map_err(Error::from)?,
fixed_mu,
None,
initial_guess,
)
.map_err(Error::from)?
.into_dyn(),
}),
5 => Ok(PyTransform {
inner: Transform::register(
fixed.into_dimensionality::<Ix5>().map_err(Error::from)?,
moving.into_dimensionality::<Ix5>().map_err(Error::from)?,
fixed_mu,
None,
initial_guess,
)
.map_err(Error::from)?
.into_dyn(),
}),
_ => Err(PyNotImplementedError::new_err(format!(
"registration in {} dimensions is not implemented",
fixed.ndim()
))),
})
}
#[staticmethod]
fn register_affine<'py>(
py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
fixed: PyArrayLikeDyn<f64, AllowTypeChange>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
moving: PyArrayLikeDyn<f64, AllowTypeChange>,
) -> PyResult<PyTransform> {
let fixed = fixed.as_array();
let moving = moving.as_array();
if fixed.shape() != moving.shape() {
return Err(PyErr::from(Error::ShapeMismatch(
fixed.shape().to_vec(),
moving.shape().to_vec(),
)));
}
py.detach(|| match fixed.ndim() {
0 => Ok(PyTransform {
inner: Transform::register_affine(
fixed.into_dimensionality::<Ix0>().map_err(Error::from)?,
moving.into_dimensionality::<Ix0>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
1 => Ok(PyTransform {
inner: Transform::register_affine(
fixed.into_dimensionality::<Ix1>().map_err(Error::from)?,
moving.into_dimensionality::<Ix1>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
2 => Ok(PyTransform {
inner: Transform::register_affine(
fixed.into_dimensionality::<Ix2>().map_err(Error::from)?,
moving.into_dimensionality::<Ix2>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
3 => Ok(PyTransform {
inner: Transform::register_affine(
fixed.into_dimensionality::<Ix3>().map_err(Error::from)?,
moving.into_dimensionality::<Ix3>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
4 => Ok(PyTransform {
inner: Transform::register_affine(
fixed.into_dimensionality::<Ix4>().map_err(Error::from)?,
moving.into_dimensionality::<Ix4>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
5 => Ok(PyTransform {
inner: Transform::register_affine(
fixed.into_dimensionality::<Ix5>().map_err(Error::from)?,
moving.into_dimensionality::<Ix5>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
_ => Err(PyNotImplementedError::new_err(format!(
"registration in {} dimensions is not implemented",
fixed.ndim()
))),
})
}
#[staticmethod]
fn register_translation<'py>(
py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
fixed: PyArrayLikeDyn<f64, AllowTypeChange>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
moving: PyArrayLikeDyn<f64, AllowTypeChange>,
) -> PyResult<PyTransform> {
let fixed = fixed.as_array();
let moving = moving.as_array();
if fixed.shape() != moving.shape() {
return Err(PyErr::from(Error::ShapeMismatch(
fixed.shape().to_vec(),
moving.shape().to_vec(),
)));
}
py.detach(|| match fixed.ndim() {
0 => Ok(PyTransform {
inner: Transform::register_translation(
fixed.into_dimensionality::<Ix0>().map_err(Error::from)?,
moving.into_dimensionality::<Ix0>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
1 => Ok(PyTransform {
inner: Transform::register_translation(
fixed.into_dimensionality::<Ix1>().map_err(Error::from)?,
moving.into_dimensionality::<Ix1>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
2 => Ok(PyTransform {
inner: Transform::register_translation(
fixed.into_dimensionality::<Ix2>().map_err(Error::from)?,
moving.into_dimensionality::<Ix2>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
3 => Ok(PyTransform {
inner: Transform::register_translation(
fixed.into_dimensionality::<Ix3>().map_err(Error::from)?,
moving.into_dimensionality::<Ix3>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
4 => Ok(PyTransform {
inner: Transform::register_translation(
fixed.into_dimensionality::<Ix4>().map_err(Error::from)?,
moving.into_dimensionality::<Ix4>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
5 => Ok(PyTransform {
inner: Transform::register_translation(
fixed.into_dimensionality::<Ix5>().map_err(Error::from)?,
moving.into_dimensionality::<Ix5>().map_err(Error::from)?,
)
.map_err(Error::from)?
.into_dyn(),
}),
_ => Err(PyNotImplementedError::new_err(format!(
"registration in {} dimensions is not implemented",
fixed.ndim()
))),
})
}
}
#[gen_stub_pyclass]
#[pyclass(
subclass,
from_py_object,
eq,
name = "Transforms",
module = "ndbioimage"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PyTransforms {
pub(crate) inner: Transforms,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyTransforms {
pub(crate) fn __getstate__(&self) -> PyResult<Vec<u8>> {
Ok(to_stdvec(self).map_err(Error::from)?)
}
pub(crate) fn __setstate__(&mut self, state: Vec<u8>) -> PyResult<()> {
Ok(from_bytes(&state).map_err(Error::from)?)
}
#[staticmethod]
pub(crate) fn load(path: PathBuf) -> PyResult<PyTransforms> {
Ok(PyTransforms {
inner: Transforms::load(&path)?,
})
}
pub(crate) fn save(&self, path: PathBuf) -> PyResult<()> {
Ok(self.inner.save(&path)?)
}
#[staticmethod]
fn calculate_channel_transforms_2d(
bead_files: Vec<PathBuf>,
main_channel: usize,
default_transform: Option<PyTransform>,
) -> PyResult<Vec<PyTransform>> {
Ok(Transforms::calculate_channel_transforms_2d(
&bead_files
.iter()
.map(|file| file.as_path())
.collect::<Vec<_>>(),
main_channel,
default_transform
.map(|d| d.inner.into_dimensionality().map_err(Error::from))
.transpose()?,
)?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
})
.collect())
}
#[staticmethod]
fn calculate_channel_transforms_3d(
bead_files: Vec<PathBuf>,
main_channel: usize,
default_transform: Option<PyTransform>,
) -> PyResult<Vec<PyTransform>> {
Ok(Transforms::calculate_channel_transforms_3d(
&bead_files
.iter()
.map(|file| file.as_path())
.collect::<Vec<_>>(),
main_channel,
default_transform
.map(|d| d.inner.into_dimensionality().map_err(Error::from))
.transpose()?,
)?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
})
.collect())
}
}
+11 -12
View File
@@ -100,9 +100,10 @@ impl Dimensions {
} }
/// pixel type enum /// pixel type enum
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub enum PixelType { pub enum PixelType {
/// true / false
Bool,
/// signed 8-bit integer /// signed 8-bit integer
I8, I8,
/// unsigned 8-bit integer /// unsigned 8-bit integer
@@ -135,7 +136,7 @@ impl PixelType {
/// number of bytes per pixel for this type /// number of bytes per pixel for this type
pub fn bytes_per_pixel(&self) -> usize { pub fn bytes_per_pixel(&self) -> usize {
match self { match self {
PixelType::I8 | PixelType::U8 => 1, PixelType::Bool | PixelType::I8 | PixelType::U8 => 1,
PixelType::I16 | PixelType::U16 => 2, PixelType::I16 | PixelType::U16 => 2,
PixelType::I32 | PixelType::U32 | PixelType::F32 => 4, PixelType::I32 | PixelType::U32 | PixelType::F32 => 4,
PixelType::I64 | PixelType::U64 | PixelType::F64 => 8, PixelType::I64 | PixelType::U64 | PixelType::F64 => 8,
@@ -145,7 +146,6 @@ impl PixelType {
} }
/// array data with a specific pixel type /// array data with a specific pixel type
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ArrayT<D: Dimension> { pub enum ArrayT<D: Dimension> {
/// signed 8-bit integer array /// signed 8-bit integer array
@@ -207,7 +207,6 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
} }
/// retrieve frame at channel c, slice 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>; fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error>;
/// the path to the image file /// the path to the image file
@@ -486,7 +485,7 @@ impl Reader for DynReader {
DynReader::BioFormatsRust(r) => r.reader_name(), DynReader::BioFormatsRust(r) => r.reader_name(),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.reader_name(), DynReader::BioFormatsJava(r) => r.reader_name(),
#[allow(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -503,7 +502,7 @@ impl Reader for DynReader {
DynReader::BioFormatsRust(r) => r.metadata()?, DynReader::BioFormatsRust(r) => r.metadata()?,
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.metadata()?, DynReader::BioFormatsJava(r) => r.metadata()?,
#[allow(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
}) })
} }
@@ -520,7 +519,7 @@ impl Reader for DynReader {
DynReader::BioFormatsRust(r) => r.get_frame(c, z, t), DynReader::BioFormatsRust(r) => r.get_frame(c, z, t),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.get_frame(c, z, t), DynReader::BioFormatsJava(r) => r.get_frame(c, z, t),
#[allow(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -537,7 +536,7 @@ impl Reader for DynReader {
DynReader::BioFormatsRust(r) => r.path(), DynReader::BioFormatsRust(r) => r.path(),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.path(), DynReader::BioFormatsJava(r) => r.path(),
#[allow(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -554,7 +553,7 @@ impl Reader for DynReader {
DynReader::BioFormatsRust(r) => r.series(), DynReader::BioFormatsRust(r) => r.series(),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.series(), DynReader::BioFormatsJava(r) => r.series(),
#[allow(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -571,7 +570,7 @@ impl Reader for DynReader {
DynReader::BioFormatsRust(r) => r.position(), DynReader::BioFormatsRust(r) => r.position(),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.position(), DynReader::BioFormatsJava(r) => r.position(),
#[allow(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -588,7 +587,7 @@ impl Reader for DynReader {
DynReader::BioFormatsRust(r) => r.shape(), DynReader::BioFormatsRust(r) => r.shape(),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.shape(), DynReader::BioFormatsJava(r) => r.shape(),
#[allow(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
} }
@@ -605,7 +604,7 @@ impl Reader for DynReader {
DynReader::BioFormatsRust(r) => r.pixel_type(), DynReader::BioFormatsRust(r) => r.pixel_type(),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.pixel_type(), DynReader::BioFormatsJava(r) => r.pixel_type(),
#[allow(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
} }
+24 -1
View File
@@ -389,6 +389,7 @@ impl BioFormatsJavaReader {
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>, Error> { fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>, Error> {
let chunk_size = match self.pixel_type { let chunk_size = match self.pixel_type {
PixelType::Bool => 1,
PixelType::I8 => 1, PixelType::I8 => 1,
PixelType::U8 => 1, PixelType::U8 => 1,
PixelType::I16 => 2, PixelType::I16 => 2,
@@ -413,6 +414,26 @@ impl BioFormatsJavaReader {
fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame, Error> { fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame, Error> {
macro_rules! get_frame { macro_rules! get_frame {
(bool, <$n:expr) => {
Ok(ArrayT::from(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
bytes
.iter()
.map(|x| [x & 128, x & 64, x & 32, x & 16, x & 8, x & 4, x & 2, x & 1])
.flatten()
.collect(),
)?))
};
(bool, >$n:expr) => {
Ok(ArrayT::from(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
bytes
.iter()
.map(|x| [x & 1, x & 2, x & 4, x & 8, x & 16, x & 32, x & 64, x & 128])
.flatten()
.collect(),
)?))
};
($t:tt, <$n:expr) => { ($t:tt, <$n:expr) => {
Ok(ArrayT::from(Array2::from_shape_vec( Ok(ArrayT::from(Array2::from_shape_vec(
(self.shape.y, self.shape.x), (self.shape.y, self.shape.x),
@@ -434,6 +455,7 @@ impl BioFormatsJavaReader {
} }
match (&self.pixel_type, self.little_endian) { match (&self.pixel_type, self.little_endian) {
(PixelType::Bool, true) => get_frame!(bool, <1),
(PixelType::I8, true) => get_frame!(i8, <1), (PixelType::I8, true) => get_frame!(i8, <1),
(PixelType::U8, true) => get_frame!(u8, <1), (PixelType::U8, true) => get_frame!(u8, <1),
(PixelType::I16, true) => get_frame!(i16, <2), (PixelType::I16, true) => get_frame!(i16, <2),
@@ -447,6 +469,7 @@ impl BioFormatsJavaReader {
(PixelType::I128, true) => get_frame!(i128, <16), (PixelType::I128, true) => get_frame!(i128, <16),
(PixelType::U128, true) => get_frame!(u128, <16), (PixelType::U128, true) => get_frame!(u128, <16),
(PixelType::F128, true) => get_frame!(f64, <8), (PixelType::F128, true) => get_frame!(f64, <8),
(PixelType::Bool, false) => get_frame!(bool, >1),
(PixelType::I8, false) => get_frame!(i8, >1), (PixelType::I8, false) => get_frame!(i8, >1),
(PixelType::U8, false) => get_frame!(u8, >1), (PixelType::U8, false) => get_frame!(u8, >1),
(PixelType::I16, false) => get_frame!(i16, >2), (PixelType::I16, false) => get_frame!(i16, >2),
@@ -501,7 +524,7 @@ impl Reader for BioFormatsJavaReader {
Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string()) Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string())
})?; })?;
} }
let mut new = BioFormatsJavaReader { let mut new = Self {
reader: ThreadLocal::default(), reader: ThreadLocal::default(),
path, path,
series, series,
+45 -14
View File
@@ -78,19 +78,20 @@ impl Deref for BioFormatsRustReader {
} }
} }
fn map_pixel_type(bf: bioformats::PixelType) -> Result<PixelType, Error> { impl From<bioformats::PixelType> for PixelType {
use bioformats::PixelType as Bf; fn from(bf: bioformats::PixelType) -> Self {
Ok(match bf { match bf {
Bf::Int8 => PixelType::I8, bioformats::PixelType::Bit => PixelType::Bool,
Bf::Uint8 => PixelType::U8, bioformats::PixelType::Int8 => PixelType::I8,
Bf::Int16 => PixelType::I16, bioformats::PixelType::Uint8 => PixelType::U8,
Bf::Uint16 => PixelType::U16, bioformats::PixelType::Int16 => PixelType::I16,
Bf::Int32 => PixelType::I32, bioformats::PixelType::Uint16 => PixelType::U16,
Bf::Uint32 => PixelType::U32, bioformats::PixelType::Int32 => PixelType::I32,
Bf::Float32 => PixelType::F32, bioformats::PixelType::Uint32 => PixelType::U32,
Bf::Float64 => PixelType::F64, bioformats::PixelType::Float32 => PixelType::F32,
Bf::Bit => PixelType::U8, bioformats::PixelType::Float64 => PixelType::F64,
}) }
}
} }
impl BioFormatsRustReader { impl BioFormatsRustReader {
@@ -126,6 +127,7 @@ impl BioFormatsRustReader {
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>, Error> { fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>, Error> {
let chunk_size = match self.pixel_type { let chunk_size = match self.pixel_type {
PixelType::Bool => 1,
PixelType::I8 => 1, PixelType::I8 => 1,
PixelType::U8 => 1, PixelType::U8 => 1,
PixelType::I16 => 2, PixelType::I16 => 2,
@@ -150,6 +152,26 @@ impl BioFormatsRustReader {
fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame, Error> { fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame, Error> {
macro_rules! get_frame { macro_rules! get_frame {
(bool, <$n:expr) => {
Ok(ArrayT::from(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
bytes
.iter()
.map(|x| [x & 128, x & 64, x & 32, x & 16, x & 8, x & 4, x & 2, x & 1])
.flatten()
.collect(),
)?))
};
(bool, >$n:expr) => {
Ok(ArrayT::from(Array2::from_shape_vec(
(self.shape.y, self.shape.x),
bytes
.iter()
.map(|x| [x & 1, x & 2, x & 4, x & 8, x & 16, x & 32, x & 64, x & 128])
.flatten()
.collect(),
)?))
};
($t:tt, <$n:expr) => { ($t:tt, <$n:expr) => {
Ok(ArrayT::from(Array2::from_shape_vec( Ok(ArrayT::from(Array2::from_shape_vec(
(self.shape.y, self.shape.x), (self.shape.y, self.shape.x),
@@ -171,6 +193,7 @@ impl BioFormatsRustReader {
} }
match (&self.pixel_type, self.little_endian) { match (&self.pixel_type, self.little_endian) {
(PixelType::Bool, true) => get_frame!(bool, <1),
(PixelType::I8, true) => get_frame!(i8, <1), (PixelType::I8, true) => get_frame!(i8, <1),
(PixelType::U8, true) => get_frame!(u8, <1), (PixelType::U8, true) => get_frame!(u8, <1),
(PixelType::I16, true) => get_frame!(i16, <2), (PixelType::I16, true) => get_frame!(i16, <2),
@@ -184,6 +207,7 @@ impl BioFormatsRustReader {
(PixelType::I128, true) => get_frame!(i128, <16), (PixelType::I128, true) => get_frame!(i128, <16),
(PixelType::U128, true) => get_frame!(u128, <16), (PixelType::U128, true) => get_frame!(u128, <16),
(PixelType::F128, true) => get_frame!(f64, <8), (PixelType::F128, true) => get_frame!(f64, <8),
(PixelType::Bool, false) => get_frame!(bool, >1),
(PixelType::I8, false) => get_frame!(i8, >1), (PixelType::I8, false) => get_frame!(i8, >1),
(PixelType::U8, false) => get_frame!(u8, >1), (PixelType::U8, false) => get_frame!(u8, >1),
(PixelType::I16, false) => get_frame!(i16, >2), (PixelType::I16, false) => get_frame!(i16, >2),
@@ -246,7 +270,7 @@ impl Reader for BioFormatsRustReader {
new.shape.y = metadata.size_y as usize; new.shape.y = metadata.size_y as usize;
new.shape.x = metadata.size_x as usize; new.shape.x = metadata.size_x as usize;
new.little_endian = metadata.is_little_endian; new.little_endian = metadata.is_little_endian;
new.pixel_type = map_pixel_type(metadata.pixel_type)?; new.pixel_type = PixelType::from(metadata.pixel_type);
Ok(new) Ok(new)
} }
@@ -304,6 +328,13 @@ impl Reader for BioFormatsRustReader {
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
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 reader = ImageReader::open(path.as_ref()) let reader = ImageReader::open(path.as_ref())
.map_err(|e| Error::Parse(format!("bioformats failed to open: {}", e)))?; .map_err(|e| Error::Parse(format!("bioformats failed to open: {}", e)))?;
let n = reader.series_count(); let n = reader.series_count();
+39
View File
@@ -1053,6 +1053,44 @@ impl Reader for CziReader {
} }
macro_rules! get_frame { macro_rules! get_frame {
(bool, $n:expr) => {{
let mut array = Array2::zeros((self.shape.y, self.shape.x));
if let Some(indices) = self.block_map.get(&(c, z, t)) {
for &i in indices {
let sub_block = reader.read_sub_block(i)?;
let bitmap = sub_block.create_bitmap()?.lock()?;
let bytes = bitmap.lock_info.get_data_roi();
let info = sub_block.get_info()?;
let rect = info.get_logical_rect();
let x = (rect.get_x() - min_x) as usize;
let y = (rect.get_y() - min_y) as usize;
let w = rect.get_w() as usize;
let h = rect.get_h() as usize;
array
.slice_mut(s![x..x + w, y..y + h])
.assign(&Array2::from_shape_vec(
(w, h),
bytes
.iter()
.map(|x| {
[
x & 128,
x & 64,
x & 32,
x & 16,
x & 8,
x & 4,
x & 2,
x & 1,
]
})
.flatten()
.collect(),
)?);
}
}
Ok(ArrayT::from(array))
}};
($t:tt, $n:expr) => {{ ($t:tt, $n:expr) => {{
let mut array = Array2::zeros((self.shape.y, self.shape.x)); let mut array = Array2::zeros((self.shape.y, self.shape.x));
if let Some(indices) = self.block_map.get(&(c, z, t)) { if let Some(indices) = self.block_map.get(&(c, z, t)) {
@@ -1082,6 +1120,7 @@ impl Reader for CziReader {
} }
match self.pixel_type { match self.pixel_type {
PixelType::Bool => get_frame!(bool, 1),
PixelType::I8 => get_frame!(i8, 1), PixelType::I8 => get_frame!(i8, 1),
PixelType::U8 => get_frame!(u8, 1), PixelType::U8 => get_frame!(u8, 1),
PixelType::I16 => get_frame!(i16, 2), PixelType::I16 => get_frame!(i16, 2),
+9 -9
View File
@@ -22,7 +22,7 @@ pub struct TiffSeqReader {
filedict: HashMap<(usize, usize, usize), PathBuf>, filedict: HashMap<(usize, usize, usize), PathBuf>,
cnamelist: Vec<String>, cnamelist: Vec<String>,
#[serde(skip)] #[serde(skip)]
metadata_map: HashMap<String, serde_yaml::Value>, metadata_map: HashMap<String, yaml_serde::Value>,
} }
impl From<TiffSeqReader> for DynReader { impl From<TiffSeqReader> for DynReader {
@@ -73,10 +73,10 @@ impl TiffSeqReader {
Ok(files) Ok(files)
} }
fn read_metadata_from_file(dir: &Path) -> Result<HashMap<String, serde_yaml::Value>, Error> { fn read_metadata_from_file(dir: &Path) -> Result<HashMap<String, yaml_serde::Value>, Error> {
let md_path = dir.join("metadata.txt"); let md_path = dir.join("metadata.txt");
let text = std::fs::read_to_string(&md_path)?; let text = std::fs::read_to_string(&md_path)?;
let parsed: serde_yaml::Value = serde_yaml::from_str(&text)?; let parsed: yaml_serde::Value = yaml_serde::from_str(&text)?;
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("Info".to_string(), parsed); map.insert("Info".to_string(), parsed);
Ok(map) Ok(map)
@@ -129,11 +129,11 @@ impl Reader for TiffSeqReader {
.ok_or_else(|| Error::Parse("missing Info key in tag 50839".to_string()))?; .ok_or_else(|| Error::Parse("missing Info key in tag 50839".to_string()))?;
let lookup = |key: &str| { let lookup = |key: &str| {
info.get(serde_yaml::Value::String(key.to_string())) info.get(yaml_serde::Value::String(key.to_string()))
.or_else(|| { .or_else(|| {
info.get(serde_yaml::Value::String("Summary".to_string())) info.get(yaml_serde::Value::String("Summary".to_string()))
.and_then(|s| s.as_mapping()) .and_then(|s| s.as_mapping())
.and_then(|s| s.get(serde_yaml::Value::String(key.to_string()))) .and_then(|s| s.get(yaml_serde::Value::String(key.to_string())))
}) })
}; };
@@ -240,12 +240,12 @@ impl Reader for TiffSeqReader {
let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping()); let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping());
let slookup = let slookup =
|key: &str| info.and_then(|m| m.get(serde_yaml::Value::String(key.to_string()))); |key: &str| info.and_then(|m| m.get(yaml_serde::Value::String(key.to_string())));
let summary = slookup("Summary").and_then(|v| v.as_mapping()); let summary = slookup("Summary").and_then(|v| v.as_mapping());
let summary_lookup = let summary_lookup =
|key: &str| summary.and_then(|m| m.get(serde_yaml::Value::String(key.to_string()))); |key: &str| summary.and_then(|m| m.get(yaml_serde::Value::String(key.to_string())));
let first_frame = info.and_then(|m| { let first_frame = info.and_then(|m| {
m.iter() m.iter()
@@ -254,7 +254,7 @@ impl Reader for TiffSeqReader {
}); });
let frame_lookup = let frame_lookup =
|key: &str| first_frame.and_then(|m| m.get(serde_yaml::Value::String(key.to_string()))); |key: &str| first_frame.and_then(|m| m.get(yaml_serde::Value::String(key.to_string())));
let ome_pixel_type = match self.pixel_type { let ome_pixel_type = match self.pixel_type {
PixelType::I8 => ome::PixelType::Int8, PixelType::I8 => ome::PixelType::Int8,
+1
View File
@@ -165,6 +165,7 @@ where
P: AsRef<Path>, P: AsRef<Path>,
{ {
match self.pixel_type() { match self.pixel_type() {
PixelType::Bool => self.save_as_tiff_with_type::<u8, P>(path, options)?,
PixelType::I8 => self.save_as_tiff_with_type::<i8, P>(path, options)?, PixelType::I8 => self.save_as_tiff_with_type::<i8, P>(path, options)?,
PixelType::U8 => self.save_as_tiff_with_type::<u8, P>(path, options)?, PixelType::U8 => self.save_as_tiff_with_type::<u8, P>(path, options)?,
PixelType::I16 => self.save_as_tiff_with_type::<i16, P>(path, options)?, PixelType::I16 => self.save_as_tiff_with_type::<i16, P>(path, options)?,
+162 -7
View File
@@ -1,20 +1,175 @@
use crate::readers::Reader; use crate::axes::Axis;
use crate::error::Error;
use crate::readers::{DynReader, Reader};
use crate::view::View; use crate::view::View;
pub use image_registration::transform::Transform; pub use image_registration::transform::Transform;
use ndarray::{Dimension, Ix2, Ix3}; use ndarray::{Dimension, Ix2, Ix3, s};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path;
#[expect(clippy::upper_case_acronyms)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum TransformD { pub enum TransformD {
YX(Transform<Ix2>), YX(Transform<Ix2>),
ZYX(Transform<Ix3>), ZYX(Transform<Ix3>),
} }
impl<D: Dimension, R: Reader> View<D, R> {}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Transforms { pub struct Transforms {
channel: Vec<TransformD>, pub channel: Vec<TransformD>,
drift: Vec<TransformD>, pub drift: Vec<TransformD>,
}
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 save(&self, path: &Path) -> Result<(), Error> {
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
Ok(yaml_serde::to_writer(file, self)?)
}
pub fn calculate_channel_transforms_2d(
bead_files: &[&Path],
main_channel: usize,
default_transform: Option<Transform<Ix2>>,
) -> Result<Vec<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
.iter()
.map(|t| (&t[channel] * &default_transform).matrix())
.reduce(|a, b| a + b)
.unwrap()
/ n_channels as f64;
let dmatrix = 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(
Transform::default()
.with_matrix(matrix.view())
.with_dmatrix(dmatrix.view()),
);
}
Ok(average_transforms)
}
pub fn calculate_channel_transforms_3d(
bead_files: &[&Path],
main_channel: usize,
default_transform: Option<Transform<Ix3>>,
) -> Result<Vec<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
.iter()
.map(|t| (&t[channel] * &default_transform).matrix())
.reduce(|a, b| a + b)
.unwrap()
/ n_channels as f64;
let dmatrix = 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(
Transform::default()
.with_matrix(matrix.view())
.with_dmatrix(dmatrix.view()),
);
}
Ok(average_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)?;
Ok(self)
}
pub fn load_transform_from_yaml(&mut self, path: &Path) -> Result<(), Error> {
self.transforms = Transforms::load(path)?;
Ok(())
}
pub fn calculate_channel_transforms_2d(
&self,
main_channel: usize,
) -> Result<Vec<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 {
if channel == main_channel {
transforms.push(Transform::default());
} 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())?);
}
}
Ok(transforms)
}
pub fn calculate_channel_transforms_3d(
&self,
main_channel: usize,
) -> Result<Vec<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 {
if channel == main_channel {
transforms.push(Transform::default());
} else {
let max = self
.slice_cztyx(s![channel, .., 0, .., ..])?
.as_array::<f64>()?;
transforms.push(Transform::register_affine(main_max.view(), max.view())?);
}
}
Ok(transforms)
}
pub fn calculate_drift_transform_2d(&self) -> Result<Vec<Transform<Ix2>>, Error> {
todo!()
}
pub fn calculate_drift_transform_3d(&self) -> Result<Vec<Transform<Ix3>>, Error> {
todo!()
}
} }
#[cfg(test)] #[cfg(test)]
@@ -23,7 +178,7 @@ mod tests {
#[test] #[test]
fn test_transforms() -> Result<(), Box<dyn std::error::Error>> { fn test_transforms() -> Result<(), Box<dyn std::error::Error>> {
let t = Transforms::default(); let _t = Transforms::default();
Ok(()) Ok(())
} }
+5 -5
View File
@@ -91,7 +91,7 @@ pub struct View<D: Dimension, R: Reader = DynReader> {
operations: IndexMap<Axis, Operation>, operations: IndexMap<Axis, Operation>,
dimensionality: PhantomData<D>, dimensionality: PhantomData<D>,
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
transforms: Transforms, pub(crate) transforms: Transforms,
} }
impl<D, R> Hash for View<D, R> impl<D, R> Hash for View<D, R>
@@ -123,7 +123,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
} }
} }
#[allow(dead_code)] #[expect(dead_code)]
pub(crate) fn new_with_axes(reader: R, axes: Vec<Axis>) -> Result<Self, Error> { pub(crate) fn new_with_axes(reader: R, axes: Vec<Axis>) -> Result<Self, Error> {
let mut slice = Vec::new(); let mut slice = Vec::new();
let shape = reader.shape(); let shape = reader.shape();
@@ -251,7 +251,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
operations: self.operations, operations: self.operations,
dimensionality: PhantomData, dimensionality: PhantomData,
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
transforms: Transforms::default(), transforms: self.transforms,
}) })
} else { } else {
Err(Error::DimensionalityMismatch(d, self.ndim())) Err(Error::DimensionalityMismatch(d, self.ndim()))
@@ -264,7 +264,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
operations: self.operations, operations: self.operations,
dimensionality: PhantomData, dimensionality: PhantomData,
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
transforms: Transforms::default(), transforms: self.transforms,
}) })
} }
} }
@@ -274,7 +274,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
&self.axes &self.axes
} }
#[allow(dead_code)] #[expect(dead_code)]
pub(crate) fn get_operations(&self) -> &IndexMap<Axis, Operation> { pub(crate) fn get_operations(&self) -> &IndexMap<Axis, Operation> {
&self.operations &self.operations
} }