- implement sliced views, including min, max, sum and mean operations
This commit is contained in:
21
Cargo.toml
21
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ndbioimage"
|
||||
version = "2025.2.3"
|
||||
version = "2025.4.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.78.0"
|
||||
authors = ["Wim Pomp <w.pomp@nki.nl>"]
|
||||
@@ -19,15 +19,20 @@ name = "ndbioimage"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.95"
|
||||
anyhow = "1.0.98"
|
||||
itertools = "0.14.0"
|
||||
indexmap = { version = "2.9.0", features = ["serde"] }
|
||||
j4rs = "0.22.0"
|
||||
ndarray = "0.16.1"
|
||||
ndarray = { version = "0.16.1", features = ["serde"] }
|
||||
num = "0.4.3"
|
||||
numpy = { version = "0.23.0", optional = true }
|
||||
numpy = { version = "0.24.0", optional = true }
|
||||
serde = { version = "1.0.219", features = ["rc"] }
|
||||
serde_json = { version = "1.0.140", optional = true }
|
||||
serde_with = "3.12.0"
|
||||
thread_local = "1.1.8"
|
||||
|
||||
[dependencies.pyo3]
|
||||
version = "0.23.4"
|
||||
version = "0.24.2"
|
||||
features = ["extension-module", "abi3-py310", "generate-import-lib", "anyhow"]
|
||||
optional = true
|
||||
|
||||
@@ -35,10 +40,10 @@ optional = true
|
||||
rayon = "1.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.95"
|
||||
anyhow = "1.0.98"
|
||||
j4rs = "0.22.0"
|
||||
retry = "2.0.0"
|
||||
retry = "2.1.0"
|
||||
|
||||
[features]
|
||||
python = ["dep:pyo3", "dep:numpy"]
|
||||
python = ["dep:pyo3", "dep:numpy", "dep:serde_json"]
|
||||
gpl-formats = []
|
||||
32
README.md
32
README.md
@@ -74,7 +74,7 @@ use ndarray::Array2;
|
||||
use ndbioimage::Reader;
|
||||
|
||||
let path = "/path/to/file";
|
||||
let reader = Reader::new(&path, 0).unwrap();
|
||||
let reader = Reader::new(&path, 0)?;
|
||||
println!("size: {}, {}", reader.size_y, reader.size_y);
|
||||
let frame = reader.get_frame(0, 0, 0).unwrap();
|
||||
if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
|
||||
@@ -86,30 +86,22 @@ let xml = reader.get_ome_xml().unwrap();
|
||||
println!("{}", xml);
|
||||
```
|
||||
|
||||
```
|
||||
use ndarray::Array2;
|
||||
use ndbioimage::Reader;
|
||||
|
||||
let path = "/path/to/file";
|
||||
let reader = Reader::new(&path, 0)?;
|
||||
let view = reader.view();
|
||||
let view = view.max_proj(3)?;
|
||||
let array = view.as_array::<u16>()?
|
||||
```
|
||||
|
||||
### Command line
|
||||
```ndbioimage --help```: show help
|
||||
```ndbioimage image```: show metadata about image
|
||||
```ndbioimage image -w {name}.tif -r```: copy image into image.tif (replacing {name} with image), while registering channels
|
||||
```ndbioimage image -w image.mp4 -C cyan lime red``` copy image into image.mp4 (z will be max projected), make channel colors cyan lime and red
|
||||
|
||||
## Adding more formats
|
||||
Readers for image formats subclass AbstractReader. When an image reader is imported, Imread will
|
||||
automatically recognize it and use it to open the appropriate file format. Image readers
|
||||
are required to implement the following methods:
|
||||
|
||||
- staticmethod _can_open(path): return True if path can be opened by this reader
|
||||
- \_\_frame__(self, c, z, t): return the frame at channel=c, z-slice=z, time=t from the file
|
||||
|
||||
Optional methods:
|
||||
- get_ome: reads metadata from file and adds them to an OME object imported
|
||||
from the ome-types library
|
||||
- open(self): maybe open some file handle
|
||||
- close(self): close any file handles
|
||||
|
||||
Optional fields:
|
||||
- priority (int): Imread will try readers with a lower number first, default: 99
|
||||
- do_not_pickle (strings): any attributes that should not be included when the object is pickled,
|
||||
for example: any file handles
|
||||
|
||||
# TODO
|
||||
- more image formats
|
||||
|
||||
2
build.rs
2
build.rs
@@ -8,7 +8,7 @@ use retry::{delay, delay::Exponential, retry};
|
||||
use j4rs::Jvm;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo::rerun-if-changed=build.rs");
|
||||
|
||||
#[cfg(not(feature = "python"))]
|
||||
if std::env::var("DOCS_RS").is_err() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ except ImportError:
|
||||
DataFrame, Series, concat = None, None, None
|
||||
|
||||
|
||||
if hasattr(yaml, 'full_load'):
|
||||
if hasattr(yaml, "full_load"):
|
||||
yamlload = yaml.full_load
|
||||
else:
|
||||
yamlload = yaml.load
|
||||
@@ -34,7 +34,7 @@ class Transforms(dict):
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, file, C=True, T=True):
|
||||
with open(Path(file).with_suffix('.yml')) as f:
|
||||
with open(Path(file).with_suffix(".yml")) as f:
|
||||
return cls.from_dict(yamlload(f), C, T)
|
||||
|
||||
@classmethod
|
||||
@@ -42,7 +42,9 @@ class Transforms(dict):
|
||||
new = cls()
|
||||
for key, value in d.items():
|
||||
if isinstance(key, str) and C:
|
||||
new[key.replace(r'\:', ':').replace('\\\\', '\\')] = Transform.from_dict(value)
|
||||
new[key.replace(r"\:", ":").replace("\\\\", "\\")] = (
|
||||
Transform.from_dict(value)
|
||||
)
|
||||
elif T:
|
||||
new[key] = Transform.from_dict(value)
|
||||
return new
|
||||
@@ -69,11 +71,19 @@ class Transforms(dict):
|
||||
return new
|
||||
|
||||
def asdict(self):
|
||||
return {key.replace('\\', '\\\\').replace(':', r'\:') if isinstance(key, str) else key: value.asdict()
|
||||
for key, value in self.items()}
|
||||
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)
|
||||
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
|
||||
@@ -88,7 +98,7 @@ class Transforms(dict):
|
||||
return hash(frozenset((*self.__dict__.items(), *self.items())))
|
||||
|
||||
def save(self, file):
|
||||
with open(Path(file).with_suffix('.yml'), 'w') as f:
|
||||
with open(Path(file).with_suffix(".yml"), "w") as f:
|
||||
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
|
||||
|
||||
def copy(self):
|
||||
@@ -109,8 +119,10 @@ class Transforms(dict):
|
||||
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}')
|
||||
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]
|
||||
|
||||
@@ -124,37 +136,54 @@ class Transforms(dict):
|
||||
|
||||
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
|
||||
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']))
|
||||
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.')
|
||||
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]
|
||||
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]
|
||||
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()
|
||||
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'):
|
||||
if file.name.lower().startswith("beads"):
|
||||
try:
|
||||
with Imread(file):
|
||||
files.append(file)
|
||||
@@ -162,18 +191,18 @@ class Transforms(dict):
|
||||
pass
|
||||
files = sorted(files)
|
||||
if not files:
|
||||
raise Exception('No bead file found!')
|
||||
raise Exception("No bead file found!")
|
||||
checked_files = []
|
||||
for file in files:
|
||||
try:
|
||||
if file.is_dir():
|
||||
file /= 'Pos0'
|
||||
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!')
|
||||
raise Exception("No bead file found!")
|
||||
return checked_files
|
||||
|
||||
@staticmethod
|
||||
@@ -182,12 +211,16 @@ class Transforms(dict):
|
||||
in the horizontal direction"""
|
||||
from . import Imread
|
||||
|
||||
with Imread(bead_file, axes='zcyx') as im: # noqa
|
||||
max_ims = im.max('z')
|
||||
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']
|
||||
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:
|
||||
@@ -200,54 +233,81 @@ class Transforms(dict):
|
||||
matrix[0, 0] = 0.86
|
||||
transform.matrix = matrix
|
||||
transforms = Transforms()
|
||||
for c in tqdm(goodch, desc='Calculating channel transforms'): # noqa
|
||||
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
|
||||
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'])
|
||||
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)
|
||||
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'])))]
|
||||
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 = [
|
||||
(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}
|
||||
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:]
|
||||
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.'))
|
||||
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):
|
||||
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']]):
|
||||
for i, shift in enumerate(shifts[: im.shape["t"]]):
|
||||
self[i] = Transform.from_shift(shift)
|
||||
return self
|
||||
|
||||
@@ -257,9 +317,11 @@ class Transform:
|
||||
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.]
|
||||
self.shape = [512., 512.]
|
||||
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
|
||||
|
||||
@@ -276,10 +338,12 @@ class Transform:
|
||||
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')
|
||||
raise ImportError(
|
||||
"SimpleElastix is not installed: "
|
||||
"https://simpleelastix.readthedocs.io/GettingStarted.html"
|
||||
)
|
||||
new = cls()
|
||||
kind = kind or 'affine'
|
||||
kind = kind or "affine"
|
||||
new.shape = fix.shape
|
||||
fix, mov = new.cast_image(fix), new.cast_image(mov)
|
||||
# TODO: implement RigidTransform
|
||||
@@ -290,16 +354,18 @@ class Transform:
|
||||
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']]
|
||||
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)')
|
||||
raise NotImplementedError(f"{kind} tranforms not implemented (yet)")
|
||||
new.dparameters = 6 * [np.nan]
|
||||
return new
|
||||
|
||||
@@ -315,18 +381,35 @@ class Transform:
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, file):
|
||||
with open(Path(file).with_suffix('.yml')) as f:
|
||||
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., 1., 0., 0.) if d['TransformParameters'] is None else
|
||||
[float(i) for i in d['TransformParameters']])
|
||||
new.dparameters = ([(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']]
|
||||
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
|
||||
@@ -359,9 +442,13 @@ class Transform:
|
||||
|
||||
@property
|
||||
def matrix(self):
|
||||
return np.array(((*self.parameters[:2], self.parameters[4]),
|
||||
return np.array(
|
||||
(
|
||||
(*self.parameters[:2], self.parameters[4]),
|
||||
(*self.parameters[2:4], self.parameters[5]),
|
||||
(0, 0, 1)))
|
||||
(0, 0, 1),
|
||||
)
|
||||
)
|
||||
|
||||
@matrix.setter
|
||||
def matrix(self, value):
|
||||
@@ -370,9 +457,13 @@ class Transform:
|
||||
|
||||
@property
|
||||
def dmatrix(self):
|
||||
return np.array(((*self.dparameters[:2], self.dparameters[4]),
|
||||
return np.array(
|
||||
(
|
||||
(*self.dparameters[:2], self.dparameters[4]),
|
||||
(*self.dparameters[2:4], self.dparameters[5]),
|
||||
(0, 0, 0)))
|
||||
(0, 0, 0),
|
||||
)
|
||||
)
|
||||
|
||||
@dmatrix.setter
|
||||
def dmatrix(self, value):
|
||||
@@ -384,7 +475,7 @@ class Transform:
|
||||
if self.transform is not None:
|
||||
return list(self.transform.GetParameters())
|
||||
else:
|
||||
return [1., 0., 0., 1., 0., 0.]
|
||||
return [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
|
||||
|
||||
@parameters.setter
|
||||
def parameters(self, value):
|
||||
@@ -420,20 +511,32 @@ class Transform:
|
||||
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()}
|
||||
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')
|
||||
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)
|
||||
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,
|
||||
@@ -442,21 +545,28 @@ class Transform:
|
||||
if self.is_unity():
|
||||
return array.copy()
|
||||
elif DataFrame is not None and isinstance(array, (DataFrame, Series)):
|
||||
columns = columns or ['x', 'y']
|
||||
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]
|
||||
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)])
|
||||
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:
|
||||
if not file[-3:] == "yml":
|
||||
file += ".yml"
|
||||
with open(file, "w") as f:
|
||||
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
|
||||
|
||||
218
src/axes.rs
Normal file
218
src/axes.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use crate::stats::MinMax;
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use ndarray::{Array, Dimension, Ix2, SliceInfo, SliceInfoElem};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_with::{DeserializeAs, SerializeAs};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// a trait to find axis indices from any object
|
||||
pub trait Ax {
|
||||
/// C: 0, Z: 1, T: 2, Y: 3, X: 4
|
||||
fn n(&self) -> usize;
|
||||
|
||||
/// the indices of axes in self.axes, which always has all of CZTYX
|
||||
fn pos(&self, axes: &[Axis], slice: &[SliceInfoElem]) -> Result<usize>;
|
||||
|
||||
/// the indices of axes in self.axes, which always has all of CZTYX, but skip axes with an operation
|
||||
fn pos_op(&self, axes: &[Axis], slice: &[SliceInfoElem], op_axes: &[Axis]) -> Result<usize>;
|
||||
}
|
||||
|
||||
/// Enum for CZTYX axes or a new axis
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
pub enum Axis {
|
||||
C,
|
||||
Z,
|
||||
T,
|
||||
Y,
|
||||
X,
|
||||
New,
|
||||
}
|
||||
|
||||
impl Hash for Axis {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
(*self as usize).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Axis {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match s.to_uppercase().as_str() {
|
||||
"C" => Ok(Axis::C),
|
||||
"Z" => Ok(Axis::Z),
|
||||
"T" => Ok(Axis::T),
|
||||
"Y" => Ok(Axis::Y),
|
||||
"X" => Ok(Axis::X),
|
||||
"NEW" => Ok(Axis::New),
|
||||
_ => Err(anyhow!("invalid axis: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ax for Axis {
|
||||
fn n(&self) -> usize {
|
||||
*self as usize
|
||||
}
|
||||
|
||||
fn pos(&self, axes: &[Axis], _slice: &[SliceInfoElem]) -> Result<usize> {
|
||||
if let Some(pos) = axes.iter().position(|a| a == self) {
|
||||
Ok(pos)
|
||||
} else {
|
||||
Err(Error::msg("Axis not found in axes"))
|
||||
}
|
||||
}
|
||||
|
||||
fn pos_op(&self, axes: &[Axis], _slice: &[SliceInfoElem], _op_axes: &[Axis]) -> Result<usize> {
|
||||
self.pos(axes, _slice)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ax for usize {
|
||||
fn n(&self) -> usize {
|
||||
*self
|
||||
}
|
||||
|
||||
fn pos(&self, _axes: &[Axis], slice: &[SliceInfoElem]) -> Result<usize> {
|
||||
let idx: Vec<_> = slice
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, s)| if s.is_index() { None } else { Some(i) })
|
||||
.collect();
|
||||
Ok(idx[*self])
|
||||
}
|
||||
|
||||
fn pos_op(&self, axes: &[Axis], slice: &[SliceInfoElem], op_axes: &[Axis]) -> Result<usize> {
|
||||
let idx: Vec<_> = axes
|
||||
.iter()
|
||||
.zip(slice.iter())
|
||||
.enumerate()
|
||||
.filter_map(|(i, (ax, s))| {
|
||||
if s.is_index() | op_axes.contains(ax) {
|
||||
None
|
||||
} else {
|
||||
Some(i)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert!(*self < idx.len(), "self: {}, idx: {:?}", self, idx);
|
||||
Ok(idx[*self])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum Operation {
|
||||
Max,
|
||||
Min,
|
||||
Sum,
|
||||
Mean,
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
pub(crate) fn operate<T, D>(
|
||||
&self,
|
||||
array: Array<T, D>,
|
||||
axis: usize,
|
||||
) -> Result<<Array<T, D> as MinMax>::Output>
|
||||
where
|
||||
D: Dimension,
|
||||
Array<T, D>: MinMax,
|
||||
{
|
||||
match self {
|
||||
Operation::Max => array.max(axis),
|
||||
Operation::Min => array.min(axis),
|
||||
Operation::Sum => array.sum(axis),
|
||||
Operation::Mean => array.mean(axis),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Axis {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(*self as u8) == (*other as u8)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn slice_info<D: Dimension>(
|
||||
info: &[SliceInfoElem],
|
||||
) -> Result<SliceInfo<&[SliceInfoElem], Ix2, D>> {
|
||||
Ok(info.try_into()?)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "SliceInfoElem")]
|
||||
pub(crate) enum SliceInfoElemDef {
|
||||
Slice {
|
||||
start: isize,
|
||||
end: Option<isize>,
|
||||
step: isize,
|
||||
},
|
||||
Index(isize),
|
||||
NewAxis,
|
||||
}
|
||||
|
||||
impl SerializeAs<SliceInfoElem> for SliceInfoElemDef {
|
||||
fn serialize_as<S>(
|
||||
source: &SliceInfoElem,
|
||||
serializer: S,
|
||||
) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
SliceInfoElemDef::serialize(source, serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeAs<'de, SliceInfoElem> for SliceInfoElemDef {
|
||||
fn deserialize_as<D>(deserializer: D) -> std::result::Result<SliceInfoElem, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
SliceInfoElemDef::deserialize(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Slice {
|
||||
start: isize,
|
||||
end: isize,
|
||||
step: isize,
|
||||
}
|
||||
|
||||
impl Slice {
|
||||
pub(crate) fn new(start: isize, end: isize, step: isize) -> Self {
|
||||
Self { start, end, step }
|
||||
}
|
||||
|
||||
pub(crate) fn empty() -> Self {
|
||||
Self {
|
||||
start: 0,
|
||||
end: 0,
|
||||
step: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Slice {
|
||||
type Item = isize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.end - self.start >= self.step {
|
||||
let r = self.start;
|
||||
self.start += self.step;
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for &Slice {
|
||||
type Item = isize;
|
||||
type IntoIter = Slice;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,18 @@ macro_rules! method {
|
||||
};
|
||||
}
|
||||
|
||||
fn transmute_vec<T, U>(vec: Vec<T>) -> Vec<U> {
|
||||
unsafe {
|
||||
// Ensure the original vector is not dropped.
|
||||
let mut v_clone = std::mem::ManuallyDrop::new(vec);
|
||||
Vec::from_raw_parts(
|
||||
v_clone.as_mut_ptr() as *mut U,
|
||||
v_clone.len(),
|
||||
v_clone.capacity(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around bioformats java class loci.common.DebugTools
|
||||
pub struct DebugTools;
|
||||
|
||||
@@ -125,8 +137,7 @@ impl ChannelSeparator {
|
||||
}
|
||||
|
||||
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>> {
|
||||
let bi8 = self.open_bi8(index)?;
|
||||
Ok(unsafe { std::mem::transmute::<Vec<i8>, Vec<u8>>(bi8) })
|
||||
Ok(transmute_vec(self.open_bi8(index)?))
|
||||
}
|
||||
|
||||
method!(open_bi8, "openBytes", [index: i32|p] => Vec<i8>|c);
|
||||
@@ -149,8 +160,7 @@ impl ImageReader {
|
||||
}
|
||||
|
||||
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>> {
|
||||
let bi8 = self.open_bi8(index)?;
|
||||
Ok(unsafe { std::mem::transmute::<Vec<i8>, Vec<u8>>(bi8) })
|
||||
Ok(transmute_vec(self.open_bi8(index)?))
|
||||
}
|
||||
|
||||
pub(crate) fn ome_xml(&self) -> Result<String> {
|
||||
|
||||
488
src/lib.rs
488
src/lib.rs
@@ -1,333 +1,23 @@
|
||||
mod bioformats;
|
||||
|
||||
mod axes;
|
||||
#[cfg(feature = "python")]
|
||||
mod py;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use bioformats::{DebugTools, ImageReader, MetadataTools};
|
||||
use ndarray::Array2;
|
||||
use num::{FromPrimitive, Zero};
|
||||
use std::any::type_name;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use thread_local::ThreadLocal;
|
||||
|
||||
/// Pixel types (u)int(8/16/32) or float(32/64)
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PixelType {
|
||||
INT8 = 0,
|
||||
UINT8 = 1,
|
||||
INT16 = 2,
|
||||
UINT16 = 3,
|
||||
INT32 = 4,
|
||||
UINT32 = 5,
|
||||
FLOAT = 6,
|
||||
DOUBLE = 7,
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for PixelType {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(PixelType::INT8),
|
||||
1 => Ok(PixelType::UINT8),
|
||||
2 => Ok(PixelType::INT16),
|
||||
3 => Ok(PixelType::UINT16),
|
||||
4 => Ok(PixelType::INT32),
|
||||
5 => Ok(PixelType::UINT32),
|
||||
6 => Ok(PixelType::FLOAT),
|
||||
7 => Ok(PixelType::DOUBLE),
|
||||
_ => Err(anyhow::anyhow!("Unknown pixel type {}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct containing frame data in one of eight pixel types. Cast to `Array2<T>` using try_into.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Frame {
|
||||
INT8(Array2<i8>),
|
||||
UINT8(Array2<u8>),
|
||||
INT16(Array2<i16>),
|
||||
UINT16(Array2<u16>),
|
||||
INT32(Array2<i32>),
|
||||
UINT32(Array2<u32>),
|
||||
FLOAT(Array2<f32>),
|
||||
DOUBLE(Array2<f64>),
|
||||
}
|
||||
|
||||
macro_rules! impl_frame_cast {
|
||||
($t:tt, $s:ident) => {
|
||||
impl From<Array2<$t>> for Frame {
|
||||
fn from(value: Array2<$t>) -> Self {
|
||||
Frame::$s(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_frame_cast!(i8, INT8);
|
||||
impl_frame_cast!(u8, UINT8);
|
||||
impl_frame_cast!(i16, INT16);
|
||||
impl_frame_cast!(u16, UINT16);
|
||||
impl_frame_cast!(i32, INT32);
|
||||
impl_frame_cast!(u32, UINT32);
|
||||
impl_frame_cast!(f32, FLOAT);
|
||||
impl_frame_cast!(f64, DOUBLE);
|
||||
|
||||
impl<T> TryInto<Array2<T>> for Frame
|
||||
where
|
||||
T: FromPrimitive + Zero + 'static,
|
||||
{
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_into(self) -> std::result::Result<Array2<T>, Self::Error> {
|
||||
let mut err = Ok(());
|
||||
let arr = match self {
|
||||
Frame::INT8(v) => v.mapv_into_any(|x| {
|
||||
T::from_i8(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::UINT8(v) => v.mapv_into_any(|x| {
|
||||
T::from_u8(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::INT16(v) => v.mapv_into_any(|x| {
|
||||
T::from_i16(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::UINT16(v) => v.mapv_into_any(|x| {
|
||||
T::from_u16(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::INT32(v) => v.mapv_into_any(|x| {
|
||||
T::from_i32(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::UINT32(v) => v.mapv_into_any(|x| {
|
||||
T::from_u32(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::FLOAT(v) => v.mapv_into_any(|x| {
|
||||
T::from_f32(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::DOUBLE(v) => v.mapv_into_any(|x| {
|
||||
T::from_f64(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
};
|
||||
match err {
|
||||
Err(err) => Err(err),
|
||||
Ok(()) => Ok(arr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reader interface to file. Use get_frame to get data.
|
||||
pub struct Reader {
|
||||
image_reader: ThreadLocal<ImageReader>,
|
||||
/// path to file
|
||||
pub path: PathBuf,
|
||||
/// which (if more than 1) of the series in the file to open
|
||||
pub series: i32,
|
||||
/// size x (horizontal)
|
||||
pub size_x: usize,
|
||||
/// size y (vertical)
|
||||
pub size_y: usize,
|
||||
/// size c (# channels)
|
||||
pub size_c: usize,
|
||||
/// size z (# slices)
|
||||
pub size_z: usize,
|
||||
/// size t (# time/frames)
|
||||
pub size_t: usize,
|
||||
/// pixel type ((u)int(8/16/32) or float(32/64))
|
||||
pub pixel_type: PixelType,
|
||||
little_endian: bool,
|
||||
}
|
||||
|
||||
impl Deref for Reader {
|
||||
type Target = ImageReader;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.image_reader.get_or(|| {
|
||||
let reader = ImageReader::new().unwrap();
|
||||
let meta_data_tools = MetadataTools::new().unwrap();
|
||||
let ome_meta = meta_data_tools.create_ome_xml_metadata().unwrap();
|
||||
reader.set_metadata_store(ome_meta).unwrap();
|
||||
reader.set_id(self.path.to_str().unwrap()).unwrap();
|
||||
reader.set_series(self.series).unwrap();
|
||||
reader
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Reader {
|
||||
fn clone(&self) -> Self {
|
||||
Reader::new(&self.path, self.series).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Reader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Reader")
|
||||
.field("path", &self.path)
|
||||
.field("series", &self.series)
|
||||
.field("size_x", &self.size_x)
|
||||
.field("size_y", &self.size_y)
|
||||
.field("size_c", &self.size_c)
|
||||
.field("size_z", &self.size_z)
|
||||
.field("size_t", &self.size_t)
|
||||
.field("pixel_type", &self.pixel_type)
|
||||
.field("little_endian", &self.little_endian)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
/// Create new reader for image file at path.
|
||||
pub fn new(path: &Path, series: i32) -> Result<Self> {
|
||||
DebugTools::set_root_level("ERROR")?;
|
||||
let mut reader = Reader {
|
||||
image_reader: ThreadLocal::new(),
|
||||
path: PathBuf::from(path),
|
||||
series,
|
||||
size_x: 0,
|
||||
size_y: 0,
|
||||
size_c: 0,
|
||||
size_z: 0,
|
||||
size_t: 0,
|
||||
pixel_type: PixelType::INT8,
|
||||
little_endian: false,
|
||||
};
|
||||
reader.size_x = reader.get_size_x()? as usize;
|
||||
reader.size_y = reader.get_size_y()? as usize;
|
||||
reader.size_c = reader.get_size_c()? as usize;
|
||||
reader.size_z = reader.get_size_z()? as usize;
|
||||
reader.size_t = reader.get_size_t()? as usize;
|
||||
reader.pixel_type = PixelType::try_from(reader.get_pixel_type()?)?;
|
||||
reader.little_endian = reader.is_little_endian()?;
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
/// Get ome metadata as xml string
|
||||
pub fn get_ome_xml(&self) -> Result<String> {
|
||||
self.ome_xml()
|
||||
}
|
||||
|
||||
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>> {
|
||||
let chunk_size = match self.pixel_type {
|
||||
PixelType::INT8 => 1,
|
||||
PixelType::UINT8 => 1,
|
||||
PixelType::INT16 => 2,
|
||||
PixelType::UINT16 => 2,
|
||||
PixelType::INT32 => 4,
|
||||
PixelType::UINT32 => 4,
|
||||
PixelType::FLOAT => 4,
|
||||
PixelType::DOUBLE => 8,
|
||||
};
|
||||
Ok(bytes
|
||||
.chunks(chunk_size)
|
||||
.skip(channel)
|
||||
.step_by(self.size_c)
|
||||
.flat_map(|a| a.to_vec())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Retrieve fame at channel c, slize z and time t.
|
||||
pub fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame> {
|
||||
let bytes = if self.is_rgb()? & self.is_interleaved()? {
|
||||
let index = self.get_index(z as i32, 0, t as i32)?;
|
||||
self.deinterleave(self.open_bytes(index)?, c)?
|
||||
} else if self.get_rgb_channel_count()? > 1 {
|
||||
let channel_separator = bioformats::ChannelSeparator::new(self)?;
|
||||
let index = channel_separator.get_index(z as i32, c as i32, t as i32)?;
|
||||
channel_separator.open_bytes(index)?
|
||||
} else if self.is_indexed()? {
|
||||
let index = self.get_index(z as i32, 0, t as i32)?;
|
||||
self.open_bytes(index)?
|
||||
// TODO: apply LUT
|
||||
// let _bytes_lut = match self.pixel_type {
|
||||
// PixelType::INT8 | PixelType::UINT8 => {
|
||||
// let _lut = self.image_reader.get_8bit_lookup_table()?;
|
||||
// }
|
||||
// PixelType::INT16 | PixelType::UINT16 => {
|
||||
// let _lut = self.image_reader.get_16bit_lookup_table()?;
|
||||
// }
|
||||
// _ => {}
|
||||
// };
|
||||
} else {
|
||||
let index = self.get_index(z as i32, c as i32, t as i32)?;
|
||||
self.open_bytes(index)?
|
||||
};
|
||||
self.bytes_to_frame(bytes)
|
||||
}
|
||||
|
||||
fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame> {
|
||||
macro_rules! get_frame {
|
||||
($t:tt, <$n:expr) => {
|
||||
Ok(Frame::from(Array2::from_shape_vec(
|
||||
(self.size_y, self.size_x),
|
||||
bytes
|
||||
.chunks($n)
|
||||
.map(|x| $t::from_le_bytes(x.try_into().unwrap()))
|
||||
.collect(),
|
||||
)?))
|
||||
};
|
||||
($t:tt, >$n:expr) => {
|
||||
Ok(Frame::from(Array2::from_shape_vec(
|
||||
(self.size_y, self.size_x),
|
||||
bytes
|
||||
.chunks($n)
|
||||
.map(|x| $t::from_be_bytes(x.try_into().unwrap()))
|
||||
.collect(),
|
||||
)?))
|
||||
};
|
||||
}
|
||||
|
||||
match (&self.pixel_type, self.little_endian) {
|
||||
(PixelType::INT8, true) => get_frame!(i8, <1),
|
||||
(PixelType::UINT8, true) => get_frame!(u8, <1),
|
||||
(PixelType::INT16, true) => get_frame!(i16, <2),
|
||||
(PixelType::UINT16, true) => get_frame!(u16, <2),
|
||||
(PixelType::INT32, true) => get_frame!(i32, <4),
|
||||
(PixelType::UINT32, true) => get_frame!(u32, <4),
|
||||
(PixelType::FLOAT, true) => get_frame!(f32, <4),
|
||||
(PixelType::DOUBLE, true) => get_frame!(f64, <8),
|
||||
(PixelType::INT8, false) => get_frame!(i8, >1),
|
||||
(PixelType::UINT8, false) => get_frame!(u8, >1),
|
||||
(PixelType::INT16, false) => get_frame!(i16, >2),
|
||||
(PixelType::UINT16, false) => get_frame!(u16, >2),
|
||||
(PixelType::INT32, false) => get_frame!(i32, >4),
|
||||
(PixelType::UINT32, false) => get_frame!(u32, >4),
|
||||
(PixelType::FLOAT, false) => get_frame!(f32, >4),
|
||||
(PixelType::DOUBLE, false) => get_frame!(f64, >8),
|
||||
}
|
||||
}
|
||||
}
|
||||
mod reader;
|
||||
mod stats;
|
||||
mod view;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::stats::MinMax;
|
||||
use ndarray::{Array, Array4, Array5, NewAxis};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::axes::Axis;
|
||||
use crate::reader::{Frame, Reader};
|
||||
use anyhow::Result;
|
||||
use ndarray::{s, Array2};
|
||||
|
||||
fn open(file: &str) -> Result<Reader> {
|
||||
let path = std::env::current_dir()?
|
||||
.join("tests")
|
||||
@@ -413,4 +103,160 @@ mod tests {
|
||||
println!("{}", xml);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view() -> Result<()> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = view.slice(s![0, 5, 0, .., ..])?;
|
||||
let b = reader.get_frame(0, 5, 0)?;
|
||||
let c: Array2<isize> = a.try_into()?;
|
||||
let d: Array2<isize> = b.try_into()?;
|
||||
assert_eq!(c, d);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_shape() -> Result<()> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = view.slice(s![0, ..5, 0, .., 100..200])?;
|
||||
let shape = a.shape();
|
||||
assert_eq!(shape, vec![5, 1024, 100]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_new_axis() -> Result<()> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = Array5::<u8>::zeros((1, 9, 1, 1024, 1024));
|
||||
let a = a.slice(s![0, ..5, 0, NewAxis, 100..200, ..]);
|
||||
let v = view.slice(s![0, ..5, 0, NewAxis, 100..200, ..])?;
|
||||
assert_eq!(v.shape(), a.shape());
|
||||
println!("\nshape: {:?}", v.shape());
|
||||
let a = a.slice(s![NewAxis, .., .., NewAxis, .., .., NewAxis]);
|
||||
let v = v.slice(s![NewAxis, .., .., NewAxis, .., .., NewAxis])?;
|
||||
assert_eq!(v.shape(), a.shape());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_permute_axes() -> Result<()> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let s = view.shape();
|
||||
let mut a = Array5::<u8>::zeros((s[0], s[1], s[2], s[3], s[4]));
|
||||
assert_eq!(view.shape(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
|
||||
let view = view.swap_axes(Axis::C, Axis::Z)?;
|
||||
a.swap_axes(0, 1);
|
||||
assert_eq!(view.shape(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
|
||||
println!("{:?}", view.axes());
|
||||
let view = view.permute_axes(&[Axis::X, Axis::Z, Axis::Y])?;
|
||||
println!("{:?}", view.axes());
|
||||
let a = a.permuted_axes([4, 1, 2, 0, 3]);
|
||||
assert_eq!(view.shape(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! test_max {
|
||||
($($name:ident: $b:expr $(,)?)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() -> Result<()> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let array: Array5<usize> = view.clone().try_into()?;
|
||||
let view = view.max_proj($b)?;
|
||||
let a: Array4<usize> = view.clone().try_into()?;
|
||||
let b = array.max($b)?;
|
||||
assert_eq!(a.shape(), b.shape());
|
||||
assert_eq!(a, b);
|
||||
Ok(())
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_max! {
|
||||
max_c: 0
|
||||
max_z: 1
|
||||
max_t: 2
|
||||
max_y: 3
|
||||
max_x: 4
|
||||
}
|
||||
|
||||
macro_rules! test_index {
|
||||
($($name:ident: $b:expr $(,)?)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() -> Result<()> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let v4: Array<usize, _> = view.slice($b)?.try_into()?;
|
||||
let a5: Array5<usize> = reader.view().try_into()?;
|
||||
let a4 = a5.slice($b).to_owned();
|
||||
assert_eq!(a4, v4);
|
||||
Ok(())
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_index! {
|
||||
index_0: s![.., .., .., .., ..]
|
||||
index_1: s![0, .., .., .., ..]
|
||||
index_2: s![.., 0, .., .., ..]
|
||||
index_3: s![.., .., 0, .., ..]
|
||||
index_4: s![.., .., .., 0, ..]
|
||||
index_5: s![.., .., .., .., 0]
|
||||
index_6: s![0, 0, .., .., ..]
|
||||
index_7: s![0, .., 0, .., ..]
|
||||
index_8: s![0, .., .., 0, ..]
|
||||
index_9: s![0, .., .., .., 0]
|
||||
index_a: s![.., 0, 0, .., ..]
|
||||
index_b: s![.., 0, .., 0, ..]
|
||||
index_c: s![.., 0, .., .., 0]
|
||||
index_d: s![.., .., 0, 0, ..]
|
||||
index_e: s![.., .., 0, .., 0]
|
||||
index_f: s![.., .., .., 0, 0]
|
||||
index_g: s![0, 0, 0, .., ..]
|
||||
index_h: s![0, 0, .., 0, ..]
|
||||
index_i: s![0, 0, .., .., 0]
|
||||
index_j: s![0, .., 0, 0, ..]
|
||||
index_k: s![0, .., 0, .., 0]
|
||||
index_l: s![0, .., .., 0, 0]
|
||||
index_m: s![0, 0, 0, 0, ..]
|
||||
index_n: s![0, 0, 0, .., 0]
|
||||
index_o: s![0, 0, .., 0, 0]
|
||||
index_p: s![0, .., 0, 0, 0]
|
||||
index_q: s![.., 0, 0, 0, 0]
|
||||
index_r: s![0, 0, 0, 0, 0]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dyn_view() -> Result<()> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let a = reader.view().into_dyn();
|
||||
let b = a.max_proj(1)?;
|
||||
let c = b.slice(s![0, 0, .., ..])?;
|
||||
let d = c.as_array::<usize>()?;
|
||||
assert_eq!(d.shape(), [1024, 1024]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
601
src/py.rs
601
src/py.rs
@@ -1,64 +1,600 @@
|
||||
use crate::axes::Axis;
|
||||
use crate::bioformats::download_bioformats;
|
||||
use crate::{Frame, Reader};
|
||||
use numpy::ToPyArray;
|
||||
use crate::reader::{PixelType, Reader};
|
||||
use crate::view::{Item, View};
|
||||
use anyhow::{anyhow, Result};
|
||||
use ndarray::{Ix0, IxDyn, SliceInfoElem};
|
||||
use numpy::IntoPyArray;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyEllipsis, PyInt, PyList, PySlice, PySliceMethods, PyString, PyTuple};
|
||||
use pyo3::IntoPyObjectExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{from_str, to_string};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[pyclass(subclass)]
|
||||
#[pyo3(name = "Reader")]
|
||||
#[derive(Debug)]
|
||||
struct PyReader {
|
||||
reader: Reader,
|
||||
#[pyclass(module = "ndbioimage.ndbioimage_rs")]
|
||||
struct ViewConstructor;
|
||||
|
||||
#[pymethods]
|
||||
impl ViewConstructor {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn __getstate__(&self) -> (u8,) {
|
||||
(0,)
|
||||
}
|
||||
|
||||
fn __setstate__(&self, _state: (u8,)) {}
|
||||
|
||||
#[staticmethod]
|
||||
fn __call__(state: String) -> PyResult<PyView> {
|
||||
if let Ok(new) = from_str(&state) {
|
||||
Ok(new)
|
||||
} else {
|
||||
Err(anyhow!("cannot parse state").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(subclass, module = "ndbioimage.ndbioimage_rs")]
|
||||
#[pyo3(name = "View")]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct PyView {
|
||||
view: View<IxDyn>,
|
||||
dtype: PixelType,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyReader {
|
||||
impl PyView {
|
||||
#[new]
|
||||
fn new(path: &str, series: usize) -> PyResult<Self> {
|
||||
#[pyo3(signature = (path, series = 0, dtype = "uint16"))]
|
||||
/// new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64)
|
||||
fn new(path: &str, series: usize, dtype: &str) -> PyResult<Self> {
|
||||
let mut path = PathBuf::from(path);
|
||||
if path.is_dir() {
|
||||
for file in path.read_dir()? {
|
||||
if let Ok(f) = file {
|
||||
let p = f.path();
|
||||
if f.path().is_file() & (p.extension() == Some("tif".as_ref())) {
|
||||
for file in path.read_dir()?.flatten() {
|
||||
let p = file.path();
|
||||
if file.path().is_file() & (p.extension() == Some("tif".as_ref())) {
|
||||
path = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(PyReader {
|
||||
reader: Reader::new(&path, series as i32)?,
|
||||
Ok(Self {
|
||||
view: Reader::new(&path, series as i32)?.view().into_dyn(),
|
||||
dtype: dtype.parse()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// close the file: does nothing as this is handled automatically
|
||||
fn close(&self) -> PyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy(&self) -> PyView {
|
||||
PyView {
|
||||
view: self.view.clone(),
|
||||
dtype: self.dtype.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// slice the view and return a new view or a single number
|
||||
fn __getitem__<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
n: Bound<'py, PyAny>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let slice: Vec<_> = if n.is_instance_of::<PyTuple>() {
|
||||
n.downcast_into::<PyTuple>()?.into_iter().collect()
|
||||
} else if n.is_instance_of::<PyList>() {
|
||||
n.downcast_into::<PyList>()?.into_iter().collect()
|
||||
} else {
|
||||
vec![n]
|
||||
};
|
||||
let mut new_slice = Vec::new();
|
||||
let mut ellipsis = None;
|
||||
let shape = self.view.shape();
|
||||
for (i, (s, t)) in slice.iter().zip(shape.iter()).enumerate() {
|
||||
if s.is_instance_of::<PyInt>() {
|
||||
new_slice.push(SliceInfoElem::Index(
|
||||
s.downcast::<PyInt>()?.extract::<isize>()?,
|
||||
));
|
||||
} else if s.is_instance_of::<PySlice>() {
|
||||
let u = s.downcast::<PySlice>()?.indices(*t as isize)?;
|
||||
new_slice.push(SliceInfoElem::Slice {
|
||||
start: u.start,
|
||||
end: Some(u.stop),
|
||||
step: u.step,
|
||||
});
|
||||
} else if s.is_instance_of::<PyEllipsis>() {
|
||||
if ellipsis.is_some() {
|
||||
return Err(anyhow!("cannot have more than one ellipsis").into());
|
||||
}
|
||||
let _ = ellipsis.insert(i);
|
||||
} else {
|
||||
return Err(anyhow!("cannot convert {:?} to slice", s).into());
|
||||
}
|
||||
}
|
||||
if new_slice.len() > shape.len() {
|
||||
return Err(anyhow!(
|
||||
"got more indices ({}) than dimensions ({})",
|
||||
new_slice.len(),
|
||||
shape.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
while new_slice.len() < shape.len() {
|
||||
if let Some(i) = ellipsis {
|
||||
new_slice.insert(
|
||||
i,
|
||||
SliceInfoElem::Slice {
|
||||
start: 0,
|
||||
end: None,
|
||||
step: 1,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
new_slice.push(SliceInfoElem::Slice {
|
||||
start: 0,
|
||||
end: None,
|
||||
step: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
let view = self.view.slice(new_slice.as_slice())?;
|
||||
if view.ndim() == 0 {
|
||||
Ok(match self.dtype {
|
||||
PixelType::I8 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<i8>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::U8 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<u8>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::I16 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<i16>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::U16 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<u16>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::I32 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<i32>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::U32 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<u32>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::F32 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<f32>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::F64 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<f64>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::I64 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<i64>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::U64 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<u64>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::I128 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<i128>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::U128 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<u128>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
PixelType::F128 => view
|
||||
.into_dimensionality::<Ix0>()?
|
||||
.item::<f64>()?
|
||||
.into_pyobject(py)?
|
||||
.into_any(),
|
||||
})
|
||||
} else {
|
||||
PyView {
|
||||
view,
|
||||
dtype: self.dtype.clone(),
|
||||
}
|
||||
.into_bound_py_any(py)
|
||||
}
|
||||
}
|
||||
|
||||
fn __reduce__(&self) -> PyResult<(ViewConstructor, (String,))> {
|
||||
if let Ok(s) = to_string(self) {
|
||||
Ok((ViewConstructor, (s,)))
|
||||
} else {
|
||||
Err(anyhow!("cannot get state").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// retrieve a single frame at czt, sliced accordingly
|
||||
fn get_frame<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
c: usize,
|
||||
z: usize,
|
||||
t: usize,
|
||||
c: isize,
|
||||
z: isize,
|
||||
t: isize,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
Ok(match self.reader.get_frame(c, z, t)? {
|
||||
Frame::INT8(arr) => arr.to_pyarray(py).into_any(),
|
||||
Frame::UINT8(arr) => arr.to_pyarray(py).into_any(),
|
||||
Frame::INT16(arr) => arr.to_pyarray(py).into_any(),
|
||||
Frame::UINT16(arr) => arr.to_pyarray(py).into_any(),
|
||||
Frame::INT32(arr) => arr.to_pyarray(py).into_any(),
|
||||
Frame::UINT32(arr) => arr.to_pyarray(py).into_any(),
|
||||
Frame::FLOAT(arr) => arr.to_pyarray(py).into_any(),
|
||||
Frame::DOUBLE(arr) => arr.to_pyarray(py).into_any(),
|
||||
Ok(match self.dtype {
|
||||
PixelType::I8 => self
|
||||
.view
|
||||
.get_frame::<i8>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::U8 => self
|
||||
.view
|
||||
.get_frame::<u8>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::I16 => self
|
||||
.view
|
||||
.get_frame::<i16>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::U16 => self
|
||||
.view
|
||||
.get_frame::<u16>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::I32 => self
|
||||
.view
|
||||
.get_frame::<i32>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::U32 => self
|
||||
.view
|
||||
.get_frame::<u32>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::F32 => self
|
||||
.view
|
||||
.get_frame::<f32>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::F64 => self
|
||||
.view
|
||||
.get_frame::<f64>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::I64 => self
|
||||
.view
|
||||
.get_frame::<i64>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::U64 => self
|
||||
.view
|
||||
.get_frame::<u64>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::I128 => self
|
||||
.view
|
||||
.get_frame::<i64>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::U128 => self
|
||||
.view
|
||||
.get_frame::<u64>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
PixelType::F128 => self
|
||||
.view
|
||||
.get_frame::<f64>(c, z, t)?
|
||||
.into_pyarray(py)
|
||||
.into_any(),
|
||||
})
|
||||
}
|
||||
|
||||
/// retrieve the ome metadata as an xml string
|
||||
fn get_ome_xml(&self) -> PyResult<String> {
|
||||
Ok(self.reader.get_ome_xml()?)
|
||||
Ok(self.view.get_ome_xml()?)
|
||||
}
|
||||
|
||||
fn close(&mut self) -> PyResult<()> {
|
||||
self.reader.close()?;
|
||||
/// the file path
|
||||
#[getter]
|
||||
fn path(&self) -> PyResult<String> {
|
||||
Ok(self.view.path.display().to_string())
|
||||
}
|
||||
|
||||
/// the series in the file
|
||||
#[getter]
|
||||
fn series(&self) -> PyResult<i32> {
|
||||
Ok(self.view.series)
|
||||
}
|
||||
|
||||
/// the axes in the view
|
||||
#[getter]
|
||||
fn axes(&self) -> Vec<String> {
|
||||
self.view
|
||||
.axes()
|
||||
.iter()
|
||||
.map(|a| format!("{:?}", a))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// the shape of the view
|
||||
#[getter]
|
||||
fn shape(&self) -> Vec<usize> {
|
||||
self.view.shape()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn slice(&self) -> PyResult<Vec<String>> {
|
||||
Ok(self
|
||||
.view
|
||||
.get_slice()
|
||||
.iter()
|
||||
.map(|s| format!("{:#?}", s))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// the number of pixels in the view
|
||||
#[getter]
|
||||
fn size(&self) -> usize {
|
||||
self.view.size()
|
||||
}
|
||||
|
||||
/// the number of dimensions in the view
|
||||
#[getter]
|
||||
fn ndim(&self) -> usize {
|
||||
self.view.ndim()
|
||||
}
|
||||
|
||||
/// find the position of an axis
|
||||
#[pyo3(text_signature = "axis: str | int")]
|
||||
fn get_ax(&self, axis: Bound<'_, PyAny>) -> PyResult<usize> {
|
||||
if axis.is_instance_of::<PyString>() {
|
||||
let axis = axis
|
||||
.downcast_into::<PyString>()?
|
||||
.extract::<String>()?
|
||||
.parse::<Axis>()?;
|
||||
Ok(self
|
||||
.view
|
||||
.axes()
|
||||
.iter()
|
||||
.position(|a| *a == axis)
|
||||
.ok_or_else(|| anyhow!("cannot find axis {:?}", axis))?)
|
||||
} else if axis.is_instance_of::<PyInt>() {
|
||||
Ok(axis.downcast_into::<PyInt>()?.extract::<usize>()?)
|
||||
} else {
|
||||
Err(anyhow!("cannot convert to axis").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// swap two axes
|
||||
#[pyo3(text_signature = "ax0: str | int, ax1: str | int")]
|
||||
fn swap_axes(&self, ax0: Bound<'_, PyAny>, ax1: Bound<'_, PyAny>) -> PyResult<Self> {
|
||||
let ax0 = self.get_ax(ax0)?;
|
||||
let ax1 = self.get_ax(ax1)?;
|
||||
let view = self.view.swap_axes(ax0, ax1)?;
|
||||
Ok(PyView {
|
||||
view,
|
||||
dtype: self.dtype.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// permute the order of the axes
|
||||
#[pyo3(signature = (axes = None), text_signature = "axes: list[str | int] = None")]
|
||||
fn transpose(&self, axes: Option<Vec<Bound<'_, PyAny>>>) -> PyResult<Self> {
|
||||
let view = if let Some(axes) = axes {
|
||||
let ax = axes
|
||||
.into_iter()
|
||||
.map(|a| self.get_ax(a))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
self.view.permute_axes(&ax)?
|
||||
} else {
|
||||
self.view.transpose()?
|
||||
};
|
||||
Ok(PyView {
|
||||
view,
|
||||
dtype: self.dtype.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// collect data into a numpy array
|
||||
fn as_array<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
Ok(match self.dtype {
|
||||
PixelType::I8 => self.view.as_array_dyn::<i8>()?.into_pyarray(py).into_any(),
|
||||
PixelType::U8 => self.view.as_array_dyn::<u8>()?.into_pyarray(py).into_any(),
|
||||
PixelType::I16 => self.view.as_array_dyn::<i16>()?.into_pyarray(py).into_any(),
|
||||
PixelType::U16 => self.view.as_array_dyn::<u16>()?.into_pyarray(py).into_any(),
|
||||
PixelType::I32 => self.view.as_array_dyn::<i32>()?.into_pyarray(py).into_any(),
|
||||
PixelType::U32 => self.view.as_array_dyn::<u32>()?.into_pyarray(py).into_any(),
|
||||
PixelType::F32 => self.view.as_array_dyn::<f32>()?.into_pyarray(py).into_any(),
|
||||
PixelType::F64 => self.view.as_array_dyn::<f64>()?.into_pyarray(py).into_any(),
|
||||
PixelType::I64 => self.view.as_array_dyn::<i64>()?.into_pyarray(py).into_any(),
|
||||
PixelType::U64 => self.view.as_array_dyn::<u64>()?.into_pyarray(py).into_any(),
|
||||
PixelType::I128 => self.view.as_array_dyn::<i64>()?.into_pyarray(py).into_any(),
|
||||
PixelType::U128 => self.view.as_array_dyn::<u64>()?.into_pyarray(py).into_any(),
|
||||
PixelType::F128 => self.view.as_array_dyn::<f64>()?.into_pyarray(py).into_any(),
|
||||
})
|
||||
}
|
||||
|
||||
/// change the data type of the view: (u)int(8/16/32) or float(32/64)
|
||||
fn as_type(&self, dtype: String) -> PyResult<Self> {
|
||||
Ok(PyView {
|
||||
view: self.view.clone(),
|
||||
dtype: dtype.parse()?,
|
||||
})
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn get_dtype(&self) -> PyResult<&str> {
|
||||
Ok(match self.dtype {
|
||||
PixelType::I8 => "int8",
|
||||
PixelType::U8 => "uint8",
|
||||
PixelType::I16 => "int16",
|
||||
PixelType::U16 => "uint16",
|
||||
PixelType::I32 => "int32",
|
||||
PixelType::U32 => "uint32",
|
||||
PixelType::F32 => "float32",
|
||||
PixelType::F64 => "float64",
|
||||
PixelType::I64 => "int64",
|
||||
PixelType::U64 => "uint64",
|
||||
PixelType::I128 => "int128",
|
||||
PixelType::U128 => "uint128",
|
||||
PixelType::F128 => "float128",
|
||||
})
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_dtype(&mut self, dtype: String) -> PyResult<()> {
|
||||
self.dtype = dtype.parse()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get the maximum overall or along a given axis
|
||||
#[pyo3(signature = (axis = None), text_signature = "axis: str | int")]
|
||||
fn max<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
axis: Option<Bound<'py, PyAny>>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
if let Some(axis) = axis {
|
||||
PyView {
|
||||
dtype: self.dtype.clone(),
|
||||
view: self.view.max_proj(self.get_ax(axis)?)?,
|
||||
}
|
||||
.into_bound_py_any(py)
|
||||
} else {
|
||||
Ok(match self.dtype {
|
||||
PixelType::I8 => self.view.max::<i8>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U8 => self.view.max::<u8>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I16 => self.view.max::<i16>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U16 => self.view.max::<u16>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I32 => self.view.max::<i32>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U32 => self.view.max::<u32>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::F32 => self.view.max::<f32>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::F64 => self.view.max::<f64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I64 => self.view.max::<i64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U64 => self.view.max::<u64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I128 => self.view.max::<i64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U128 => self.view.max::<u64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::F128 => self.view.max::<f64>()?.into_pyobject(py)?.into_any(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// get the minimum overall or along a given axis
|
||||
#[pyo3(signature = (axis = None), text_signature = "axis: str | int")]
|
||||
fn min<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
axis: Option<Bound<'py, PyAny>>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
if let Some(axis) = axis {
|
||||
PyView {
|
||||
dtype: self.dtype.clone(),
|
||||
view: self.view.min_proj(self.get_ax(axis)?)?,
|
||||
}
|
||||
.into_bound_py_any(py)
|
||||
} else {
|
||||
Ok(match self.dtype {
|
||||
PixelType::I8 => self.view.min::<i8>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U8 => self.view.min::<u8>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I16 => self.view.min::<i16>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U16 => self.view.min::<u16>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I32 => self.view.min::<i32>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U32 => self.view.min::<u32>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::F32 => self.view.min::<f32>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::F64 => self.view.min::<f64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I64 => self.view.min::<i64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U64 => self.view.min::<u64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I128 => self.view.min::<i64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U128 => self.view.min::<u64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::F128 => self.view.min::<f64>()?.into_pyobject(py)?.into_any(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// get the mean overall or along a given axis
|
||||
#[pyo3(signature = (axis = None), text_signature = "axis: str | int")]
|
||||
fn mean<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
axis: Option<Bound<'py, PyAny>>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
if let Some(axis) = axis {
|
||||
let dtype = if let PixelType::F32 = self.dtype {
|
||||
PixelType::F32
|
||||
} else {
|
||||
PixelType::F64
|
||||
};
|
||||
PyView {
|
||||
dtype,
|
||||
view: self.view.mean_proj(self.get_ax(axis)?)?,
|
||||
}
|
||||
.into_bound_py_any(py)
|
||||
} else {
|
||||
Ok(match self.dtype {
|
||||
PixelType::F32 => self.view.mean::<f32>()?.into_pyobject(py)?.into_any(),
|
||||
_ => self.view.mean::<f64>()?.into_pyobject(py)?.into_any(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// get the sum overall or along a given axis
|
||||
#[pyo3(signature = (axis = None), text_signature = "axis: str | int")]
|
||||
fn sum<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
axis: Option<Bound<'py, PyAny>>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let dtype = match self.dtype {
|
||||
PixelType::I8 => PixelType::I16,
|
||||
PixelType::U8 => PixelType::U16,
|
||||
PixelType::I16 => PixelType::I32,
|
||||
PixelType::U16 => PixelType::U32,
|
||||
PixelType::I32 => PixelType::I64,
|
||||
PixelType::U32 => PixelType::U64,
|
||||
PixelType::F32 => PixelType::F32,
|
||||
PixelType::F64 => PixelType::F64,
|
||||
PixelType::I64 => PixelType::I128,
|
||||
PixelType::U64 => PixelType::U128,
|
||||
PixelType::I128 => PixelType::I128,
|
||||
PixelType::U128 => PixelType::U128,
|
||||
PixelType::F128 => PixelType::F128,
|
||||
};
|
||||
if let Some(axis) = axis {
|
||||
PyView {
|
||||
dtype,
|
||||
view: self.view.sum_proj(self.get_ax(axis)?)?,
|
||||
}
|
||||
.into_bound_py_any(py)
|
||||
} else {
|
||||
Ok(match self.dtype {
|
||||
PixelType::F32 => self.view.sum::<f32>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::F64 => self.view.sum::<f64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I64 => self.view.sum::<i64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U64 => self.view.sum::<u64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::I128 => self.view.sum::<i64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::U128 => self.view.sum::<u64>()?.into_pyobject(py)?.into_any(),
|
||||
PixelType::F128 => self.view.sum::<f64>()?.into_pyobject(py)?.into_any(),
|
||||
_ => self.view.sum::<i64>()?.into_pyobject(py)?.into_any(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ndbioimage_file() -> anyhow::Result<PathBuf> {
|
||||
@@ -75,7 +611,8 @@ pub(crate) fn ndbioimage_file() -> anyhow::Result<PathBuf> {
|
||||
#[pymodule]
|
||||
#[pyo3(name = "ndbioimage_rs")]
|
||||
fn ndbioimage_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyReader>()?;
|
||||
m.add_class::<PyView>()?;
|
||||
m.add_class::<ViewConstructor>()?;
|
||||
|
||||
#[pyfn(m)]
|
||||
#[pyo3(name = "download_bioformats")]
|
||||
|
||||
442
src/reader.rs
Normal file
442
src/reader.rs
Normal file
@@ -0,0 +1,442 @@
|
||||
use crate::axes::Axis;
|
||||
use crate::bioformats;
|
||||
use crate::bioformats::{DebugTools, ImageReader, MetadataTools};
|
||||
use crate::view::View;
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use ndarray::{s, Array2, Ix5};
|
||||
use num::{FromPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::any::type_name;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use thread_local::ThreadLocal;
|
||||
|
||||
/// Pixel types (u)int(8/16/32) or float(32/64), (u/i)(64/128) are not included in bioformats
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PixelType {
|
||||
I8,
|
||||
U8,
|
||||
I16,
|
||||
U16,
|
||||
I32,
|
||||
U32,
|
||||
F32,
|
||||
F64,
|
||||
I64,
|
||||
U64,
|
||||
I128,
|
||||
U128,
|
||||
F128,
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for PixelType {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(PixelType::I8),
|
||||
1 => Ok(PixelType::U8),
|
||||
2 => Ok(PixelType::I16),
|
||||
3 => Ok(PixelType::U16),
|
||||
4 => Ok(PixelType::I32),
|
||||
5 => Ok(PixelType::U32),
|
||||
6 => Ok(PixelType::F32),
|
||||
7 => Ok(PixelType::F64),
|
||||
8 => Ok(PixelType::I64),
|
||||
9 => Ok(PixelType::U64),
|
||||
10 => Ok(PixelType::I128),
|
||||
11 => Ok(PixelType::U128),
|
||||
12 => Ok(PixelType::F128),
|
||||
_ => Err(anyhow::anyhow!("Unknown pixel type {}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PixelType {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"int8" | "i8" => Ok(PixelType::I8),
|
||||
"uint8" | "u8" => Ok(PixelType::U8),
|
||||
"int16" | "i16" => Ok(PixelType::I16),
|
||||
"uint16" | "u16" => Ok(PixelType::U16),
|
||||
"int32" | "i32" => Ok(PixelType::I32),
|
||||
"uint32" | "u32" => Ok(PixelType::U32),
|
||||
"float" | "f32" | "float32" => Ok(PixelType::F32),
|
||||
"double" | "f64" | "float64" => Ok(PixelType::F64),
|
||||
"int64" | "i64" => Ok(PixelType::I64),
|
||||
"uint64" | "u64" => Ok(PixelType::U64),
|
||||
"int128" | "i128" => Ok(PixelType::I128),
|
||||
"uint128" | "u128" => Ok(PixelType::U128),
|
||||
"extended" | "f128" => Ok(PixelType::F128),
|
||||
_ => Err(anyhow::anyhow!("Unknown pixel type {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct containing frame data in one of eight pixel types. Cast to `Array2<T>` using try_into.
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Frame {
|
||||
I8(Array2<i8>),
|
||||
U8(Array2<u8>),
|
||||
I16(Array2<i16>),
|
||||
U16(Array2<u16>),
|
||||
I32(Array2<i32>),
|
||||
U32(Array2<u32>),
|
||||
F32(Array2<f32>),
|
||||
F64(Array2<f64>),
|
||||
I64(Array2<i64>),
|
||||
U64(Array2<u64>),
|
||||
I128(Array2<i128>),
|
||||
U128(Array2<u128>),
|
||||
F128(Array2<f64>), // f128 is nightly
|
||||
}
|
||||
|
||||
macro_rules! impl_frame_cast {
|
||||
($($t:tt: $s:ident $(,)?)*) => {
|
||||
$(
|
||||
impl From<Array2<$t>> for Frame {
|
||||
fn from(value: Array2<$t>) -> Self {
|
||||
Frame::$s(value)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_frame_cast! {
|
||||
u8: U8
|
||||
i8: I8
|
||||
i16: I16
|
||||
u16: U16
|
||||
i32: I32
|
||||
u32: U32
|
||||
f32: F32
|
||||
f64: F64
|
||||
i64: I64
|
||||
u64: U64
|
||||
i128: I128
|
||||
u128: U128
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
impl_frame_cast! {
|
||||
usize: UINT32
|
||||
isize: INT32
|
||||
}
|
||||
|
||||
impl<T> TryInto<Array2<T>> for Frame
|
||||
where
|
||||
T: FromPrimitive + Zero + 'static,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<Array2<T>, Self::Error> {
|
||||
let mut err = Ok(());
|
||||
let arr = match self {
|
||||
Frame::I8(v) => v.mapv_into_any(|x| {
|
||||
T::from_i8(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::U8(v) => v.mapv_into_any(|x| {
|
||||
T::from_u8(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::I16(v) => v.mapv_into_any(|x| {
|
||||
T::from_i16(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::U16(v) => v.mapv_into_any(|x| {
|
||||
T::from_u16(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::I32(v) => v.mapv_into_any(|x| {
|
||||
T::from_i32(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::U32(v) => v.mapv_into_any(|x| {
|
||||
T::from_u32(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::F32(v) => v.mapv_into_any(|x| {
|
||||
T::from_f32(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::F64(v) | Frame::F128(v) => v.mapv_into_any(|x| {
|
||||
T::from_f64(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::I64(v) => v.mapv_into_any(|x| {
|
||||
T::from_i64(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::U64(v) => v.mapv_into_any(|x| {
|
||||
T::from_u64(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::I128(v) => v.mapv_into_any(|x| {
|
||||
T::from_i128(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::U128(v) => v.mapv_into_any(|x| {
|
||||
T::from_u128(x).unwrap_or_else(|| {
|
||||
err = Err(anyhow!("cannot convert {} into {}", x, type_name::<T>()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
};
|
||||
match err {
|
||||
Err(err) => Err(err),
|
||||
Ok(()) => Ok(arr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reader interface to file. Use get_frame to get data.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Reader {
|
||||
#[serde(skip)]
|
||||
image_reader: ThreadLocal<ImageReader>,
|
||||
/// path to file
|
||||
pub path: PathBuf,
|
||||
/// which (if more than 1) of the series in the file to open
|
||||
pub series: i32,
|
||||
/// size x (horizontal)
|
||||
pub size_x: usize,
|
||||
/// size y (vertical)
|
||||
pub size_y: usize,
|
||||
/// size c (# channels)
|
||||
pub size_c: usize,
|
||||
/// size z (# slices)
|
||||
pub size_z: usize,
|
||||
/// size t (# time/frames)
|
||||
pub size_t: usize,
|
||||
/// pixel type ((u)int(8/16/32) or float(32/64))
|
||||
pub pixel_type: PixelType,
|
||||
little_endian: bool,
|
||||
}
|
||||
|
||||
impl Deref for Reader {
|
||||
type Target = ImageReader;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.image_reader.get_or(|| {
|
||||
let reader = ImageReader::new().unwrap();
|
||||
let meta_data_tools = MetadataTools::new().unwrap();
|
||||
let ome_meta = meta_data_tools.create_ome_xml_metadata().unwrap();
|
||||
reader.set_metadata_store(ome_meta).unwrap();
|
||||
reader.set_id(self.path.to_str().unwrap()).unwrap();
|
||||
reader.set_series(self.series).unwrap();
|
||||
reader
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Reader {
|
||||
fn clone(&self) -> Self {
|
||||
Reader::new(&self.path, self.series).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Reader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Reader")
|
||||
.field("path", &self.path)
|
||||
.field("series", &self.series)
|
||||
.field("size_x", &self.size_x)
|
||||
.field("size_y", &self.size_y)
|
||||
.field("size_c", &self.size_c)
|
||||
.field("size_z", &self.size_z)
|
||||
.field("size_t", &self.size_t)
|
||||
.field("pixel_type", &self.pixel_type)
|
||||
.field("little_endian", &self.little_endian)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
/// Create a new reader for the image file at a path, and open series #.
|
||||
pub fn new(path: &Path, series: i32) -> Result<Self> {
|
||||
DebugTools::set_root_level("ERROR")?;
|
||||
let mut reader = Reader {
|
||||
image_reader: ThreadLocal::default(),
|
||||
path: PathBuf::from(path),
|
||||
series,
|
||||
size_x: 0,
|
||||
size_y: 0,
|
||||
size_c: 0,
|
||||
size_z: 0,
|
||||
size_t: 0,
|
||||
pixel_type: PixelType::I8,
|
||||
little_endian: false,
|
||||
};
|
||||
reader.size_x = reader.get_size_x()? as usize;
|
||||
reader.size_y = reader.get_size_y()? as usize;
|
||||
reader.size_c = reader.get_size_c()? as usize;
|
||||
reader.size_z = reader.get_size_z()? as usize;
|
||||
reader.size_t = reader.get_size_t()? as usize;
|
||||
reader.pixel_type = PixelType::try_from(reader.get_pixel_type()?)?;
|
||||
reader.little_endian = reader.is_little_endian()?;
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
/// Get ome metadata as xml string
|
||||
pub fn get_ome_xml(&self) -> Result<String> {
|
||||
self.ome_xml()
|
||||
}
|
||||
|
||||
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>> {
|
||||
let chunk_size = match self.pixel_type {
|
||||
PixelType::I8 => 1,
|
||||
PixelType::U8 => 1,
|
||||
PixelType::I16 => 2,
|
||||
PixelType::U16 => 2,
|
||||
PixelType::I32 => 4,
|
||||
PixelType::U32 => 4,
|
||||
PixelType::F32 => 4,
|
||||
PixelType::F64 => 8,
|
||||
PixelType::I64 => 8,
|
||||
PixelType::U64 => 8,
|
||||
PixelType::I128 => 16,
|
||||
PixelType::U128 => 16,
|
||||
PixelType::F128 => 8,
|
||||
};
|
||||
Ok(bytes
|
||||
.chunks(chunk_size)
|
||||
.skip(channel)
|
||||
.step_by(self.size_c)
|
||||
.flat_map(|a| a.to_vec())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Retrieve fame at channel c, slize z and time t.
|
||||
pub fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame> {
|
||||
let bytes = if self.is_rgb()? & self.is_interleaved()? {
|
||||
let index = self.get_index(z as i32, 0, t as i32)?;
|
||||
self.deinterleave(self.open_bytes(index)?, c)?
|
||||
} else if self.get_rgb_channel_count()? > 1 {
|
||||
let channel_separator = bioformats::ChannelSeparator::new(self)?;
|
||||
let index = channel_separator.get_index(z as i32, c as i32, t as i32)?;
|
||||
channel_separator.open_bytes(index)?
|
||||
} else if self.is_indexed()? {
|
||||
let index = self.get_index(z as i32, 0, t as i32)?;
|
||||
self.open_bytes(index)?
|
||||
// TODO: apply LUT
|
||||
// let _bytes_lut = match self.pixel_type {
|
||||
// PixelType::INT8 | PixelType::UINT8 => {
|
||||
// let _lut = self.image_reader.get_8bit_lookup_table()?;
|
||||
// }
|
||||
// PixelType::INT16 | PixelType::UINT16 => {
|
||||
// let _lut = self.image_reader.get_16bit_lookup_table()?;
|
||||
// }
|
||||
// _ => {}
|
||||
// };
|
||||
} else {
|
||||
let index = self.get_index(z as i32, c as i32, t as i32)?;
|
||||
self.open_bytes(index)?
|
||||
};
|
||||
self.bytes_to_frame(bytes)
|
||||
}
|
||||
|
||||
fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame> {
|
||||
macro_rules! get_frame {
|
||||
($t:tt, <$n:expr) => {
|
||||
Ok(Frame::from(Array2::from_shape_vec(
|
||||
(self.size_y, self.size_x),
|
||||
bytes
|
||||
.chunks($n)
|
||||
.map(|x| $t::from_le_bytes(x.try_into().unwrap()))
|
||||
.collect(),
|
||||
)?))
|
||||
};
|
||||
($t:tt, >$n:expr) => {
|
||||
Ok(Frame::from(Array2::from_shape_vec(
|
||||
(self.size_y, self.size_x),
|
||||
bytes
|
||||
.chunks($n)
|
||||
.map(|x| $t::from_be_bytes(x.try_into().unwrap()))
|
||||
.collect(),
|
||||
)?))
|
||||
};
|
||||
}
|
||||
|
||||
match (&self.pixel_type, self.little_endian) {
|
||||
(PixelType::I8, true) => get_frame!(i8, <1),
|
||||
(PixelType::U8, true) => get_frame!(u8, <1),
|
||||
(PixelType::I16, true) => get_frame!(i16, <2),
|
||||
(PixelType::U16, true) => get_frame!(u16, <2),
|
||||
(PixelType::I32, true) => get_frame!(i32, <4),
|
||||
(PixelType::U32, true) => get_frame!(u32, <4),
|
||||
(PixelType::F32, true) => get_frame!(f32, <4),
|
||||
(PixelType::F64, true) => get_frame!(f64, <8),
|
||||
(PixelType::I64, true) => get_frame!(i64, <8),
|
||||
(PixelType::U64, true) => get_frame!(u64, <8),
|
||||
(PixelType::I128, true) => get_frame!(i128, <16),
|
||||
(PixelType::U128, true) => get_frame!(u128, <16),
|
||||
(PixelType::F128, true) => get_frame!(f64, <8),
|
||||
(PixelType::I8, false) => get_frame!(i8, >1),
|
||||
(PixelType::U8, false) => get_frame!(u8, >1),
|
||||
(PixelType::I16, false) => get_frame!(i16, >2),
|
||||
(PixelType::U16, false) => get_frame!(u16, >2),
|
||||
(PixelType::I32, false) => get_frame!(i32, >4),
|
||||
(PixelType::U32, false) => get_frame!(u32, >4),
|
||||
(PixelType::F32, false) => get_frame!(f32, >4),
|
||||
(PixelType::F64, false) => get_frame!(f64, >8),
|
||||
(PixelType::I64, false) => get_frame!(i64, >8),
|
||||
(PixelType::U64, false) => get_frame!(u64, >8),
|
||||
(PixelType::I128, false) => get_frame!(i128, >16),
|
||||
(PixelType::U128, false) => get_frame!(u128, >16),
|
||||
(PixelType::F128, false) => get_frame!(f64, >8),
|
||||
}
|
||||
}
|
||||
|
||||
/// get a slicable view on the image file
|
||||
pub fn view(&self) -> View<Ix5> {
|
||||
let slice = s![
|
||||
0..self.size_c,
|
||||
0..self.size_z,
|
||||
0..self.size_t,
|
||||
0..self.size_y,
|
||||
0..self.size_x
|
||||
];
|
||||
View::new(
|
||||
Arc::new(self.clone()),
|
||||
slice.as_ref().to_vec(),
|
||||
vec![Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Reader {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.close();
|
||||
}
|
||||
}
|
||||
201
src/stats.rs
Normal file
201
src/stats.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ndarray::{Array, ArrayD, ArrayView, Axis, Dimension, RemoveAxis};
|
||||
|
||||
/// a trait to define the min, max, sum and mean operations along an axis
|
||||
pub trait MinMax {
|
||||
type Output;
|
||||
|
||||
fn max(self, axis: usize) -> Result<Self::Output>;
|
||||
fn min(self, axis: usize) -> Result<Self::Output>;
|
||||
fn sum(self, axis: usize) -> Result<Self::Output>;
|
||||
fn mean(self, axis: usize) -> Result<Self::Output>;
|
||||
}
|
||||
|
||||
macro_rules! impl_frame_stats_float_view {
|
||||
($($t:tt),+ $(,)?) => {
|
||||
$(
|
||||
impl<D> MinMax for ArrayView<'_, $t, D>
|
||||
where
|
||||
D: Dimension + RemoveAxis,
|
||||
{
|
||||
type Output = Array<$t, D::Smaller>;
|
||||
|
||||
fn max(self, axis: usize) -> Result<Self::Output> {
|
||||
let a: Vec<_> = self
|
||||
.lanes(Axis(axis))
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
x.iter()
|
||||
.fold($t::NEG_INFINITY, |prev, curr| prev.max(*curr))
|
||||
})
|
||||
.collect();
|
||||
let mut shape = self.shape().to_vec();
|
||||
shape.remove(axis);
|
||||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
fn min(self, axis: usize) -> Result<Self::Output> {
|
||||
let a: Vec<_> = self
|
||||
.lanes(Axis(axis))
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
x.iter()
|
||||
.fold($t::INFINITY, |prev, curr| prev.min(*curr))
|
||||
})
|
||||
.collect();
|
||||
let mut shape = self.shape().to_vec();
|
||||
shape.remove(axis);
|
||||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
fn sum(self, axis: usize) -> Result<Self::Output> {
|
||||
Ok(self.sum_axis(Axis(axis)))
|
||||
}
|
||||
|
||||
fn mean(self, axis: usize) -> Result<Self::Output> {
|
||||
self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean"))
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_frame_stats_int_view {
|
||||
($($t:tt),+ $(,)?) => {
|
||||
$(
|
||||
impl<D> MinMax for ArrayView<'_, $t, D>
|
||||
where
|
||||
D: Dimension + RemoveAxis,
|
||||
{
|
||||
type Output = Array<$t, D::Smaller>;
|
||||
|
||||
fn max(self, axis: usize) -> Result<Self::Output> {
|
||||
let a: Vec<_> = self
|
||||
.lanes(Axis(axis))
|
||||
.into_iter()
|
||||
.map(|x| *x.iter().max().unwrap())
|
||||
.collect();
|
||||
let mut shape = self.shape().to_vec();
|
||||
shape.remove(axis);
|
||||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
fn min(self, axis: usize) -> Result<Self::Output> {
|
||||
let a: Vec<_> = self
|
||||
.lanes(Axis(axis))
|
||||
.into_iter()
|
||||
.map(|x| *x.iter().min().unwrap())
|
||||
.collect();
|
||||
let mut shape = self.shape().to_vec();
|
||||
shape.remove(axis);
|
||||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
fn sum(self, axis: usize) -> Result<Self::Output> {
|
||||
Ok(self.sum_axis(Axis(axis)))
|
||||
}
|
||||
|
||||
fn mean(self, axis: usize) -> Result<Self::Output> {
|
||||
self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean"))
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_frame_stats_float {
|
||||
($($t:tt),+ $(,)?) => {
|
||||
$(
|
||||
impl<D> MinMax for Array<$t, D>
|
||||
where
|
||||
D: Dimension + RemoveAxis,
|
||||
{
|
||||
type Output = Array<$t, D::Smaller>;
|
||||
|
||||
fn max(self, axis: usize) -> Result<Self::Output> {
|
||||
let a: Vec<_> = self
|
||||
.lanes(Axis(axis))
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
x.iter()
|
||||
.fold($t::NEG_INFINITY, |prev, curr| prev.max(*curr))
|
||||
})
|
||||
.collect();
|
||||
let mut shape = self.shape().to_vec();
|
||||
shape.remove(axis);
|
||||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
fn min(self, axis: usize) -> Result<Self::Output> {
|
||||
let a: Vec<_> = self
|
||||
.lanes(Axis(axis))
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
x.iter()
|
||||
.fold($t::INFINITY, |prev, curr| prev.min(*curr))
|
||||
})
|
||||
.collect();
|
||||
let mut shape = self.shape().to_vec();
|
||||
shape.remove(axis);
|
||||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
fn sum(self, axis: usize) -> Result<Self::Output> {
|
||||
Ok(self.sum_axis(Axis(axis)))
|
||||
}
|
||||
|
||||
fn mean(self, axis: usize) -> Result<Self::Output> {
|
||||
self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean"))
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_frame_stats_int {
|
||||
($($t:tt),+ $(,)?) => {
|
||||
$(
|
||||
impl<D> MinMax for Array<$t, D>
|
||||
where
|
||||
D: Dimension + RemoveAxis,
|
||||
{
|
||||
type Output = Array<$t, D::Smaller>;
|
||||
|
||||
fn max(self, axis: usize) -> Result<Self::Output> {
|
||||
let a: Vec<_> = self
|
||||
.lanes(Axis(axis))
|
||||
.into_iter()
|
||||
.map(|x| *x.iter().max().unwrap())
|
||||
.collect();
|
||||
let mut shape = self.shape().to_vec();
|
||||
shape.remove(axis);
|
||||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
fn min(self, axis: usize) -> Result<Self::Output> {
|
||||
let a: Vec<_> = self
|
||||
.lanes(Axis(axis))
|
||||
.into_iter()
|
||||
.map(|x| *x.iter().min().unwrap())
|
||||
.collect();
|
||||
let mut shape = self.shape().to_vec();
|
||||
shape.remove(axis);
|
||||
Ok(ArrayD::from_shape_vec(shape, a)?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
fn sum(self, axis: usize) -> Result<Self::Output> {
|
||||
Ok(self.sum_axis(Axis(axis)))
|
||||
}
|
||||
|
||||
fn mean(self, axis: usize) -> Result<Self::Output> {
|
||||
self.mean_axis(Axis(axis)).ok_or_else(|| anyhow!("no mean"))
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_frame_stats_float_view!(f32, f64);
|
||||
impl_frame_stats_int_view!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize);
|
||||
impl_frame_stats_float!(f32, f64);
|
||||
impl_frame_stats_int!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize);
|
||||
821
src/view.rs
Normal file
821
src/view.rs
Normal file
@@ -0,0 +1,821 @@
|
||||
use crate::axes::{slice_info, Ax, Axis, Operation, Slice, SliceInfoElemDef};
|
||||
use crate::reader::Reader;
|
||||
use crate::stats::MinMax;
|
||||
use anyhow::{anyhow, Error, Result};
|
||||
use indexmap::IndexMap;
|
||||
use itertools::iproduct;
|
||||
use ndarray::{
|
||||
s, Array, Array0, Array1, Array2, ArrayD, Dimension, IntoDimension, Ix0, Ix1, Ix2, Ix5, IxDyn,
|
||||
SliceArg, SliceInfoElem,
|
||||
};
|
||||
use num::{Bounded, FromPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
use std::any::type_name;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::Sum;
|
||||
use std::marker::PhantomData;
|
||||
use std::mem::{transmute, transmute_copy};
|
||||
use std::ops::{AddAssign, Deref, Div};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn idx_bnd(idx: isize, bnd: isize) -> Result<isize> {
|
||||
if idx < -bnd {
|
||||
Err(anyhow!("Index {} out of bounds {}", idx, bnd))
|
||||
} else if idx < 0 {
|
||||
Ok(bnd - idx)
|
||||
} else if idx < bnd {
|
||||
Ok(idx)
|
||||
} else {
|
||||
Err(anyhow!("Index {} out of bounds {}", idx, bnd))
|
||||
}
|
||||
}
|
||||
|
||||
fn slc_bnd(idx: isize, bnd: isize) -> Result<isize> {
|
||||
if idx < -bnd {
|
||||
Err(anyhow!("Index {} out of bounds {}", idx, bnd))
|
||||
} else if idx < 0 {
|
||||
Ok(bnd - idx)
|
||||
} else if idx <= bnd {
|
||||
Ok(idx)
|
||||
} else {
|
||||
Err(anyhow!("Index {} out of bounds {}", idx, bnd))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Number:
|
||||
'static + AddAssign + Bounded + Clone + Div<Self, Output = Self> + FromPrimitive + PartialOrd + Zero
|
||||
{
|
||||
}
|
||||
impl<T> Number for T where
|
||||
T: 'static
|
||||
+ AddAssign
|
||||
+ Bounded
|
||||
+ Clone
|
||||
+ Div<Self, Output = Self>
|
||||
+ FromPrimitive
|
||||
+ PartialOrd
|
||||
+ Zero
|
||||
{
|
||||
}
|
||||
|
||||
/// sliceable view on an image file
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct View<D: Dimension> {
|
||||
reader: Arc<Reader>,
|
||||
#[serde_as(as = "Vec<SliceInfoElemDef>")]
|
||||
slice: Vec<SliceInfoElem>,
|
||||
axes: Vec<Axis>,
|
||||
operations: IndexMap<Axis, Operation>,
|
||||
dimensionality: PhantomData<D>,
|
||||
}
|
||||
|
||||
impl<D: Dimension> View<D> {
|
||||
pub(crate) fn new(reader: Arc<Reader>, slice: Vec<SliceInfoElem>, axes: Vec<Axis>) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
slice,
|
||||
axes,
|
||||
operations: IndexMap::new(),
|
||||
dimensionality: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// the file path
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
&self.reader.path
|
||||
}
|
||||
|
||||
/// the series in the file
|
||||
pub fn series(&self) -> i32 {
|
||||
self.reader.series
|
||||
}
|
||||
|
||||
fn with_operations(mut self, operations: IndexMap<Axis, Operation>) -> Self {
|
||||
self.operations = operations;
|
||||
self
|
||||
}
|
||||
|
||||
/// change the dimension into a dynamic dimension
|
||||
pub fn into_dyn(self) -> View<IxDyn> {
|
||||
View {
|
||||
reader: self.reader,
|
||||
slice: self.slice,
|
||||
axes: self.axes,
|
||||
operations: self.operations,
|
||||
dimensionality: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// change the dimension into a concrete dimension
|
||||
pub fn into_dimensionality<D2: Dimension>(self) -> Result<View<D2>> {
|
||||
if let Some(d) = D2::NDIM {
|
||||
if d == self.ndim() {
|
||||
Ok(View {
|
||||
reader: self.reader,
|
||||
slice: self.slice,
|
||||
axes: self.axes,
|
||||
operations: self.operations,
|
||||
dimensionality: PhantomData,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("Dimensionality mismatch: {} != {}", d, self.ndim()))
|
||||
}
|
||||
} else {
|
||||
Ok(View {
|
||||
reader: self.reader,
|
||||
slice: self.slice,
|
||||
axes: self.axes,
|
||||
operations: self.operations,
|
||||
dimensionality: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// the order of the axes, including axes sliced out
|
||||
pub fn get_axes(&self) -> &[Axis] {
|
||||
&self.axes
|
||||
}
|
||||
|
||||
/// the slice defining the view
|
||||
pub fn get_slice(&self) -> &[SliceInfoElem] {
|
||||
&self.slice
|
||||
}
|
||||
|
||||
/// the axes in the view
|
||||
pub fn axes(&self) -> Vec<Axis> {
|
||||
self.axes
|
||||
.iter()
|
||||
.zip(self.slice.iter())
|
||||
.filter_map(|(ax, s)| {
|
||||
if s.is_index() || self.operations.contains_key(ax) {
|
||||
None
|
||||
} else {
|
||||
Some(*ax)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn op_axes(&self) -> Vec<Axis> {
|
||||
self.operations.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// the number of dimensions in the view
|
||||
pub fn ndim(&self) -> usize {
|
||||
if let Some(d) = D::NDIM {
|
||||
d
|
||||
} else {
|
||||
self.shape().len()
|
||||
}
|
||||
}
|
||||
|
||||
/// the number of pixels in the view
|
||||
pub fn size(&self) -> usize {
|
||||
self.shape().into_iter().product()
|
||||
}
|
||||
|
||||
/// the shape of the view
|
||||
pub fn shape(&self) -> Vec<usize> {
|
||||
let mut shape = Vec::<usize>::new();
|
||||
for (ax, s) in self.axes.iter().zip(self.slice.iter()) {
|
||||
match s {
|
||||
SliceInfoElem::Slice { start, end, step } => {
|
||||
if !self.operations.contains_key(ax) {
|
||||
if let Some(e) = end {
|
||||
shape.push(((e - start).max(0) / step) as usize);
|
||||
} else {
|
||||
panic!("slice has no end")
|
||||
}
|
||||
}
|
||||
}
|
||||
SliceInfoElem::Index(_) => {}
|
||||
SliceInfoElem::NewAxis => {
|
||||
if !self.operations.contains_key(ax) {
|
||||
shape.push(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shape
|
||||
}
|
||||
|
||||
fn shape_all(&self) -> Vec<usize> {
|
||||
let mut shape = Vec::<usize>::new();
|
||||
for s in self.slice.iter() {
|
||||
match s {
|
||||
SliceInfoElem::Slice { start, end, step } => {
|
||||
if let Some(e) = end {
|
||||
shape.push(((e - start).max(0) / step) as usize);
|
||||
} else {
|
||||
panic!("slice has no end")
|
||||
}
|
||||
}
|
||||
_ => shape.push(1),
|
||||
}
|
||||
}
|
||||
shape
|
||||
}
|
||||
|
||||
/// swap two axes
|
||||
pub fn swap_axes<A: Ax>(&self, axis0: A, axis1: A) -> Result<Self> {
|
||||
let idx0 = axis0.pos_op(&self.axes, &self.slice, &self.op_axes())?;
|
||||
let idx1 = axis1.pos_op(&self.axes, &self.slice, &self.op_axes())?;
|
||||
let mut slice = self.slice.to_vec();
|
||||
slice.swap(idx0, idx1);
|
||||
let mut axes = self.axes.clone();
|
||||
axes.swap(idx0, idx1);
|
||||
Ok(View::new(self.reader.clone(), slice, axes))
|
||||
}
|
||||
|
||||
/// subset of gives axes will be reordered in given order
|
||||
pub fn permute_axes<A: Ax>(&self, axes: &[A]) -> Result<Self> {
|
||||
let idx: Vec<usize> = axes
|
||||
.iter()
|
||||
.map(|a| a.pos_op(&self.axes, &self.slice, &self.op_axes()).unwrap())
|
||||
.collect();
|
||||
let mut jdx = idx.clone();
|
||||
jdx.sort();
|
||||
let mut slice = self.slice.to_vec();
|
||||
let mut axes = self.axes.clone();
|
||||
for (&i, j) in idx.iter().zip(jdx) {
|
||||
slice[j] = self.slice[i];
|
||||
axes[j] = self.axes[i];
|
||||
}
|
||||
Ok(View::new(self.reader.clone(), slice, axes))
|
||||
}
|
||||
|
||||
/// reverse the order of the axes
|
||||
pub fn transpose(&self) -> Result<Self> {
|
||||
Ok(View::new(
|
||||
self.reader.clone(),
|
||||
self.slice.iter().rev().cloned().collect(),
|
||||
self.axes.iter().rev().cloned().collect(),
|
||||
))
|
||||
}
|
||||
|
||||
fn operate<A: Ax>(&self, axis: A, operation: Operation) -> Result<View<D::Smaller>> {
|
||||
let pos = axis.pos_op(&self.axes, &self.slice, &self.op_axes())?;
|
||||
let mut operations = self.operations.clone();
|
||||
operations.insert(self.axes[pos], operation);
|
||||
Ok(
|
||||
View::new(self.reader.clone(), self.slice.clone(), self.axes.clone())
|
||||
.with_operations(operations),
|
||||
)
|
||||
}
|
||||
|
||||
/// maximum along axis
|
||||
pub fn max_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>> {
|
||||
self.operate(axis, Operation::Max)
|
||||
}
|
||||
|
||||
/// minimum along axis
|
||||
pub fn min_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>> {
|
||||
self.operate(axis, Operation::Min)
|
||||
}
|
||||
|
||||
/// sum along axis
|
||||
pub fn sum_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>> {
|
||||
self.operate(axis, Operation::Sum)
|
||||
}
|
||||
|
||||
/// mean along axis
|
||||
pub fn mean_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>> {
|
||||
self.operate(axis, Operation::Mean)
|
||||
}
|
||||
|
||||
/// created a new sliced view
|
||||
pub fn slice<I>(&self, info: I) -> Result<View<I::OutDim>>
|
||||
where
|
||||
I: SliceArg<D>,
|
||||
{
|
||||
if self.slice.out_ndim() < info.in_ndim() {
|
||||
return Err(Error::msg("not enough free dimensions"));
|
||||
}
|
||||
let info = info.as_ref();
|
||||
let mut n_idx = 0;
|
||||
let mut r_idx = 0;
|
||||
let mut new_slice = Vec::new();
|
||||
let mut new_axes = Vec::new();
|
||||
let reader_slice = self.slice.as_slice();
|
||||
while (r_idx < reader_slice.len()) & (n_idx < info.len()) {
|
||||
let n = &info[n_idx];
|
||||
let r = &reader_slice[r_idx];
|
||||
let a = &self.axes[r_idx];
|
||||
if self.operations.contains_key(a) {
|
||||
new_slice.push(*r);
|
||||
new_axes.push(*a);
|
||||
r_idx += 1;
|
||||
} else {
|
||||
match (n, r) {
|
||||
(
|
||||
SliceInfoElem::Slice {
|
||||
start: info_start,
|
||||
end: info_end,
|
||||
step: info_step,
|
||||
},
|
||||
SliceInfoElem::Slice { start, end, step },
|
||||
) => {
|
||||
let new_start = start + info_start;
|
||||
let new_end = match (info_end, end) {
|
||||
(Some(m), Some(n)) => *n.min(&(start + info_step * m)),
|
||||
(None, Some(n)) => *n,
|
||||
_ => panic!("slice has no end"),
|
||||
};
|
||||
let new_step = (step * info_step).abs();
|
||||
new_slice.push(SliceInfoElem::Slice {
|
||||
start: new_start,
|
||||
end: Some(new_end),
|
||||
step: new_step,
|
||||
});
|
||||
new_axes.push(*a);
|
||||
n_idx += 1;
|
||||
r_idx += 1;
|
||||
}
|
||||
(SliceInfoElem::Index(k), SliceInfoElem::Slice { start, end, step }) => {
|
||||
if *k < 0 {
|
||||
new_slice.push(SliceInfoElem::Index(end.unwrap_or(0) + step.abs() * k))
|
||||
} else {
|
||||
new_slice.push(SliceInfoElem::Index(start + step.abs() * k));
|
||||
}
|
||||
new_axes.push(*a);
|
||||
n_idx += 1;
|
||||
r_idx += 1;
|
||||
}
|
||||
(SliceInfoElem::NewAxis, SliceInfoElem::NewAxis) => {
|
||||
new_slice.push(SliceInfoElem::NewAxis);
|
||||
new_slice.push(SliceInfoElem::NewAxis);
|
||||
new_axes.push(Axis::New);
|
||||
new_axes.push(Axis::New);
|
||||
n_idx += 1;
|
||||
r_idx += 1;
|
||||
}
|
||||
(_, SliceInfoElem::NewAxis) => {
|
||||
new_slice.push(SliceInfoElem::NewAxis);
|
||||
new_axes.push(Axis::New);
|
||||
n_idx += 1;
|
||||
r_idx += 1;
|
||||
}
|
||||
(SliceInfoElem::NewAxis, _) => {
|
||||
new_slice.push(SliceInfoElem::NewAxis);
|
||||
new_axes.push(Axis::New);
|
||||
n_idx += 1;
|
||||
}
|
||||
(_, SliceInfoElem::Index(k)) => {
|
||||
new_slice.push(SliceInfoElem::Index(*k));
|
||||
new_axes.push(*a);
|
||||
r_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug_assert_eq!(r_idx, reader_slice.len());
|
||||
while n_idx < info.len() {
|
||||
debug_assert!(info[n_idx].is_new_axis());
|
||||
new_slice.push(SliceInfoElem::NewAxis);
|
||||
new_axes.push(Axis::New);
|
||||
n_idx += 1;
|
||||
}
|
||||
Ok(View::new(self.reader.clone(), new_slice, new_axes)
|
||||
.with_operations(self.operations.clone()))
|
||||
}
|
||||
|
||||
/// slice, but slice elements are in cztyx order, all cztyx must be given,
|
||||
/// but axes not present in view will be ignored, view axes are reordered in cztyx order
|
||||
pub fn slice_cztyx<I>(&self, info: I) -> Result<View<I::OutDim>>
|
||||
where
|
||||
I: SliceArg<Ix5>,
|
||||
{
|
||||
let axes = self.axes();
|
||||
let slice: Vec<_> = info
|
||||
.as_ref()
|
||||
.iter()
|
||||
.zip(self.axes.iter())
|
||||
.filter_map(|(&s, ax)| if axes.contains(ax) { Some(s) } else { None })
|
||||
.collect();
|
||||
let new_axes: Vec<_> = [Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X]
|
||||
.into_iter()
|
||||
.filter(|ax| axes.contains(ax))
|
||||
.collect();
|
||||
self.clone()
|
||||
.into_dyn()
|
||||
.permute_axes(&new_axes)?
|
||||
.slice(slice.as_slice())?
|
||||
.into_dimensionality()
|
||||
}
|
||||
|
||||
/// the pixel intensity at a given index
|
||||
pub fn item_at<T>(&self, index: &[isize]) -> Result<T>
|
||||
where
|
||||
T: Number,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
let slice: Vec<_> = index.iter().map(|s| SliceInfoElem::Index(*s)).collect();
|
||||
let view = self.clone().into_dyn().slice(slice.as_slice())?;
|
||||
let arr = view.as_array()?;
|
||||
Ok(arr.first().unwrap().clone())
|
||||
}
|
||||
|
||||
/// collect the view into an ndarray
|
||||
pub fn as_array<T>(&self) -> Result<Array<T, D>>
|
||||
where
|
||||
T: Number,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
Ok(self.as_array_dyn()?.into_dimensionality()?)
|
||||
}
|
||||
|
||||
/// collect the view into a dynamic-dimension ndarray
|
||||
pub fn as_array_dyn<T>(&self) -> Result<ArrayD<T>>
|
||||
where
|
||||
T: Number,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
let mut op_xy = IndexMap::new();
|
||||
if let Some((&ax, op)) = self.operations.first() {
|
||||
if (ax == Axis::X) || (ax == Axis::Y) {
|
||||
op_xy.insert(ax, op.clone());
|
||||
if let Some((&ax2, op2)) = self.operations.get_index(1) {
|
||||
if (ax2 == Axis::X) || (ax2 == Axis::Y) {
|
||||
op_xy.insert(ax2, op2.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let op_czt = if let Some((&ax, op)) = self.operations.get_index(op_xy.len()) {
|
||||
IndexMap::from([(ax, op.clone())])
|
||||
} else {
|
||||
IndexMap::new()
|
||||
};
|
||||
let mut shape_out = Vec::new();
|
||||
let mut slice = Vec::new();
|
||||
let mut ax_out = Vec::new();
|
||||
for (s, a) in self.slice.iter().zip(&self.axes) {
|
||||
match s {
|
||||
SliceInfoElem::Slice { start, end, step } => {
|
||||
if let Some(e) = end {
|
||||
if !op_xy.contains_key(a) && !op_czt.contains_key(a) {
|
||||
shape_out.push(((e - start).max(0) / step) as usize);
|
||||
slice.push(SliceInfoElem::Slice {
|
||||
start: 0,
|
||||
end: None,
|
||||
step: 1,
|
||||
});
|
||||
ax_out.push(*a);
|
||||
}
|
||||
} else {
|
||||
panic!("slice has no end")
|
||||
}
|
||||
}
|
||||
SliceInfoElem::Index(_) => {}
|
||||
SliceInfoElem::NewAxis => {
|
||||
shape_out.push(1);
|
||||
slice.push(SliceInfoElem::Index(0));
|
||||
ax_out.push(*a);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut slice_reader = vec![Slice::empty(); 5];
|
||||
let mut xy_dim = 0usize;
|
||||
let shape = [
|
||||
self.size_c as isize,
|
||||
self.size_z as isize,
|
||||
self.size_t as isize,
|
||||
self.size_y as isize,
|
||||
self.size_x as isize,
|
||||
];
|
||||
for (s, &axis) in self.slice.iter().zip(&self.axes) {
|
||||
match axis {
|
||||
Axis::New => {}
|
||||
_ => match s {
|
||||
SliceInfoElem::Slice { start, end, step } => {
|
||||
if let Axis::X | Axis::Y = axis {
|
||||
if !op_xy.contains_key(&axis) {
|
||||
xy_dim += 1;
|
||||
}
|
||||
}
|
||||
slice_reader[axis as usize] = Slice::new(
|
||||
idx_bnd(*start, shape[axis as usize])?,
|
||||
slc_bnd(end.unwrap(), shape[axis as usize])?,
|
||||
*step,
|
||||
);
|
||||
}
|
||||
SliceInfoElem::Index(j) => {
|
||||
slice_reader[axis as usize] = Slice::new(
|
||||
idx_bnd(*j, shape[axis as usize])?,
|
||||
slc_bnd(*j + 1, shape[axis as usize])?,
|
||||
1,
|
||||
);
|
||||
}
|
||||
SliceInfoElem::NewAxis => panic!("axis cannot be a new axis"),
|
||||
},
|
||||
}
|
||||
}
|
||||
let xy = [
|
||||
self.slice[Axis::Y.pos(&self.axes, &self.slice)?],
|
||||
self.slice[Axis::X.pos(&self.axes, &self.slice)?],
|
||||
];
|
||||
let mut array = if let Some((_, op)) = op_czt.first() {
|
||||
match op {
|
||||
Operation::Max => {
|
||||
ArrayD::<T>::from_elem(shape_out.into_dimension(), T::min_value())
|
||||
}
|
||||
Operation::Min => {
|
||||
ArrayD::<T>::from_elem(shape_out.into_dimension(), T::max_value())
|
||||
}
|
||||
_ => ArrayD::<T>::zeros(shape_out.into_dimension()),
|
||||
}
|
||||
} else {
|
||||
ArrayD::<T>::zeros(shape_out.into_dimension())
|
||||
};
|
||||
let size_c = self.reader.size_c as isize;
|
||||
let size_z = self.reader.size_z as isize;
|
||||
let size_t = self.reader.size_t as isize;
|
||||
|
||||
let mut axes_out_idx = [None; 5];
|
||||
for (i, ax) in ax_out.iter().enumerate() {
|
||||
axes_out_idx[*ax as usize] = Some(i);
|
||||
}
|
||||
|
||||
for (c, z, t) in iproduct!(&slice_reader[0], &slice_reader[1], &slice_reader[2]) {
|
||||
if let Some(i) = axes_out_idx[0] {
|
||||
slice[i] = SliceInfoElem::Index(c)
|
||||
};
|
||||
if let Some(i) = axes_out_idx[1] {
|
||||
slice[i] = SliceInfoElem::Index(z)
|
||||
};
|
||||
if let Some(i) = axes_out_idx[2] {
|
||||
slice[i] = SliceInfoElem::Index(t)
|
||||
};
|
||||
let frame = self.reader.get_frame(
|
||||
(c % size_c) as usize,
|
||||
(z % size_z) as usize,
|
||||
(t % size_t) as usize,
|
||||
)?;
|
||||
|
||||
let arr_frame: Array2<T> = frame.try_into()?;
|
||||
let arr_frame = match xy_dim {
|
||||
0 => {
|
||||
if op_xy.contains_key(&Axis::X) && op_xy.contains_key(&Axis::Y) {
|
||||
let xys = slice_info::<Ix2>(&xy)?;
|
||||
let (&ax0, op0) = op_xy.first().unwrap();
|
||||
let (&ax1, op1) = op_xy.get_index(1).unwrap();
|
||||
let a = arr_frame.slice(xys).to_owned();
|
||||
let b = op0.operate(a, ax0 as usize - 3)?;
|
||||
let c: &Array1<T> = unsafe { transmute(&b) };
|
||||
let d = op1.operate(c.to_owned(), ax1 as usize - 3)?;
|
||||
let e: &Array0<T> = unsafe { transmute(&d) };
|
||||
e.to_owned().into_dyn()
|
||||
} else if op_xy.contains_key(&Axis::X) || op_xy.contains_key(&Axis::Y) {
|
||||
let xys = slice_info::<Ix1>(&xy)?;
|
||||
let (&ax, op) = op_xy.first().unwrap();
|
||||
let a = arr_frame.slice(xys).to_owned();
|
||||
let b = op.operate(a, ax as usize - 3)?;
|
||||
let c: &Array0<T> = unsafe { transmute(&b) };
|
||||
c.to_owned().into_dyn()
|
||||
} else {
|
||||
let xys = slice_info::<Ix0>(&xy)?;
|
||||
arr_frame.slice(xys).to_owned().into_dyn()
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if op_xy.contains_key(&Axis::X) || op_xy.contains_key(&Axis::Y) {
|
||||
let xys = slice_info::<Ix2>(&xy)?;
|
||||
let (&ax, op) = op_xy.first().unwrap();
|
||||
let a = arr_frame.slice(xys).to_owned();
|
||||
let b = op.operate(a, ax as usize - 3)?;
|
||||
let c: &Array1<T> = unsafe { transmute(&b) };
|
||||
c.to_owned().into_dyn()
|
||||
} else {
|
||||
let xys = slice_info::<Ix1>(&xy)?;
|
||||
arr_frame.slice(xys).to_owned().into_dyn()
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let xys = slice_info::<Ix2>(&xy)?;
|
||||
if axes_out_idx[4] < axes_out_idx[3] {
|
||||
arr_frame.t().slice(xys).to_owned().into_dyn()
|
||||
} else {
|
||||
arr_frame.slice(xys).to_owned().into_dyn()
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("xy cannot be 3d or more");
|
||||
}
|
||||
};
|
||||
if let Some((_, op)) = op_czt.first() {
|
||||
match op {
|
||||
Operation::Max => {
|
||||
array
|
||||
.slice_mut(slice.as_slice())
|
||||
.zip_mut_with(&arr_frame, |x, y| {
|
||||
*x = if *x >= *y { x.clone() } else { y.clone() }
|
||||
});
|
||||
}
|
||||
Operation::Min => {
|
||||
array
|
||||
.slice_mut(slice.as_slice())
|
||||
.zip_mut_with(&arr_frame, |x, y| {
|
||||
*x = if *x < *y { x.clone() } else { y.clone() }
|
||||
});
|
||||
}
|
||||
Operation::Sum => {
|
||||
array
|
||||
.slice_mut(slice.as_slice())
|
||||
.zip_mut_with(&arr_frame, |x, y| *x += y.clone());
|
||||
}
|
||||
Operation::Mean => {
|
||||
array
|
||||
.slice_mut(slice.as_slice())
|
||||
.zip_mut_with(&arr_frame, |x, y| *x += y.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
array.slice_mut(slice.as_slice()).assign(&arr_frame)
|
||||
}
|
||||
}
|
||||
let mut out = Some(array);
|
||||
let ax_out: HashMap<Axis, usize> = ax_out
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, a)| (a, i))
|
||||
.collect();
|
||||
for (ax, op) in self.operations.iter().skip(op_xy.len() + op_czt.len()) {
|
||||
if let Some(&idx) = ax_out.get(ax) {
|
||||
let arr = out.take().unwrap();
|
||||
let _ = out.insert(unsafe { transmute_copy(&op.operate(arr, idx)?) });
|
||||
}
|
||||
}
|
||||
let mut n = 1;
|
||||
for (&ax, size) in self.axes.iter().zip(self.shape_all().iter()) {
|
||||
if let Some(Operation::Mean) = self.operations.get(&ax) {
|
||||
if (ax == Axis::C) || (ax == Axis::Z) || (ax == Axis::T) {
|
||||
n *= size;
|
||||
}
|
||||
}
|
||||
}
|
||||
let array = if n == 1 {
|
||||
out.take().unwrap()
|
||||
} else {
|
||||
let m = T::from_usize(n).unwrap_or_else(|| T::zero());
|
||||
out.take().unwrap().mapv(|x| x / m.clone())
|
||||
};
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// retrieve a single frame at czt, sliced accordingly
|
||||
pub fn get_frame<T>(&self, c: isize, z: isize, t: isize) -> Result<Array2<T>>
|
||||
where
|
||||
T: Number,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
self.slice_cztyx(s![c, z, t, .., ..])?.as_array()
|
||||
}
|
||||
|
||||
fn get_stat<T>(&self, operation: Operation) -> Result<T>
|
||||
where
|
||||
T: Number + Sum,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
let arr: ArrayD<T> = self.as_array_dyn()?;
|
||||
Ok(match operation {
|
||||
Operation::Max => arr
|
||||
.flatten()
|
||||
.into_iter()
|
||||
.reduce(|a, b| if a > b { a } else { b })
|
||||
.unwrap_or_else(|| T::min_value()),
|
||||
Operation::Min => arr
|
||||
.flatten()
|
||||
.into_iter()
|
||||
.reduce(|a, b| if a < b { a } else { b })
|
||||
.unwrap_or_else(|| T::max_value()),
|
||||
Operation::Sum => arr.flatten().into_iter().sum(),
|
||||
Operation::Mean => {
|
||||
arr.flatten().into_iter().sum::<T>()
|
||||
/ T::from_usize(arr.len()).ok_or_else(|| {
|
||||
anyhow!("cannot convert {} into {}", arr.len(), type_name::<T>())
|
||||
})?
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// maximum intensity
|
||||
pub fn max<T>(&self) -> Result<T>
|
||||
where
|
||||
T: Number + Sum,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
self.get_stat(Operation::Max)
|
||||
}
|
||||
|
||||
/// minimum intensity
|
||||
pub fn min<T>(&self) -> Result<T>
|
||||
where
|
||||
T: Number + Sum,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
self.get_stat(Operation::Min)
|
||||
}
|
||||
|
||||
/// sum intensity
|
||||
pub fn sum<T>(&self) -> Result<T>
|
||||
where
|
||||
T: Number + Sum,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
self.get_stat(Operation::Sum)
|
||||
}
|
||||
|
||||
/// mean intensity
|
||||
pub fn mean<T>(&self) -> Result<T>
|
||||
where
|
||||
T: Number + Sum,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
self.get_stat(Operation::Mean)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Dimension> Deref for View<D> {
|
||||
type Target = Reader;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.reader.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, D> TryFrom<View<D>> for Array<T, D>
|
||||
where
|
||||
T: Number,
|
||||
D: Dimension,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(view: View<D>) -> Result<Self, Self::Error> {
|
||||
view.as_array()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, D> TryFrom<&View<D>> for Array<T, D>
|
||||
where
|
||||
T: Number,
|
||||
D: Dimension,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(view: &View<D>) -> Result<Self, Self::Error> {
|
||||
view.as_array()
|
||||
}
|
||||
}
|
||||
|
||||
/// trait to define a function to retrieve the only item in a 0d array
|
||||
pub trait Item {
|
||||
fn item<T>(&self) -> Result<T>
|
||||
where
|
||||
T: Number,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax;
|
||||
}
|
||||
|
||||
impl Item for View<Ix0> {
|
||||
fn item<T>(&self) -> Result<T>
|
||||
where
|
||||
T: Number,
|
||||
ArrayD<T>: MinMax,
|
||||
Array1<T>: MinMax,
|
||||
Array2<T>: MinMax,
|
||||
{
|
||||
Ok(self
|
||||
.as_array()?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("Empty view"))?
|
||||
.clone())
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,20 @@ import pytest
|
||||
from ndbioimage import Imread
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file',
|
||||
[file for file in (Path(__file__).parent / 'files').iterdir() if not file.suffix == '.pzl'])
|
||||
@pytest.mark.parametrize(
|
||||
"file",
|
||||
[
|
||||
file
|
||||
for file in (Path(__file__).parent / "files").iterdir()
|
||||
if not file.suffix == ".pzl"
|
||||
],
|
||||
)
|
||||
def test_open(file):
|
||||
with Imread(file) as im:
|
||||
mean = im[dict(c=0, z=0, t=0)].mean()
|
||||
with Imread(file, axes="cztyx") as im:
|
||||
mean = im[0, 0, 0].mean()
|
||||
b = pickle.dumps(im)
|
||||
jm = pickle.loads(b)
|
||||
assert jm[dict(c=0, z=0, t=0)].mean() == mean
|
||||
v = im.view()
|
||||
assert v[dict(c=0, z=0, t=0)].mean() == mean
|
||||
b = pickle.dumps(v)
|
||||
w = pickle.loads(b)
|
||||
assert w[dict(c=0, z=0, t=0)].mean() == mean
|
||||
assert jm.get_frame(0, 0, 0).mean() == mean
|
||||
b = pickle.dumps(im)
|
||||
jm = pickle.loads(b)
|
||||
assert jm[0, 0, 0].mean() == mean
|
||||
|
||||
@@ -12,19 +12,24 @@ from ndbioimage import Imread
|
||||
|
||||
@pytest.fixture
|
||||
def array():
|
||||
return np.random.randint(0, 255, (64, 64, 2, 3, 4), 'uint8')
|
||||
return np.random.randint(0, 255, (64, 64, 2, 3, 4), "uint8")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def image(array):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
file = Path(folder) / "test.tif"
|
||||
tiffwrite(file, array, 'yxczt')
|
||||
with Imread(file, axes='yxczt') as im:
|
||||
tiffwrite(file, array, "yxczt")
|
||||
with Imread(file, axes="yxczt") as im:
|
||||
yield im
|
||||
|
||||
|
||||
@pytest.mark.parametrize('s', combinations_with_replacement(
|
||||
(0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5))
|
||||
@pytest.mark.parametrize(
|
||||
"s",
|
||||
combinations_with_replacement(
|
||||
(0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5
|
||||
),
|
||||
)
|
||||
def test_slicing(s, image, array):
|
||||
s_im, s_a = image[s], array[s]
|
||||
if isinstance(s_a, Number):
|
||||
|
||||
@@ -11,21 +11,42 @@ from ndbioimage import Imread
|
||||
|
||||
@pytest.fixture
|
||||
def array():
|
||||
return np.random.randint(0, 255, (64, 64, 2, 3, 4), 'uint16')
|
||||
return np.random.randint(0, 255, (64, 64, 2, 3, 4), "uint16")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def image(array):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
file = Path(folder) / "test.tif"
|
||||
tiffwrite(file, array, 'yxczt')
|
||||
with Imread(file, axes='yxczt') as im:
|
||||
tiffwrite(file, array, "yxczt")
|
||||
with Imread(file, axes="yxczt") as im:
|
||||
yield im
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fun_and_axis', product(
|
||||
(np.sum, np.nansum, np.min, np.nanmin, np.max, np.nanmax, np.argmin, np.argmax,
|
||||
np.mean, np.nanmean, np.var, np.nanvar, np.std, np.nanstd), (None, 0, 1, 2, 3, 4)))
|
||||
@pytest.mark.parametrize(
|
||||
"fun_and_axis",
|
||||
product(
|
||||
(
|
||||
np.sum,
|
||||
np.nansum,
|
||||
np.min,
|
||||
np.nanmin,
|
||||
np.max,
|
||||
np.nanmax,
|
||||
np.argmin,
|
||||
np.argmax,
|
||||
np.mean,
|
||||
np.nanmean,
|
||||
np.var,
|
||||
np.nanvar,
|
||||
np.std,
|
||||
np.nanstd,
|
||||
),
|
||||
(None, 0, 1, 2, 3, 4),
|
||||
),
|
||||
)
|
||||
def test_ufuncs(fun_and_axis, image, array):
|
||||
fun, axis = fun_and_axis
|
||||
assert np.all(np.isclose(fun(image, axis), fun(array, axis))), \
|
||||
f'function {fun.__name__} over axis {axis} does not give the correct result'
|
||||
assert np.all(np.isclose(np.asarray(fun(image, axis)), fun(array, axis))), (
|
||||
f"function {fun.__name__} over axis {axis} does not give the correct result"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user