- 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]
|
[package]
|
||||||
name = "ndbioimage"
|
name = "ndbioimage"
|
||||||
version = "2025.2.3"
|
version = "2025.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.78.0"
|
rust-version = "1.78.0"
|
||||||
authors = ["Wim Pomp <w.pomp@nki.nl>"]
|
authors = ["Wim Pomp <w.pomp@nki.nl>"]
|
||||||
@@ -19,15 +19,20 @@ name = "ndbioimage"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.95"
|
anyhow = "1.0.98"
|
||||||
|
itertools = "0.14.0"
|
||||||
|
indexmap = { version = "2.9.0", features = ["serde"] }
|
||||||
j4rs = "0.22.0"
|
j4rs = "0.22.0"
|
||||||
ndarray = "0.16.1"
|
ndarray = { version = "0.16.1", features = ["serde"] }
|
||||||
num = "0.4.3"
|
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"
|
thread_local = "1.1.8"
|
||||||
|
|
||||||
[dependencies.pyo3]
|
[dependencies.pyo3]
|
||||||
version = "0.23.4"
|
version = "0.24.2"
|
||||||
features = ["extension-module", "abi3-py310", "generate-import-lib", "anyhow"]
|
features = ["extension-module", "abi3-py310", "generate-import-lib", "anyhow"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
@@ -35,10 +40,10 @@ optional = true
|
|||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.95"
|
anyhow = "1.0.98"
|
||||||
j4rs = "0.22.0"
|
j4rs = "0.22.0"
|
||||||
retry = "2.0.0"
|
retry = "2.1.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
python = ["dep:pyo3", "dep:numpy"]
|
python = ["dep:pyo3", "dep:numpy", "dep:serde_json"]
|
||||||
gpl-formats = []
|
gpl-formats = []
|
||||||
32
README.md
32
README.md
@@ -74,7 +74,7 @@ use ndarray::Array2;
|
|||||||
use ndbioimage::Reader;
|
use ndbioimage::Reader;
|
||||||
|
|
||||||
let path = "/path/to/file";
|
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);
|
println!("size: {}, {}", reader.size_y, reader.size_y);
|
||||||
let frame = reader.get_frame(0, 0, 0).unwrap();
|
let frame = reader.get_frame(0, 0, 0).unwrap();
|
||||||
if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
|
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);
|
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
|
### Command line
|
||||||
```ndbioimage --help```: show help
|
```ndbioimage --help```: show help
|
||||||
```ndbioimage image```: show metadata about image
|
```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 {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
|
```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
|
# TODO
|
||||||
- more image formats
|
- more image formats
|
||||||
|
|||||||
2
build.rs
2
build.rs
@@ -8,7 +8,7 @@ use retry::{delay, delay::Exponential, retry};
|
|||||||
use j4rs::Jvm;
|
use j4rs::Jvm;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo::rerun-if-changed=build.rs");
|
||||||
|
|
||||||
#[cfg(not(feature = "python"))]
|
#[cfg(not(feature = "python"))]
|
||||||
if std::env::var("DOCS_RS").is_err() {
|
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
|
DataFrame, Series, concat = None, None, None
|
||||||
|
|
||||||
|
|
||||||
if hasattr(yaml, 'full_load'):
|
if hasattr(yaml, "full_load"):
|
||||||
yamlload = yaml.full_load
|
yamlload = yaml.full_load
|
||||||
else:
|
else:
|
||||||
yamlload = yaml.load
|
yamlload = yaml.load
|
||||||
@@ -34,7 +34,7 @@ class Transforms(dict):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, file, C=True, T=True):
|
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)
|
return cls.from_dict(yamlload(f), C, T)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -42,7 +42,9 @@ class Transforms(dict):
|
|||||||
new = cls()
|
new = cls()
|
||||||
for key, value in d.items():
|
for key, value in d.items():
|
||||||
if isinstance(key, str) and C:
|
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:
|
elif T:
|
||||||
new[key] = Transform.from_dict(value)
|
new[key] = Transform.from_dict(value)
|
||||||
return new
|
return new
|
||||||
@@ -69,11 +71,19 @@ class Transforms(dict):
|
|||||||
return new
|
return new
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
return {key.replace('\\', '\\\\').replace(':', r'\:') if isinstance(key, str) else key: value.asdict()
|
return {
|
||||||
for key, value in self.items()}
|
key.replace("\\", "\\\\").replace(":", r"\:")
|
||||||
|
if isinstance(key, str)
|
||||||
|
else key: value.asdict()
|
||||||
|
for key, value in self.items()
|
||||||
|
}
|
||||||
|
|
||||||
def __getitem__(self, item):
|
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):
|
def __missing__(self, key):
|
||||||
return self.default
|
return self.default
|
||||||
@@ -88,7 +98,7 @@ class Transforms(dict):
|
|||||||
return hash(frozenset((*self.__dict__.items(), *self.items())))
|
return hash(frozenset((*self.__dict__.items(), *self.items())))
|
||||||
|
|
||||||
def save(self, file):
|
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)
|
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
@@ -109,8 +119,10 @@ class Transforms(dict):
|
|||||||
transform_channels = {key for key in self.keys() if isinstance(key, str)}
|
transform_channels = {key for key in self.keys() if isinstance(key, str)}
|
||||||
if set(channel_names) - transform_channels:
|
if set(channel_names) - transform_channels:
|
||||||
mapping = key_map(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,'
|
warnings.warn(
|
||||||
f' creating a mapping: {mapping}')
|
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():
|
for key_im, key_t in mapping.items():
|
||||||
self[key_im] = self[key_t]
|
self[key_im] = self[key_t]
|
||||||
|
|
||||||
@@ -124,37 +136,54 @@ class Transforms(dict):
|
|||||||
|
|
||||||
def coords_pandas(self, array, channel_names, columns=None):
|
def coords_pandas(self, array, channel_names, columns=None):
|
||||||
if isinstance(array, DataFrame):
|
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):
|
elif isinstance(array, Series):
|
||||||
key = []
|
key = []
|
||||||
if 'C' in array:
|
if "C" in array:
|
||||||
key.append(channel_names[int(array['C'])])
|
key.append(channel_names[int(array["C"])])
|
||||||
if 'T' in array:
|
if "T" in array:
|
||||||
key.append(int(array['T']))
|
key.append(int(array["T"]))
|
||||||
return self[tuple(key)].coords(array, columns)
|
return self[tuple(key)].coords(array, columns)
|
||||||
else:
|
else:
|
||||||
raise TypeError('Not a pandas DataFrame or Series.')
|
raise TypeError("Not a pandas DataFrame or Series.")
|
||||||
|
|
||||||
def with_beads(self, cyllens, bead_files):
|
def with_beads(self, cyllens, bead_files):
|
||||||
assert len(bead_files) > 0, 'At least one file is needed to calculate the registration.'
|
assert len(bead_files) > 0, (
|
||||||
transforms = [self.calculate_channel_transforms(file, cyllens) for file in bead_files]
|
"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()}:
|
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:
|
if len(new_transforms) == 1:
|
||||||
self[key] = new_transforms[0]
|
self[key] = new_transforms[0]
|
||||||
else:
|
else:
|
||||||
self[key] = Transform()
|
self[key] = Transform()
|
||||||
self[key].parameters = np.mean([t.parameters for t in new_transforms], 0)
|
self[key].parameters = np.mean(
|
||||||
self[key].dparameters = (np.std([t.parameters for t in new_transforms], 0) /
|
[t.parameters for t in new_transforms], 0
|
||||||
np.sqrt(len(new_transforms))).tolist()
|
)
|
||||||
|
self[key].dparameters = (
|
||||||
|
np.std([t.parameters for t in new_transforms], 0)
|
||||||
|
/ np.sqrt(len(new_transforms))
|
||||||
|
).tolist()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_bead_files(path):
|
def get_bead_files(path):
|
||||||
from . import Imread
|
from . import Imread
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
for file in path.iterdir():
|
for file in path.iterdir():
|
||||||
if file.name.lower().startswith('beads'):
|
if file.name.lower().startswith("beads"):
|
||||||
try:
|
try:
|
||||||
with Imread(file):
|
with Imread(file):
|
||||||
files.append(file)
|
files.append(file)
|
||||||
@@ -162,18 +191,18 @@ class Transforms(dict):
|
|||||||
pass
|
pass
|
||||||
files = sorted(files)
|
files = sorted(files)
|
||||||
if not files:
|
if not files:
|
||||||
raise Exception('No bead file found!')
|
raise Exception("No bead file found!")
|
||||||
checked_files = []
|
checked_files = []
|
||||||
for file in files:
|
for file in files:
|
||||||
try:
|
try:
|
||||||
if file.is_dir():
|
if file.is_dir():
|
||||||
file /= 'Pos0'
|
file /= "Pos0"
|
||||||
with Imread(file): # check for errors opening the file
|
with Imread(file): # check for errors opening the file
|
||||||
checked_files.append(file)
|
checked_files.append(file)
|
||||||
except (Exception,):
|
except (Exception,):
|
||||||
continue
|
continue
|
||||||
if not checked_files:
|
if not checked_files:
|
||||||
raise Exception('No bead file found!')
|
raise Exception("No bead file found!")
|
||||||
return checked_files
|
return checked_files
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -182,12 +211,16 @@ class Transforms(dict):
|
|||||||
in the horizontal direction"""
|
in the horizontal direction"""
|
||||||
from . import Imread
|
from . import Imread
|
||||||
|
|
||||||
with Imread(bead_file, axes='zcyx') as im: # noqa
|
with Imread(bead_file, axes="zcyx") as im: # noqa
|
||||||
max_ims = im.max('z')
|
max_ims = im.max("z")
|
||||||
goodch = [c for c, max_im in enumerate(max_ims) if not im.is_noise(max_im)]
|
goodch = [c for c, max_im in enumerate(max_ims) if not im.is_noise(max_im)]
|
||||||
if not goodch:
|
if not goodch:
|
||||||
goodch = list(range(len(max_ims)))
|
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))
|
good_and_untrans = sorted(set(goodch) & set(untransformed))
|
||||||
if good_and_untrans:
|
if good_and_untrans:
|
||||||
@@ -200,54 +233,81 @@ class Transforms(dict):
|
|||||||
matrix[0, 0] = 0.86
|
matrix[0, 0] = 0.86
|
||||||
transform.matrix = matrix
|
transform.matrix = matrix
|
||||||
transforms = Transforms()
|
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:
|
if c == masterch:
|
||||||
transforms[im.channel_names[c]] = transform
|
transforms[im.channel_names[c]] = transform
|
||||||
else:
|
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
|
return transforms
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_channel_transform_tiff(bead_files, tiffile):
|
def save_channel_transform_tiff(bead_files, tiffile):
|
||||||
from . import Imread
|
from . import Imread
|
||||||
|
|
||||||
n_channels = 0
|
n_channels = 0
|
||||||
for file in bead_files:
|
for file in bead_files:
|
||||||
with Imread(file) as im:
|
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:
|
with IJTiffFile(tiffile) as tif:
|
||||||
for t, file in enumerate(bead_files):
|
for t, file in enumerate(bead_files):
|
||||||
with Imread(file) as im:
|
with Imread(file) as im:
|
||||||
with Imread(file).with_transform() as jm:
|
with Imread(file).with_transform() as jm:
|
||||||
for c in range(im.shape['c']):
|
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)
|
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):
|
def with_drift(self, im):
|
||||||
"""Calculate shifts relative to the first frame
|
"""Calculate shifts relative to the first frame
|
||||||
divide the sequence into groups,
|
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
|
compare each frame to the frame in the middle of the group and compare these middle frames to each other
|
||||||
"""
|
"""
|
||||||
im = im.transpose('tzycx')
|
im = im.transpose("tzycx")
|
||||||
t_groups = [list(chunk) for chunk in Chunks(range(im.shape['t']), size=round(np.sqrt(im.shape['t'])))]
|
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_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:]))
|
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):
|
def fun(t_key_t, im, fmaxz_keys):
|
||||||
t_key, t = t_key_t
|
t_key, t = t_key_t
|
||||||
if t_key == t:
|
if t_key == t:
|
||||||
return 0, 0
|
return 0, 0
|
||||||
else:
|
else:
|
||||||
fmaxz = filters.gaussian(im[t].max('z'), 5)
|
fmaxz = filters.gaussian(im[t].max("z"), 5)
|
||||||
return Transform.register(fmaxz_keys[t_key], fmaxz, 'translation').parameters[4:]
|
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)
|
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
|
shift_keys_cum += shift_keys
|
||||||
shifts[t_group] += shift_keys_cum
|
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)
|
self[i] = Transform.from_shift(shift)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -257,9 +317,11 @@ class Transform:
|
|||||||
if sitk is None:
|
if sitk is None:
|
||||||
self.transform = None
|
self.transform = None
|
||||||
else:
|
else:
|
||||||
self.transform = sitk.ReadTransform(str(Path(__file__).parent / 'transform.txt'))
|
self.transform = sitk.ReadTransform(
|
||||||
self.dparameters = [0., 0., 0., 0., 0., 0.]
|
str(Path(__file__).parent / "transform.txt")
|
||||||
self.shape = [512., 512.]
|
)
|
||||||
|
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.origin = [255.5, 255.5]
|
||||||
self._last, self._inverse = None, None
|
self._last, self._inverse = None, None
|
||||||
|
|
||||||
@@ -276,10 +338,12 @@ class Transform:
|
|||||||
def register(cls, fix, mov, kind=None):
|
def register(cls, fix, mov, kind=None):
|
||||||
"""kind: 'affine', 'translation', 'rigid'"""
|
"""kind: 'affine', 'translation', 'rigid'"""
|
||||||
if sitk is None:
|
if sitk is None:
|
||||||
raise ImportError('SimpleElastix is not installed: '
|
raise ImportError(
|
||||||
'https://simpleelastix.readthedocs.io/GettingStarted.html')
|
"SimpleElastix is not installed: "
|
||||||
|
"https://simpleelastix.readthedocs.io/GettingStarted.html"
|
||||||
|
)
|
||||||
new = cls()
|
new = cls()
|
||||||
kind = kind or 'affine'
|
kind = kind or "affine"
|
||||||
new.shape = fix.shape
|
new.shape = fix.shape
|
||||||
fix, mov = new.cast_image(fix), new.cast_image(mov)
|
fix, mov = new.cast_image(fix), new.cast_image(mov)
|
||||||
# TODO: implement RigidTransform
|
# TODO: implement RigidTransform
|
||||||
@@ -290,16 +354,18 @@ class Transform:
|
|||||||
tfilter.SetParameterMap(sitk.GetDefaultParameterMap(kind))
|
tfilter.SetParameterMap(sitk.GetDefaultParameterMap(kind))
|
||||||
tfilter.Execute()
|
tfilter.Execute()
|
||||||
transform = tfilter.GetTransformParameterMap()[0]
|
transform = tfilter.GetTransformParameterMap()[0]
|
||||||
if kind == 'affine':
|
if kind == "affine":
|
||||||
new.parameters = [float(t) for t in transform['TransformParameters']]
|
new.parameters = [float(t) for t in transform["TransformParameters"]]
|
||||||
new.shape = [float(t) for t in transform['Size']]
|
new.shape = [float(t) for t in transform["Size"]]
|
||||||
new.origin = [float(t) for t in transform['CenterOfRotationPoint']]
|
new.origin = [float(t) for t in transform["CenterOfRotationPoint"]]
|
||||||
elif kind == 'translation':
|
elif kind == "translation":
|
||||||
new.parameters = [1.0, 0.0, 0.0, 1.0] + [float(t) for t in transform['TransformParameters']]
|
new.parameters = [1.0, 0.0, 0.0, 1.0] + [
|
||||||
new.shape = [float(t) for t in transform['Size']]
|
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]
|
new.origin = [(t - 1) / 2 for t in new.shape]
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f'{kind} tranforms not implemented (yet)')
|
raise NotImplementedError(f"{kind} tranforms not implemented (yet)")
|
||||||
new.dparameters = 6 * [np.nan]
|
new.dparameters = 6 * [np.nan]
|
||||||
return new
|
return new
|
||||||
|
|
||||||
@@ -315,18 +381,35 @@ class Transform:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, file):
|
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))
|
return cls.from_dict(yamlload(f))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, d):
|
def from_dict(cls, d):
|
||||||
new = cls()
|
new = cls()
|
||||||
new.origin = None if d['CenterOfRotationPoint'] is None else [float(i) for i in d['CenterOfRotationPoint']]
|
new.origin = (
|
||||||
new.parameters = ((1., 0., 0., 1., 0., 0.) if d['TransformParameters'] is None else
|
None
|
||||||
[float(i) for i in d['TransformParameters']])
|
if d["CenterOfRotationPoint"] is None
|
||||||
new.dparameters = ([(0., 0., 0., 0., 0., 0.) if i is None else float(i) for i in d['dTransformParameters']]
|
else [float(i) for i in d["CenterOfRotationPoint"]]
|
||||||
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.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
|
return new
|
||||||
|
|
||||||
def __mul__(self, other): # TODO: take care of dmatrix
|
def __mul__(self, other): # TODO: take care of dmatrix
|
||||||
@@ -359,9 +442,13 @@ class Transform:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def matrix(self):
|
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]),
|
(*self.parameters[2:4], self.parameters[5]),
|
||||||
(0, 0, 1)))
|
(0, 0, 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@matrix.setter
|
@matrix.setter
|
||||||
def matrix(self, value):
|
def matrix(self, value):
|
||||||
@@ -370,9 +457,13 @@ class Transform:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def dmatrix(self):
|
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]),
|
(*self.dparameters[2:4], self.dparameters[5]),
|
||||||
(0, 0, 0)))
|
(0, 0, 0),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@dmatrix.setter
|
@dmatrix.setter
|
||||||
def dmatrix(self, value):
|
def dmatrix(self, value):
|
||||||
@@ -384,7 +475,7 @@ class Transform:
|
|||||||
if self.transform is not None:
|
if self.transform is not None:
|
||||||
return list(self.transform.GetParameters())
|
return list(self.transform.GetParameters())
|
||||||
else:
|
else:
|
||||||
return [1., 0., 0., 1., 0., 0.]
|
return [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
|
||||||
|
|
||||||
@parameters.setter
|
@parameters.setter
|
||||||
def parameters(self, value):
|
def parameters(self, value):
|
||||||
@@ -420,20 +511,32 @@ class Transform:
|
|||||||
self.shape = shape[:2]
|
self.shape = shape[:2]
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
return {'CenterOfRotationPoint': self.origin, 'Size': self.shape, 'TransformParameters': self.parameters,
|
return {
|
||||||
'dTransformParameters': np.nan_to_num(self.dparameters, nan=1e99).tolist()}
|
"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):
|
def frame(self, im, default=0):
|
||||||
if self.is_unity():
|
if self.is_unity():
|
||||||
return im
|
return im
|
||||||
else:
|
else:
|
||||||
if sitk is None:
|
if sitk is None:
|
||||||
raise ImportError('SimpleElastix is not installed: '
|
raise ImportError(
|
||||||
'https://simpleelastix.readthedocs.io/GettingStarted.html')
|
"SimpleElastix is not installed: "
|
||||||
|
"https://simpleelastix.readthedocs.io/GettingStarted.html"
|
||||||
|
)
|
||||||
dtype = im.dtype
|
dtype = im.dtype
|
||||||
im = im.astype('float')
|
im = im.astype("float")
|
||||||
intp = sitk.sitkBSpline if np.issubdtype(dtype, np.floating) else sitk.sitkNearestNeighbor
|
intp = (
|
||||||
return self.cast_array(sitk.Resample(self.cast_image(im), self.transform, intp, default)).astype(dtype)
|
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):
|
def coords(self, array, columns=None):
|
||||||
"""Transform coordinates in 2 column numpy array,
|
"""Transform coordinates in 2 column numpy array,
|
||||||
@@ -442,21 +545,28 @@ class Transform:
|
|||||||
if self.is_unity():
|
if self.is_unity():
|
||||||
return array.copy()
|
return array.copy()
|
||||||
elif DataFrame is not None and isinstance(array, (DataFrame, Series)):
|
elif DataFrame is not None and isinstance(array, (DataFrame, Series)):
|
||||||
columns = columns or ['x', 'y']
|
columns = columns or ["x", "y"]
|
||||||
array = array.copy()
|
array = array.copy()
|
||||||
if isinstance(array, DataFrame):
|
if isinstance(array, DataFrame):
|
||||||
array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy()))
|
array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy()))
|
||||||
elif isinstance(array, Series):
|
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
|
return array
|
||||||
else: # somehow we need to use the inverse here to get the same effect as when using self.frame
|
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):
|
def save(self, file):
|
||||||
"""save the parameters of the transform calculated
|
"""save the parameters of the transform calculated
|
||||||
with affine_registration to a yaml file
|
with affine_registration to a yaml file
|
||||||
"""
|
"""
|
||||||
if not file[-3:] == 'yml':
|
if not file[-3:] == "yml":
|
||||||
file += '.yml'
|
file += ".yml"
|
||||||
with open(file, 'w') as f:
|
with open(file, "w") as f:
|
||||||
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
|
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
|
/// Wrapper around bioformats java class loci.common.DebugTools
|
||||||
pub struct DebugTools;
|
pub struct DebugTools;
|
||||||
|
|
||||||
@@ -125,8 +137,7 @@ impl ChannelSeparator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>> {
|
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>> {
|
||||||
let bi8 = self.open_bi8(index)?;
|
Ok(transmute_vec(self.open_bi8(index)?))
|
||||||
Ok(unsafe { std::mem::transmute::<Vec<i8>, Vec<u8>>(bi8) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
method!(open_bi8, "openBytes", [index: i32|p] => Vec<i8>|c);
|
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>> {
|
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>> {
|
||||||
let bi8 = self.open_bi8(index)?;
|
Ok(transmute_vec(self.open_bi8(index)?))
|
||||||
Ok(unsafe { std::mem::transmute::<Vec<i8>, Vec<u8>>(bi8) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn ome_xml(&self) -> Result<String> {
|
pub(crate) fn ome_xml(&self) -> Result<String> {
|
||||||
|
|||||||
488
src/lib.rs
488
src/lib.rs
@@ -1,333 +1,23 @@
|
|||||||
mod bioformats;
|
mod bioformats;
|
||||||
|
|
||||||
|
mod axes;
|
||||||
#[cfg(feature = "python")]
|
#[cfg(feature = "python")]
|
||||||
mod py;
|
mod py;
|
||||||
|
mod reader;
|
||||||
use anyhow::{anyhow, Result};
|
mod stats;
|
||||||
use bioformats::{DebugTools, ImageReader, MetadataTools};
|
mod view;
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use crate::stats::MinMax;
|
||||||
|
use ndarray::{Array, Array4, Array5, NewAxis};
|
||||||
use rayon::prelude::*;
|
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> {
|
fn open(file: &str) -> Result<Reader> {
|
||||||
let path = std::env::current_dir()?
|
let path = std::env::current_dir()?
|
||||||
.join("tests")
|
.join("tests")
|
||||||
@@ -413,4 +103,160 @@ mod tests {
|
|||||||
println!("{}", xml);
|
println!("{}", xml);
|
||||||
Ok(())
|
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::bioformats::download_bioformats;
|
||||||
use crate::{Frame, Reader};
|
use crate::reader::{PixelType, Reader};
|
||||||
use numpy::ToPyArray;
|
use crate::view::{Item, View};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use ndarray::{Ix0, IxDyn, SliceInfoElem};
|
||||||
|
use numpy::IntoPyArray;
|
||||||
use pyo3::prelude::*;
|
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;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[pyclass(subclass)]
|
#[pyclass(module = "ndbioimage.ndbioimage_rs")]
|
||||||
#[pyo3(name = "Reader")]
|
struct ViewConstructor;
|
||||||
#[derive(Debug)]
|
|
||||||
struct PyReader {
|
#[pymethods]
|
||||||
reader: Reader,
|
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]
|
#[pymethods]
|
||||||
impl PyReader {
|
impl PyView {
|
||||||
#[new]
|
#[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);
|
let mut path = PathBuf::from(path);
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
for file in path.read_dir()? {
|
for file in path.read_dir()?.flatten() {
|
||||||
if let Ok(f) = file {
|
let p = file.path();
|
||||||
let p = f.path();
|
if file.path().is_file() & (p.extension() == Some("tif".as_ref())) {
|
||||||
if f.path().is_file() & (p.extension() == Some("tif".as_ref())) {
|
|
||||||
path = p;
|
path = p;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Ok(Self {
|
||||||
Ok(PyReader {
|
view: Reader::new(&path, series as i32)?.view().into_dyn(),
|
||||||
reader: Reader::new(&path, series as i32)?,
|
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>(
|
fn get_frame<'py>(
|
||||||
&self,
|
&self,
|
||||||
py: Python<'py>,
|
py: Python<'py>,
|
||||||
c: usize,
|
c: isize,
|
||||||
z: usize,
|
z: isize,
|
||||||
t: usize,
|
t: isize,
|
||||||
) -> PyResult<Bound<'py, PyAny>> {
|
) -> PyResult<Bound<'py, PyAny>> {
|
||||||
Ok(match self.reader.get_frame(c, z, t)? {
|
Ok(match self.dtype {
|
||||||
Frame::INT8(arr) => arr.to_pyarray(py).into_any(),
|
PixelType::I8 => self
|
||||||
Frame::UINT8(arr) => arr.to_pyarray(py).into_any(),
|
.view
|
||||||
Frame::INT16(arr) => arr.to_pyarray(py).into_any(),
|
.get_frame::<i8>(c, z, t)?
|
||||||
Frame::UINT16(arr) => arr.to_pyarray(py).into_any(),
|
.into_pyarray(py)
|
||||||
Frame::INT32(arr) => arr.to_pyarray(py).into_any(),
|
.into_any(),
|
||||||
Frame::UINT32(arr) => arr.to_pyarray(py).into_any(),
|
PixelType::U8 => self
|
||||||
Frame::FLOAT(arr) => arr.to_pyarray(py).into_any(),
|
.view
|
||||||
Frame::DOUBLE(arr) => arr.to_pyarray(py).into_any(),
|
.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> {
|
fn get_ome_xml(&self) -> PyResult<String> {
|
||||||
Ok(self.reader.get_ome_xml()?)
|
Ok(self.view.get_ome_xml()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(&mut self) -> PyResult<()> {
|
/// the file path
|
||||||
self.reader.close()?;
|
#[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(())
|
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> {
|
pub(crate) fn ndbioimage_file() -> anyhow::Result<PathBuf> {
|
||||||
@@ -75,7 +611,8 @@ pub(crate) fn ndbioimage_file() -> anyhow::Result<PathBuf> {
|
|||||||
#[pymodule]
|
#[pymodule]
|
||||||
#[pyo3(name = "ndbioimage_rs")]
|
#[pyo3(name = "ndbioimage_rs")]
|
||||||
fn ndbioimage_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
fn ndbioimage_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
m.add_class::<PyReader>()?;
|
m.add_class::<PyView>()?;
|
||||||
|
m.add_class::<ViewConstructor>()?;
|
||||||
|
|
||||||
#[pyfn(m)]
|
#[pyfn(m)]
|
||||||
#[pyo3(name = "download_bioformats")]
|
#[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
|
from ndbioimage import Imread
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('file',
|
@pytest.mark.parametrize(
|
||||||
[file for file in (Path(__file__).parent / 'files').iterdir() if not file.suffix == '.pzl'])
|
"file",
|
||||||
|
[
|
||||||
|
file
|
||||||
|
for file in (Path(__file__).parent / "files").iterdir()
|
||||||
|
if not file.suffix == ".pzl"
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_open(file):
|
def test_open(file):
|
||||||
with Imread(file) as im:
|
with Imread(file, axes="cztyx") as im:
|
||||||
mean = im[dict(c=0, z=0, t=0)].mean()
|
mean = im[0, 0, 0].mean()
|
||||||
b = pickle.dumps(im)
|
b = pickle.dumps(im)
|
||||||
jm = pickle.loads(b)
|
jm = pickle.loads(b)
|
||||||
assert jm[dict(c=0, z=0, t=0)].mean() == mean
|
assert jm.get_frame(0, 0, 0).mean() == mean
|
||||||
v = im.view()
|
b = pickle.dumps(im)
|
||||||
assert v[dict(c=0, z=0, t=0)].mean() == mean
|
jm = pickle.loads(b)
|
||||||
b = pickle.dumps(v)
|
assert jm[0, 0, 0].mean() == mean
|
||||||
w = pickle.loads(b)
|
|
||||||
assert w[dict(c=0, z=0, t=0)].mean() == mean
|
|
||||||
|
|||||||
@@ -12,19 +12,24 @@ from ndbioimage import Imread
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def array():
|
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()
|
@pytest.fixture()
|
||||||
def image(array):
|
def image(array):
|
||||||
with tempfile.TemporaryDirectory() as folder:
|
with tempfile.TemporaryDirectory() as folder:
|
||||||
file = Path(folder) / "test.tif"
|
file = Path(folder) / "test.tif"
|
||||||
tiffwrite(file, array, 'yxczt')
|
tiffwrite(file, array, "yxczt")
|
||||||
with Imread(file, axes='yxczt') as im:
|
with Imread(file, axes="yxczt") as im:
|
||||||
yield im
|
yield im
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('s', combinations_with_replacement(
|
@pytest.mark.parametrize(
|
||||||
(0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5))
|
"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):
|
def test_slicing(s, image, array):
|
||||||
s_im, s_a = image[s], array[s]
|
s_im, s_a = image[s], array[s]
|
||||||
if isinstance(s_a, Number):
|
if isinstance(s_a, Number):
|
||||||
|
|||||||
@@ -11,21 +11,42 @@ from ndbioimage import Imread
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def array():
|
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()
|
@pytest.fixture()
|
||||||
def image(array):
|
def image(array):
|
||||||
with tempfile.TemporaryDirectory() as folder:
|
with tempfile.TemporaryDirectory() as folder:
|
||||||
file = Path(folder) / "test.tif"
|
file = Path(folder) / "test.tif"
|
||||||
tiffwrite(file, array, 'yxczt')
|
tiffwrite(file, array, "yxczt")
|
||||||
with Imread(file, axes='yxczt') as im:
|
with Imread(file, axes="yxczt") as im:
|
||||||
yield im
|
yield im
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('fun_and_axis', product(
|
@pytest.mark.parametrize(
|
||||||
(np.sum, np.nansum, np.min, np.nanmin, np.max, np.nanmax, np.argmin, np.argmax,
|
"fun_and_axis",
|
||||||
np.mean, np.nanmean, np.var, np.nanvar, np.std, np.nanstd), (None, 0, 1, 2, 3, 4)))
|
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):
|
def test_ufuncs(fun_and_axis, image, array):
|
||||||
fun, axis = fun_and_axis
|
fun, axis = fun_and_axis
|
||||||
assert np.all(np.isclose(fun(image, axis), fun(array, axis))), \
|
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'
|
f"function {fun.__name__} over axis {axis} does not give the correct result"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user