Compare commits
43
Commits
old_python
...
41aa841b14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41aa841b14 | ||
|
|
6041cda475 | ||
|
|
4074770fd3 | ||
|
|
6913e65420 | ||
|
|
5ad9a44ecd | ||
|
|
b950f4a39d | ||
|
|
a0d2359e55 | ||
|
|
e4de76f26c | ||
|
|
9f5f9debc5 | ||
|
|
c975b1943f | ||
|
|
c16e71bf30 | ||
|
|
b063a63869 | ||
|
|
1471ec00af | ||
|
|
0ed70edd3e | ||
|
|
a5ec4125f9 | ||
|
|
0d347b7967 | ||
|
|
25d2070ac3 | ||
|
|
916d301f1b | ||
|
|
0ce1187a43 | ||
|
|
ff7cd562af | ||
|
|
705ca16379 | ||
|
|
d2c725440c | ||
|
|
b1f8dd998b | ||
|
|
8db873c425 | ||
|
|
3c14168878 | ||
|
|
3dc8e6af04 | ||
|
|
24af64ac7e | ||
|
|
5195ccfcb5 | ||
|
|
87e9715f97 | ||
|
|
2247a994be | ||
|
|
e5c6361086 | ||
|
|
83ea9722f6 | ||
|
|
372b816f93 | ||
|
|
fefdd6448b | ||
|
|
a3dfc075a8 | ||
|
|
15eae99272 | ||
|
|
1a8c3f22e5 | ||
|
|
b612d33a35 | ||
|
|
3c22cf743a | ||
|
|
dd5e2d393f | ||
|
|
45aa72d14c | ||
|
|
1aad79b441 | ||
|
|
3db6dc8ee1 |
No files matched your search
@@ -0,0 +1,66 @@
|
|||||||
|
name: Publish
|
||||||
|
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
crates_io_publish:
|
||||||
|
name: Publish (crates.io)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 25
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Restore cache
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo
|
||||||
|
key: cache-ubuntu-cargo-publish
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
run: |-
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
if ! command -v rustc >/dev/null 2>&1; then
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
else
|
||||||
|
rustup update
|
||||||
|
fi
|
||||||
|
cargo install cargo-release
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# allow-branch HEAD is because GitHub actions switches
|
||||||
|
# to the tag while building, which is a detached head
|
||||||
|
|
||||||
|
# Publishing is currently messy, because:
|
||||||
|
#
|
||||||
|
# * `peace_rt_model_core` exports `NativeError` or `WebError` depending on the target.
|
||||||
|
# * `peace_rt_model_web` fails to build when publishing the workspace for a native target.
|
||||||
|
# * `peace_rt_model_web` still needs its dependencies to be published before it can be
|
||||||
|
# published.
|
||||||
|
# * `peace_rt_model_hack` needs `peace_rt_model_web` to be published before it can be
|
||||||
|
# published.
|
||||||
|
#
|
||||||
|
# We *could* pass through `--no-verify` so `cargo` doesn't build the crate before publishing,
|
||||||
|
# which is reasonable, since this job only runs after the Linux, Windows, and WASM builds
|
||||||
|
# have passed.
|
||||||
|
- name: "cargo release publish"
|
||||||
|
run: |-
|
||||||
|
export PATH="$HOME/.osxcross/bin:$PATH"
|
||||||
|
cargo login ${{ secrets.CRATES_IO_API_TOKEN }}
|
||||||
|
cargo release \
|
||||||
|
publish \
|
||||||
|
--workspace \
|
||||||
|
--all-features \
|
||||||
|
--allow-branch "main" \
|
||||||
|
--no-confirm \
|
||||||
|
--no-verify \
|
||||||
|
--execute
|
||||||
|
|
||||||
|
- name: Store cache
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo
|
||||||
|
key: cache-ubuntu-cargo-publish
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pypi_publish:
|
||||||
|
name: Publish (pypi.org)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
|
||||||
|
- name: Restore cache
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/pip
|
||||||
|
~/.cache/pip-wheel
|
||||||
|
~/.cache/sccache
|
||||||
|
~/.cache/cargo-xwin
|
||||||
|
~/.cargo
|
||||||
|
~/.osxcross
|
||||||
|
key: cache-ubuntu-maturin-cross-compile
|
||||||
|
|
||||||
|
- name: Install llvm
|
||||||
|
run: |
|
||||||
|
if ! command -v llvm-dlltool >/dev/null 2>&1; then
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y llvm
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
if ! command -v rustc >/dev/null 2>&1; then
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
else
|
||||||
|
rustup update
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install sccache and maturin
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install maturin ziglang
|
||||||
|
if ! command -v sccache >/dev/null 2>&1; then
|
||||||
|
cargo install sccache || pip install sccache
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install xwin
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
if ! command -v cargo-xwin >/dev/null 2>&1; then
|
||||||
|
cargo install cargo-xwin || pip install cargo-xwin
|
||||||
|
cargo xwin cache xwin
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install osxcross
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.osxcross/bin:$PATH"
|
||||||
|
if ! command -v osxcross >/dev/null 2>&1; then
|
||||||
|
wget ${{ secrets.OSXCROSS_LINK }} -O osxcross.tar.gz
|
||||||
|
tar -xzf osxcross.tar.gz -C ~/
|
||||||
|
mv ~/osxcross ~/.osxcross
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build wheels
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$HOME/.osxcross/bin:$PATH"
|
||||||
|
maturin sdist --out dist
|
||||||
|
rustup default nightly
|
||||||
|
|
||||||
|
rustup target add x86_64-unknown-linux-gnu --toolchain nightly
|
||||||
|
maturin build --release --out dist --target x86_64-unknown-linux-gnu
|
||||||
|
rustup target add aarch64-unknown-linux-gnu --toolchain nightly
|
||||||
|
maturin build --release --out dist --target aarch64-unknown-linux-gnu --zig
|
||||||
|
|
||||||
|
rustup target add x86_64-pc-windows-msvc --toolchain nightly
|
||||||
|
maturin build --release --out dist --target x86_64-pc-windows-msvc
|
||||||
|
rustup target add aarch64-pc-windows-msvc --toolchain nightly
|
||||||
|
maturin build --release --out dist --target aarch64-pc-windows-msvc
|
||||||
|
|
||||||
|
rustup target add x86_64-apple-darwin --toolchain nightly
|
||||||
|
maturin build --release --out dist --target x86_64-apple-darwin --zig
|
||||||
|
rustup target add aarch64-apple-darwin --toolchain nightly
|
||||||
|
maturin build --release --out dist --target aarch64-apple-darwin --zig
|
||||||
|
|
||||||
|
- name: Store cache
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/pip
|
||||||
|
~/.cache/pip-wheel
|
||||||
|
~/.cache/sccache
|
||||||
|
~/.cache/cargo-xwin
|
||||||
|
~/.cargo
|
||||||
|
~/.osxcross
|
||||||
|
key: cache-ubuntu-maturin-cross-compile
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
env:
|
||||||
|
GITHUB_WORKFLOW_REF: 1.10.1
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
name: PyTest
|
|
||||||
|
|
||||||
on: [workflow_call, push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pytest:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
pip install .[test]
|
|
||||||
- name: Test with pytest
|
|
||||||
run: pytest
|
|
||||||
+81
-11
@@ -1,12 +1,82 @@
|
|||||||
._*
|
/target
|
||||||
*.pyc
|
/Cargo.lock
|
||||||
/build/
|
|
||||||
*.egg-info
|
# Byte-compiled / optimized / DLL files
|
||||||
/venv/
|
__pycache__/
|
||||||
.idea
|
.pytest_cache/
|
||||||
/.pytest_cache/
|
*.py[cod]
|
||||||
/ndbioimage/_version.py
|
|
||||||
/ndbioimage/jars
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
bin/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
include/
|
||||||
|
man/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
pip-selfcheck.json
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Mr Developer
|
||||||
|
.mr.developer.cfg
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
|
||||||
|
# Rope
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
/tests/files/*
|
/tests/files/*
|
||||||
/poetry.lock
|
AGENTS.md
|
||||||
/dist/
|
.agentbridge
|
||||||
|
.agent-work
|
||||||
|
|
||||||
|
py/ndbioimage/jassets
|
||||||
|
py/ndbioimage/deps
|
||||||
|
/build_wheels.sh
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
[package]
|
||||||
|
name = "ndbioimage"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.94.0"
|
||||||
|
authors = ["Wim Pomp <w.pomp@nki.nl>"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
description = "Read bio image formats using the bio-formats java package."
|
||||||
|
homepage = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs"
|
||||||
|
repository = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs"
|
||||||
|
documentation = "https://docs.rs/ndbioimage"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["bioformats", "imread", "ndarray", "metadata"]
|
||||||
|
categories = ["multimedia::images", "science"]
|
||||||
|
exclude = ["/tests"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "ndbioimage"
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bioformats = { version = "0.1", optional = true, default-features = false, features = ["zarr", "tissuefaxs"] }
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
console = { version = "0.16", optional = true }
|
||||||
|
downloader = { version = "0.2", optional = true, default-features = false, features = ["rustls-tls"] }
|
||||||
|
ffmpeg-sidecar = { version = "2", optional = true }
|
||||||
|
itertools = "0.15"
|
||||||
|
image-registration = { path = "../image-registration", optional = true }
|
||||||
|
indexmap = { version = "2", features = ["serde"] }
|
||||||
|
indicatif = { version = "0.18", features = ["rayon"], optional = true }
|
||||||
|
j4rs = { version = "0.25", optional = true }
|
||||||
|
libczirw-sys = { version = "0.5", optional = true }
|
||||||
|
ndarray = { version = "0.17", features = ["serde"] }
|
||||||
|
num = "0.4"
|
||||||
|
numpy = { version = "0.29", optional = true }
|
||||||
|
ome-metadata = "0.5"
|
||||||
|
ordered-float = { version = "5", optional = true }
|
||||||
|
phf = { version = "0.14", features = ["macros"] }
|
||||||
|
postcard = { version = "1", features = ["use-std"], optional = true }
|
||||||
|
pyo3 = { version = "0.29", features = ["abi3-py310", "anyhow", "generate-import-lib"], optional = true }
|
||||||
|
pyo3-stub-gen = { version = "0.23", optional = true }
|
||||||
|
rayon = { version = "1", optional = true }
|
||||||
|
regex = "1"
|
||||||
|
serde = { version = "1", features = ["rc", "derive"] }
|
||||||
|
serde_with = "3"
|
||||||
|
strum = { version = "0.28", features = ["derive"] }
|
||||||
|
thiserror = "2"
|
||||||
|
tiff = { version = "0.11", features = ["zstd"], optional = true }
|
||||||
|
tiffwrite = { version = "2026.6.0", optional = true }
|
||||||
|
tokio = { version = "1", features = ["rt", "rt-multi-thread"], optional = true }
|
||||||
|
thread_local = { version = "1", optional = true }
|
||||||
|
xmltree = { version = "0.12", optional = true }
|
||||||
|
yaml_serde = { version = "0.10", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rayon = "1"
|
||||||
|
regex = "1"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
j4rs = "0.25"
|
||||||
|
ffmpeg-sidecar = "2"
|
||||||
|
retry = "2"
|
||||||
|
toml = "1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "movie", "tiffwrite"]
|
||||||
|
all = ["bioformats_java", "bioformats_rust", "czi", "gpl-formats", "movie", "tiffseq", "tiffwrite", "tiff", "transforms"]
|
||||||
|
gpl-formats = []
|
||||||
|
python = ["dep:pyo3", "dep:numpy", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"]
|
||||||
|
czi = ["dep:libczirw-sys", "dep:xmltree", "dep:thread_local"]
|
||||||
|
bioformats_rust = ["dep:bioformats", "dep:thread_local"]
|
||||||
|
bioformats_java = ["dep:j4rs", "dep:thread_local", "dep:downloader"]
|
||||||
|
tiffwrite = ["dep:tiffwrite", "dep:indicatif", "dep:console", "dep:rayon"]
|
||||||
|
tiffseq = ["dep:tiff", "dep:yaml_serde"]
|
||||||
|
tiff = ["dep:tiff", "dep:thread_local"]
|
||||||
|
movie = ["dep:ffmpeg-sidecar", "dep:tokio", "dep:ordered-float", "dep:indicatif", "dep:console"]
|
||||||
|
transforms = ["dep:image-registration", "dep:yaml_serde"]
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
no-default-features = true
|
||||||
|
features = ["bioformats_java", "bioformats_rust", "czi", "movie", "tiffseq", "tiffwrite", "tiff", "transforms"]
|
||||||
|
|
||||||
|
[profile.test]
|
||||||
|
inherits = "release"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
[profile.dev.package.backtrace]
|
||||||
|
opt-level = 3
|
||||||
@@ -1,674 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
|
||||||
or can get the source code. And you must show them these terms so they
|
|
||||||
know their rights.
|
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
|
||||||
that there is no warranty for this free software. For both users' and
|
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
|
||||||
changed, so that their problems will not be attributed erroneously to
|
|
||||||
authors of previous versions.
|
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the special requirements of the GNU Affero General Public License,
|
|
||||||
section 13, concerning interaction through a network will apply to the
|
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
|
||||||
notice like this when it starts in an interactive mode:
|
|
||||||
|
|
||||||
<program> Copyright (C) <year> <name of author>
|
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2015 - 2021 Ulrik Sverdrup "bluss",
|
||||||
|
Jim Turner,
|
||||||
|
and ndarray developers
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
@@ -1,44 +1,53 @@
|
|||||||
[](https://github.com/wimpomp/ndbioimage/actions/workflows/pytest.yml)
|
|
||||||
|
|
||||||
# ndbioimage
|
# ndbioimage
|
||||||
|
|
||||||
Exposes (bio) images as a numpy ndarray-like object, but without loading the whole
|
[](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml)
|
||||||
image into memory, reading from the file only when needed. Some metadata is read
|
|
||||||
|
## Work in progress
|
||||||
|
|
||||||
|
Rust rewrite of python version. Read bio image formats using the bio-formats java package.
|
||||||
|
[https://www.openmicroscopy.org/bio-formats/](https://www.openmicroscopy.org/bio-formats/)
|
||||||
|
|
||||||
|
Exposes (bio) images as a numpy ndarray-like object (Python) or a struct that can be sliced like an ndarray Array
|
||||||
|
(Rust), but without loading the whole image into memory, reading from the file only when needed. Some metadata is read
|
||||||
and stored in an [ome](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2005-6-5-r47) structure.
|
and stored in an [ome](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2005-6-5-r47) structure.
|
||||||
Additionally, it can automatically calculate an affine transform that corrects for chromatic aberrations etc. and apply
|
Additionally, it can automatically calculate an affine transform that corrects for chromatic aberrations etc. and apply
|
||||||
it on the fly to the image.
|
it on the fly to the image.
|
||||||
|
|
||||||
Currently, it supports imagej tif files, czi files, micromanager tif sequences and anything
|
Currently, it supports imagej tif files, czi files, micromanager tif sequences and anything
|
||||||
[bioformats](https://www.openmicroscopy.org/bio-formats/) can handle.
|
[bioformats](https://www.openmicroscopy.org/bio-formats/) can handle.
|
||||||
|
|
||||||
|
To transition to semver, versions before 0.1.0 were yanked from crates.io.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```
|
```sh
|
||||||
pip install ndbioimage
|
pip install ndbioimage
|
||||||
```
|
```
|
||||||
|
|
||||||
### Installation with option to write mp4 or mkv:
|
### Installation with option to write mp4 or mkv:
|
||||||
|
|
||||||
Work in progress! Make sure ffmpeg is installed.
|
Work in progress! Make sure ffmpeg is installed.
|
||||||
|
|
||||||
```
|
```sh
|
||||||
pip install ndbioimage[write]
|
pip install ndbioimage[write]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Python
|
### Python
|
||||||
|
|
||||||
- Reading an image file and plotting the frame at channel=2, time=1
|
- Reading an image file and plotting the frame at channel=2, time=1
|
||||||
|
|
||||||
```
|
```python
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from ndbioimage import Imread
|
from ndbioimage import Imread
|
||||||
with Imread('image_file.tif', axes='ctyx', dtype=int) as im:
|
with Imread('image_file.tif', axes='ctyx', dtype=int) as im:
|
||||||
plt.imshow(im[2, 1])
|
plt.imshow(im[2, 1])
|
||||||
```
|
```
|
||||||
|
|
||||||
- Showing some image metadata
|
- Showing some image metadata
|
||||||
|
|
||||||
```
|
```python
|
||||||
from ndbioimage import Imread
|
from ndbioimage import Imread
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
with Imread('image_file.tif') as im:
|
with Imread('image_file.tif') as im:
|
||||||
@@ -47,7 +56,7 @@ with Imread('image_file.tif') as im:
|
|||||||
|
|
||||||
- Slicing the image without loading the image into memory
|
- Slicing the image without loading the image into memory
|
||||||
|
|
||||||
```
|
```python
|
||||||
from ndbioimage import Imread
|
from ndbioimage import Imread
|
||||||
with Imread('image_file.tif', axes='cztyx') as im:
|
with Imread('image_file.tif', axes='cztyx') as im:
|
||||||
sliced_im = im[1, :, :, 100:200, 100:200]
|
sliced_im = im[1, :, :, 100:200, 100:200]
|
||||||
@@ -55,40 +64,55 @@ with Imread('image_file.tif', axes='cztyx') as im:
|
|||||||
|
|
||||||
sliced_im is an instance of Imread which will load any image data from file only when needed
|
sliced_im is an instance of Imread which will load any image data from file only when needed
|
||||||
|
|
||||||
|
|
||||||
- Converting (part) of the image to a numpy ndarray
|
- Converting (part) of the image to a numpy ndarray
|
||||||
|
|
||||||
```
|
```python
|
||||||
from ndbioimage import Imread
|
from ndbioimage import Imread
|
||||||
import numpy as np
|
import numpy as np
|
||||||
with Imread('image_file.tif', axes='cztyx') as im:
|
with Imread('image_file.tif', axes='cztyx') as im:
|
||||||
array = np.asarray(im[0, 0])
|
array = np.asarray(im[0, 0])
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Rust
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ndarray::Array2;
|
||||||
|
use ndbioimage::{DynReader, Frame, Reader};
|
||||||
|
|
||||||
|
fn main() -> Result<(), ndbioimage::error::Error> {
|
||||||
|
let path = "/path/to/file";
|
||||||
|
let reader = DynReader::new(&path, 0, 0)?;
|
||||||
|
println!("shape: {}", reader.shape());
|
||||||
|
let frame = reader.get_frame(0, 0, 0)?;
|
||||||
|
if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
|
||||||
|
println!("{:?}", arr);
|
||||||
|
} else {
|
||||||
|
println!("could not convert Frame to Array<i8>");
|
||||||
|
}
|
||||||
|
let xml = reader.metadata()?.to_xml()?;
|
||||||
|
println!("{}", xml);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ndbioimage::{DynReader, Reader};
|
||||||
|
|
||||||
|
fn main() -> Result<(), ndbioimage::error::Error> {
|
||||||
|
let path = "/path/to/file";
|
||||||
|
let reader = DynReader::new(&path, 0, 0)?;
|
||||||
|
let view = reader.view();
|
||||||
|
let view = view.max_proj(3)?;
|
||||||
|
let array = view.as_array::<u16>()?;
|
||||||
|
println!("{:?}", array.shape());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Command line
|
### Command line
|
||||||
|
|
||||||
```ndbioimage --help```: show help
|
```ndbioimage --help```: show help
|
||||||
```ndbioimage image```: show metadata about image
|
```ndbioimage info image```: show metadata about image
|
||||||
```ndbioimage image -w {name}.tif -r```: copy image into image.tif (replacing {name} with image), while registering channels
|
```ndbioimage tiff image image.tif -r```: copy image into image.tif, 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 movie image image.mp4 -C cyan lime red``` copy image into image.mp4 (z will be max projected), make channel
|
||||||
|
colors cyan lime and red
|
||||||
## Adding more formats
|
|
||||||
Readers for image formats subclass AbstractReader. When an image reader is imported, Imread will
|
|
||||||
automatically recognize it and use it to open the appropriate file format. Image readers
|
|
||||||
are required to implement the following methods:
|
|
||||||
|
|
||||||
- staticmethod _can_open(path): return True if path can be opened by this reader
|
|
||||||
- \_\_frame__(self, c, z, t): return the frame at channel=c, z-slice=z, time=t from the file
|
|
||||||
|
|
||||||
Optional methods:
|
|
||||||
- get_ome: reads metadata from file and adds them to an OME object imported
|
|
||||||
from the ome-types library
|
|
||||||
- open(self): maybe open some file handle
|
|
||||||
- close(self): close any file handles
|
|
||||||
|
|
||||||
Optional fields:
|
|
||||||
- priority (int): Imread will try readers with a lower number first, default: 99
|
|
||||||
- do_not_pickle (strings): any attributes that should not be included when the object is pickled,
|
|
||||||
for example: any file handles
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
- more image formats
|
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
const BIOFORMATS_VERSION: &str = "8.5.0";
|
||||||
|
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
mod bioformats {
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum BuildError {
|
||||||
|
J4rsVersionNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for BuildError {
|
||||||
|
fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
match self {
|
||||||
|
Self::J4rsVersionNotFound => write!(fmt, "J4rsVersion not found in Cargo.lock"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for BuildError {}
|
||||||
|
|
||||||
|
fn get_j4rs_version() -> Result<String, Box<dyn Error>> {
|
||||||
|
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
|
||||||
|
let lock_path = std::path::Path::new(&manifest_dir).join("Cargo.lock");
|
||||||
|
let lock_toml = std::fs::read_to_string(&lock_path)?;
|
||||||
|
let value: toml::Value = toml::from_str(&lock_toml)?;
|
||||||
|
if let Some(packages) = value.get("package").and_then(|v| v.as_array()) {
|
||||||
|
for package in packages {
|
||||||
|
if let (Some(name), Some(version)) = (
|
||||||
|
package.get("name").and_then(|v| v.as_str()),
|
||||||
|
package.get("version"),
|
||||||
|
) && name == "j4rs"
|
||||||
|
{
|
||||||
|
return Ok(version
|
||||||
|
.to_string()
|
||||||
|
.strip_prefix("\"")
|
||||||
|
.and_then(|v| v.strip_suffix("\""))
|
||||||
|
.ok_or(BuildError::J4rsVersionNotFound)?
|
||||||
|
.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Box::new(BuildError::J4rsVersionNotFound {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build() -> Result<(), Box<dyn Error>> {
|
||||||
|
let j4rs_version = get_j4rs_version()?;
|
||||||
|
let out_dir = std::env::var("OUT_DIR")?;
|
||||||
|
let dest_path = std::path::Path::new(&out_dir).join("constants.rs");
|
||||||
|
let contents = format!(
|
||||||
|
r#"
|
||||||
|
/// Generated by build.rs
|
||||||
|
pub(super) const J4RS_VERSION: &str = "{}";
|
||||||
|
pub(super) const BIOFORMATS_VERSION: &str = "{}";
|
||||||
|
"#,
|
||||||
|
j4rs_version,
|
||||||
|
crate::BIOFORMATS_VERSION,
|
||||||
|
);
|
||||||
|
std::fs::write(&dest_path, contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(feature = "python"), feature = "bioformats_java"))]
|
||||||
|
mod no_python_bioformats {
|
||||||
|
use j4rs::errors::J4RsError;
|
||||||
|
use j4rs::{JvmBuilder, MavenArtifact, MavenArtifactRepo, MavenSettings};
|
||||||
|
use retry::delay::Exponential;
|
||||||
|
use retry::{delay, retry};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum BuildError {
|
||||||
|
BioFormatsNotDownloaded,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for BuildError {
|
||||||
|
fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(fmt, "Bioformats package not downloaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for BuildError {}
|
||||||
|
|
||||||
|
pub(crate) fn build() -> Result<(), Box<dyn Error>> {
|
||||||
|
retry(
|
||||||
|
Exponential::from_millis(1000).map(delay::jitter).take(4),
|
||||||
|
deploy_java_artifacts,
|
||||||
|
)?;
|
||||||
|
let path = default_jassets_path()?;
|
||||||
|
if !path.join("bioformats_package-8.5.0.jar").exists() {
|
||||||
|
Err(BuildError::BioFormatsNotDownloaded)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_jassets_path() -> Result<std::path::PathBuf, J4RsError> {
|
||||||
|
let is_build_script = std::env::var("OUT_DIR").is_ok();
|
||||||
|
|
||||||
|
let mut start_path = if is_build_script {
|
||||||
|
std::path::PathBuf::from(std::env::var("OUT_DIR")?)
|
||||||
|
} else {
|
||||||
|
std::env::current_exe()?
|
||||||
|
};
|
||||||
|
start_path = std::fs::canonicalize(start_path)?;
|
||||||
|
|
||||||
|
while start_path.pop() {
|
||||||
|
for entry in std::fs::read_dir(&start_path)? {
|
||||||
|
let path = entry?.path();
|
||||||
|
if path.file_name().map(|x| x == "jassets").unwrap_or(false) {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(J4RsError::GeneralError(
|
||||||
|
"Can not find jassets directory".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deploy_java_artifacts() -> Result<(), J4RsError> {
|
||||||
|
let jvm = JvmBuilder::new()
|
||||||
|
.skip_setting_native_lib()
|
||||||
|
.with_maven_settings(MavenSettings::new(vec![MavenArtifactRepo::from(
|
||||||
|
"openmicroscopy::https://artifacts.openmicroscopy.org/artifactory/ome.releases",
|
||||||
|
)]))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
jvm.deploy_artifact(&MavenArtifact::from("ome:bioformats_package:8.5.0"))?;
|
||||||
|
|
||||||
|
#[cfg(feature = "gpl-formats")]
|
||||||
|
jvm.deploy_artifact(&MavenArtifact::from("ome:formats-gpl:8.5.0"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "python", feature = "bioformats_java"))]
|
||||||
|
mod python_bioformats {
|
||||||
|
pub(crate) fn build() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let py_src_path = std::env::current_dir()?.join("py").join("ndbioimage");
|
||||||
|
let py_jassets_path = py_src_path.join("jassets");
|
||||||
|
let py_deps_path = py_src_path.join("deps");
|
||||||
|
if py_jassets_path.exists() {
|
||||||
|
std::fs::remove_dir_all(&py_jassets_path)?;
|
||||||
|
}
|
||||||
|
if py_deps_path.exists() {
|
||||||
|
std::fs::remove_dir_all(&py_deps_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
j4rs::Jvm::copy_j4rs_libs_under(py_src_path.to_str().unwrap())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("cargo::rerun-if-changed=build.rs");
|
||||||
|
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
bioformats::build()?;
|
||||||
|
|
||||||
|
if std::env::var("DOCS_RS").is_err() {
|
||||||
|
#[cfg(feature = "movie")]
|
||||||
|
ffmpeg_sidecar::download::auto_download()?;
|
||||||
|
|
||||||
|
#[cfg(all(not(feature = "python"), feature = "bioformats_java"))]
|
||||||
|
no_python_bioformats::build()?;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "python", feature = "bioformats_java"))]
|
||||||
|
python_bioformats::build()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,77 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from urllib import request
|
|
||||||
|
|
||||||
|
|
||||||
class JVMException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
class JVM:
|
|
||||||
""" There can be only one java virtual machine per python process,
|
|
||||||
so this is a singleton class to manage the jvm.
|
|
||||||
"""
|
|
||||||
_instance = None
|
|
||||||
vm_started = False
|
|
||||||
vm_killed = False
|
|
||||||
success = True
|
|
||||||
|
|
||||||
def __new__(cls, *args):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = object.__new__(cls)
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self, jars=None):
|
|
||||||
if not self.vm_started and not self.vm_killed:
|
|
||||||
try:
|
|
||||||
jar_path = Path(__file__).parent / 'jars'
|
|
||||||
if jars is None:
|
|
||||||
jars = {}
|
|
||||||
for jar, src in jars.items():
|
|
||||||
if not (jar_path / jar).exists():
|
|
||||||
JVM.download(src, jar_path / jar)
|
|
||||||
classpath = [str(jar_path / jar) for jar in jars.keys()]
|
|
||||||
|
|
||||||
import jpype
|
|
||||||
jpype.startJVM(classpath=classpath)
|
|
||||||
except Exception: # noqa
|
|
||||||
self.vm_started = False
|
|
||||||
else:
|
|
||||||
self.vm_started = True
|
|
||||||
try:
|
|
||||||
import jpype.imports
|
|
||||||
from loci.common import DebugTools # noqa
|
|
||||||
from loci.formats import ChannelSeparator # noqa
|
|
||||||
from loci.formats import FormatTools # noqa
|
|
||||||
from loci.formats import ImageReader # noqa
|
|
||||||
from loci.formats import MetadataTools # noqa
|
|
||||||
|
|
||||||
DebugTools.setRootLevel("ERROR")
|
|
||||||
|
|
||||||
self.image_reader = ImageReader
|
|
||||||
self.channel_separator = ChannelSeparator
|
|
||||||
self.format_tools = FormatTools
|
|
||||||
self.metadata_tools = MetadataTools
|
|
||||||
except Exception: # noqa
|
|
||||||
pass
|
|
||||||
|
|
||||||
if self.vm_killed:
|
|
||||||
raise Exception('The JVM was killed before, and cannot be restarted in this Python process.')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def download(src, dest):
|
|
||||||
print(f'Downloading {dest.name} to {dest}.')
|
|
||||||
dest.parent.mkdir(exist_ok=True)
|
|
||||||
dest.write_bytes(request.urlopen(src).read())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def kill_vm(cls):
|
|
||||||
self = cls._instance
|
|
||||||
if self is not None and self.vm_started and not self.vm_killed:
|
|
||||||
import jpype
|
|
||||||
jpype.shutdownJVM() # noqa
|
|
||||||
self.vm_started = False
|
|
||||||
self.vm_killed = True
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
JVM = None
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
__all__ = 'bfread', 'cziread', 'fijiread', 'ndread', 'seqread', 'tifread', 'metaseriesread'
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import multiprocessing
|
|
||||||
from abc import ABC
|
|
||||||
from multiprocessing import queues
|
|
||||||
from pathlib import Path
|
|
||||||
from traceback import format_exc
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from .. import JVM, AbstractReader, JVMException
|
|
||||||
|
|
||||||
jars = {'bioformats_package.jar': 'https://downloads.openmicroscopy.org/bio-formats/latest/artifacts/'
|
|
||||||
'bioformats_package.jar'}
|
|
||||||
|
|
||||||
|
|
||||||
class JVMReader:
|
|
||||||
def __init__(self, path: Path, series: int) -> None:
|
|
||||||
mp = multiprocessing.get_context('spawn')
|
|
||||||
self.path = path
|
|
||||||
self.series = series
|
|
||||||
self.queue_in = mp.Queue()
|
|
||||||
self.queue_out = mp.Queue()
|
|
||||||
self.done = mp.Event()
|
|
||||||
self.process = mp.Process(target=self.run)
|
|
||||||
self.process.start()
|
|
||||||
status, message = self.queue_out.get()
|
|
||||||
if status == 'status' and message == 'started':
|
|
||||||
self.is_alive = True
|
|
||||||
else:
|
|
||||||
raise JVMException(message)
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
if self.is_alive:
|
|
||||||
self.done.set()
|
|
||||||
while not self.queue_in.empty():
|
|
||||||
self.queue_in.get()
|
|
||||||
self.queue_in.close()
|
|
||||||
self.queue_in.join_thread()
|
|
||||||
while not self.queue_out.empty():
|
|
||||||
print(self.queue_out.get())
|
|
||||||
self.queue_out.close()
|
|
||||||
self.process.join()
|
|
||||||
self.process.close()
|
|
||||||
self.is_alive = False
|
|
||||||
|
|
||||||
def frame(self, c: int, z: int, t: int) -> np.ndarray:
|
|
||||||
self.queue_in.put((c, z, t))
|
|
||||||
status, message = self.queue_out.get()
|
|
||||||
if status == 'frame':
|
|
||||||
return message
|
|
||||||
else:
|
|
||||||
raise JVMException(message)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
""" Read planes from the image reader file.
|
|
||||||
adapted from python-bioformats/bioformats/formatreader.py
|
|
||||||
"""
|
|
||||||
jvm = None
|
|
||||||
try:
|
|
||||||
jvm = JVM(jars)
|
|
||||||
reader = jvm.image_reader()
|
|
||||||
ome_meta = jvm.metadata_tools.createOMEXMLMetadata()
|
|
||||||
reader.setMetadataStore(ome_meta)
|
|
||||||
reader.setId(str(self.path))
|
|
||||||
reader.setSeries(self.series)
|
|
||||||
|
|
||||||
open_bytes_func = reader.openBytes
|
|
||||||
width, height = int(reader.getSizeX()), int(reader.getSizeY())
|
|
||||||
|
|
||||||
pixel_type = reader.getPixelType()
|
|
||||||
little_endian = reader.isLittleEndian()
|
|
||||||
|
|
||||||
if pixel_type == jvm.format_tools.INT8:
|
|
||||||
dtype = np.int8
|
|
||||||
elif pixel_type == jvm.format_tools.UINT8:
|
|
||||||
dtype = np.uint8
|
|
||||||
elif pixel_type == jvm.format_tools.UINT16:
|
|
||||||
dtype = '<u2' if little_endian else '>u2'
|
|
||||||
elif pixel_type == jvm.format_tools.INT16:
|
|
||||||
dtype = '<i2' if little_endian else '>i2'
|
|
||||||
elif pixel_type == jvm.format_tools.UINT32:
|
|
||||||
dtype = '<u4' if little_endian else '>u4'
|
|
||||||
elif pixel_type == jvm.format_tools.INT32:
|
|
||||||
dtype = '<i4' if little_endian else '>i4'
|
|
||||||
elif pixel_type == jvm.format_tools.FLOAT:
|
|
||||||
dtype = '<f4' if little_endian else '>f4'
|
|
||||||
elif pixel_type == jvm.format_tools.DOUBLE:
|
|
||||||
dtype = '<f8' if little_endian else '>f8'
|
|
||||||
else:
|
|
||||||
dtype = None
|
|
||||||
self.queue_out.put(('status', 'started'))
|
|
||||||
|
|
||||||
while not self.done.is_set():
|
|
||||||
try:
|
|
||||||
c, z, t = self.queue_in.get(True, 0.02)
|
|
||||||
if reader.isRGB() and reader.isInterleaved():
|
|
||||||
index = reader.getIndex(z, 0, t)
|
|
||||||
image = np.frombuffer(open_bytes_func(index), dtype)
|
|
||||||
image.shape = (height, width, reader.getSizeC())
|
|
||||||
if image.shape[2] > 3:
|
|
||||||
image = image[:, :, :3]
|
|
||||||
elif c is not None and reader.getRGBChannelCount() == 1:
|
|
||||||
index = reader.getIndex(z, c, t)
|
|
||||||
image = np.frombuffer(open_bytes_func(index), dtype)
|
|
||||||
image.shape = (height, width)
|
|
||||||
elif reader.getRGBChannelCount() > 1:
|
|
||||||
n_planes = reader.getRGBChannelCount()
|
|
||||||
rdr = jvm.channel_separator(reader)
|
|
||||||
planes = [np.frombuffer(rdr.openBytes(rdr.getIndex(z, i, t)), dtype) for i in range(n_planes)]
|
|
||||||
if len(planes) > 3:
|
|
||||||
planes = planes[:3]
|
|
||||||
elif len(planes) < 3:
|
|
||||||
# > 1 and < 3 means must be 2
|
|
||||||
# see issue #775
|
|
||||||
planes.append(np.zeros(planes[0].shape, planes[0].dtype))
|
|
||||||
image = np.dstack(planes)
|
|
||||||
image.shape = (height, width, 3)
|
|
||||||
del rdr
|
|
||||||
elif reader.getSizeC() > 1:
|
|
||||||
images = [np.frombuffer(open_bytes_func(reader.getIndex(z, i, t)), dtype)
|
|
||||||
for i in range(reader.getSizeC())]
|
|
||||||
image = np.dstack(images)
|
|
||||||
image.shape = (height, width, reader.getSizeC())
|
|
||||||
# if not channel_names is None:
|
|
||||||
# metadata = MetadataRetrieve(self.metadata)
|
|
||||||
# for i in range(self.reader.getSizeC()):
|
|
||||||
# index = self.reader.getIndex(z, 0, t)
|
|
||||||
# channel_name = metadata.getChannelName(index, i)
|
|
||||||
# if channel_name is None:
|
|
||||||
# channel_name = metadata.getChannelID(index, i)
|
|
||||||
# channel_names.append(channel_name)
|
|
||||||
elif reader.isIndexed():
|
|
||||||
#
|
|
||||||
# The image data is indexes into a color lookup-table
|
|
||||||
# But sometimes the table is the identity table and just generates
|
|
||||||
# a monochrome RGB image
|
|
||||||
#
|
|
||||||
index = reader.getIndex(z, 0, t)
|
|
||||||
image = np.frombuffer(open_bytes_func(index), dtype)
|
|
||||||
if pixel_type in (jvm.format_tools.INT16, jvm.format_tools.UINT16):
|
|
||||||
lut = reader.get16BitLookupTable()
|
|
||||||
if lut is not None:
|
|
||||||
lut = np.array(lut)
|
|
||||||
# lut = np.array(
|
|
||||||
# [env.get_short_array_elements(d)
|
|
||||||
# for d in env.get_object_array_elements(lut)]) \
|
|
||||||
# .transpose()
|
|
||||||
else:
|
|
||||||
lut = reader.get8BitLookupTable()
|
|
||||||
if lut is not None:
|
|
||||||
lut = np.array(lut)
|
|
||||||
# lut = np.array(
|
|
||||||
# [env.get_byte_array_elements(d)
|
|
||||||
# for d in env.get_object_array_elements(lut)]) \
|
|
||||||
# .transpose()
|
|
||||||
image.shape = (height, width)
|
|
||||||
if (lut is not None) and not np.all(lut == np.arange(lut.shape[0])[:, np.newaxis]):
|
|
||||||
image = lut[image, :]
|
|
||||||
else:
|
|
||||||
index = reader.getIndex(z, 0, t)
|
|
||||||
image = np.frombuffer(open_bytes_func(index), dtype)
|
|
||||||
image.shape = (height, width)
|
|
||||||
|
|
||||||
if image.ndim == 3:
|
|
||||||
self.queue_out.put(('frame', image[..., c]))
|
|
||||||
else:
|
|
||||||
self.queue_out.put(('frame', image))
|
|
||||||
except queues.Empty: # noqa
|
|
||||||
continue
|
|
||||||
except (Exception,):
|
|
||||||
self.queue_out.put(('error', format_exc()))
|
|
||||||
finally:
|
|
||||||
if jvm is not None:
|
|
||||||
jvm.kill_vm()
|
|
||||||
|
|
||||||
|
|
||||||
def can_open(path: Path) -> bool:
|
|
||||||
try:
|
|
||||||
jvm = JVM(jars)
|
|
||||||
reader = jvm.image_reader()
|
|
||||||
reader.getFormat(str(path))
|
|
||||||
return True
|
|
||||||
except (Exception,):
|
|
||||||
return False
|
|
||||||
finally:
|
|
||||||
jvm.kill_vm() # noqa
|
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
|
||||||
""" This class is used as a last resort, when we don't have another way to open the file. We don't like it
|
|
||||||
because it requires the java vm.
|
|
||||||
"""
|
|
||||||
priority = 99 # panic and open with BioFormats
|
|
||||||
do_not_pickle = 'reader', 'key', 'jvm'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _can_open(path: Path) -> bool:
|
|
||||||
""" Use java BioFormats to make an ome metadata structure. """
|
|
||||||
with multiprocessing.get_context('spawn').Pool(1) as pool:
|
|
||||||
return pool.map(can_open, (path,))[0]
|
|
||||||
|
|
||||||
def open(self) -> None:
|
|
||||||
self.reader = JVMReader(self.path, self.series)
|
|
||||||
|
|
||||||
def __frame__(self, c: int, z: int, t: int) -> np.ndarray:
|
|
||||||
return self.reader.frame(c, z, t)
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self.reader.close()
|
|
||||||
@@ -1,606 +0,0 @@
|
|||||||
import re
|
|
||||||
import warnings
|
|
||||||
from abc import ABC
|
|
||||||
from functools import cached_property
|
|
||||||
from io import BytesIO
|
|
||||||
from itertools import product
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Callable, Optional, TypeVar
|
|
||||||
|
|
||||||
import czifile
|
|
||||||
import imagecodecs
|
|
||||||
import numpy as np
|
|
||||||
from lxml import etree
|
|
||||||
from ome_types import OME, model
|
|
||||||
from tifffile import repeat_nd
|
|
||||||
|
|
||||||
from .. import AbstractReader
|
|
||||||
|
|
||||||
try:
|
|
||||||
# TODO: use zoom from imagecodecs implementation when available
|
|
||||||
from scipy.ndimage.interpolation import zoom
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
from ndimage.interpolation import zoom
|
|
||||||
except ImportError:
|
|
||||||
zoom = None
|
|
||||||
|
|
||||||
|
|
||||||
Element = TypeVar('Element')
|
|
||||||
|
|
||||||
|
|
||||||
def zstd_decode(data: bytes) -> bytes: # noqa
|
|
||||||
""" decode zstd bytes, copied from BioFormats ZeissCZIReader """
|
|
||||||
def read_var_int(stream: BytesIO) -> int: # noqa
|
|
||||||
a = stream.read(1)[0]
|
|
||||||
if a & 128:
|
|
||||||
b = stream.read(1)[0]
|
|
||||||
if b & 128:
|
|
||||||
c = stream.read(1)[0]
|
|
||||||
return (c << 14) | ((b & 127) << 7) | (a & 127)
|
|
||||||
return (b << 7) | (a & 127)
|
|
||||||
return a & 255
|
|
||||||
|
|
||||||
try:
|
|
||||||
with BytesIO(data) as stream:
|
|
||||||
size_of_header = read_var_int(stream)
|
|
||||||
high_low_unpacking = False
|
|
||||||
while stream.tell() < size_of_header:
|
|
||||||
chunk_id = read_var_int(stream)
|
|
||||||
# only one chunk ID defined so far
|
|
||||||
if chunk_id == 1:
|
|
||||||
high_low_unpacking = (stream.read(1)[0] & 1) == 1
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Invalid chunk id: {chunk_id}')
|
|
||||||
pointer = stream.tell()
|
|
||||||
except Exception: # noqa
|
|
||||||
high_low_unpacking = False
|
|
||||||
pointer = 0
|
|
||||||
|
|
||||||
decoded = imagecodecs.zstd_decode(data[pointer:])
|
|
||||||
if high_low_unpacking:
|
|
||||||
second_half = len(decoded) // 2
|
|
||||||
return bytes([decoded[second_half + i // 2] if i % 2 else decoded[i // 2] for i in range(len(decoded))])
|
|
||||||
else:
|
|
||||||
return decoded
|
|
||||||
|
|
||||||
|
|
||||||
def data(self, raw: bool = False, resize: bool = True, order: int = 0) -> np.ndarray:
|
|
||||||
"""Read image data from file and return as numpy array."""
|
|
||||||
DECOMPRESS = czifile.czifile.DECOMPRESS # noqa
|
|
||||||
DECOMPRESS[5] = imagecodecs.zstd_decode
|
|
||||||
DECOMPRESS[6] = zstd_decode
|
|
||||||
|
|
||||||
de = self.directory_entry
|
|
||||||
fh = self._fh
|
|
||||||
if raw:
|
|
||||||
with fh.lock:
|
|
||||||
fh.seek(self.data_offset)
|
|
||||||
data = fh.read(self.data_size) # noqa
|
|
||||||
return data
|
|
||||||
if de.compression:
|
|
||||||
# if de.compression not in DECOMPRESS:
|
|
||||||
# raise ValueError('compression unknown or not supported')
|
|
||||||
with fh.lock:
|
|
||||||
fh.seek(self.data_offset)
|
|
||||||
data = fh.read(self.data_size) # noqa
|
|
||||||
data = DECOMPRESS[de.compression](data) # noqa
|
|
||||||
if de.compression == 2:
|
|
||||||
# LZW
|
|
||||||
data = np.fromstring(data, de.dtype) # noqa
|
|
||||||
elif de.compression in (5, 6):
|
|
||||||
# ZSTD
|
|
||||||
data = np.frombuffer(data, de.dtype) # noqa
|
|
||||||
else:
|
|
||||||
dtype = np.dtype(de.dtype)
|
|
||||||
with fh.lock:
|
|
||||||
fh.seek(self.data_offset)
|
|
||||||
data = fh.read_array(dtype, self.data_size // dtype.itemsize) # noqa
|
|
||||||
|
|
||||||
data = data.reshape(de.stored_shape) # noqa
|
|
||||||
if de.compression != 4 and de.stored_shape[-1] in (3, 4):
|
|
||||||
if de.stored_shape[-1] == 3:
|
|
||||||
# BGR -> RGB
|
|
||||||
data = data[..., ::-1] # noqa
|
|
||||||
else:
|
|
||||||
# BGRA -> RGBA
|
|
||||||
tmp = data[..., 0].copy()
|
|
||||||
data[..., 0] = data[..., 2]
|
|
||||||
data[..., 2] = tmp
|
|
||||||
if de.stored_shape == de.shape or not resize:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# sub / supersampling
|
|
||||||
factors = [j / i for i, j in zip(de.stored_shape, de.shape)]
|
|
||||||
factors = [(int(round(f)) if abs(f - round(f)) < 0.0001 else f)
|
|
||||||
for f in factors]
|
|
||||||
|
|
||||||
# use repeat if possible
|
|
||||||
if order == 0 and all(isinstance(f, int) for f in factors):
|
|
||||||
data = repeat_nd(data, factors).copy() # noqa
|
|
||||||
data.shape = de.shape
|
|
||||||
return data
|
|
||||||
|
|
||||||
# remove leading dimensions with size 1 for speed
|
|
||||||
shape = list(de.stored_shape)
|
|
||||||
i = 0
|
|
||||||
for s in shape:
|
|
||||||
if s != 1:
|
|
||||||
break
|
|
||||||
i += 1
|
|
||||||
shape = shape[i:]
|
|
||||||
factors = factors[i:]
|
|
||||||
data.shape = shape
|
|
||||||
|
|
||||||
# resize RGB components separately for speed
|
|
||||||
if zoom is None:
|
|
||||||
raise ImportError("cannot import 'zoom' from scipy or ndimage")
|
|
||||||
if shape[-1] in (3, 4) and factors[-1] == 1.0:
|
|
||||||
factors = factors[:-1]
|
|
||||||
old = data
|
|
||||||
data = np.empty(de.shape, de.dtype[-2:]) # noqa
|
|
||||||
for i in range(shape[-1]):
|
|
||||||
data[..., i] = zoom(old[..., i], zoom=factors, order=order)
|
|
||||||
else:
|
|
||||||
data = zoom(data, zoom=factors, order=order) # noqa
|
|
||||||
|
|
||||||
data.shape = de.shape
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# monkeypatch zstd into czifile
|
|
||||||
czifile.czifile.SubBlockSegment.data = data
|
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
|
||||||
priority = 0
|
|
||||||
do_not_pickle = 'reader', 'filedict'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _can_open(path: Path) -> bool:
|
|
||||||
return isinstance(path, Path) and path.suffix == '.czi'
|
|
||||||
|
|
||||||
def open(self) -> None:
|
|
||||||
self.reader = czifile.CziFile(self.path)
|
|
||||||
filedict = {}
|
|
||||||
for directory_entry in self.reader.filtered_subblock_directory:
|
|
||||||
idx = self.get_index(directory_entry, self.reader.start)
|
|
||||||
if 'S' not in self.reader.axes or self.series in range(*idx[self.reader.axes.index('S')]):
|
|
||||||
for c in range(*idx[self.reader.axes.index('C')]):
|
|
||||||
for z in range(*idx[self.reader.axes.index('Z')]):
|
|
||||||
for t in range(*idx[self.reader.axes.index('T')]):
|
|
||||||
if (c, z, t) in filedict:
|
|
||||||
filedict[c, z, t].append(directory_entry)
|
|
||||||
else:
|
|
||||||
filedict[c, z, t] = [directory_entry]
|
|
||||||
if len(filedict) == 0:
|
|
||||||
raise FileNotFoundError(f'Series {self.series} not found in {self.path}.')
|
|
||||||
self.filedict = filedict # noqa
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self.reader.close()
|
|
||||||
|
|
||||||
def get_ome(self) -> OME:
|
|
||||||
return OmeParse.get_ome(self.reader, self.filedict)
|
|
||||||
|
|
||||||
def __frame__(self, c: int = 0, z: int = 0, t: int = 0) -> np.ndarray:
|
|
||||||
f = np.zeros(self.base_shape['yx'], self.dtype)
|
|
||||||
if (c, z, t) in self.filedict:
|
|
||||||
directory_entries = self.filedict[c, z, t]
|
|
||||||
x_min = min([f.start[f.axes.index('X')] for f in directory_entries])
|
|
||||||
y_min = min([f.start[f.axes.index('Y')] for f in directory_entries])
|
|
||||||
xy_min = {'X': x_min, 'Y': y_min}
|
|
||||||
for directory_entry in directory_entries:
|
|
||||||
subblock = directory_entry.data_segment()
|
|
||||||
tile = subblock.data(resize=True, order=0)
|
|
||||||
axes_min = [xy_min.get(ax, 0) for ax in directory_entry.axes]
|
|
||||||
index = [slice(i - j - m, i - j + k)
|
|
||||||
for i, j, k, m in zip(directory_entry.start, self.reader.start, tile.shape, axes_min)]
|
|
||||||
index = tuple(index[self.reader.axes.index(i)] for i in 'YX')
|
|
||||||
f[index] = tile.squeeze()
|
|
||||||
return f
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_index(directory_entry: czifile.DirectoryEntryDV, start: tuple[int]) -> list[tuple[int, int]]:
|
|
||||||
return [(i - j, i - j + k) for i, j, k in zip(directory_entry.start, start, directory_entry.shape)]
|
|
||||||
|
|
||||||
|
|
||||||
class OmeParse:
|
|
||||||
size_x: int
|
|
||||||
size_y: int
|
|
||||||
size_c: int
|
|
||||||
size_z: int
|
|
||||||
size_t: int
|
|
||||||
|
|
||||||
nm = model.UnitsLength.NANOMETER
|
|
||||||
um = model.UnitsLength.MICROMETER
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_ome(cls, reader: czifile.CziFile, filedict: dict[tuple[int, int, int], Any]) -> OME:
|
|
||||||
new = cls(reader, filedict)
|
|
||||||
new.parse()
|
|
||||||
return new.ome
|
|
||||||
|
|
||||||
def __init__(self, reader: czifile.CziFile, filedict: dict[tuple[int, int, int], Any]) -> None:
|
|
||||||
self.reader = reader
|
|
||||||
self.filedict = filedict
|
|
||||||
xml = reader.metadata()
|
|
||||||
self.attachments = {i.attachment_entry.name: i.attachment_entry.data_segment()
|
|
||||||
for i in reader.attachments()}
|
|
||||||
self.tree = etree.fromstring(xml)
|
|
||||||
self.metadata = self.tree.find('Metadata')
|
|
||||||
version = self.metadata.find('Version')
|
|
||||||
if version is not None:
|
|
||||||
self.version = version.text
|
|
||||||
else:
|
|
||||||
self.version = self.metadata.find('Experiment').attrib['Version']
|
|
||||||
|
|
||||||
self.ome = OME()
|
|
||||||
self.information = self.metadata.find('Information')
|
|
||||||
self.display_setting = self.metadata.find('DisplaySetting')
|
|
||||||
self.experiment = self.metadata.find('Experiment')
|
|
||||||
self.acquisition_block = self.experiment.find('ExperimentBlocks').find('AcquisitionBlock')
|
|
||||||
self.instrument = self.information.find('Instrument')
|
|
||||||
self.image = self.information.find('Image')
|
|
||||||
|
|
||||||
if self.version == '1.0':
|
|
||||||
self.experiment = self.metadata.find('Experiment')
|
|
||||||
self.acquisition_block = self.experiment.find('ExperimentBlocks').find('AcquisitionBlock')
|
|
||||||
self.multi_track_setup = self.acquisition_block.find('MultiTrackSetup')
|
|
||||||
else:
|
|
||||||
self.experiment = None
|
|
||||||
self.acquisition_block = None
|
|
||||||
self.multi_track_setup = None
|
|
||||||
|
|
||||||
def parse(self) -> None:
|
|
||||||
self.get_experimenters()
|
|
||||||
self.get_instruments()
|
|
||||||
self.get_detectors()
|
|
||||||
self.get_objectives()
|
|
||||||
self.get_tubelenses()
|
|
||||||
self.get_light_sources()
|
|
||||||
self.get_filters()
|
|
||||||
self.get_pixels()
|
|
||||||
self.get_channels()
|
|
||||||
self.get_planes()
|
|
||||||
self.get_annotations()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def text(item: Optional[Element], default: str = "") -> str:
|
|
||||||
return default if item is None else item.text
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def def_list(item: Any) -> list[Any]:
|
|
||||||
return [] if item is None else item
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def try_default(fun: Callable[[Any, ...], Any] | type, default: Any = None, *args: Any, **kwargs: Any) -> Any:
|
|
||||||
try:
|
|
||||||
return fun(*args, **kwargs)
|
|
||||||
except Exception: # noqa
|
|
||||||
return default
|
|
||||||
|
|
||||||
def get_experimenters(self) -> None:
|
|
||||||
if self.version == '1.0':
|
|
||||||
self.ome.experimenters = [
|
|
||||||
model.Experimenter(id='Experimenter:0',
|
|
||||||
user_name=self.information.find('User').find('DisplayName').text)]
|
|
||||||
elif self.version in ('1.1', '1.2'):
|
|
||||||
self.ome.experimenters = [
|
|
||||||
model.Experimenter(id='Experimenter:0',
|
|
||||||
user_name=self.information.find('Document').find('UserName').text)]
|
|
||||||
|
|
||||||
def get_instruments(self) -> None:
|
|
||||||
if self.version == '1.0':
|
|
||||||
self.ome.instruments.append(model.Instrument(id=self.instrument.attrib['Id']))
|
|
||||||
elif self.version in ('1.1', '1.2'):
|
|
||||||
for _ in self.instrument.find('Microscopes'):
|
|
||||||
self.ome.instruments.append(model.Instrument(id='Instrument:0'))
|
|
||||||
|
|
||||||
def get_detectors(self) -> None:
|
|
||||||
if self.version == '1.0':
|
|
||||||
for detector in self.instrument.find('Detectors'):
|
|
||||||
try:
|
|
||||||
detector_type = model.Detector_Type(self.text(detector.find('Type')).upper() or "")
|
|
||||||
except ValueError:
|
|
||||||
detector_type = model.Detector_Type.OTHER
|
|
||||||
|
|
||||||
self.ome.instruments[0].detectors.append(
|
|
||||||
model.Detector(
|
|
||||||
id=detector.attrib['Id'], model=self.text(detector.find('Manufacturer').find('Model')),
|
|
||||||
amplification_gain=float(self.text(detector.find('AmplificationGain'))),
|
|
||||||
gain=float(self.text(detector.find('Gain'))), zoom=float(self.text(detector.find('Zoom'))),
|
|
||||||
type=detector_type
|
|
||||||
))
|
|
||||||
elif self.version in ('1.1', '1.2'):
|
|
||||||
for detector in self.instrument.find('Detectors'):
|
|
||||||
try:
|
|
||||||
detector_type = model.Detector_Type(self.text(detector.find('Type')).upper() or "")
|
|
||||||
except ValueError:
|
|
||||||
detector_type = model.Detector_Type.OTHER
|
|
||||||
|
|
||||||
self.ome.instruments[0].detectors.append(
|
|
||||||
model.Detector(
|
|
||||||
id=detector.attrib['Id'].replace(' ', ''),
|
|
||||||
model=self.text(detector.find('Manufacturer').find('Model')),
|
|
||||||
type=detector_type
|
|
||||||
))
|
|
||||||
|
|
||||||
def get_objectives(self) -> None:
|
|
||||||
for objective in self.instrument.find('Objectives'):
|
|
||||||
self.ome.instruments[0].objectives.append(
|
|
||||||
model.Objective(
|
|
||||||
id=objective.attrib['Id'],
|
|
||||||
model=self.text(objective.find('Manufacturer').find('Model')),
|
|
||||||
immersion=self.text(objective.find('Immersion')), # type: ignore
|
|
||||||
lens_na=float(self.text(objective.find('LensNA'))),
|
|
||||||
nominal_magnification=float(self.text(objective.find('NominalMagnification')))))
|
|
||||||
|
|
||||||
def get_tubelenses(self) -> None:
|
|
||||||
if self.version == '1.0':
|
|
||||||
for idx, tube_lens in enumerate({self.text(track_setup.find('TubeLensPosition'))
|
|
||||||
for track_setup in self.multi_track_setup}):
|
|
||||||
try:
|
|
||||||
nominal_magnification = float(re.findall(r'\d+[,.]\d*', tube_lens)[0].replace(',', '.'))
|
|
||||||
except Exception: # noqa
|
|
||||||
nominal_magnification = 1.0
|
|
||||||
|
|
||||||
self.ome.instruments[0].objectives.append(
|
|
||||||
model.Objective(id=f'Objective:Tubelens:{idx}', model=tube_lens,
|
|
||||||
nominal_magnification=nominal_magnification))
|
|
||||||
elif self.version in ('1.1', '1.2'):
|
|
||||||
for tubelens in self.def_list(self.instrument.find('TubeLenses')):
|
|
||||||
try:
|
|
||||||
nominal_magnification = float(re.findall(r'\d+(?:[,.]\d*)?',
|
|
||||||
tubelens.attrib['Name'])[0].replace(',', '.'))
|
|
||||||
except Exception: # noqa
|
|
||||||
nominal_magnification = 1.0
|
|
||||||
|
|
||||||
self.ome.instruments[0].objectives.append(
|
|
||||||
model.Objective(
|
|
||||||
id=f"Objective:{tubelens.attrib['Id']}",
|
|
||||||
model=tubelens.attrib['Name'],
|
|
||||||
nominal_magnification=nominal_magnification))
|
|
||||||
|
|
||||||
def get_light_sources(self) -> None:
|
|
||||||
if self.version == '1.0':
|
|
||||||
for light_source in self.def_list(self.instrument.find('LightSources')):
|
|
||||||
try:
|
|
||||||
if light_source.find('LightSourceType').find('Laser') is not None:
|
|
||||||
self.ome.instruments[0].lasers.append(
|
|
||||||
model.Laser(
|
|
||||||
id=light_source.attrib['Id'],
|
|
||||||
model=self.text(light_source.find('Manufacturer').find('Model')),
|
|
||||||
power=float(self.text(light_source.find('Power'))),
|
|
||||||
wavelength=float(
|
|
||||||
self.text(light_source.find('LightSourceType').find('Laser').find('Wavelength')))))
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
elif self.version in ('1.1', '1.2'):
|
|
||||||
for light_source in self.def_list(self.instrument.find('LightSources')):
|
|
||||||
try:
|
|
||||||
if light_source.find('LightSourceType').find('Laser') is not None:
|
|
||||||
self.ome.instruments[0].lasers.append(
|
|
||||||
model.Laser(
|
|
||||||
id=f"LightSource:{light_source.attrib['Id']}",
|
|
||||||
power=float(self.text(light_source.find('Power'))),
|
|
||||||
wavelength=float(light_source.attrib['Id'][-3:]))) # TODO: follow Id reference
|
|
||||||
except (AttributeError, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_filters(self) -> None:
|
|
||||||
if self.version == '1.0':
|
|
||||||
for idx, filter_ in enumerate({self.text(beam_splitter.find('Filter'))
|
|
||||||
for track_setup in self.multi_track_setup
|
|
||||||
for beam_splitter in track_setup.find('BeamSplitters')}):
|
|
||||||
self.ome.instruments[0].filter_sets.append(
|
|
||||||
model.FilterSet(id=f'FilterSet:{idx}', model=filter_)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_pixels(self) -> None:
|
|
||||||
x_min = min([f.start[f.axes.index('X')] for f in self.filedict[0, 0, 0]])
|
|
||||||
y_min = min([f.start[f.axes.index('Y')] for f in self.filedict[0, 0, 0]])
|
|
||||||
x_max = max([f.start[f.axes.index('X')] + f.shape[f.axes.index('X')] for f in self.filedict[0, 0, 0]])
|
|
||||||
y_max = max([f.start[f.axes.index('Y')] + f.shape[f.axes.index('Y')] for f in self.filedict[0, 0, 0]])
|
|
||||||
self.size_x = x_max - x_min
|
|
||||||
self.size_y = y_max - y_min
|
|
||||||
self.size_c, self.size_z, self.size_t = (self.reader.shape[self.reader.axes.index(directory_entry)]
|
|
||||||
for directory_entry in 'CZT')
|
|
||||||
image = self.information.find('Image')
|
|
||||||
pixel_type = self.text(image.find('PixelType'), 'Gray16')
|
|
||||||
if pixel_type.startswith('Gray'):
|
|
||||||
pixel_type = 'uint' + pixel_type[4:]
|
|
||||||
objective_settings = image.find('ObjectiveSettings')
|
|
||||||
|
|
||||||
self.ome.images.append(
|
|
||||||
model.Image(
|
|
||||||
id='Image:0',
|
|
||||||
name=f"{self.text(self.information.find('Document').find('Name'))} #1",
|
|
||||||
pixels=model.Pixels(
|
|
||||||
id='Pixels:0', size_x=self.size_x, size_y=self.size_y,
|
|
||||||
size_c=self.size_c, size_z=self.size_z, size_t=self.size_t,
|
|
||||||
dimension_order='XYCZT', type=pixel_type, # type: ignore
|
|
||||||
significant_bits=int(self.text(image.find('ComponentBitCount'))),
|
|
||||||
big_endian=False, interleaved=False, metadata_only=True), # type: ignore
|
|
||||||
experimenter_ref=model.ExperimenterRef(id='Experimenter:0'),
|
|
||||||
instrument_ref=model.InstrumentRef(id='Instrument:0'),
|
|
||||||
objective_settings=model.ObjectiveSettings(
|
|
||||||
id=objective_settings.find('ObjectiveRef').attrib['Id'],
|
|
||||||
medium=self.text(objective_settings.find('Medium')), # type: ignore
|
|
||||||
refractive_index=float(self.text(objective_settings.find('RefractiveIndex')))),
|
|
||||||
stage_label=model.StageLabel(
|
|
||||||
name=f'Scene position #0',
|
|
||||||
x=self.positions[0], x_unit=self.um,
|
|
||||||
y=self.positions[1], y_unit=self.um,
|
|
||||||
z=self.positions[2], z_unit=self.um)))
|
|
||||||
|
|
||||||
for distance in self.metadata.find('Scaling').find('Items'):
|
|
||||||
if distance.attrib['Id'] == 'X':
|
|
||||||
self.ome.images[0].pixels.physical_size_x = float(self.text(distance.find('Value'))) * 1e6
|
|
||||||
elif distance.attrib['Id'] == 'Y':
|
|
||||||
self.ome.images[0].pixels.physical_size_y = float(self.text(distance.find('Value'))) * 1e6
|
|
||||||
elif self.size_z > 1 and distance.attrib['Id'] == 'Z':
|
|
||||||
self.ome.images[0].pixels.physical_size_z = float(self.text(distance.find('Value'))) * 1e6
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def positions(self) -> tuple[float, float, Optional[float]]:
|
|
||||||
if self.version == '1.0':
|
|
||||||
scenes = self.image.find('Dimensions').find('S').find('Scenes')
|
|
||||||
positions = scenes[0].find('Positions')[0]
|
|
||||||
return float(positions.attrib['X']), float(positions.attrib['Y']), float(positions.attrib['Z'])
|
|
||||||
elif self.version in ('1.1', '1.2'):
|
|
||||||
try: # TODO
|
|
||||||
scenes = self.image.find('Dimensions').find('S').find('Scenes')
|
|
||||||
center_position = [float(pos) for pos in self.text(scenes[0].find('CenterPosition')).split(',')]
|
|
||||||
except AttributeError:
|
|
||||||
center_position = [0, 0]
|
|
||||||
return center_position[0], center_position[1], None
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def channels_im(self) -> dict:
|
|
||||||
return {channel.attrib['Id']: channel for channel in self.image.find('Dimensions').find('Channels')}
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def channels_ds(self) -> dict:
|
|
||||||
return {channel.attrib['Id']: channel for channel in self.display_setting.find('Channels')}
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def channels_ts(self) -> dict:
|
|
||||||
return {detector.attrib['Id']: track_setup
|
|
||||||
for track_setup in
|
|
||||||
self.experiment.find('ExperimentBlocks').find('AcquisitionBlock').find('MultiTrackSetup')
|
|
||||||
for detector in track_setup.find('Detectors')}
|
|
||||||
|
|
||||||
def get_channels(self) -> None:
|
|
||||||
if self.version == '1.0':
|
|
||||||
for idx, (key, channel) in enumerate(self.channels_im.items()):
|
|
||||||
detector_settings = channel.find('DetectorSettings')
|
|
||||||
laser_scan_info = channel.find('LaserScanInfo')
|
|
||||||
detector = detector_settings.find('Detector')
|
|
||||||
try:
|
|
||||||
binning = model.Binning(self.text(detector_settings.find('Binning')))
|
|
||||||
except ValueError:
|
|
||||||
binning = model.Binning.OTHER
|
|
||||||
|
|
||||||
filterset = self.text(self.channels_ts[key].find('BeamSplitters')[0].find('Filter'))
|
|
||||||
filterset_idx = [filterset.model for filterset in self.ome.instruments[0].filter_sets].index(filterset)
|
|
||||||
|
|
||||||
light_sources_settings = channel.find('LightSourcesSettings')
|
|
||||||
# no space in ome for multiple lightsources simultaneously
|
|
||||||
if len(light_sources_settings) > idx:
|
|
||||||
light_source_settings = light_sources_settings[idx]
|
|
||||||
else:
|
|
||||||
light_source_settings = light_sources_settings[0]
|
|
||||||
light_source_settings = model.LightSourceSettings(
|
|
||||||
id=light_source_settings.find('LightSource').attrib['Id'],
|
|
||||||
attenuation=float(self.text(light_source_settings.find('Attenuation'))),
|
|
||||||
wavelength=float(self.text(light_source_settings.find('Wavelength'))),
|
|
||||||
wavelength_unit=self.nm)
|
|
||||||
|
|
||||||
self.ome.images[0].pixels.channels.append(
|
|
||||||
model.Channel(
|
|
||||||
id=f'Channel:{idx}',
|
|
||||||
name=channel.attrib['Name'],
|
|
||||||
acquisition_mode=self.text(channel.find('AcquisitionMode')), # type: ignore
|
|
||||||
color=model.Color(self.text(self.channels_ds[channel.attrib['Id']].find('Color'), 'white')),
|
|
||||||
detector_settings=model.DetectorSettings(id=detector.attrib['Id'], binning=binning),
|
|
||||||
# emission_wavelength=text(channel.find('EmissionWavelength')), # TODO: fix
|
|
||||||
excitation_wavelength=light_source_settings.wavelength,
|
|
||||||
filter_set_ref=model.FilterSetRef(id=self.ome.instruments[0].filter_sets[filterset_idx].id),
|
|
||||||
illumination_type=self.text(channel.find('IlluminationType')), # type: ignore
|
|
||||||
light_source_settings=light_source_settings,
|
|
||||||
samples_per_pixel=int(self.text(laser_scan_info.find('Averaging')))))
|
|
||||||
elif self.version in ('1.1', '1.2'):
|
|
||||||
for idx, (key, channel) in enumerate(self.channels_im.items()):
|
|
||||||
detector_settings = channel.find('DetectorSettings')
|
|
||||||
laser_scan_info = channel.find('LaserScanInfo')
|
|
||||||
detector = detector_settings.find('Detector')
|
|
||||||
try:
|
|
||||||
color = model.Color(self.text(self.channels_ds[channel.attrib['Id']].find('Color'), 'white'))
|
|
||||||
except Exception: # noqa
|
|
||||||
color = None
|
|
||||||
try:
|
|
||||||
if (i := self.text(channel.find('EmissionWavelength'))) != '0':
|
|
||||||
emission_wavelength = float(i)
|
|
||||||
else:
|
|
||||||
emission_wavelength = None
|
|
||||||
except Exception: # noqa
|
|
||||||
emission_wavelength = None
|
|
||||||
if laser_scan_info is not None:
|
|
||||||
samples_per_pixel = int(self.text(laser_scan_info.find('Averaging'), '1'))
|
|
||||||
else:
|
|
||||||
samples_per_pixel = 1
|
|
||||||
try:
|
|
||||||
binning = model.Binning(self.text(detector_settings.find('Binning')))
|
|
||||||
except ValueError:
|
|
||||||
binning = model.Binning.OTHER
|
|
||||||
|
|
||||||
light_sources_settings = channel.find('LightSourcesSettings')
|
|
||||||
# no space in ome for multiple lightsources simultaneously
|
|
||||||
if light_sources_settings is not None:
|
|
||||||
light_source_settings = light_sources_settings[0]
|
|
||||||
light_source_settings = model.LightSourceSettings(
|
|
||||||
id='LightSource:' + '_'.join([light_source_settings.find('LightSource').attrib['Id']
|
|
||||||
for light_source_settings in light_sources_settings]),
|
|
||||||
attenuation=self.try_default(float, None, self.text(light_source_settings.find('Attenuation'))),
|
|
||||||
wavelength=self.try_default(float, None, self.text(light_source_settings.find('Wavelength'))),
|
|
||||||
wavelength_unit=self.nm)
|
|
||||||
else:
|
|
||||||
light_source_settings = None
|
|
||||||
|
|
||||||
self.ome.images[0].pixels.channels.append(
|
|
||||||
model.Channel(
|
|
||||||
id=f'Channel:{idx}',
|
|
||||||
name=channel.attrib['Name'],
|
|
||||||
acquisition_mode=self.text(channel.find('AcquisitionMode')).replace( # type: ignore
|
|
||||||
'SingleMoleculeLocalisation', 'SingleMoleculeImaging'),
|
|
||||||
color=color,
|
|
||||||
detector_settings=model.DetectorSettings(
|
|
||||||
id=detector.attrib['Id'].replace(' ', ""),
|
|
||||||
binning=binning),
|
|
||||||
emission_wavelength=emission_wavelength,
|
|
||||||
excitation_wavelength=self.try_default(float, None,
|
|
||||||
self.text(channel.find('ExcitationWavelength'))),
|
|
||||||
# filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id),
|
|
||||||
illumination_type=self.text(channel.find('IlluminationType')), # type: ignore
|
|
||||||
light_source_settings=light_source_settings,
|
|
||||||
samples_per_pixel=samples_per_pixel))
|
|
||||||
|
|
||||||
def get_planes(self) -> None:
|
|
||||||
try:
|
|
||||||
exposure_times = [float(self.text(channel.find('LaserScanInfo').find('FrameTime')))
|
|
||||||
for channel in self.channels_im.values()]
|
|
||||||
except Exception: # noqa
|
|
||||||
exposure_times = [None] * len(self.channels_im)
|
|
||||||
delta_ts = self.attachments['TimeStamps'].data()
|
|
||||||
dt = np.diff(delta_ts)
|
|
||||||
if len(dt) and np.std(dt) / np.mean(dt) > 0.02:
|
|
||||||
dt = np.median(dt[dt > 0])
|
|
||||||
delta_ts = dt * np.arange(len(delta_ts))
|
|
||||||
warnings.warn(f'delta_t is inconsistent, using median value: {dt}')
|
|
||||||
|
|
||||||
for t, z, c in product(range(self.size_t), range(self.size_z), range(self.size_c)):
|
|
||||||
self.ome.images[0].pixels.planes.append(
|
|
||||||
model.Plane(the_c=c, the_z=z, the_t=t, delta_t=delta_ts[t],
|
|
||||||
exposure_time=exposure_times[c],
|
|
||||||
position_x=self.positions[0], position_x_unit=self.um,
|
|
||||||
position_y=self.positions[1], position_y_unit=self.um,
|
|
||||||
position_z=self.positions[2], position_z_unit=self.um))
|
|
||||||
|
|
||||||
def get_annotations(self) -> None:
|
|
||||||
idx = 0
|
|
||||||
for layer in [] if (ml := self.metadata.find('Layers')) is None else ml:
|
|
||||||
rectangle = layer.find('Elements').find('Rectangle')
|
|
||||||
if rectangle is not None:
|
|
||||||
geometry = rectangle.find('Geometry')
|
|
||||||
roi = model.ROI(id=f'ROI:{idx}', description=self.text(layer.find('Usage')))
|
|
||||||
roi.union.append(
|
|
||||||
model.Rectangle(
|
|
||||||
id='Shape:0:0',
|
|
||||||
height=float(self.text(geometry.find('Height'))),
|
|
||||||
width=float(self.text(geometry.find('Width'))),
|
|
||||||
x=float(self.text(geometry.find('Left'))),
|
|
||||||
y=float(self.text(geometry.find('Top')))))
|
|
||||||
self.ome.rois.append(roi)
|
|
||||||
self.ome.images[0].roi_refs.append(model.ROIRef(id=f'ROI:{idx}'))
|
|
||||||
idx += 1
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
from abc import ABC
|
|
||||||
from itertools import product
|
|
||||||
from pathlib import Path
|
|
||||||
from struct import unpack
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from ome_types import model
|
|
||||||
from tifffile import TiffFile
|
|
||||||
|
|
||||||
from .. import AbstractReader
|
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
|
||||||
""" Can read some tif files written with Fiji which are broken because Fiji didn't finish writing. """
|
|
||||||
priority = 90
|
|
||||||
do_not_pickle = 'reader'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _can_open(path):
|
|
||||||
if isinstance(path, Path) and path.suffix in ('.tif', '.tiff'):
|
|
||||||
with TiffFile(path) as tif:
|
|
||||||
return tif.is_imagej and not tif.is_bigtiff
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __frame__(self, c, z, t): # Override this, return the frame at c, z, t
|
|
||||||
self.reader.filehandle.seek(self.offset + t * self.count)
|
|
||||||
return np.reshape(unpack(self.fmt, self.reader.filehandle.read(self.count)), self.base_shape['yx'])
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
warn(f'File {self.path.name} is probably damaged, opening with fijiread.')
|
|
||||||
self.reader = TiffFile(self.path)
|
|
||||||
assert self.reader.pages[0].compression == 1, 'Can only read uncompressed tiff files.'
|
|
||||||
assert self.reader.pages[0].samplesperpixel == 1, 'Can only read 1 sample per pixel.'
|
|
||||||
self.offset = self.reader.pages[0].dataoffsets[0] # noqa
|
|
||||||
self.count = self.reader.pages[0].databytecounts[0] # noqa
|
|
||||||
self.bytes_per_sample = self.reader.pages[0].bitspersample // 8 # noqa
|
|
||||||
self.fmt = self.reader.byteorder + self.count // self.bytes_per_sample * 'BHILQ'[self.bytes_per_sample - 1] # noqa
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.reader.close()
|
|
||||||
|
|
||||||
def get_ome(self):
|
|
||||||
size_y, size_x = self.reader.pages[0].shape
|
|
||||||
size_c, size_z = 1, 1
|
|
||||||
size_t = int(np.floor((self.reader.filehandle.size - self.reader.pages[0].dataoffsets[0]) / self.count))
|
|
||||||
pixel_type = model.PixelType(self.reader.pages[0].dtype.name)
|
|
||||||
ome = model.OME()
|
|
||||||
ome.instruments.append(model.Instrument())
|
|
||||||
ome.images.append(
|
|
||||||
model.Image(
|
|
||||||
pixels=model.Pixels(
|
|
||||||
size_c=size_c, size_z=size_z, size_t=size_t, size_x=size_x, size_y=size_y,
|
|
||||||
dimension_order='XYCZT', type=pixel_type),
|
|
||||||
objective_settings=model.ObjectiveSettings(id='Objective:0')))
|
|
||||||
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
|
|
||||||
ome.images[0].pixels.planes.append(model.Plane(the_c=c, the_z=z, the_t=t, delta_t=0))
|
|
||||||
return ome
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import re
|
|
||||||
from abc import ABC
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import tifffile
|
|
||||||
from ome_types import model
|
|
||||||
from ome_types.units import _quantity_property # noqa
|
|
||||||
|
|
||||||
from .. import AbstractReader
|
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
|
||||||
priority = 20
|
|
||||||
do_not_pickle = 'last_tif'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _can_open(path):
|
|
||||||
return isinstance(path, Path) and (path.is_dir() or
|
|
||||||
(path.parent.is_dir() and path.name.lower().startswith('pos')))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_positions(path: str | Path) -> Optional[list[int]]:
|
|
||||||
pat = re.compile(rf's(\d)_t\d+\.(tif|TIF)$')
|
|
||||||
return sorted({int(m.group(1)) for file in Path(path).iterdir() if (m := pat.search(file.name))})
|
|
||||||
|
|
||||||
def get_ome(self):
|
|
||||||
ome = model.OME()
|
|
||||||
tif = self.get_tif(0)
|
|
||||||
metadata = tif.metaseries_metadata
|
|
||||||
size_z = len(tif.pages)
|
|
||||||
page = tif.pages[0]
|
|
||||||
shape = {axis.lower(): size for axis, size in zip(page.axes, page.shape)}
|
|
||||||
size_x, size_y = shape['x'], shape['y']
|
|
||||||
|
|
||||||
ome.instruments.append(model.Instrument())
|
|
||||||
|
|
||||||
size_c = 1
|
|
||||||
size_t = max(self.filedict.keys()) + 1
|
|
||||||
pixel_type = f"uint{metadata['PlaneInfo']['bits-per-pixel']}"
|
|
||||||
ome.images.append(
|
|
||||||
model.Image(
|
|
||||||
pixels=model.Pixels(
|
|
||||||
size_c=size_c, size_z=size_z, size_t=size_t,
|
|
||||||
size_x=size_x, size_y=size_y,
|
|
||||||
dimension_order='XYCZT', type=pixel_type),
|
|
||||||
objective_settings=model.ObjectiveSettings(id='Objective:0')))
|
|
||||||
return ome
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
pat = re.compile(rf's{self.series}_t\d+\.(tif|TIF)$')
|
|
||||||
filelist = sorted([file for file in self.path.iterdir() if pat.search(file.name)])
|
|
||||||
pattern = re.compile(r't(\d+)$')
|
|
||||||
self.filedict = {int(pattern.search(file.stem).group(1)) - 1: file for file in filelist}
|
|
||||||
if len(self.filedict) == 0:
|
|
||||||
raise FileNotFoundError
|
|
||||||
self.last_tif = 0, tifffile.TiffFile(self.filedict[0])
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
self.last_tif[1].close()
|
|
||||||
|
|
||||||
def get_tif(self, t: int = None):
|
|
||||||
last_t, tif = self.last_tif
|
|
||||||
if (t is None or t == last_t) and not tif.filehandle.closed:
|
|
||||||
return tif
|
|
||||||
else:
|
|
||||||
tif.close()
|
|
||||||
tif = tifffile.TiffFile(self.filedict[t])
|
|
||||||
self.last_tif = t, tif
|
|
||||||
return tif
|
|
||||||
|
|
||||||
def __frame__(self, c=0, z=0, t=0):
|
|
||||||
tif = self.get_tif(t)
|
|
||||||
page = tif.pages[z]
|
|
||||||
if page.axes.upper() == 'YX':
|
|
||||||
return page.asarray()
|
|
||||||
elif page.axes.upper() == 'XY':
|
|
||||||
return page.asarray().T
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(f'reading axes {page.axes} is not implemented')
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
from abc import ABC
|
|
||||||
from itertools import product
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from ome_types import model
|
|
||||||
|
|
||||||
from .. import AbstractReader
|
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
|
||||||
priority = 20
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _can_open(path):
|
|
||||||
return isinstance(path, np.ndarray) and 1 <= path.ndim <= 5
|
|
||||||
|
|
||||||
def get_ome(self):
|
|
||||||
def shape(size_x=1, size_y=1, size_c=1, size_z=1, size_t=1): # noqa
|
|
||||||
return size_x, size_y, size_c, size_z, size_t
|
|
||||||
size_x, size_y, size_c, size_z, size_t = shape(*self.array.shape)
|
|
||||||
try:
|
|
||||||
pixel_type = model.PixelType(self.array.dtype.name)
|
|
||||||
except ValueError:
|
|
||||||
if self.array.dtype.name.startswith('int'):
|
|
||||||
pixel_type = model.PixelType('int32')
|
|
||||||
else:
|
|
||||||
pixel_type = model.PixelType('float')
|
|
||||||
|
|
||||||
ome = model.OME()
|
|
||||||
ome.instruments.append(model.Instrument())
|
|
||||||
ome.images.append(
|
|
||||||
model.Image(
|
|
||||||
pixels=model.Pixels(
|
|
||||||
size_c=size_c, size_z=size_z, size_t=size_t, size_x=size_x, size_y=size_y,
|
|
||||||
dimension_order='XYCZT', type=pixel_type),
|
|
||||||
objective_settings=model.ObjectiveSettings(id='Objective:0')))
|
|
||||||
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
|
|
||||||
ome.images[0].pixels.planes.append(model.Plane(the_c=c, the_z=z, the_t=t, delta_t=0))
|
|
||||||
return ome
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
if isinstance(self.path, np.ndarray):
|
|
||||||
self.array = np.array(self.path)
|
|
||||||
while self.array.ndim < 5:
|
|
||||||
self.array = np.expand_dims(self.array, -1) # noqa
|
|
||||||
self.path = 'numpy array'
|
|
||||||
|
|
||||||
def __frame__(self, c, z, t):
|
|
||||||
frame = self.array[:, :, c, z, t]
|
|
||||||
if self.axes.find('y') > self.axes.find('x'):
|
|
||||||
return frame.T
|
|
||||||
else:
|
|
||||||
return frame
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import re
|
|
||||||
from abc import ABC
|
|
||||||
from datetime import datetime
|
|
||||||
from itertools import product
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import tifffile
|
|
||||||
import yaml
|
|
||||||
from ome_types import model
|
|
||||||
from ome_types.units import _quantity_property # noqa
|
|
||||||
|
|
||||||
from .. import AbstractReader
|
|
||||||
|
|
||||||
|
|
||||||
def lazy_property(function, field, *arg_fields):
|
|
||||||
def lazy(self):
|
|
||||||
if self.__dict__.get(field) is None:
|
|
||||||
self.__dict__[field] = function(*[getattr(self, arg_field) for arg_field in arg_fields])
|
|
||||||
try:
|
|
||||||
self.model_fields_set.add(field)
|
|
||||||
except Exception: # noqa
|
|
||||||
pass
|
|
||||||
return self.__dict__[field]
|
|
||||||
return property(lazy)
|
|
||||||
|
|
||||||
|
|
||||||
class Plane(model.Plane):
|
|
||||||
""" Lazily retrieve delta_t from metadata """
|
|
||||||
def __init__(self, t0, file, **kwargs): # noqa
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
# setting fields here because they would be removed by ome_types/pydantic after class definition
|
|
||||||
setattr(self.__class__, 'delta_t', lazy_property(self.get_delta_t, 'delta_t', 't0', 'file'))
|
|
||||||
setattr(self.__class__, 'delta_t_quantity', _quantity_property('delta_t'))
|
|
||||||
self.__dict__['t0'] = t0 # noqa
|
|
||||||
self.__dict__['file'] = file # noqa
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_delta_t(t0, file):
|
|
||||||
with tifffile.TiffFile(file) as tif:
|
|
||||||
info = yaml.safe_load(tif.pages[0].tags[50839].value['Info'])
|
|
||||||
return float((datetime.strptime(info['Time'], '%Y-%m-%d %H:%M:%S %z') - t0).seconds)
|
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
|
||||||
priority = 10
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _can_open(path):
|
|
||||||
pat = re.compile(r'(?:\d+-)?Pos.*', re.IGNORECASE)
|
|
||||||
return (isinstance(path, Path) and path.is_dir() and
|
|
||||||
(pat.match(path.name) or any(file.is_dir() and pat.match(file.stem) for file in path.iterdir())))
|
|
||||||
|
|
||||||
def get_ome(self):
|
|
||||||
ome = model.OME()
|
|
||||||
with tifffile.TiffFile(self.filedict[0, 0, 0]) as tif:
|
|
||||||
metadata = {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()}
|
|
||||||
ome.experimenters.append(
|
|
||||||
model.Experimenter(id='Experimenter:0', user_name=metadata['Info']['Summary']['UserName']))
|
|
||||||
objective_str = metadata['Info']['ZeissObjectiveTurret-Label']
|
|
||||||
ome.instruments.append(model.Instrument())
|
|
||||||
ome.instruments[0].objectives.append(
|
|
||||||
model.Objective(
|
|
||||||
id='Objective:0', manufacturer='Zeiss', model=objective_str,
|
|
||||||
nominal_magnification=float(re.findall(r'(\d+)x', objective_str)[0]),
|
|
||||||
lens_na=float(re.findall(r'/(\d\.\d+)', objective_str)[0]),
|
|
||||||
immersion=model.Objective_Immersion.OIL if 'oil' in objective_str.lower() else None))
|
|
||||||
tubelens_str = metadata['Info']['ZeissOptovar-Label']
|
|
||||||
ome.instruments[0].objectives.append(
|
|
||||||
model.Objective(
|
|
||||||
id='Objective:Tubelens:0', manufacturer='Zeiss', model=tubelens_str,
|
|
||||||
nominal_magnification=float(re.findall(r'\d?\d*[,.]?\d+(?=x$)', tubelens_str)[0].replace(',', '.'))))
|
|
||||||
ome.instruments[0].detectors.append(
|
|
||||||
model.Detector(
|
|
||||||
id='Detector:0', amplification_gain=100))
|
|
||||||
ome.instruments[0].filter_sets.append(
|
|
||||||
model.FilterSet(id='FilterSet:0', model=metadata['Info']['ZeissReflectorTurret-Label']))
|
|
||||||
|
|
||||||
pxsize = metadata['Info']['PixelSizeUm']
|
|
||||||
pxsize_cam = 6.5 if 'Hamamatsu' in metadata['Info']['Core-Camera'] else None
|
|
||||||
if pxsize == 0:
|
|
||||||
pxsize = pxsize_cam / ome.instruments[0].objectives[0].nominal_magnification
|
|
||||||
pixel_type = metadata['Info']['PixelType'].lower()
|
|
||||||
if pixel_type.startswith('gray'):
|
|
||||||
pixel_type = 'uint' + pixel_type[4:]
|
|
||||||
else:
|
|
||||||
pixel_type = 'uint16' # assume
|
|
||||||
|
|
||||||
size_c, size_z, size_t = (max(i) + 1 for i in zip(*self.filedict.keys()))
|
|
||||||
t0 = datetime.strptime(metadata['Info']['Time'], '%Y-%m-%d %H:%M:%S %z')
|
|
||||||
ome.images.append(
|
|
||||||
model.Image(
|
|
||||||
pixels=model.Pixels(
|
|
||||||
size_c=size_c, size_z=size_z, size_t=size_t,
|
|
||||||
size_x=metadata['Info']['Width'], size_y=metadata['Info']['Height'],
|
|
||||||
dimension_order='XYCZT', # type: ignore
|
|
||||||
type=pixel_type, physical_size_x=pxsize, physical_size_y=pxsize,
|
|
||||||
physical_size_z=metadata['Info']['Summary']['z-step_um']),
|
|
||||||
objective_settings=model.ObjectiveSettings(id='Objective:0')))
|
|
||||||
|
|
||||||
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
|
|
||||||
ome.images[0].pixels.planes.append(
|
|
||||||
Plane(t0, self.filedict[c, z, t],
|
|
||||||
the_c=c, the_z=z, the_t=t, exposure_time=metadata['Info']['Exposure-ms'] / 1000))
|
|
||||||
|
|
||||||
# compare channel names from metadata with filenames
|
|
||||||
pattern_c = re.compile(r'img_\d{3,}_(.*)_\d{3,}$', re.IGNORECASE)
|
|
||||||
for c in range(size_c):
|
|
||||||
ome.images[0].pixels.channels.append(
|
|
||||||
model.Channel(
|
|
||||||
id=f'Channel:{c}', name=pattern_c.findall(self.filedict[c, 0, 0].stem)[0],
|
|
||||||
detector_settings=model.DetectorSettings(
|
|
||||||
id='Detector:0', binning=metadata['Info']['Hamamatsu_sCMOS-Binning']),
|
|
||||||
filter_set_ref=model.FilterSetRef(id='FilterSet:0')))
|
|
||||||
return ome
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
# /some_path/Pos4: path = /some_path, series = 4
|
|
||||||
# /some_path/5-Pos_001_005: path = /some_path/5-Pos_001_005, series = 0
|
|
||||||
if re.match(r'(?:\d+-)?Pos.*', self.path.name, re.IGNORECASE) is None:
|
|
||||||
pat = re.compile(rf'^(?:\d+-)?Pos{self.series}$', re.IGNORECASE)
|
|
||||||
files = sorted(file for file in self.path.iterdir() if pat.match(file.name))
|
|
||||||
if len(files):
|
|
||||||
path = files[0]
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError(self.path / pat.pattern)
|
|
||||||
else:
|
|
||||||
path = self.path
|
|
||||||
|
|
||||||
pat = re.compile(r'^img_\d{3,}.*\d{3,}.*\.tif$', re.IGNORECASE)
|
|
||||||
filelist = sorted([file for file in path.iterdir() if pat.search(file.name)])
|
|
||||||
with tifffile.TiffFile(self.path / filelist[0]) as tif:
|
|
||||||
metadata = {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()}
|
|
||||||
|
|
||||||
# compare channel names from metadata with filenames
|
|
||||||
cnamelist = metadata['Info']['Summary']['ChNames']
|
|
||||||
cnamelist = [c for c in cnamelist if any([c in f.name for f in filelist])]
|
|
||||||
|
|
||||||
pattern_c = re.compile(r'img_\d{3,}_(.*)_\d{3,}$', re.IGNORECASE)
|
|
||||||
pattern_z = re.compile(r'(\d{3,})$')
|
|
||||||
pattern_t = re.compile(r'img_(\d{3,})', re.IGNORECASE)
|
|
||||||
self.filedict = {(cnamelist.index(pattern_c.findall(file.stem)[0]), # noqa
|
|
||||||
int(pattern_z.findall(file.stem)[0]),
|
|
||||||
int(pattern_t.findall(file.stem)[0])): file for file in filelist}
|
|
||||||
|
|
||||||
def __frame__(self, c=0, z=0, t=0):
|
|
||||||
return tifffile.imread(self.path / self.filedict[(c, z, t)])
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
from abc import ABC
|
|
||||||
from functools import cached_property
|
|
||||||
from itertools import product
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import tifffile
|
|
||||||
import yaml
|
|
||||||
from ome_types import model
|
|
||||||
|
|
||||||
from .. import AbstractReader
|
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
|
||||||
priority = 0
|
|
||||||
do_not_pickle = 'reader'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _can_open(path):
|
|
||||||
if isinstance(path, Path) and path.suffix in ('.tif', '.tiff'):
|
|
||||||
with tifffile.TiffFile(path) as tif:
|
|
||||||
return tif.is_imagej and tif.pages[-1]._nextifd() == 0 # noqa
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def metadata(self):
|
|
||||||
return {key: yaml.safe_load(value) if isinstance(value, str) else value
|
|
||||||
for key, value in self.reader.imagej_metadata.items()}
|
|
||||||
|
|
||||||
def get_ome(self):
|
|
||||||
page = self.reader.pages[0]
|
|
||||||
size_y = page.imagelength
|
|
||||||
size_x = page.imagewidth
|
|
||||||
if self.p_ndim == 3:
|
|
||||||
size_c = page.samplesperpixel
|
|
||||||
size_t = self.metadata.get('frames', 1) # // C
|
|
||||||
else:
|
|
||||||
size_c = self.metadata.get('channels', 1)
|
|
||||||
size_t = self.metadata.get('frames', 1)
|
|
||||||
size_z = self.metadata.get('slices', 1)
|
|
||||||
if 282 in page.tags and 296 in page.tags and page.tags[296].value == 1:
|
|
||||||
f = page.tags[282].value
|
|
||||||
pxsize = f[1] / f[0]
|
|
||||||
else:
|
|
||||||
pxsize = None
|
|
||||||
|
|
||||||
dtype = page.dtype.name
|
|
||||||
if dtype not in ('int8', 'int16', 'int32', 'uint8', 'uint16', 'uint32',
|
|
||||||
'float', 'double', 'complex', 'double-complex', 'bit'):
|
|
||||||
dtype = 'float'
|
|
||||||
|
|
||||||
interval_t = self.metadata.get('interval', 0)
|
|
||||||
|
|
||||||
ome = model.OME()
|
|
||||||
ome.instruments.append(model.Instrument(id='Instrument:0'))
|
|
||||||
ome.instruments[0].objectives.append(model.Objective(id='Objective:0'))
|
|
||||||
ome.images.append(
|
|
||||||
model.Image(
|
|
||||||
id='Image:0',
|
|
||||||
pixels=model.Pixels(
|
|
||||||
id='Pixels:0',
|
|
||||||
size_c=size_c, size_z=size_z, size_t=size_t, size_x=size_x, size_y=size_y,
|
|
||||||
dimension_order='XYCZT', type=dtype, # type: ignore
|
|
||||||
physical_size_x=pxsize, physical_size_y=pxsize),
|
|
||||||
objective_settings=model.ObjectiveSettings(id='Objective:0')))
|
|
||||||
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
|
|
||||||
ome.images[0].pixels.planes.append(model.Plane(the_c=c, the_z=z, the_t=t, delta_t=interval_t * t))
|
|
||||||
return ome
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
self.reader = tifffile.TiffFile(self.path)
|
|
||||||
page = self.reader.pages[0]
|
|
||||||
self.p_ndim = page.ndim # noqa
|
|
||||||
if self.p_ndim == 3:
|
|
||||||
self.p_transpose = [i for i in [page.axes.find(j) for j in 'SYX'] if i >= 0] # noqa
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.reader.close()
|
|
||||||
|
|
||||||
def __frame__(self, c, z, t):
|
|
||||||
if self.p_ndim == 3:
|
|
||||||
return np.transpose(self.reader.asarray(z + t * self.base_shape['z']), self.p_transpose)[c]
|
|
||||||
else:
|
|
||||||
return self.reader.asarray(c + z * self.base_shape['c'] + t * self.base_shape['c'] * self.base_shape['z'])
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#Insight Transform File V1.0
|
|
||||||
#Transform 0
|
|
||||||
Transform: CompositeTransform_double_2_2
|
|
||||||
#Transform 1
|
|
||||||
Transform: AffineTransform_double_2_2
|
|
||||||
Parameters: 1 0 0 1 0 0
|
|
||||||
FixedParameters: 255.5 255.5
|
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
import warnings
|
|
||||||
from copy import deepcopy
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import yaml
|
|
||||||
from parfor import Chunks, pmap
|
|
||||||
from skimage import filters
|
|
||||||
from tiffwrite import IJTiffFile
|
|
||||||
from tqdm.auto import tqdm
|
|
||||||
|
|
||||||
try:
|
|
||||||
# best if SimpleElastix is installed: https://simpleelastix.readthedocs.io/GettingStarted.html
|
|
||||||
import SimpleITK as sitk # noqa
|
|
||||||
except ImportError:
|
|
||||||
sitk = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
from pandas import DataFrame, Series, concat
|
|
||||||
except ImportError:
|
|
||||||
DataFrame, Series, concat = None, None, None
|
|
||||||
|
|
||||||
|
|
||||||
if hasattr(yaml, 'full_load'):
|
|
||||||
yamlload = yaml.full_load
|
|
||||||
else:
|
|
||||||
yamlload = yaml.load
|
|
||||||
|
|
||||||
|
|
||||||
class Transforms(dict):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.default = Transform()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_file(cls, file, C=True, T=True):
|
|
||||||
with open(Path(file).with_suffix('.yml')) as f:
|
|
||||||
return cls.from_dict(yamlload(f), C, T)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, d, C=True, T=True):
|
|
||||||
new = cls()
|
|
||||||
for key, value in d.items():
|
|
||||||
if isinstance(key, str) and C:
|
|
||||||
new[key.replace(r'\:', ':').replace('\\\\', '\\')] = Transform.from_dict(value)
|
|
||||||
elif T:
|
|
||||||
new[key] = Transform.from_dict(value)
|
|
||||||
return new
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_shifts(cls, shifts):
|
|
||||||
new = cls()
|
|
||||||
for key, shift in shifts.items():
|
|
||||||
new[key] = Transform.from_shift(shift)
|
|
||||||
return new
|
|
||||||
|
|
||||||
def __mul__(self, other):
|
|
||||||
new = Transforms()
|
|
||||||
if isinstance(other, Transforms):
|
|
||||||
for key0, value0 in self.items():
|
|
||||||
for key1, value1 in other.items():
|
|
||||||
new[key0 + key1] = value0 * value1
|
|
||||||
return new
|
|
||||||
elif other is None:
|
|
||||||
return self
|
|
||||||
else:
|
|
||||||
for key in self.keys():
|
|
||||||
new[key] = self[key] * other
|
|
||||||
return new
|
|
||||||
|
|
||||||
def asdict(self):
|
|
||||||
return {key.replace('\\', '\\\\').replace(':', r'\:') if isinstance(key, str) else key: value.asdict()
|
|
||||||
for key, value in self.items()}
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
return np.prod([self[i] for i in item[::-1]]) if isinstance(item, tuple) else super().__getitem__(item)
|
|
||||||
|
|
||||||
def __missing__(self, key):
|
|
||||||
return self.default
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
return self.__dict__
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
self.__dict__.update(state)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(frozenset((*self.__dict__.items(), *self.items())))
|
|
||||||
|
|
||||||
def save(self, file):
|
|
||||||
with open(Path(file).with_suffix('.yml'), 'w') as f:
|
|
||||||
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return deepcopy(self)
|
|
||||||
|
|
||||||
def adapt(self, origin, shape, channel_names):
|
|
||||||
def key_map(a, b):
|
|
||||||
def fun(b, key_a):
|
|
||||||
for key_b in b:
|
|
||||||
if key_b in key_a or key_a in key_b:
|
|
||||||
return key_a, key_b
|
|
||||||
|
|
||||||
return {n[0]: n[1] for key_a in a if (n := fun(b, key_a))}
|
|
||||||
|
|
||||||
for value in self.values():
|
|
||||||
value.adapt(origin, shape)
|
|
||||||
self.default.adapt(origin, shape)
|
|
||||||
transform_channels = {key for key in self.keys() if isinstance(key, str)}
|
|
||||||
if set(channel_names) - transform_channels:
|
|
||||||
mapping = key_map(channel_names, transform_channels)
|
|
||||||
warnings.warn(f'The image file and the transform do not have the same channels,'
|
|
||||||
f' creating a mapping: {mapping}')
|
|
||||||
for key_im, key_t in mapping.items():
|
|
||||||
self[key_im] = self[key_t]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inverse(self):
|
|
||||||
# TODO: check for C@T
|
|
||||||
inverse = self.copy()
|
|
||||||
for key, value in self.items():
|
|
||||||
inverse[key] = value.inverse
|
|
||||||
return inverse
|
|
||||||
|
|
||||||
def coords_pandas(self, array, channel_names, columns=None):
|
|
||||||
if isinstance(array, DataFrame):
|
|
||||||
return concat([self.coords_pandas(row, channel_names, columns) for _, row in array.iterrows()], axis=1).T
|
|
||||||
elif isinstance(array, Series):
|
|
||||||
key = []
|
|
||||||
if 'C' in array:
|
|
||||||
key.append(channel_names[int(array['C'])])
|
|
||||||
if 'T' in array:
|
|
||||||
key.append(int(array['T']))
|
|
||||||
return self[tuple(key)].coords(array, columns)
|
|
||||||
else:
|
|
||||||
raise TypeError('Not a pandas DataFrame or Series.')
|
|
||||||
|
|
||||||
def with_beads(self, cyllens, bead_files):
|
|
||||||
assert len(bead_files) > 0, 'At least one file is needed to calculate the registration.'
|
|
||||||
transforms = [self.calculate_channel_transforms(file, cyllens) for file in bead_files]
|
|
||||||
for key in {key for transform in transforms for key in transform.keys()}:
|
|
||||||
new_transforms = [transform[key] for transform in transforms if key in transform]
|
|
||||||
if len(new_transforms) == 1:
|
|
||||||
self[key] = new_transforms[0]
|
|
||||||
else:
|
|
||||||
self[key] = Transform()
|
|
||||||
self[key].parameters = np.mean([t.parameters for t in new_transforms], 0)
|
|
||||||
self[key].dparameters = (np.std([t.parameters for t in new_transforms], 0) /
|
|
||||||
np.sqrt(len(new_transforms))).tolist()
|
|
||||||
return self
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_bead_files(path):
|
|
||||||
from . import Imread
|
|
||||||
files = []
|
|
||||||
for file in path.iterdir():
|
|
||||||
if file.name.lower().startswith('beads'):
|
|
||||||
try:
|
|
||||||
with Imread(file):
|
|
||||||
files.append(file)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
files = sorted(files)
|
|
||||||
if not files:
|
|
||||||
raise Exception('No bead file found!')
|
|
||||||
checked_files = []
|
|
||||||
for file in files:
|
|
||||||
try:
|
|
||||||
if file.is_dir():
|
|
||||||
file /= 'Pos0'
|
|
||||||
with Imread(file): # check for errors opening the file
|
|
||||||
checked_files.append(file)
|
|
||||||
except (Exception,):
|
|
||||||
continue
|
|
||||||
if not checked_files:
|
|
||||||
raise Exception('No bead file found!')
|
|
||||||
return checked_files
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calculate_channel_transforms(bead_file, cyllens):
|
|
||||||
""" When no channel is not transformed by a cylindrical lens, assume that the image is scaled by a factor 1.162
|
|
||||||
in the horizontal direction """
|
|
||||||
from . import Imread
|
|
||||||
|
|
||||||
with Imread(bead_file, axes='zcyx') as im: # noqa
|
|
||||||
max_ims = im.max('z')
|
|
||||||
goodch = [c for c, max_im in enumerate(max_ims) if not im.is_noise(max_im)]
|
|
||||||
if not goodch:
|
|
||||||
goodch = list(range(len(max_ims)))
|
|
||||||
untransformed = [c for c in range(im.shape['c']) if cyllens[im.detector[c]].lower() == 'none']
|
|
||||||
|
|
||||||
good_and_untrans = sorted(set(goodch) & set(untransformed))
|
|
||||||
if good_and_untrans:
|
|
||||||
masterch = good_and_untrans[0]
|
|
||||||
else:
|
|
||||||
masterch = goodch[0]
|
|
||||||
transform = Transform()
|
|
||||||
if not good_and_untrans:
|
|
||||||
matrix = transform.matrix
|
|
||||||
matrix[0, 0] = 0.86
|
|
||||||
transform.matrix = matrix
|
|
||||||
transforms = Transforms()
|
|
||||||
for c in tqdm(goodch, desc='Calculating channel transforms'): # noqa
|
|
||||||
if c == masterch:
|
|
||||||
transforms[im.channel_names[c]] = transform
|
|
||||||
else:
|
|
||||||
transforms[im.channel_names[c]] = Transform.register(max_ims[masterch], max_ims[c]) * transform
|
|
||||||
return transforms
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def save_channel_transform_tiff(bead_files, tiffile):
|
|
||||||
from . import Imread
|
|
||||||
n_channels = 0
|
|
||||||
for file in bead_files:
|
|
||||||
with Imread(file) as im:
|
|
||||||
n_channels = max(n_channels, im.shape['c'])
|
|
||||||
with IJTiffFile(tiffile) as tif:
|
|
||||||
for t, file in enumerate(bead_files):
|
|
||||||
with Imread(file) as im:
|
|
||||||
with Imread(file).with_transform() as jm:
|
|
||||||
for c in range(im.shape['c']):
|
|
||||||
tif.save(np.hstack((im(c=c, t=0).max('z'), jm(c=c, t=0).max('z'))), c, 0, t)
|
|
||||||
|
|
||||||
def with_drift(self, im):
|
|
||||||
""" Calculate shifts relative to the first frame
|
|
||||||
divide the sequence into groups,
|
|
||||||
compare each frame to the frame in the middle of the group and compare these middle frames to each other
|
|
||||||
"""
|
|
||||||
im = im.transpose('tzycx')
|
|
||||||
t_groups = [list(chunk) for chunk in Chunks(range(im.shape['t']), size=round(np.sqrt(im.shape['t'])))]
|
|
||||||
t_keys = [int(np.round(np.mean(t_group))) for t_group in t_groups]
|
|
||||||
t_pairs = [(int(np.round(np.mean(t_group))), frame) for t_group in t_groups for frame in t_group]
|
|
||||||
t_pairs.extend(zip(t_keys, t_keys[1:]))
|
|
||||||
fmaxz_keys = {t_key: filters.gaussian(im[t_key].max('z'), 5) for t_key in t_keys}
|
|
||||||
|
|
||||||
def fun(t_key_t, im, fmaxz_keys):
|
|
||||||
t_key, t = t_key_t
|
|
||||||
if t_key == t:
|
|
||||||
return 0, 0
|
|
||||||
else:
|
|
||||||
fmaxz = filters.gaussian(im[t].max('z'), 5)
|
|
||||||
return Transform.register(fmaxz_keys[t_key], fmaxz, 'translation').parameters[4:]
|
|
||||||
|
|
||||||
shifts = np.array(pmap(fun, t_pairs, (im, fmaxz_keys), desc='Calculating image shifts.'))
|
|
||||||
shift_keys_cum = np.zeros(2)
|
|
||||||
for shift_keys, t_group in zip(np.vstack((-shifts[0], shifts[im.shape['t']:])), t_groups):
|
|
||||||
shift_keys_cum += shift_keys
|
|
||||||
shifts[t_group] += shift_keys_cum
|
|
||||||
|
|
||||||
for i, shift in enumerate(shifts[:im.shape['t']]):
|
|
||||||
self[i] = Transform.from_shift(shift)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Transform:
|
|
||||||
def __init__(self):
|
|
||||||
if sitk is None:
|
|
||||||
self.transform = None
|
|
||||||
else:
|
|
||||||
self.transform = sitk.ReadTransform(str(Path(__file__).parent / 'transform.txt'))
|
|
||||||
self.dparameters = [0., 0., 0., 0., 0., 0.]
|
|
||||||
self.shape = [512., 512.]
|
|
||||||
self.origin = [255.5, 255.5]
|
|
||||||
self._last, self._inverse = None, None
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
return self.from_dict, (self.asdict(),)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.asdict().__repr__()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.asdict().__str__()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register(cls, fix, mov, kind=None):
|
|
||||||
""" kind: 'affine', 'translation', 'rigid' """
|
|
||||||
if sitk is None:
|
|
||||||
raise ImportError('SimpleElastix is not installed: '
|
|
||||||
'https://simpleelastix.readthedocs.io/GettingStarted.html')
|
|
||||||
new = cls()
|
|
||||||
kind = kind or 'affine'
|
|
||||||
new.shape = fix.shape
|
|
||||||
fix, mov = new.cast_image(fix), new.cast_image(mov)
|
|
||||||
# TODO: implement RigidTransform
|
|
||||||
tfilter = sitk.ElastixImageFilter()
|
|
||||||
tfilter.LogToConsoleOff()
|
|
||||||
tfilter.SetFixedImage(fix)
|
|
||||||
tfilter.SetMovingImage(mov)
|
|
||||||
tfilter.SetParameterMap(sitk.GetDefaultParameterMap(kind))
|
|
||||||
tfilter.Execute()
|
|
||||||
transform = tfilter.GetTransformParameterMap()[0]
|
|
||||||
if kind == 'affine':
|
|
||||||
new.parameters = [float(t) for t in transform['TransformParameters']]
|
|
||||||
new.shape = [float(t) for t in transform['Size']]
|
|
||||||
new.origin = [float(t) for t in transform['CenterOfRotationPoint']]
|
|
||||||
elif kind == 'translation':
|
|
||||||
new.parameters = [1.0, 0.0, 0.0, 1.0] + [float(t) for t in transform['TransformParameters']]
|
|
||||||
new.shape = [float(t) for t in transform['Size']]
|
|
||||||
new.origin = [(t - 1) / 2 for t in new.shape]
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(f'{kind} tranforms not implemented (yet)')
|
|
||||||
new.dparameters = 6 * [np.nan]
|
|
||||||
return new
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_shift(cls, shift):
|
|
||||||
return cls.from_array(np.array(((1, 0, shift[0]), (0, 1, shift[1]), (0, 0, 1))))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_array(cls, array):
|
|
||||||
new = cls()
|
|
||||||
new.matrix = array
|
|
||||||
return new
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_file(cls, file):
|
|
||||||
with open(Path(file).with_suffix('.yml')) as f:
|
|
||||||
return cls.from_dict(yamlload(f))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, d):
|
|
||||||
new = cls()
|
|
||||||
new.origin = [float(i) for i in d['CenterOfRotationPoint']]
|
|
||||||
new.parameters = [float(i) for i in d['TransformParameters']]
|
|
||||||
new.dparameters = [float(i) for i in d['dTransformParameters']] if 'dTransformParameters' in d else 6 * [np.nan]
|
|
||||||
new.shape = [float(i) for i in d['Size']]
|
|
||||||
return new
|
|
||||||
|
|
||||||
def __mul__(self, other): # TODO: take care of dmatrix
|
|
||||||
result = self.copy()
|
|
||||||
if isinstance(other, Transform):
|
|
||||||
result.matrix = self.matrix @ other.matrix
|
|
||||||
result.dmatrix = self.dmatrix @ other.matrix + self.matrix @ other.dmatrix
|
|
||||||
else:
|
|
||||||
result.matrix = self.matrix @ other
|
|
||||||
result.dmatrix = self.dmatrix @ other
|
|
||||||
return result
|
|
||||||
|
|
||||||
def is_unity(self):
|
|
||||||
return self.parameters == [1, 0, 0, 1, 0, 0]
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return deepcopy(self)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def cast_image(im):
|
|
||||||
if not isinstance(im, sitk.Image):
|
|
||||||
im = sitk.GetImageFromArray(np.asarray(im))
|
|
||||||
return im
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def cast_array(im):
|
|
||||||
if isinstance(im, sitk.Image):
|
|
||||||
im = sitk.GetArrayFromImage(im)
|
|
||||||
return im
|
|
||||||
|
|
||||||
@property
|
|
||||||
def matrix(self):
|
|
||||||
return np.array(((*self.parameters[:2], self.parameters[4]),
|
|
||||||
(*self.parameters[2:4], self.parameters[5]),
|
|
||||||
(0, 0, 1)))
|
|
||||||
|
|
||||||
@matrix.setter
|
|
||||||
def matrix(self, value):
|
|
||||||
value = np.asarray(value)
|
|
||||||
self.parameters = [*value[0, :2], *value[1, :2], *value[:2, 2]]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dmatrix(self):
|
|
||||||
return np.array(((*self.dparameters[:2], self.dparameters[4]),
|
|
||||||
(*self.dparameters[2:4], self.dparameters[5]),
|
|
||||||
(0, 0, 0)))
|
|
||||||
|
|
||||||
@dmatrix.setter
|
|
||||||
def dmatrix(self, value):
|
|
||||||
value = np.asarray(value)
|
|
||||||
self.dparameters = [*value[0, :2], *value[1, :2], *value[:2, 2]]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parameters(self):
|
|
||||||
if self.transform is not None:
|
|
||||||
return list(self.transform.GetParameters())
|
|
||||||
|
|
||||||
@parameters.setter
|
|
||||||
def parameters(self, value):
|
|
||||||
if self.transform is not None:
|
|
||||||
value = np.asarray(value)
|
|
||||||
self.transform.SetParameters(value.tolist())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def origin(self):
|
|
||||||
if self.transform is not None:
|
|
||||||
return self.transform.GetFixedParameters()
|
|
||||||
|
|
||||||
@origin.setter
|
|
||||||
def origin(self, value):
|
|
||||||
if self.transform is not None:
|
|
||||||
value = np.asarray(value)
|
|
||||||
self.transform.SetFixedParameters(value.tolist())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def inverse(self):
|
|
||||||
if self.is_unity():
|
|
||||||
return self
|
|
||||||
if self._last is None or self._last != self.asdict():
|
|
||||||
self._last = self.asdict()
|
|
||||||
self._inverse = Transform.from_dict(self.asdict())
|
|
||||||
self._inverse.transform = self._inverse.transform.GetInverse()
|
|
||||||
self._inverse._last = self._inverse.asdict()
|
|
||||||
self._inverse._inverse = self
|
|
||||||
return self._inverse
|
|
||||||
|
|
||||||
def adapt(self, origin, shape):
|
|
||||||
self.origin -= np.array(origin) + (self.shape - np.array(shape)[:2]) / 2
|
|
||||||
self.shape = shape[:2]
|
|
||||||
|
|
||||||
def asdict(self):
|
|
||||||
return {'CenterOfRotationPoint': self.origin, 'Size': self.shape, 'TransformParameters': self.parameters,
|
|
||||||
'dTransformParameters': np.nan_to_num(self.dparameters, nan=1e99).tolist()}
|
|
||||||
|
|
||||||
def frame(self, im, default=0):
|
|
||||||
if self.is_unity():
|
|
||||||
return im
|
|
||||||
else:
|
|
||||||
if sitk is None:
|
|
||||||
raise ImportError('SimpleElastix is not installed: '
|
|
||||||
'https://simpleelastix.readthedocs.io/GettingStarted.html')
|
|
||||||
dtype = im.dtype
|
|
||||||
im = im.astype('float')
|
|
||||||
intp = sitk.sitkBSpline if np.issubdtype(dtype, np.floating) else sitk.sitkNearestNeighbor
|
|
||||||
return self.cast_array(sitk.Resample(self.cast_image(im), self.transform, intp, default)).astype(dtype)
|
|
||||||
|
|
||||||
def coords(self, array, columns=None):
|
|
||||||
""" Transform coordinates in 2 column numpy array,
|
|
||||||
or in pandas DataFrame or Series objects in columns ['x', 'y']
|
|
||||||
"""
|
|
||||||
if self.is_unity():
|
|
||||||
return array.copy()
|
|
||||||
elif DataFrame is not None and isinstance(array, (DataFrame, Series)):
|
|
||||||
columns = columns or ['x', 'y']
|
|
||||||
array = array.copy()
|
|
||||||
if isinstance(array, DataFrame):
|
|
||||||
array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy()))
|
|
||||||
elif isinstance(array, Series):
|
|
||||||
array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy()))[0]
|
|
||||||
return array
|
|
||||||
else: # somehow we need to use the inverse here to get the same effect as when using self.frame
|
|
||||||
return np.array([self.inverse.transform.TransformPoint(i.tolist()) for i in np.asarray(array)])
|
|
||||||
|
|
||||||
def save(self, file):
|
|
||||||
""" save the parameters of the transform calculated
|
|
||||||
with affine_registration to a yaml file
|
|
||||||
"""
|
|
||||||
if not file[-3:] == 'yml':
|
|
||||||
file += '.yml'
|
|
||||||
with open(file, 'w') as f:
|
|
||||||
yaml.safe_dump(self.asdict(), f, default_flow_style=None)
|
|
||||||
Executable
+106
@@ -0,0 +1,106 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
from importlib.metadata import version
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from numpy.typing import ArrayLike
|
||||||
|
|
||||||
|
os.environ["RUST_BACKTRACE"] = "full"
|
||||||
|
os.environ["COLORBT_SHOW_HIDDEN"] = "1"
|
||||||
|
|
||||||
|
from .ndbioimage_rs import Imread, generate_stub
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .ndbioimage_rs import transforms
|
||||||
|
|
||||||
|
sys.modules["ndbioimage.transforms"] = transforms
|
||||||
|
except (ImportError, ModuleNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .ndbioimage_rs import batch_to_tiff
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ome_metadata # noqa
|
||||||
|
except (ImportError, ModuleNotFoundError):
|
||||||
|
from .ndbioimage_rs import ome_metadata # noqa
|
||||||
|
|
||||||
|
sys.modules["ome_metadata"] = ome_metadata
|
||||||
|
import ome_metadata # noqa
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
__version__ = version(Path(__file__).parent.name)
|
||||||
|
except Exception: # noqa
|
||||||
|
__version__ = "unknown"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(Path(__file__).parent.parent / ".git" / "HEAD") as g:
|
||||||
|
head = g.read().split(":")[1].strip()
|
||||||
|
with open(Path(__file__).parent.parent / ".git" / head) as h:
|
||||||
|
__git_commit_hash__ = h.read().rstrip("\n")
|
||||||
|
except Exception: # noqa
|
||||||
|
__git_commit_hash__ = "unknown"
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore", "Reference to unknown ID")
|
||||||
|
Number = int | float | np.integer | np.floating
|
||||||
|
|
||||||
|
|
||||||
|
# backwards compatibility
|
||||||
|
class JVMException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# backwards compatibility
|
||||||
|
class ReaderNotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def ndbioimage_generate_stub():
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
path = Path(sys.argv[1]).resolve()
|
||||||
|
else:
|
||||||
|
path = Path.cwd().resolve()
|
||||||
|
nd = path / "py" / "ndbioimage"
|
||||||
|
if (nd / "__init__.pyi").exists():
|
||||||
|
(nd / "__init__.pyi").rename(nd / "ndbioimage.pyi")
|
||||||
|
try:
|
||||||
|
if (nd / "__init__.py").exists():
|
||||||
|
generate_stub(str(path)) # noqa
|
||||||
|
else:
|
||||||
|
raise ModuleNotFoundError(str(nd / "__init__.py"))
|
||||||
|
rs = nd / "ndbioimage_rs"
|
||||||
|
rs.mkdir(parents=True, exist_ok=True)
|
||||||
|
(nd / "__init__.pyi").rename(rs / "__init__.pyi")
|
||||||
|
for stub in nd.glob("**/__init__.pyi"):
|
||||||
|
if stub.parent != rs:
|
||||||
|
stub.rename((rs / stub.parent.name).with_suffix(".pyi"))
|
||||||
|
stub.parent.rmdir()
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
if (nd / "ndbioimage.pyi").exists():
|
||||||
|
(nd / "ndbioimage.pyi").rename(nd / "__init__.pyi")
|
||||||
|
|
||||||
|
|
||||||
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
|
||||||
|
def get_positions(path: str | Path) -> set[int]:
|
||||||
|
return Imread.get_available_series(path)
|
||||||
|
|
||||||
|
|
||||||
|
def is_noise(self, volume: ArrayLike = None) -> bool:
|
||||||
|
"""True if volume only has noise"""
|
||||||
|
if volume is None:
|
||||||
|
volume = self
|
||||||
|
fft = np.fft.fftn(volume)
|
||||||
|
corr = np.fft.fftshift(np.fft.ifftn(fft * fft.conj()).real / np.sum(volume**2)) # type: ignore
|
||||||
|
return 1 - corr[tuple([0] * corr.ndim)] < 0.006 # type: ignore
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .ndbioimage_rs import Imread, Shape
|
||||||
|
|
||||||
|
__all__ = ["Imread", "Shape"]
|
||||||
@@ -0,0 +1,782 @@
|
|||||||
|
# This file is automatically generated by pyo3_stub_gen
|
||||||
|
# ruff: noqa: E501, F401, F403, F405
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import numpy.typing
|
||||||
|
|
||||||
|
from . import transforms
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Flags",
|
||||||
|
"Imread",
|
||||||
|
"Shape",
|
||||||
|
"batch_to_tiff",
|
||||||
|
"generate_stub",
|
||||||
|
"main",
|
||||||
|
"transforms",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Flags:
|
||||||
|
r"""
|
||||||
|
https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def c_contiguous(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def f_contiguous(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def owndata(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def writable(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def aligned(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def writebackifcopy(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def fnc(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def forc(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def behaved(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def carray(self) -> builtins.bool: ...
|
||||||
|
@property
|
||||||
|
def farray(self) -> builtins.bool: ...
|
||||||
|
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
|
||||||
|
def __hash__(self) -> builtins.int: ...
|
||||||
|
def __new__(cls) -> Flags: ...
|
||||||
|
def __getnewargs__(self) -> None: ...
|
||||||
|
def __getitem__(self, item: builtins.str) -> builtins.bool: ...
|
||||||
|
|
||||||
|
class Imread:
|
||||||
|
r"""
|
||||||
|
class to read image files, while taking good care of important metadata,
|
||||||
|
currently optimized for .czi files, but can open anything that bioformats can handle
|
||||||
|
path: path to the image file
|
||||||
|
optional:
|
||||||
|
axes: order of axes, default: cztyx, but omitting any axes with lenght 1
|
||||||
|
dtype: datatype to be used when returning frames
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>> im = Imread('/path/to/file.image', axes='czt)
|
||||||
|
>> im
|
||||||
|
<< shows summary
|
||||||
|
>> im.shape
|
||||||
|
<< (15, 26, 1000, 1000)
|
||||||
|
>> im.axes
|
||||||
|
<< 'ztyx'
|
||||||
|
>> plt.imshow(im[1, 0])
|
||||||
|
<< plots frame at position z=1, t=0 (python type indexing)
|
||||||
|
>> plt.imshow(im[:, 0].max('z'))
|
||||||
|
<< plots max-z projection at t=0
|
||||||
|
>> im.pxsize
|
||||||
|
<< 0.09708737864077668 image-plane pixel size in um
|
||||||
|
>> im.laserwavelengths
|
||||||
|
<< [642, 488]
|
||||||
|
>> im.laserpowers
|
||||||
|
<< [0.02, 0.0005] in %
|
||||||
|
|
||||||
|
TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var
|
||||||
|
"""
|
||||||
|
|
||||||
|
available_readers: builtins.list[builtins.str]
|
||||||
|
@property
|
||||||
|
def flags(self) -> Flags:
|
||||||
|
r"""
|
||||||
|
https://numpy.org/doc/stable/reference/generated/numpy.ndarray.flags.html
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def reader_name(self) -> builtins.str:
|
||||||
|
r"""
|
||||||
|
the name of the reader used to open the file
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def transforms(self) -> transforms.Transforms:
|
||||||
|
r"""
|
||||||
|
get the transformation
|
||||||
|
"""
|
||||||
|
@transforms.setter
|
||||||
|
def transforms(self, value: transforms.Transforms) -> None:
|
||||||
|
r"""
|
||||||
|
set the transformation
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def path(self) -> pathlib.Path:
|
||||||
|
r"""
|
||||||
|
the file path
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def series(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
the series in the file
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def axes(self) -> builtins.str:
|
||||||
|
r"""
|
||||||
|
the axes in the view
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def shape(self) -> Shape:
|
||||||
|
r"""
|
||||||
|
the shape of the view
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def slice(self) -> builtins.list[builtins.str]:
|
||||||
|
r"""
|
||||||
|
the current slice applied to the view
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def size(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
the number of pixels in the view
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def ndim(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
the number of dimensions in the view
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def T(self) -> Imread:
|
||||||
|
r"""
|
||||||
|
transposed view (alias for transpose(None))
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def dtype(self) -> numpy.dtype:
|
||||||
|
r"""
|
||||||
|
the numpy dtype of the view
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def z_stack(self) -> builtins.bool:
|
||||||
|
r"""
|
||||||
|
whether the view contains a z-stack (more than one z slice)
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def zstack(self) -> builtins.bool:
|
||||||
|
r"""
|
||||||
|
backwards compatibility
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def time_series(self) -> builtins.bool:
|
||||||
|
r"""
|
||||||
|
whether the view contains a time series (more than one time point)
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def timeseries(self) -> builtins.bool:
|
||||||
|
r"""
|
||||||
|
backwards compatibility
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def pixel_size(self) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
the pixel size in micrometers
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def pxsize_um(self) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
backwards compatibility
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def deltaz_um(self) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
backwards compatibility
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def delta_z(self) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
the z-step size in micrometers
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def time_interval(self) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
the time interval between frames in seconds
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def timeinterval(self) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
backwards compatibility
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def exposuretime_s(self) -> builtins.list[typing.Optional[builtins.float]]:
|
||||||
|
r"""
|
||||||
|
backwards compatibility
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def objective_name(self) -> typing.Optional[builtins.str]:
|
||||||
|
r"""
|
||||||
|
the name of the objective
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def magnification(self) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
the total magnification (objective × tube lens)
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def tube_lens_name(self) -> typing.Optional[builtins.str]:
|
||||||
|
r"""
|
||||||
|
the name of the tube lens
|
||||||
|
"""
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
path: str | pathlib.Path | Imread | bytes,
|
||||||
|
dtype: typing.Optional[numpy.typing.DTypeLike] = None,
|
||||||
|
axes: builtins.str = "cztyx",
|
||||||
|
reader: typing.Optional[builtins.str] = None,
|
||||||
|
) -> Imread:
|
||||||
|
r"""
|
||||||
|
new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64)
|
||||||
|
"""
|
||||||
|
def set_flags(
|
||||||
|
self,
|
||||||
|
write: typing.Optional[builtins.bool] = None,
|
||||||
|
align: typing.Optional[builtins.bool] = None,
|
||||||
|
uic: typing.Optional[builtins.bool] = None,
|
||||||
|
) -> None:
|
||||||
|
r"""
|
||||||
|
https://numpy.org/doc/stable/reference/generated/numpy.ndarray.setflags.html#numpy.ndarray.setflags
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def get_positions(
|
||||||
|
path: str | pathlib.Path | Imread | bytes,
|
||||||
|
) -> builtins.set[builtins.int]:
|
||||||
|
r"""
|
||||||
|
get all available positions (series) in the file
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def kill_vm() -> None:
|
||||||
|
r"""
|
||||||
|
only remains for backwards compatibility
|
||||||
|
"""
|
||||||
|
def reshape(self, order: builtins.str, copy: builtins.bool) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
reshape the view with a new axis order
|
||||||
|
"""
|
||||||
|
def with_transform(
|
||||||
|
self,
|
||||||
|
channels: builtins.bool = True,
|
||||||
|
drift: builtins.bool = False,
|
||||||
|
file: typing.Optional[typing.Any] = None,
|
||||||
|
bead_files: typing.Optional[typing.Any] = None,
|
||||||
|
main_channel: typing.Optional[builtins.int] = None,
|
||||||
|
default_transform: typing.Optional[typing.Sequence[builtins.float]] = None,
|
||||||
|
) -> Imread:
|
||||||
|
r"""
|
||||||
|
return a new view with transformations applied (channel alignment, drift correction)
|
||||||
|
backward compatibility
|
||||||
|
"""
|
||||||
|
def with_transforms(
|
||||||
|
self,
|
||||||
|
channels: builtins.bool = True,
|
||||||
|
drift: builtins.bool = False,
|
||||||
|
file: typing.Optional[typing.Any] = None,
|
||||||
|
bead_files: typing.Optional[typing.Any] = None,
|
||||||
|
main_channel: typing.Optional[builtins.int] = None,
|
||||||
|
default_transform: typing.Optional[typing.Sequence[builtins.float]] = None,
|
||||||
|
) -> Imread:
|
||||||
|
r"""
|
||||||
|
return a new view with transformations applied (channel alignment, drift correction)
|
||||||
|
"""
|
||||||
|
def load_transforms_from_yaml(
|
||||||
|
self, path: builtins.str | os.PathLike | pathlib.Path
|
||||||
|
) -> None: ...
|
||||||
|
def calculate_channel_transforms_2d(
|
||||||
|
self, main_channel: builtins.int
|
||||||
|
) -> builtins.dict[builtins.str, transforms.Transform]: ...
|
||||||
|
def calculate_channel_transforms_3d(
|
||||||
|
self, main_channel: builtins.int
|
||||||
|
) -> builtins.dict[builtins.str, transforms.Transform]: ...
|
||||||
|
def calculate_drift_transforms_2d(self) -> builtins.list[transforms.Transform]: ...
|
||||||
|
def calculate_drift_transforms_3d(self) -> builtins.list[transforms.Transform]: ...
|
||||||
|
def squeeze(self) -> numpy.ndarray | int | float: ...
|
||||||
|
def close(self) -> None:
|
||||||
|
r"""
|
||||||
|
close the file: does nothing as this is handled automatically
|
||||||
|
"""
|
||||||
|
def as_type(self, dtype: numpy.typing.DTypeLike) -> Imread:
|
||||||
|
r"""
|
||||||
|
change the data type of the view: (u)int(8/16/32) or float(32/64)
|
||||||
|
"""
|
||||||
|
def astype(self, dtype: numpy.typing.DTypeLike) -> Imread:
|
||||||
|
r"""
|
||||||
|
change the data type of the view: (u)int(8/16/32) or float(32/64)
|
||||||
|
"""
|
||||||
|
def __getitem__(self, n: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
slice the view and return a new view or a single number
|
||||||
|
"""
|
||||||
|
def __array__(
|
||||||
|
self,
|
||||||
|
dtype: typing.Optional[numpy.typing.DTypeLike] = None,
|
||||||
|
copy: typing.Optional[builtins.bool] = None,
|
||||||
|
) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
convert to a numpy array, optionally with a different dtype
|
||||||
|
"""
|
||||||
|
def __contains__(self, _item: typing.Any) -> builtins.bool:
|
||||||
|
r"""
|
||||||
|
check if an item is contained in the view (not implemented)
|
||||||
|
"""
|
||||||
|
def __lt__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise less than comparison
|
||||||
|
"""
|
||||||
|
def __le__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise less than or equal comparison
|
||||||
|
"""
|
||||||
|
def __eq__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise equality comparison
|
||||||
|
"""
|
||||||
|
def __ne__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise not equal comparison
|
||||||
|
"""
|
||||||
|
def __gt__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise greater than comparison
|
||||||
|
"""
|
||||||
|
def __ge__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise greater than or equal comparison
|
||||||
|
"""
|
||||||
|
def __add__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise addition
|
||||||
|
"""
|
||||||
|
def __radd__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise addition (reflected)
|
||||||
|
"""
|
||||||
|
def __sub__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise subtraction
|
||||||
|
"""
|
||||||
|
def __rsub__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise subtraction (reflected)
|
||||||
|
"""
|
||||||
|
def __mul__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise multiplication
|
||||||
|
"""
|
||||||
|
def __rmul__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise multiplication (reflected)
|
||||||
|
"""
|
||||||
|
def __truediv__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise true division
|
||||||
|
"""
|
||||||
|
def __rtruediv__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise true division (reflected)
|
||||||
|
"""
|
||||||
|
def __floordiv__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise floor division
|
||||||
|
"""
|
||||||
|
def __rfloordiv__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise floor division (reflected)
|
||||||
|
"""
|
||||||
|
def __mod__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise modulo
|
||||||
|
"""
|
||||||
|
def __rmod__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise modulo (reflected)
|
||||||
|
"""
|
||||||
|
def __matmul__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise matrix multiplication
|
||||||
|
"""
|
||||||
|
def __rmatmul__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise matrix multiplication (reflected)
|
||||||
|
"""
|
||||||
|
def __and__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise bitwise AND
|
||||||
|
"""
|
||||||
|
def __rand__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise bitwise AND (reflected)
|
||||||
|
"""
|
||||||
|
def __or__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise bitwise OR
|
||||||
|
"""
|
||||||
|
def __ror__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise bitwise OR (reflected)
|
||||||
|
"""
|
||||||
|
def __xor__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise bitwise XOR
|
||||||
|
"""
|
||||||
|
def __rxor__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise bitwise XOR (reflected)
|
||||||
|
"""
|
||||||
|
def __lshift__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise left shift
|
||||||
|
"""
|
||||||
|
def __rlshift__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise left shift (reflected)
|
||||||
|
"""
|
||||||
|
def __rshift__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise right shift
|
||||||
|
"""
|
||||||
|
def __rrshift__(self, other: typing.Any) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise right shift (reflected)
|
||||||
|
"""
|
||||||
|
def __neg__(self) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise negation
|
||||||
|
"""
|
||||||
|
def __pos__(self) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise positive
|
||||||
|
"""
|
||||||
|
def __abs__(self) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise absolute value
|
||||||
|
"""
|
||||||
|
def __invert__(self) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
element-wise bitwise inversion
|
||||||
|
"""
|
||||||
|
def __enter__(self) -> Imread:
|
||||||
|
r"""
|
||||||
|
context manager entry
|
||||||
|
"""
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: typing.Optional[typing.Any] = None,
|
||||||
|
exc_val: typing.Optional[typing.Any] = None,
|
||||||
|
exc_tb: typing.Optional[typing.Any] = None,
|
||||||
|
) -> None:
|
||||||
|
r"""
|
||||||
|
context manager exit
|
||||||
|
"""
|
||||||
|
def __getnewargs__(self) -> tuple[builtins.list[builtins.int]]:
|
||||||
|
r"""
|
||||||
|
arguments for pickling
|
||||||
|
"""
|
||||||
|
def __copy__(self) -> Imread:
|
||||||
|
r"""
|
||||||
|
shallow copy of the view
|
||||||
|
"""
|
||||||
|
def __deepcopy__(self) -> Imread:
|
||||||
|
r"""
|
||||||
|
deep copy of the view (same as shallow copy for this type)
|
||||||
|
"""
|
||||||
|
def copy(self) -> Imread:
|
||||||
|
r"""
|
||||||
|
create a copy of the view
|
||||||
|
"""
|
||||||
|
def __iter__(self) -> Imread:
|
||||||
|
r"""
|
||||||
|
iterate over the first axis
|
||||||
|
"""
|
||||||
|
def __next__(self) -> typing.Optional[typing.Any]:
|
||||||
|
r"""
|
||||||
|
get the next item in the iteration
|
||||||
|
"""
|
||||||
|
def __len__(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
number of elements in the first axis
|
||||||
|
"""
|
||||||
|
def __repr__(self) -> builtins.str:
|
||||||
|
r"""
|
||||||
|
string representation with a summary of the image
|
||||||
|
"""
|
||||||
|
def __str__(self) -> builtins.str:
|
||||||
|
r"""
|
||||||
|
the file path as a string
|
||||||
|
"""
|
||||||
|
def get_frame(
|
||||||
|
self, c: builtins.int, z: builtins.int, t: builtins.int
|
||||||
|
) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
retrieve a single frame at czt, sliced accordingly
|
||||||
|
"""
|
||||||
|
def flatten(self) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
flatten the view into a 1D numpy array
|
||||||
|
"""
|
||||||
|
def to_bytes(self) -> builtins.list[builtins.int]:
|
||||||
|
r"""
|
||||||
|
convert the view to bytes
|
||||||
|
"""
|
||||||
|
def tobytes(self) -> builtins.list[builtins.int]:
|
||||||
|
r"""
|
||||||
|
convert the view to bytes (alias for to_bytes)
|
||||||
|
"""
|
||||||
|
def get_ax(self, axis: int | str) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
find the position of an axis
|
||||||
|
"""
|
||||||
|
def swap_axes(self, ax0: int | str, ax1: int | str) -> Imread:
|
||||||
|
r"""
|
||||||
|
swap two axes
|
||||||
|
"""
|
||||||
|
def transpose(
|
||||||
|
self, axes: typing.Optional[typing.Sequence[int | str]] = None
|
||||||
|
) -> Imread:
|
||||||
|
r"""
|
||||||
|
permute the order of the axes
|
||||||
|
"""
|
||||||
|
def as_array(self) -> typing.Any:
|
||||||
|
r"""
|
||||||
|
collect data into a numpy array
|
||||||
|
"""
|
||||||
|
def exposure_time(self, channel: builtins.int) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
the exposure time for a given channel in seconds
|
||||||
|
"""
|
||||||
|
def binning(self, channel: builtins.int) -> typing.Optional[builtins.int]:
|
||||||
|
r"""
|
||||||
|
the binning for a given channel
|
||||||
|
"""
|
||||||
|
def laser_wavelengths(
|
||||||
|
self, channel: builtins.int
|
||||||
|
) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
the laser wavelength for a given channel in nanometers
|
||||||
|
"""
|
||||||
|
def laser_power(self, channel: builtins.int) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
the laser power for a given channel as a fraction
|
||||||
|
"""
|
||||||
|
def filter_set_name(self, channel: builtins.int) -> typing.Optional[builtins.str]:
|
||||||
|
r"""
|
||||||
|
the name of the filter set for a given channel
|
||||||
|
"""
|
||||||
|
def gain(self, channel: builtins.int) -> typing.Optional[builtins.float]:
|
||||||
|
r"""
|
||||||
|
the detector gain for a given channel
|
||||||
|
"""
|
||||||
|
def summary(self) -> builtins.str:
|
||||||
|
r"""
|
||||||
|
gives a helpful summary of the recorded experiment
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def get_available_series(
|
||||||
|
path: str | pathlib.Path | Imread, reader: typing.Optional[builtins.str] = None
|
||||||
|
) -> builtins.set[builtins.int]:
|
||||||
|
r"""
|
||||||
|
get all series contained in the file
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def get_available_positions(
|
||||||
|
path: str | pathlib.Path | Imread,
|
||||||
|
series: builtins.int,
|
||||||
|
reader: typing.Optional[builtins.str] = None,
|
||||||
|
) -> builtins.set[builtins.int]:
|
||||||
|
r"""
|
||||||
|
get all series contained in the file
|
||||||
|
"""
|
||||||
|
def save_as_tiff(
|
||||||
|
self,
|
||||||
|
file: builtins.str | os.PathLike | pathlib.Path,
|
||||||
|
colors: typing.Optional[typing.Sequence[builtins.str]] = None,
|
||||||
|
overwrite: builtins.bool = False,
|
||||||
|
bar: builtins.bool = True,
|
||||||
|
) -> None:
|
||||||
|
r"""
|
||||||
|
save the view as a TIFF file
|
||||||
|
"""
|
||||||
|
def save_as_movie(
|
||||||
|
self,
|
||||||
|
file: builtins.str | os.PathLike | pathlib.Path,
|
||||||
|
speed: builtins.float = 1.0,
|
||||||
|
brightness: typing.Optional[typing.Sequence[builtins.float]] = None,
|
||||||
|
scale: builtins.float = 1.0,
|
||||||
|
colors: typing.Optional[typing.Sequence[builtins.str]] = None,
|
||||||
|
overwrite: builtins.bool = False,
|
||||||
|
register: builtins.bool = False,
|
||||||
|
no_scaling: builtins.bool = False,
|
||||||
|
) -> None:
|
||||||
|
r"""
|
||||||
|
save the view as a movie file (MP4)
|
||||||
|
"""
|
||||||
|
def set_cache_size(self, size: builtins.int) -> None:
|
||||||
|
r"""
|
||||||
|
backwards compatibility
|
||||||
|
"""
|
||||||
|
def __pow__(
|
||||||
|
self,
|
||||||
|
other: Imread | numpy.typing.NDArray | int | float,
|
||||||
|
mod: typing.Any = None,
|
||||||
|
/,
|
||||||
|
) -> numpy.typing.NDArray: ...
|
||||||
|
def __rpow__(
|
||||||
|
self,
|
||||||
|
other: Imread | numpy.typing.NDArray | int | float,
|
||||||
|
mod: typing.Any = None,
|
||||||
|
/,
|
||||||
|
) -> numpy.typing.NDArray: ...
|
||||||
|
def max(
|
||||||
|
self,
|
||||||
|
axis: int | str = None,
|
||||||
|
dtype: typing.Optional[numpy.typing.DTypeLike] = None,
|
||||||
|
out: typing.Any = None,
|
||||||
|
keepdims: bool = False,
|
||||||
|
initial: int | float = None,
|
||||||
|
where: bool = True,
|
||||||
|
) -> Imread | numpy.typing.NDArray | int | float:
|
||||||
|
r"""
|
||||||
|
Return the maximum along a given axis. Arguments beyond axis are not implemented
|
||||||
|
"""
|
||||||
|
def min(
|
||||||
|
self,
|
||||||
|
axis: int | str = None,
|
||||||
|
dtype: typing.Optional[numpy.typing.DTypeLike] = None,
|
||||||
|
out: typing.Any = None,
|
||||||
|
keepdims: bool = False,
|
||||||
|
initial: int | float = None,
|
||||||
|
where: bool = True,
|
||||||
|
) -> Imread | numpy.typing.NDArray | int | float:
|
||||||
|
r"""
|
||||||
|
Return the minimum along a given axis. Arguments beyond axis are not implemented
|
||||||
|
"""
|
||||||
|
def mean(
|
||||||
|
self,
|
||||||
|
axis: int | str = None,
|
||||||
|
dtype: typing.Optional[numpy.typing.DTypeLike] = None,
|
||||||
|
out: typing.Any = None,
|
||||||
|
keepdims: bool = False,
|
||||||
|
) -> Imread | numpy.typing.NDArray | int | float:
|
||||||
|
r"""
|
||||||
|
Return the mean along a given axis. Arguments beyond axis are not implemented
|
||||||
|
"""
|
||||||
|
def sum(
|
||||||
|
self,
|
||||||
|
axis: int | str = None,
|
||||||
|
dtype: typing.Optional[numpy.typing.DTypeLike] = None,
|
||||||
|
out: typing.Any = None,
|
||||||
|
keepdims: bool = False,
|
||||||
|
initial: int | float = None,
|
||||||
|
where: bool = True,
|
||||||
|
) -> Imread | numpy.typing.NDArray | int | float:
|
||||||
|
r"""
|
||||||
|
Return the sum along a given axis. Arguments beyond axis are not implemented
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Shape:
|
||||||
|
r"""
|
||||||
|
represents the shape of an image with named dimensions (c, z, t, y, x)
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def c(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
the number of channels
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def z(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
the number of z slices
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def t(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
the number of time points
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def y(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
the number of pixels along y
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def x(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
the number of pixels along x
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def axes(self) -> builtins.str:
|
||||||
|
r"""
|
||||||
|
the axis order as a string (e.g., "CZTYX")
|
||||||
|
"""
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
order: builtins.str,
|
||||||
|
c: builtins.int = 1,
|
||||||
|
z: builtins.int = 1,
|
||||||
|
t: builtins.int = 1,
|
||||||
|
y: builtins.int = 1,
|
||||||
|
x: builtins.int = 1,
|
||||||
|
) -> Shape:
|
||||||
|
r"""
|
||||||
|
create a new shape with the given dimensions
|
||||||
|
"""
|
||||||
|
def __str__(self) -> builtins.str:
|
||||||
|
r"""
|
||||||
|
string representation
|
||||||
|
"""
|
||||||
|
def __repr__(self) -> builtins.str:
|
||||||
|
r"""
|
||||||
|
detailed representation for debugging
|
||||||
|
"""
|
||||||
|
def __getnewargs__(
|
||||||
|
self,
|
||||||
|
) -> tuple[
|
||||||
|
builtins.str,
|
||||||
|
builtins.int,
|
||||||
|
builtins.int,
|
||||||
|
builtins.int,
|
||||||
|
builtins.int,
|
||||||
|
builtins.int,
|
||||||
|
]:
|
||||||
|
r"""
|
||||||
|
arguments for pickling
|
||||||
|
"""
|
||||||
|
def __getitem__(
|
||||||
|
self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int]
|
||||||
|
) -> typing.Optional[int | tuple[int]]:
|
||||||
|
r"""
|
||||||
|
get dimension size by index or axis name
|
||||||
|
"""
|
||||||
|
def __len__(self) -> builtins.int:
|
||||||
|
r"""
|
||||||
|
number of dimensions in the shape
|
||||||
|
"""
|
||||||
|
def to_list(self) -> builtins.list[builtins.int]:
|
||||||
|
r"""
|
||||||
|
convert shape to a list of dimension sizes in order
|
||||||
|
"""
|
||||||
|
|
||||||
|
def batch_to_tiff(
|
||||||
|
files_in: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
|
||||||
|
files_out: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
|
||||||
|
operations: typing.Optional[
|
||||||
|
typing.Sequence[tuple[builtins.str, builtins.str]]
|
||||||
|
] = None,
|
||||||
|
colors: typing.Optional[typing.Sequence[builtins.str]] = None,
|
||||||
|
overwrite: builtins.bool = False,
|
||||||
|
bar: builtins.bool = True,
|
||||||
|
message: typing.Optional[builtins.str] = None,
|
||||||
|
) -> None:
|
||||||
|
r"""
|
||||||
|
batch convert multiple image files to TIFF format
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generate_stub(dest_path: builtins.str) -> None:
|
||||||
|
r"""
|
||||||
|
generates ndbioimage/__init__.pyi
|
||||||
|
"""
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
r"""
|
||||||
|
main entry point for the command-line interface
|
||||||
|
"""
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
# This file is automatically generated by pyo3_stub_gen
|
||||||
|
# ruff: noqa: E501, F401, F403, F405
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import numpy.typing
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Optimizer",
|
||||||
|
"RegistrationStep",
|
||||||
|
"SamplingArg",
|
||||||
|
"Sigma",
|
||||||
|
"Transform",
|
||||||
|
"Transforms",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Optimizer:
|
||||||
|
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
|
||||||
|
def __new__(cls, optimizer: builtins.str) -> Optimizer: ...
|
||||||
|
def __getnewargs__(self) -> tuple[builtins.str]: ...
|
||||||
|
@staticmethod
|
||||||
|
def lbfgs() -> Optimizer: ...
|
||||||
|
@staticmethod
|
||||||
|
def asgd() -> Optimizer: ...
|
||||||
|
|
||||||
|
class RegistrationStep:
|
||||||
|
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
sigma: Sigma,
|
||||||
|
sample: SamplingArg,
|
||||||
|
n_bins: builtins.int,
|
||||||
|
tolerance: builtins.float,
|
||||||
|
edge: builtins.float,
|
||||||
|
max_iterations: builtins.int,
|
||||||
|
learning_rate: builtins.float,
|
||||||
|
optimizer: Optimizer,
|
||||||
|
) -> RegistrationStep: ...
|
||||||
|
def __getnewargs__(
|
||||||
|
self,
|
||||||
|
) -> tuple[
|
||||||
|
Sigma,
|
||||||
|
SamplingArg,
|
||||||
|
builtins.int,
|
||||||
|
builtins.float,
|
||||||
|
builtins.float,
|
||||||
|
builtins.int,
|
||||||
|
builtins.float,
|
||||||
|
Optimizer,
|
||||||
|
]: ...
|
||||||
|
@staticmethod
|
||||||
|
def default_steps(
|
||||||
|
ndim: builtins.int, n: builtins.int
|
||||||
|
) -> builtins.list[RegistrationStep]: ...
|
||||||
|
|
||||||
|
class SamplingArg:
|
||||||
|
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
fixed: typing.Optional[builtins.int] = None,
|
||||||
|
fixed_at: typing.Optional[
|
||||||
|
typing.Sequence[typing.Sequence[builtins.float]]
|
||||||
|
] = None,
|
||||||
|
random: typing.Optional[builtins.int] = None,
|
||||||
|
) -> SamplingArg: ...
|
||||||
|
def __getnewargs__(
|
||||||
|
self,
|
||||||
|
) -> tuple[
|
||||||
|
typing.Optional[builtins.int],
|
||||||
|
typing.Optional[builtins.list[builtins.list[builtins.float]]],
|
||||||
|
typing.Optional[builtins.int],
|
||||||
|
]: ...
|
||||||
|
@staticmethod
|
||||||
|
def fixed(size: builtins.int) -> SamplingArg: ...
|
||||||
|
@staticmethod
|
||||||
|
def fixed_at(
|
||||||
|
f: typing.Sequence[typing.Sequence[builtins.float]],
|
||||||
|
) -> SamplingArg: ...
|
||||||
|
@staticmethod
|
||||||
|
def random(size: builtins.int) -> SamplingArg: ...
|
||||||
|
|
||||||
|
class Sigma:
|
||||||
|
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
absolute: typing.Optional[typing.Sequence[builtins.float]] = None,
|
||||||
|
relative: typing.Optional[typing.Sequence[builtins.float]] = None,
|
||||||
|
) -> Sigma: ...
|
||||||
|
def __getnewargs__(
|
||||||
|
self,
|
||||||
|
) -> tuple[builtins.str, typing.Optional[builtins.list[builtins.float]]]: ...
|
||||||
|
@staticmethod
|
||||||
|
def none() -> Sigma: ...
|
||||||
|
@staticmethod
|
||||||
|
def absolute(sigma: typing.Sequence[builtins.float]) -> Sigma: ...
|
||||||
|
@staticmethod
|
||||||
|
def relative(sigma: typing.Sequence[builtins.float]) -> Sigma: ...
|
||||||
|
def smooth(
|
||||||
|
self, array: numpy.typing.ArrayLike
|
||||||
|
) -> numpy.typing.NDArray[numpy.float64]: ...
|
||||||
|
def sigma(
|
||||||
|
self, shape: typing.Sequence[builtins.int]
|
||||||
|
) -> typing.Optional[builtins.list[builtins.float]]: ...
|
||||||
|
|
||||||
|
class Transform:
|
||||||
|
@property
|
||||||
|
def parameters(self) -> builtins.list[builtins.float]: ...
|
||||||
|
@parameters.setter
|
||||||
|
def parameters(self, value: typing.Sequence[builtins.float]) -> None: ...
|
||||||
|
@property
|
||||||
|
def dparameters(self) -> builtins.list[builtins.float]: ...
|
||||||
|
@dparameters.setter
|
||||||
|
def dparameters(self, value: typing.Sequence[builtins.float]) -> None: ...
|
||||||
|
@property
|
||||||
|
def center(self) -> builtins.list[builtins.float]: ...
|
||||||
|
@center.setter
|
||||||
|
def center(self, value: typing.Sequence[builtins.float]) -> None: ...
|
||||||
|
@property
|
||||||
|
def shape(self) -> builtins.list[builtins.int]: ...
|
||||||
|
@shape.setter
|
||||||
|
def shape(self, value: typing.Sequence[builtins.int]) -> None: ...
|
||||||
|
@property
|
||||||
|
def ndim(self) -> builtins.int: ...
|
||||||
|
@property
|
||||||
|
def matrix(self) -> numpy.typing.NDArray[numpy.float64]: ...
|
||||||
|
@matrix.setter
|
||||||
|
def matrix(self, value: numpy.typing.ArrayLike) -> None: ...
|
||||||
|
@property
|
||||||
|
def dmatrix(self) -> numpy.typing.NDArray[numpy.float64]: ...
|
||||||
|
@dmatrix.setter
|
||||||
|
def dmatrix(self, value: numpy.typing.ArrayLike) -> None: ...
|
||||||
|
@property
|
||||||
|
def inverse(self) -> Transform: ...
|
||||||
|
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
parameters: typing.Optional[typing.Sequence[builtins.float]] = None,
|
||||||
|
shape: typing.Optional[typing.Sequence[builtins.int]] = None,
|
||||||
|
center: typing.Optional[typing.Sequence[builtins.float]] = None,
|
||||||
|
) -> Transform: ...
|
||||||
|
def __getnewargs__(
|
||||||
|
self,
|
||||||
|
) -> tuple[
|
||||||
|
builtins.list[builtins.float],
|
||||||
|
builtins.list[builtins.int],
|
||||||
|
typing.Optional[builtins.list[builtins.float]],
|
||||||
|
]: ...
|
||||||
|
def __getstate__(self) -> builtins.list[builtins.float]: ...
|
||||||
|
def __setstate__(self, state: typing.Sequence[builtins.float]) -> None: ...
|
||||||
|
def __add__(self, other: Transform) -> Transform: ...
|
||||||
|
def __radd__(self, other: Transform) -> Transform: ...
|
||||||
|
def __sub__(self, other: Transform) -> Transform: ...
|
||||||
|
def __rsub__(self, other: Transform) -> Transform: ...
|
||||||
|
def __mul__(self, other: Transform | float) -> Transform: ...
|
||||||
|
def __rmul__(self, other: Transform | float) -> Transform: ...
|
||||||
|
def __truediv__(self, other: builtins.float) -> Transform: ...
|
||||||
|
def adapt(
|
||||||
|
self,
|
||||||
|
center: typing.Sequence[builtins.float],
|
||||||
|
shape: typing.Sequence[builtins.int],
|
||||||
|
) -> None: ...
|
||||||
|
@staticmethod
|
||||||
|
def from_scaling(scaling: typing.Sequence[builtins.float]) -> Transform: ...
|
||||||
|
@staticmethod
|
||||||
|
def from_translation(translation: typing.Sequence[builtins.float]) -> Transform: ...
|
||||||
|
@staticmethod
|
||||||
|
def from_rotation(
|
||||||
|
theta: builtins.float, center: typing.Sequence[builtins.float]
|
||||||
|
) -> Transform: ...
|
||||||
|
def with_scaling(self, scaling: typing.Sequence[builtins.float]) -> Transform: ...
|
||||||
|
def with_translation(
|
||||||
|
self, translation: typing.Sequence[builtins.float]
|
||||||
|
) -> Transform: ...
|
||||||
|
def with_rotation(
|
||||||
|
self, theta: builtins.float, center: typing.Sequence[builtins.float]
|
||||||
|
) -> Transform: ...
|
||||||
|
def interpolate(
|
||||||
|
self, order: builtins.int, image: numpy.typing.ArrayLike
|
||||||
|
) -> numpy.typing.NDArray[numpy.float64]: ...
|
||||||
|
def interpolate_par(
|
||||||
|
self, order: builtins.int, image: numpy.typing.ArrayLike
|
||||||
|
) -> numpy.typing.NDArray[numpy.float64]: ...
|
||||||
|
def is_unity(self) -> builtins.bool: ...
|
||||||
|
def transform_point(
|
||||||
|
self, point: numpy.typing.ArrayLike
|
||||||
|
) -> numpy.typing.NDArray[numpy.float64]: ...
|
||||||
|
def transform_points(
|
||||||
|
self, points: numpy.typing.ArrayLike
|
||||||
|
) -> numpy.typing.NDArray[numpy.float64]: ...
|
||||||
|
@staticmethod
|
||||||
|
def register(
|
||||||
|
fixed: numpy.typing.ArrayLike,
|
||||||
|
moving: numpy.typing.ArrayLike,
|
||||||
|
fixed_mu: typing.Sequence[typing.Optional[builtins.float]],
|
||||||
|
initial_guess: typing.Optional[typing.Sequence[builtins.float]] = None,
|
||||||
|
) -> Transform: ...
|
||||||
|
@staticmethod
|
||||||
|
def register_affine(
|
||||||
|
fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike
|
||||||
|
) -> Transform: ...
|
||||||
|
@staticmethod
|
||||||
|
def register_translation(
|
||||||
|
fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike
|
||||||
|
) -> Transform: ...
|
||||||
|
|
||||||
|
class Transforms:
|
||||||
|
@property
|
||||||
|
def channel_transforms(self) -> builtins.dict[builtins.str, Transform]: ...
|
||||||
|
@channel_transforms.setter
|
||||||
|
def channel_transforms(
|
||||||
|
self, value: typing.Mapping[builtins.str, Transform]
|
||||||
|
) -> None: ...
|
||||||
|
@property
|
||||||
|
def drift_transforms(self) -> builtins.list[Transform]: ...
|
||||||
|
@drift_transforms.setter
|
||||||
|
def drift_transforms(self, value: typing.Sequence[Transform]) -> None: ...
|
||||||
|
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
|
||||||
|
def __new__(
|
||||||
|
cls,
|
||||||
|
channel: typing.Mapping[builtins.str, Transform] = {},
|
||||||
|
drift: typing.Sequence[Transform] = [],
|
||||||
|
) -> Transforms: ...
|
||||||
|
def __getnewargs__(
|
||||||
|
self,
|
||||||
|
) -> tuple[builtins.dict[builtins.str, Transform], builtins.list[Transform]]: ...
|
||||||
|
@staticmethod
|
||||||
|
def load(path: builtins.str | os.PathLike | pathlib.Path) -> Transforms: ...
|
||||||
|
def save(self, path: builtins.str | os.PathLike | pathlib.Path) -> None: ...
|
||||||
|
@staticmethod
|
||||||
|
def calculate_channel_transforms_2d(
|
||||||
|
bead_files: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
|
||||||
|
main_channel: builtins.int,
|
||||||
|
default_transform: typing.Optional[Transform],
|
||||||
|
) -> builtins.dict[builtins.str, Transform]: ...
|
||||||
|
@staticmethod
|
||||||
|
def calculate_channel_transforms_3d(
|
||||||
|
bead_files: typing.Sequence[builtins.str | os.PathLike | pathlib.Path],
|
||||||
|
main_channel: builtins.int,
|
||||||
|
default_transform: typing.Optional[Transform],
|
||||||
|
) -> builtins.dict[builtins.str, Transform]: ...
|
||||||
Whitespace-only changes.
@@ -0,0 +1,17 @@
|
|||||||
|
from .ndbioimage_rs.transforms import (
|
||||||
|
Optimizer,
|
||||||
|
RegistrationStep,
|
||||||
|
SamplingArg,
|
||||||
|
Sigma,
|
||||||
|
Transform,
|
||||||
|
Transforms,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Optimizer",
|
||||||
|
"RegistrationStep",
|
||||||
|
"SamplingArg",
|
||||||
|
"Sigma",
|
||||||
|
"Transform",
|
||||||
|
"Transforms",
|
||||||
|
]
|
||||||
+36
-46
@@ -1,54 +1,44 @@
|
|||||||
[tool.poetry]
|
[build-system]
|
||||||
|
requires = ["maturin>=1.9.4,<2.0"]
|
||||||
|
build-backend = "maturin"
|
||||||
|
|
||||||
|
[project]
|
||||||
name = "ndbioimage"
|
name = "ndbioimage"
|
||||||
version = "2025.1.2"
|
version = "2027.0.5"
|
||||||
description = "Bio image reading, metadata and some affine registration."
|
requires-python = ">=3.10"
|
||||||
authors = ["W. Pomp <w.pomp@nki.nl>"]
|
classifiers = [
|
||||||
license = "GPLv3"
|
"License :: OSI Approved :: MIT License",
|
||||||
readme = "README.md"
|
"Programming Language :: Rust",
|
||||||
keywords = ["bioformats", "imread", "numpy", "metadata"]
|
"Programming Language :: Python",
|
||||||
include = ["transform.txt"]
|
"Programming Language :: Python :: 3",
|
||||||
repository = "https://github.com/wimpomp/ndbioimage"
|
]
|
||||||
exclude = ["ndbioimage/jars"]
|
dynamic = [
|
||||||
|
"description",
|
||||||
[tool.poetry.dependencies]
|
"readme",
|
||||||
python = "^3.10"
|
"license",
|
||||||
numpy = ">=1.20.0"
|
"license-files",
|
||||||
pandas = "*"
|
"authors",
|
||||||
tifffile = "*"
|
"maintainers",
|
||||||
czifile = "2019.7.2"
|
"keywords",
|
||||||
tiffwrite = ">=2024.12.1"
|
"urls",
|
||||||
ome-types = ">=0.4.0"
|
]
|
||||||
pint = "*"
|
dependencies = [
|
||||||
tqdm = "*"
|
"numpy >= 1.16.0",
|
||||||
lxml = "*"
|
|
||||||
pyyaml = "*"
|
|
||||||
parfor = ">=2025.1.0"
|
|
||||||
JPype1 = "*"
|
|
||||||
SimpleITK-SimpleElastix = [
|
|
||||||
{ version = "*", python = "<3.12" },
|
|
||||||
{ version = "*", python = ">=3.12", markers = "sys_platform != 'darwin'" },
|
|
||||||
{ version = "*", python = ">=3.12", markers = "platform_machine == 'aarch64'" },
|
|
||||||
]
|
]
|
||||||
scikit-image = "*"
|
|
||||||
imagecodecs = "*"
|
|
||||||
xsdata = "^23" # until pydantic is up-to-date
|
|
||||||
matplotlib = { version = "*", optional = true }
|
|
||||||
scikit-video = { version = "*", optional = true }
|
|
||||||
pytest = { version = "*", optional = true }
|
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[project.optional-dependencies]
|
||||||
test = ["pytest"]
|
test = ["pytest", "tiffwrite"]
|
||||||
write = ["matplotlib", "scikit-video"]
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[project.scripts]
|
||||||
ndbioimage = "ndbioimage:main"
|
ndbioimage = "ndbioimage:ndbioimage_rs.main"
|
||||||
|
ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.maturin]
|
||||||
filterwarnings = ["ignore:::(colorcet)"]
|
python-source = "py"
|
||||||
|
features = ["python", "bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"]
|
||||||
|
no-default-features = true
|
||||||
|
module-name = "ndbioimage.ndbioimage_rs"
|
||||||
|
include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"]
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
line_length = 119
|
line_length = 119
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
+444
@@ -0,0 +1,444 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use crate::stats::MinMax;
|
||||||
|
use ndarray::{Array, Dimension, Ix2, SliceInfo, SliceInfoElem};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use serde_with::{DeserializeAs, SerializeAs};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::ops::Index;
|
||||||
|
use strum::{AsRefStr, Display, EnumString};
|
||||||
|
|
||||||
|
/// 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, Error>;
|
||||||
|
|
||||||
|
/// 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, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum for CZTYX axes or a new axis
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, Eq, Ord, PartialOrd, Serialize, Deserialize, EnumString, AsRefStr, Display,
|
||||||
|
)]
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
pub enum Axis {
|
||||||
|
/// channel
|
||||||
|
C,
|
||||||
|
/// z slice
|
||||||
|
Z,
|
||||||
|
/// time
|
||||||
|
T,
|
||||||
|
/// y coordinate
|
||||||
|
Y,
|
||||||
|
/// x coordinate
|
||||||
|
X,
|
||||||
|
#[strum(serialize = "N")]
|
||||||
|
/// a new axis created by slicing, can be operated on but has no reader data
|
||||||
|
New,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Axis {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
(*self as usize).hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ax for Axis {
|
||||||
|
fn n(&self) -> usize {
|
||||||
|
*self as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pos(&self, axes: &[Axis], _slice: &[SliceInfoElem]) -> Result<usize, Error> {
|
||||||
|
if let Some(pos) = axes.iter().position(|a| a == self) {
|
||||||
|
Ok(pos)
|
||||||
|
} else {
|
||||||
|
Err(Error::AxisNotFound(
|
||||||
|
format!("{:?}", self),
|
||||||
|
format!("{:?}", axes),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pos_op(
|
||||||
|
&self,
|
||||||
|
axes: &[Axis],
|
||||||
|
_slice: &[SliceInfoElem],
|
||||||
|
_op_axes: &[Axis],
|
||||||
|
) -> Result<usize, Error> {
|
||||||
|
self.pos(axes, _slice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ax for usize {
|
||||||
|
fn n(&self) -> usize {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pos(&self, _axes: &[Axis], slice: &[SliceInfoElem]) -> Result<usize, Error> {
|
||||||
|
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, Error> {
|
||||||
|
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();
|
||||||
|
debug_assert!(*self < idx.len(), "self: {}, idx: {:?}", self, idx);
|
||||||
|
Ok(idx[*self])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Debug, Serialize, Deserialize, EnumString, AsRefStr, Display, PartialEq, Eq, Hash,
|
||||||
|
)]
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
/// an operation to reduce an axis
|
||||||
|
pub enum Operation {
|
||||||
|
/// take the max along the axis
|
||||||
|
Max,
|
||||||
|
/// take the min along the axis
|
||||||
|
Min,
|
||||||
|
/// take the sum along the axis
|
||||||
|
Sum,
|
||||||
|
/// take the mean along the axis
|
||||||
|
Mean,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation {
|
||||||
|
pub(crate) fn operate<T, D>(
|
||||||
|
&self,
|
||||||
|
array: Array<T, D>,
|
||||||
|
axis: usize,
|
||||||
|
) -> Result<<Array<T, D> as MinMax>::Output, Error>
|
||||||
|
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>, Error> {
|
||||||
|
match info.try_into() {
|
||||||
|
Ok(slice) => Ok(slice),
|
||||||
|
Err(err) => Err(Error::TryInto(err.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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) -> 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) -> 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the size of a view along the axes it contains
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct Shape {
|
||||||
|
/// size c (# channels)
|
||||||
|
pub c: usize,
|
||||||
|
/// size z (# slices)
|
||||||
|
pub z: usize,
|
||||||
|
/// size t (# time/frames)
|
||||||
|
pub t: usize,
|
||||||
|
/// size y (vertical)
|
||||||
|
pub y: usize,
|
||||||
|
/// size x (horizontal)
|
||||||
|
pub x: usize,
|
||||||
|
/// the axes that are present in the view, in order
|
||||||
|
pub order: Vec<Axis>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Shape {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
c: 1,
|
||||||
|
z: 1,
|
||||||
|
t: 1,
|
||||||
|
y: 1,
|
||||||
|
x: 1,
|
||||||
|
order: vec![Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Shape {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let order = self
|
||||||
|
.order
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("");
|
||||||
|
let shape = self
|
||||||
|
.order
|
||||||
|
.iter()
|
||||||
|
.map(|i| format!("{}", self[i]))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
write!(f, "{}: {}", order, shape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<Axis> for Shape {
|
||||||
|
type Output = usize;
|
||||||
|
|
||||||
|
fn index(&self, ax: Axis) -> &Self::Output {
|
||||||
|
&self[&ax]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<&Axis> for Shape {
|
||||||
|
type Output = usize;
|
||||||
|
|
||||||
|
fn index(&self, ax: &Axis) -> &Self::Output {
|
||||||
|
match ax {
|
||||||
|
Axis::C => &self.c,
|
||||||
|
Axis::Z => &self.z,
|
||||||
|
Axis::T => &self.t,
|
||||||
|
Axis::Y => &self.y,
|
||||||
|
Axis::X => &self.x,
|
||||||
|
Axis::New => &1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for Shape {
|
||||||
|
type Output = usize;
|
||||||
|
|
||||||
|
fn index(&self, dim: usize) -> &Self::Output {
|
||||||
|
&self[self.order[dim % self.order.len()]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<&usize> for Shape {
|
||||||
|
type Output = usize;
|
||||||
|
|
||||||
|
fn index(&self, dim: &usize) -> &Self::Output {
|
||||||
|
&self[self.order[dim % self.order.len()]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Shape> for Vec<usize> {
|
||||||
|
fn from(shape: Shape) -> Self {
|
||||||
|
shape.order.iter().map(|axis| shape[axis]).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Shape> for HashMap<Axis, usize> {
|
||||||
|
fn from(shape: Shape) -> Self {
|
||||||
|
shape
|
||||||
|
.order
|
||||||
|
.iter()
|
||||||
|
.map(|axis| (*axis, shape[axis]))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iterator over the sizes along the axes of a [`Shape`]
|
||||||
|
pub struct ShapeIter {
|
||||||
|
shape: Shape,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for ShapeIter {
|
||||||
|
type Item = usize;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let r = self.shape[self.shape.order.get(self.index)?];
|
||||||
|
self.index += 1;
|
||||||
|
Some(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iterator over the sizes along the axes of a borrowed [`Shape`]
|
||||||
|
pub struct ShapeIterBorrow<'a> {
|
||||||
|
shape: &'a Shape,
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for ShapeIterBorrow<'a> {
|
||||||
|
type Item = &'a usize;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let r = &self.shape[self.shape.order.get(self.index)?];
|
||||||
|
self.index += 1;
|
||||||
|
Some(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Shape {
|
||||||
|
type Item = usize;
|
||||||
|
type IntoIter = ShapeIter;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
ShapeIter {
|
||||||
|
shape: self,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shape {
|
||||||
|
/// create an empty shape with all sizes set to 1 and no axes
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
c: 1,
|
||||||
|
z: 1,
|
||||||
|
t: 1,
|
||||||
|
y: 1,
|
||||||
|
x: 1,
|
||||||
|
order: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// iterate over the sizes along the axes, in order
|
||||||
|
pub fn iter(&self) -> ShapeIterBorrow<'_> {
|
||||||
|
ShapeIterBorrow {
|
||||||
|
shape: self,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the number of axes in the shape
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.order.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// whether the shape has no axes
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.order.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the sizes along the axes, in order
|
||||||
|
pub fn to_vec(&self) -> Vec<usize> {
|
||||||
|
self.order.iter().map(|axis| self[axis]).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the sizes along the axes as a map from axis to size
|
||||||
|
pub fn to_hashmap(&self) -> HashMap<Axis, usize> {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for axis in self.order.iter() {
|
||||||
|
map.insert(*axis, self[axis]);
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the size of an axis
|
||||||
|
pub fn set_axis(&mut self, axis: &Axis, value: usize) {
|
||||||
|
match axis {
|
||||||
|
Axis::C => self.c = value,
|
||||||
|
Axis::Z => self.z = value,
|
||||||
|
Axis::T => self.t = value,
|
||||||
|
Axis::Y => self.y = value,
|
||||||
|
Axis::X => self.x = value,
|
||||||
|
Axis::New => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+322
@@ -0,0 +1,322 @@
|
|||||||
|
use crate::axes::{Axis, Operation};
|
||||||
|
use indexmap::{Equivalent, IndexMap};
|
||||||
|
use ndarray::{ArrayD, SliceInfoElem};
|
||||||
|
use std::any::Any;
|
||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
|
use crate::readers::Frame;
|
||||||
|
|
||||||
|
/// maximum number of frames held in the cache
|
||||||
|
const DEFAULT_FRAME_CACHE_SIZE: usize = 128;
|
||||||
|
|
||||||
|
/// maximum number of materialized arrays held in the cache
|
||||||
|
const DEFAULT_ARRAY_CACHE_SIZE: usize = 32;
|
||||||
|
|
||||||
|
/// identity of the reader a frame was read from
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub(crate) struct ReaderKey {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) path: PathBuf,
|
||||||
|
pub(crate) series: usize,
|
||||||
|
pub(crate) position: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// borrowed view of [`ReaderKey`] for cache lookups without allocation.
|
||||||
|
/// hashes byte-identically to [`ReaderKey`] (str/String and Path/PathBuf hash
|
||||||
|
/// the same), so it only matches the same reader identity.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct ReaderKeyRef<'a> {
|
||||||
|
pub(crate) name: &'a str,
|
||||||
|
pub(crate) path: &'a Path,
|
||||||
|
pub(crate) series: usize,
|
||||||
|
pub(crate) position: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for ReaderKeyRef<'_> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.name.hash(state);
|
||||||
|
self.path.hash(state);
|
||||||
|
self.series.hash(state);
|
||||||
|
self.position.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Equivalent<ReaderKey> for ReaderKeyRef<'_> {
|
||||||
|
fn equivalent(&self, key: &ReaderKey) -> bool {
|
||||||
|
self.name == key.name
|
||||||
|
&& self.path == key.path.as_path()
|
||||||
|
&& self.series == key.series
|
||||||
|
&& self.position == key.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReaderKeyRef<'_> {
|
||||||
|
pub(crate) fn to_owned(&self) -> ReaderKey {
|
||||||
|
ReaderKey {
|
||||||
|
name: self.name.to_string(),
|
||||||
|
path: self.path.to_path_buf(),
|
||||||
|
series: self.series,
|
||||||
|
position: self.position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type FrameKey = (ReaderKey, usize, usize, usize);
|
||||||
|
|
||||||
|
/// borrowed view of [`FrameKey`] for cache lookups without allocation.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct FrameKeyRef<'a> {
|
||||||
|
pub(crate) reader: ReaderKeyRef<'a>,
|
||||||
|
pub(crate) c: usize,
|
||||||
|
pub(crate) z: usize,
|
||||||
|
pub(crate) t: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for FrameKeyRef<'_> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.reader.hash(state);
|
||||||
|
self.c.hash(state);
|
||||||
|
self.z.hash(state);
|
||||||
|
self.t.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Equivalent<FrameKey> for FrameKeyRef<'_> {
|
||||||
|
fn equivalent(&self, key: &FrameKey) -> bool {
|
||||||
|
let (rk, c, z, t) = key;
|
||||||
|
self.reader.equivalent(rk) && self.c == *c && self.z == *z && self.t == *t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameKeyRef<'_> {
|
||||||
|
pub(crate) fn to_owned(&self) -> FrameKey {
|
||||||
|
(self.reader.to_owned(), self.c, self.z, self.t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// identity of a materialized array in the process-wide cache
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub(crate) struct ArrayKey {
|
||||||
|
pub(crate) reader: ReaderKey,
|
||||||
|
pub(crate) dtype: &'static str,
|
||||||
|
pub(crate) slice: Vec<SliceInfoElem>,
|
||||||
|
pub(crate) axes: Vec<Axis>,
|
||||||
|
pub(crate) operations: Vec<(Axis, Operation)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// borrowed view of [`ArrayKey`] for cache lookups without allocation.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct ArrayKeyRef<'a> {
|
||||||
|
pub(crate) reader: ReaderKeyRef<'a>,
|
||||||
|
pub(crate) dtype: &'static str,
|
||||||
|
pub(crate) slice: &'a [SliceInfoElem],
|
||||||
|
pub(crate) axes: &'a [Axis],
|
||||||
|
pub(crate) operations: &'a IndexMap<Axis, Operation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for ArrayKeyRef<'_> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.reader.hash(state);
|
||||||
|
self.dtype.hash(state);
|
||||||
|
self.slice.hash(state);
|
||||||
|
self.axes.hash(state);
|
||||||
|
self.operations.len().hash(state);
|
||||||
|
for (ax, op) in self.operations.iter() {
|
||||||
|
ax.hash(state);
|
||||||
|
op.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Equivalent<ArrayKey> for ArrayKeyRef<'_> {
|
||||||
|
fn equivalent(&self, key: &ArrayKey) -> bool {
|
||||||
|
self.reader.equivalent(&key.reader)
|
||||||
|
&& self.dtype == key.dtype
|
||||||
|
&& self.slice == key.slice
|
||||||
|
&& self.axes == key.axes
|
||||||
|
&& self.operations.len() == key.operations.len()
|
||||||
|
&& self
|
||||||
|
.operations
|
||||||
|
.iter()
|
||||||
|
.zip(&key.operations)
|
||||||
|
.all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArrayKeyRef<'_> {
|
||||||
|
pub(crate) fn to_owned(&self) -> ArrayKey {
|
||||||
|
ArrayKey {
|
||||||
|
reader: self.reader.to_owned(),
|
||||||
|
dtype: self.dtype,
|
||||||
|
slice: self.slice.to_vec(),
|
||||||
|
axes: self.axes.to_vec(),
|
||||||
|
operations: self
|
||||||
|
.operations
|
||||||
|
.iter()
|
||||||
|
.map(|(ax, op)| (*ax, op.clone()))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// process-wide LRU cache of frames, shared between all views and threads
|
||||||
|
static GLOBAL_FRAME_CACHE: OnceLock<FrameCache> = OnceLock::new();
|
||||||
|
|
||||||
|
/// thread-safe LRU cache of frames read from the underlying reader
|
||||||
|
pub(crate) struct FrameCache {
|
||||||
|
inner: Mutex<FrameCacheInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FrameCacheInner {
|
||||||
|
map: IndexMap<FrameKey, Arc<Frame>>,
|
||||||
|
capacity: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FrameCache {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(DEFAULT_FRAME_CACHE_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameCache {
|
||||||
|
pub(crate) fn new(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Mutex::new(FrameCacheInner {
|
||||||
|
map: IndexMap::with_capacity(capacity),
|
||||||
|
capacity,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn global() -> &'static FrameCache {
|
||||||
|
GLOBAL_FRAME_CACHE.get_or_init(FrameCache::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn capacity(&self) -> usize {
|
||||||
|
self.inner.lock().unwrap().capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_capacity(&self, capacity: usize) {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.capacity = capacity;
|
||||||
|
while inner.map.len() > inner.capacity {
|
||||||
|
inner.map.shift_remove_index(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get<Q>(&self, key: &Q) -> Option<Arc<Frame>>
|
||||||
|
where
|
||||||
|
Q: ?Sized + Hash + Equivalent<FrameKey>,
|
||||||
|
{
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
if let Some(idx) = inner.map.get_index_of(key) {
|
||||||
|
let (key, frame) = inner.map.shift_remove_index(idx).unwrap();
|
||||||
|
inner.map.insert(key, frame.clone());
|
||||||
|
Some(frame)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert(&self, key: FrameKey, frame: Arc<Frame>) {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.map.insert(key, frame);
|
||||||
|
while inner.map.len() > inner.capacity {
|
||||||
|
inner.map.shift_remove_index(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.inner.lock().unwrap().map.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for FrameCache {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("FrameCache").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// process-wide LRU cache of materialized arrays, shared between all views and threads
|
||||||
|
static GLOBAL_ARRAY_CACHE: OnceLock<ArrayCache> = OnceLock::new();
|
||||||
|
|
||||||
|
/// thread-safe LRU cache of materialized arrays produced by `as_array_dyn`
|
||||||
|
pub(crate) struct ArrayCache {
|
||||||
|
inner: Mutex<ArrayCacheInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ArrayCacheInner {
|
||||||
|
map: IndexMap<ArrayKey, Arc<dyn Any + Send + Sync>>,
|
||||||
|
capacity: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ArrayCache {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(DEFAULT_ARRAY_CACHE_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArrayCache {
|
||||||
|
pub(crate) fn new(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Mutex::new(ArrayCacheInner {
|
||||||
|
map: IndexMap::with_capacity(capacity),
|
||||||
|
capacity,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn global() -> &'static ArrayCache {
|
||||||
|
GLOBAL_ARRAY_CACHE.get_or_init(ArrayCache::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn capacity(&self) -> usize {
|
||||||
|
self.inner.lock().unwrap().capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_capacity(&self, capacity: usize) {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.capacity = capacity;
|
||||||
|
while inner.map.len() > inner.capacity {
|
||||||
|
inner.map.shift_remove_index(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get<T, Q>(&self, key: &Q) -> Option<Arc<ArrayD<T>>>
|
||||||
|
where
|
||||||
|
Q: ?Sized + Hash + Equivalent<ArrayKey>,
|
||||||
|
T: Any + Send + Sync,
|
||||||
|
{
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
if let Some(idx) = inner.map.get_index_of(key) {
|
||||||
|
let (key, array) = inner.map.shift_remove_index(idx).unwrap();
|
||||||
|
inner.map.insert(key, array.clone());
|
||||||
|
array.downcast::<ArrayD<T>>().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert<T: Any + Send + Sync>(&self, key: ArrayKey, array: ArrayD<T>) {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
inner.map.insert(key, Arc::new(array));
|
||||||
|
while inner.map.len() > inner.capacity {
|
||||||
|
inner.map.shift_remove_index(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn len(&self) -> usize {
|
||||||
|
self.inner.lock().unwrap().map.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for ArrayCache {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ArrayCache").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use phf::phf_map;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// a static map from color names (like `"red"` or single letters like `"r"`) to hex strings
|
||||||
|
pub static COLORS: phf::Map<&'static str, &'static str> = phf_map! {
|
||||||
|
"b" => "#0000FF",
|
||||||
|
"g" => "#008000",
|
||||||
|
"r" => "#FF0000",
|
||||||
|
"c" => "#00BFBF",
|
||||||
|
"m" => "#BF00BF",
|
||||||
|
"y" => "#BFBF00",
|
||||||
|
"k" => "#000000",
|
||||||
|
"w" => "#FFFFFF",
|
||||||
|
"aliceblue" => "#F0F8FF",
|
||||||
|
"antiquewhite" => "#FAEBD7",
|
||||||
|
"aqua" => "#00FFFF",
|
||||||
|
"aquamarine" => "#7FFFD4",
|
||||||
|
"azure" => "#F0FFFF",
|
||||||
|
"beige" => "#F5F5DC",
|
||||||
|
"bisque" => "#FFE4C4",
|
||||||
|
"black" => "#000000",
|
||||||
|
"blanchedalmond" => "#FFEBCD",
|
||||||
|
"blue" => "#0000FF",
|
||||||
|
"blueviolet" => "#8A2BE2",
|
||||||
|
"brown" => "#A52A2A",
|
||||||
|
"burlywood" => "#DEB887",
|
||||||
|
"cadetblue" => "#5F9EA0",
|
||||||
|
"chartreuse" => "#7FFF00",
|
||||||
|
"chocolate" => "#D2691E",
|
||||||
|
"coral" => "#FF7F50",
|
||||||
|
"cornflowerblue" => "#6495ED",
|
||||||
|
"cornsilk" => "#FFF8DC",
|
||||||
|
"crimson" => "#DC143C",
|
||||||
|
"cyan" => "#00FFFF",
|
||||||
|
"darkblue" => "#00008B",
|
||||||
|
"darkcyan" => "#008B8B",
|
||||||
|
"darkgoldenrod" => "#B8860B",
|
||||||
|
"darkgray" => "#A9A9A9",
|
||||||
|
"darkgreen" => "#006400",
|
||||||
|
"darkgrey" => "#A9A9A9",
|
||||||
|
"darkkhaki" => "#BDB76B",
|
||||||
|
"darkmagenta" => "#8B008B",
|
||||||
|
"darkolivegreen" => "#556B2F",
|
||||||
|
"darkorange" => "#FF8C00",
|
||||||
|
"darkorchid" => "#9932CC",
|
||||||
|
"darkred" => "#8B0000",
|
||||||
|
"darksalmon" => "#E9967A",
|
||||||
|
"darkseagreen" => "#8FBC8F",
|
||||||
|
"darkslateblue" => "#483D8B",
|
||||||
|
"darkslategray" => "#2F4F4F",
|
||||||
|
"darkslategrey" => "#2F4F4F",
|
||||||
|
"darkturquoise" => "#00CED1",
|
||||||
|
"darkviolet" => "#9400D3",
|
||||||
|
"deeppink" => "#FF1493",
|
||||||
|
"deepskyblue" => "#00BFFF",
|
||||||
|
"dimgray" => "#696969",
|
||||||
|
"dimgrey" => "#696969",
|
||||||
|
"dodgerblue" => "#1E90FF",
|
||||||
|
"firebrick" => "#B22222",
|
||||||
|
"floralwhite" => "#FFFAF0",
|
||||||
|
"forestgreen" => "#228B22",
|
||||||
|
"fuchsia" => "#FF00FF",
|
||||||
|
"gainsboro" => "#DCDCDC",
|
||||||
|
"ghostwhite" => "#F8F8FF",
|
||||||
|
"gold" => "#FFD700",
|
||||||
|
"goldenrod" => "#DAA520",
|
||||||
|
"gray" => "#808080",
|
||||||
|
"green" => "#008000",
|
||||||
|
"greenyellow" => "#ADFF2F",
|
||||||
|
"grey" => "#808080",
|
||||||
|
"honeydew" => "#F0FFF0",
|
||||||
|
"hotpink" => "#FF69B4",
|
||||||
|
"indianred" => "#CD5C5C",
|
||||||
|
"indigo" => "#4B0082",
|
||||||
|
"ivory" => "#FFFFF0",
|
||||||
|
"khaki" => "#F0E68C",
|
||||||
|
"lavender" => "#E6E6FA",
|
||||||
|
"lavenderblush" => "#FFF0F5",
|
||||||
|
"lawngreen" => "#7CFC00",
|
||||||
|
"lemonchiffon" => "#FFFACD",
|
||||||
|
"lightblue" => "#ADD8E6",
|
||||||
|
"lightcoral" => "#F08080",
|
||||||
|
"lightcyan" => "#E0FFFF",
|
||||||
|
"lightgoldenrodyellow" => "#FAFAD2",
|
||||||
|
"lightgray" => "#D3D3D3",
|
||||||
|
"lightgreen" => "#90EE90",
|
||||||
|
"lightgrey" => "#D3D3D3",
|
||||||
|
"lightpink" => "#FFB6C1",
|
||||||
|
"lightsalmon" => "#FFA07A",
|
||||||
|
"lightseagreen" => "#20B2AA",
|
||||||
|
"lightskyblue" => "#87CEFA",
|
||||||
|
"lightslategray" => "#778899",
|
||||||
|
"lightslategrey" => "#778899",
|
||||||
|
"lightsteelblue" => "#B0C4DE",
|
||||||
|
"lightyellow" => "#FFFFE0",
|
||||||
|
"lime" => "#00FF00",
|
||||||
|
"limegreen" => "#32CD32",
|
||||||
|
"linen" => "#FAF0E6",
|
||||||
|
"magenta" => "#FF00FF",
|
||||||
|
"maroon" => "#800000",
|
||||||
|
"mediumaquamarine" => "#66CDAA",
|
||||||
|
"mediumblue" => "#0000CD",
|
||||||
|
"mediumorchid" => "#BA55D3",
|
||||||
|
"mediumpurple" => "#9370DB",
|
||||||
|
"mediumseagreen" => "#3CB371",
|
||||||
|
"mediumslateblue" => "#7B68EE",
|
||||||
|
"mediumspringgreen" => "#00FA9A",
|
||||||
|
"mediumturquoise" => "#48D1CC",
|
||||||
|
"mediumvioletred" => "#C71585",
|
||||||
|
"midnightblue" => "#191970",
|
||||||
|
"mintcream" => "#F5FFFA",
|
||||||
|
"mistyrose" => "#FFE4E1",
|
||||||
|
"moccasin" => "#FFE4B5",
|
||||||
|
"navajowhite" => "#FFDEAD",
|
||||||
|
"navy" => "#000080",
|
||||||
|
"oldlace" => "#FDF5E6",
|
||||||
|
"olive" => "#808000",
|
||||||
|
"olivedrab" => "#6B8E23",
|
||||||
|
"orange" => "#FFA500",
|
||||||
|
"orangered" => "#FF4500",
|
||||||
|
"orchid" => "#DA70D6",
|
||||||
|
"palegoldenrod" => "#EEE8AA",
|
||||||
|
"palegreen" => "#98FB98",
|
||||||
|
"paleturquoise" => "#AFEEEE",
|
||||||
|
"palevioletred" => "#DB7093",
|
||||||
|
"papayawhip" => "#FFEFD5",
|
||||||
|
"peachpuff" => "#FFDAB9",
|
||||||
|
"peru" => "#CD853F",
|
||||||
|
"pink" => "#FFC0CB",
|
||||||
|
"plum" => "#DDA0DD",
|
||||||
|
"powderblue" => "#B0E0E6",
|
||||||
|
"purple" => "#800080",
|
||||||
|
"rebeccapurple" => "#663399",
|
||||||
|
"red" => "#FF0000",
|
||||||
|
"rosybrown" => "#BC8F8F",
|
||||||
|
"royalblue" => "#4169E1",
|
||||||
|
"saddlebrown" => "#8B4513",
|
||||||
|
"salmon" => "#FA8072",
|
||||||
|
"sandybrown" => "#F4A460",
|
||||||
|
"seagreen" => "#2E8B57",
|
||||||
|
"seashell" => "#FFF5EE",
|
||||||
|
"sienna" => "#A0522D",
|
||||||
|
"silver" => "#C0C0C0",
|
||||||
|
"skyblue" => "#87CEEB",
|
||||||
|
"slateblue" => "#6A5ACD",
|
||||||
|
"slategray" => "#708090",
|
||||||
|
"slategrey" => "#708090",
|
||||||
|
"snow" => "#FFFAFA",
|
||||||
|
"springgreen" => "#00FF7F",
|
||||||
|
"steelblue" => "#4682B4",
|
||||||
|
"tan" => "#D2B48C",
|
||||||
|
"teal" => "#008080",
|
||||||
|
"thistle" => "#D8BFD8",
|
||||||
|
"tomato" => "#FF6347",
|
||||||
|
"turquoise" => "#40E0D0",
|
||||||
|
"violet" => "#EE82EE",
|
||||||
|
"wheat" => "#F5DEB3",
|
||||||
|
"white" => "#FFFFFF",
|
||||||
|
"whitesmoke" => "#F5F5F5",
|
||||||
|
"yellow" => "#FFFF00",
|
||||||
|
"yellowgreen" => "#9ACD32",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// a color with red, green and blue components, parsed from a hex string (`#RRGGBB`) or a name in [`COLORS`]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Color {
|
||||||
|
r: u8,
|
||||||
|
g: u8,
|
||||||
|
b: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Color {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let s = if !s.starts_with("#") {
|
||||||
|
if let Some(s) = COLORS.get(s) {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
return Err(Error::InvalidColor(s.to_string()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s
|
||||||
|
};
|
||||||
|
let r = u8::from_str_radix(&s[1..3], 16)?;
|
||||||
|
let g = u8::from_str_radix(&s[3..5], 16)?;
|
||||||
|
let b = u8::from_str_radix(&s[5..], 16)?;
|
||||||
|
Ok(Self { r, g, b })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Color {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
/// the color as an RGB vector `[r, g, b]`
|
||||||
|
pub fn to_rgb(&self) -> Vec<u8> {
|
||||||
|
vec![self.r, self.g, self.b]
|
||||||
|
}
|
||||||
|
}
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
use strum::IntoStaticStr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// the error type used throughout the crate
|
||||||
|
#[derive(Debug, Error, IntoStaticStr)]
|
||||||
|
pub enum Error {
|
||||||
|
/// an io error
|
||||||
|
#[error(transparent)]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
/// an ndarray shape error
|
||||||
|
#[error(transparent)]
|
||||||
|
Shape(#[from] ndarray::ShapeError),
|
||||||
|
/// an error from the j4rs java bridge
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
#[error(transparent)]
|
||||||
|
J4rs(#[from] j4rs::errors::J4RsError),
|
||||||
|
/// an infallible conversion
|
||||||
|
#[error(transparent)]
|
||||||
|
Infallible(#[from] std::convert::Infallible),
|
||||||
|
/// an integer parse error
|
||||||
|
#[error(transparent)]
|
||||||
|
ParseIntError(#[from] std::num::ParseIntError),
|
||||||
|
/// an ome metadata error
|
||||||
|
#[error(transparent)]
|
||||||
|
Ome(#[from] ome_metadata::error::Error),
|
||||||
|
/// an error while downloading (e.g. the bioformats jar)
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
#[error(transparent)]
|
||||||
|
Downloader(#[from] downloader::Error),
|
||||||
|
/// an error parsing an enum string with strum
|
||||||
|
#[error(transparent)]
|
||||||
|
Strum(#[from] strum::ParseError),
|
||||||
|
/// an indicatif progress bar template error
|
||||||
|
#[cfg(feature = "tiffwrite")]
|
||||||
|
#[error(transparent)]
|
||||||
|
TemplateError(#[from] indicatif::style::TemplateError),
|
||||||
|
/// an error from the tiffwrite crate
|
||||||
|
#[cfg(feature = "tiffwrite")]
|
||||||
|
#[error(transparent)]
|
||||||
|
TiffWrite(#[from] tiffwrite::error::Error),
|
||||||
|
/// a yaml (de)serialization error
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
#[error(transparent)]
|
||||||
|
SerdeYaml(#[from] yaml_serde::Error),
|
||||||
|
/// an error from the tiff crate
|
||||||
|
#[cfg(any(feature = "tiffseq", feature = "tiff"))]
|
||||||
|
#[error(transparent)]
|
||||||
|
Tiff(#[from] tiff::TiffError),
|
||||||
|
/// a postcard (de)serialization error
|
||||||
|
#[cfg(feature = "python")]
|
||||||
|
#[error(transparent)]
|
||||||
|
PostCard(#[from] postcard::Error),
|
||||||
|
/// an error from the libczi binding
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
#[error(transparent)]
|
||||||
|
LibCzi(#[from] libczirw_sys::error::Error),
|
||||||
|
/// a regex error
|
||||||
|
#[error(transparent)]
|
||||||
|
RegexError(#[from] regex::Error),
|
||||||
|
/// an xmltree error
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
#[error(transparent)]
|
||||||
|
XmlTree(#[from] xmltree::Error),
|
||||||
|
/// an xmltree parse error
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
#[error(transparent)]
|
||||||
|
XmlTreeParse(#[from] xmltree::ParseError),
|
||||||
|
/// a czi-specific error
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
#[error(transparent)]
|
||||||
|
Czi(#[from] crate::readers::czi::CziError),
|
||||||
|
/// an error joining a tokio task
|
||||||
|
#[cfg(feature = "movie")]
|
||||||
|
#[error(transparent)]
|
||||||
|
TokioJoin(#[from] tokio::task::JoinError),
|
||||||
|
/// an error from the bioformats rust crate
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
#[error(transparent)]
|
||||||
|
BioFormats(#[from] bioformats::error::BioFormatsError),
|
||||||
|
/// an image registration / transforms error
|
||||||
|
#[cfg(feature = "transforms")]
|
||||||
|
#[error(transparent)]
|
||||||
|
ImageRegistration(#[from] image_registration::error::Error),
|
||||||
|
|
||||||
|
/// the axis string could not be parsed
|
||||||
|
#[error("invalid axis: {0}")]
|
||||||
|
InvalidAxis(String),
|
||||||
|
/// the axis was not found in the axes
|
||||||
|
#[error("axis {0} not found in axes {1}")]
|
||||||
|
AxisNotFound(String, String),
|
||||||
|
/// a conversion error
|
||||||
|
#[error("conversion error: {0}")]
|
||||||
|
TryInto(String),
|
||||||
|
/// the target file already exists
|
||||||
|
#[error("file already exists {0}")]
|
||||||
|
FileAlreadyExists(String),
|
||||||
|
/// could not download ffmpeg
|
||||||
|
#[error("could not download ffmpeg: {0}")]
|
||||||
|
FfmpegDownload(String),
|
||||||
|
/// an ffmpeg error
|
||||||
|
#[error("FFmpeg error: {0}")]
|
||||||
|
Ffmpeg(String),
|
||||||
|
/// the index is out of bounds
|
||||||
|
#[error("index {0} out of bounds {1}")]
|
||||||
|
OutOfBounds(isize, isize),
|
||||||
|
/// the axis was not included in the view
|
||||||
|
#[error("axis {0} has length {1}, but was not included")]
|
||||||
|
OutOfBoundsAxis(String, usize),
|
||||||
|
/// the dimensionality of the data does not match
|
||||||
|
#[error("dimensionality mismatch: {0} != {0}")]
|
||||||
|
DimensionalityMismatch(usize, usize),
|
||||||
|
/// the axis already has an operation
|
||||||
|
#[error("axis {0}: {1} is already operated on!")]
|
||||||
|
AxisAlreadyOperated(usize, String),
|
||||||
|
/// not enough free dimensions
|
||||||
|
#[error("not enough free dimensions")]
|
||||||
|
NotEnoughFreeDimensions,
|
||||||
|
/// cannot cast a pixel value to the requested type
|
||||||
|
#[error("cannot cast {0} to {1}")]
|
||||||
|
Cast(String, String),
|
||||||
|
/// the view is empty
|
||||||
|
#[error("empty view")]
|
||||||
|
EmptyView,
|
||||||
|
/// the color string could not be parsed
|
||||||
|
#[error("invalid color: {0}")]
|
||||||
|
InvalidColor(String),
|
||||||
|
/// no image or pixels found in the metadata
|
||||||
|
#[error("no image or pixels found")]
|
||||||
|
NoImageOrPixels,
|
||||||
|
/// the attenuation value is invalid
|
||||||
|
#[error("invalid attenuation value: {0}")]
|
||||||
|
InvalidAttenuation(String),
|
||||||
|
/// the file name is invalid
|
||||||
|
#[error("not a valid file name")]
|
||||||
|
InvalidFileName,
|
||||||
|
/// the file has no parent directory
|
||||||
|
#[error("file has no parent")]
|
||||||
|
NoParent,
|
||||||
|
/// the pixel type is unknown
|
||||||
|
#[error("unknown pixel type {0}")]
|
||||||
|
UnknownPixelType(String),
|
||||||
|
/// cannot compute the mean of an empty axis
|
||||||
|
#[error("no mean")]
|
||||||
|
NoMean,
|
||||||
|
/// the tiff file lock is poisoned
|
||||||
|
#[error("tiff is locked")]
|
||||||
|
TiffLock,
|
||||||
|
/// this feature is not implemented
|
||||||
|
#[error("not implemented: {0}")]
|
||||||
|
NotImplemented(String),
|
||||||
|
/// a string could not be parsed
|
||||||
|
#[error("cannot parse: {0}")]
|
||||||
|
Parse(String),
|
||||||
|
/// cannot convert the libczi pixel type
|
||||||
|
#[error("cannot convert libczi pixel type: {0}")]
|
||||||
|
Conversion(String),
|
||||||
|
/// no reader could open the file
|
||||||
|
#[error("no reader found for {0}, tried: {1}")]
|
||||||
|
NoReader(String, String),
|
||||||
|
/// the reader cannot open the file
|
||||||
|
#[error("reader {0} cannot open file {1} because {2}")]
|
||||||
|
InvalidReader(String, String, String),
|
||||||
|
/// the file does not exist
|
||||||
|
#[error("file does not exist: {0}")]
|
||||||
|
FileDoesNotExist(String),
|
||||||
|
/// cannot remove axes that have a size != 1
|
||||||
|
#[error("cannot remove axes {0}, size {1} != 1")]
|
||||||
|
SizeMismatch(String, usize),
|
||||||
|
/// shape mismatch
|
||||||
|
#[error("shape mismatch: {0:?}, {1:?}")]
|
||||||
|
ShapeMismatch(Vec<usize>, Vec<usize>),
|
||||||
|
/// file mismatch
|
||||||
|
#[error("{0} do not match in {1} and {2}")]
|
||||||
|
FileMismatch(String, String, String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
/// the name of the error variant as a static string
|
||||||
|
pub fn variant_name(&self) -> &'static str {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
+372
@@ -0,0 +1,372 @@
|
|||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
//! The ndbioimage crate exposes (bio) images as a struct that can be sliced like an ndarray Array
|
||||||
|
//! (Rust), but without loading the whole image into memory, reading from the file only when needed.
|
||||||
|
//! Some metadata is read
|
||||||
|
//! and stored in an [ome](https://genomebiology.biomedcentral.com/articles/10.1186/gb-2005-6-5-r47)
|
||||||
|
//! structure. Additionally, it can automatically calculate an affine transform that corrects for
|
||||||
|
//! chromatic aberrations etc. and apply it on the fly to the image.
|
||||||
|
//!
|
||||||
|
//! Currently, it supports imagej tif files, czi files, micromanager tif sequences and anything
|
||||||
|
//! [bioformats](https://www.openmicroscopy.org/bio-formats/) can handle.
|
||||||
|
//!
|
||||||
|
//! ```rust,no_run
|
||||||
|
//! use ndarray::Array2;
|
||||||
|
//! use ndbioimage::readers::{DynReader, Frame, Reader};
|
||||||
|
//!
|
||||||
|
//! # fn main() -> Result<(), ndbioimage::error::Error> {
|
||||||
|
//! let path = "/path/to/file";
|
||||||
|
//! let reader = DynReader::new(&path, 0, 0)?;
|
||||||
|
//! println!("shape: {}", reader.shape());
|
||||||
|
//! let frame = reader.get_frame(0, 0, 0)?;
|
||||||
|
//! if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
|
||||||
|
//! println!("{:?}", arr);
|
||||||
|
//! } else {
|
||||||
|
//! println!("could not convert Frame to Array<i8>");
|
||||||
|
//! }
|
||||||
|
//! let xml = reader.metadata()?.to_xml()?;
|
||||||
|
//! println!("{}", xml);
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ```rust,no_run
|
||||||
|
//! use ndbioimage::readers::{DynReader, Reader};
|
||||||
|
//!
|
||||||
|
//! # fn main() -> Result<(), ndbioimage::error::Error> {
|
||||||
|
//! let path = "/path/to/file";
|
||||||
|
//! let reader = DynReader::new(&path, 0, 0)?;
|
||||||
|
//! let view = reader.view();
|
||||||
|
//! let view = view.max_proj(3)?;
|
||||||
|
//! let array = view.as_array::<u16>()?;
|
||||||
|
//! println!("{:?}", array.shape());
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
/// axis handling: axis enum, slicing and shape
|
||||||
|
pub mod axes;
|
||||||
|
/// process-wide LRU caches for frames and materialized arrays
|
||||||
|
mod cache;
|
||||||
|
#[cfg(feature = "python")]
|
||||||
|
mod py;
|
||||||
|
/// min/max/sum/mean operations along an axis
|
||||||
|
pub mod stats;
|
||||||
|
/// the main data structure: an on-disk image that can be sliced without loading it fully
|
||||||
|
pub mod view;
|
||||||
|
|
||||||
|
/// named colors and color conversion
|
||||||
|
pub mod colors;
|
||||||
|
/// the error type used throughout the crate
|
||||||
|
pub mod error;
|
||||||
|
/// ome metadata helpers
|
||||||
|
pub mod metadata;
|
||||||
|
#[cfg(feature = "movie")]
|
||||||
|
/// saving views as movies
|
||||||
|
pub mod movie;
|
||||||
|
/// readers for the different supported image formats
|
||||||
|
pub mod readers;
|
||||||
|
#[cfg(feature = "tiffwrite")]
|
||||||
|
/// saving views as tiff files
|
||||||
|
pub mod tiffwrite;
|
||||||
|
#[cfg(feature = "transforms")]
|
||||||
|
/// image registration and warping images using affine transforms
|
||||||
|
mod transforms;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
/// main entry point for the application
|
||||||
|
pub mod main {
|
||||||
|
#[cfg(feature = "tiffwrite")]
|
||||||
|
use crate::axes::{Axis, Operation};
|
||||||
|
use crate::error::Error;
|
||||||
|
#[cfg(feature = "movie")]
|
||||||
|
use crate::movie::MovieOptions;
|
||||||
|
use crate::readers::{Dimensions, DynReader, Reader};
|
||||||
|
use crate::view::View;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
#[cfg(feature = "movie")]
|
||||||
|
use ndarray::SliceInfoElem;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(arg_required_else_help = true, version, about, long_about = None, propagate_version = true
|
||||||
|
)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Print some metadata
|
||||||
|
Info {
|
||||||
|
#[arg(value_name = "FILE", num_args(1..))]
|
||||||
|
file: Vec<PathBuf>,
|
||||||
|
},
|
||||||
|
/// save ome metadata as xml
|
||||||
|
ExtractOME {
|
||||||
|
#[arg(value_name = "FILE", num_args(1..))]
|
||||||
|
file: Vec<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Save the image as tiff file
|
||||||
|
#[cfg(feature = "tiffwrite")]
|
||||||
|
Tiff {
|
||||||
|
#[arg(value_name = "FILE", num_args(1..))]
|
||||||
|
file: Vec<PathBuf>,
|
||||||
|
#[arg(short = 'C', long, value_name = "COLOR", num_args(1..))]
|
||||||
|
colors: Vec<String>,
|
||||||
|
#[arg(short = 'f', long, value_name = "OVERWRITE")]
|
||||||
|
overwrite: bool,
|
||||||
|
#[arg(short = 'q', long, value_name = "OPERATIONS", num_args(1..))]
|
||||||
|
operations: Vec<String>,
|
||||||
|
#[arg(short = 'o', long, value_name = "OUTPUT")]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Save the image as mp4 file
|
||||||
|
#[cfg(feature = "movie")]
|
||||||
|
Movie {
|
||||||
|
#[arg(value_name = "FILE", num_args(1..))]
|
||||||
|
file: Vec<PathBuf>,
|
||||||
|
#[arg(short, long, value_name = "VELOCITY", default_value = "3.6")]
|
||||||
|
velocity: f64,
|
||||||
|
#[arg(short, long, value_name = "BRIGHTNESS", num_args(1..))]
|
||||||
|
brightness: Vec<f64>,
|
||||||
|
#[arg(short, long, value_name = "SCALE", default_value = "1.0")]
|
||||||
|
scale: f64,
|
||||||
|
#[arg(short = 'C', long, value_name = "COLOR", num_args(1..))]
|
||||||
|
colors: Vec<String>,
|
||||||
|
#[arg(short = 'f', long, value_name = "OVERWRITE")]
|
||||||
|
overwrite: bool,
|
||||||
|
#[arg(short, long, value_name = "REGISTER")]
|
||||||
|
register: bool,
|
||||||
|
#[arg(short, long, value_name = "CHANNEL")]
|
||||||
|
channel: Option<isize>,
|
||||||
|
#[arg(short, long, value_name = "ZSLICE")]
|
||||||
|
zslice: Option<String>,
|
||||||
|
#[arg(short, long, value_name = "TIME")]
|
||||||
|
time: Option<String>,
|
||||||
|
#[arg(short, long, value_name = "NO-SCALE-BRIGHTNESS")]
|
||||||
|
no_scaling: bool,
|
||||||
|
#[arg(short = 'o', long, value_name = "OUTPUT")]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "movie")]
|
||||||
|
fn parse_slice(s: &str) -> Result<SliceInfoElem, Error> {
|
||||||
|
let mut t = s
|
||||||
|
.trim()
|
||||||
|
.replace("..", ":")
|
||||||
|
.split(":")
|
||||||
|
.map(|i| i.parse().ok())
|
||||||
|
.collect::<Vec<Option<isize>>>();
|
||||||
|
if t.len() > 3 {
|
||||||
|
return Err(Error::Parse(s.to_string()));
|
||||||
|
}
|
||||||
|
while t.len() < 3 {
|
||||||
|
t.push(None);
|
||||||
|
}
|
||||||
|
match t[..] {
|
||||||
|
[Some(start), None, None] => Ok(SliceInfoElem::Index(start)),
|
||||||
|
[Some(start), end, None] => Ok(SliceInfoElem::Slice {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
step: 1,
|
||||||
|
}),
|
||||||
|
[Some(start), end, Some(step)] => Ok(SliceInfoElem::Slice { start, end, step }),
|
||||||
|
[None, end, None] => Ok(SliceInfoElem::Slice {
|
||||||
|
start: 0,
|
||||||
|
end,
|
||||||
|
step: 1,
|
||||||
|
}),
|
||||||
|
[None, end, Some(step)] => Ok(SliceInfoElem::Slice {
|
||||||
|
start: 0,
|
||||||
|
end,
|
||||||
|
step,
|
||||||
|
}),
|
||||||
|
_ => Err(Error::Parse(s.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the command line interface, run `ndbioimage --help` for an overview of the commands
|
||||||
|
pub fn main(args: Option<Vec<String>>) -> Result<(), Error> {
|
||||||
|
let cli = if let Some(args) = args {
|
||||||
|
Cli::parse_from(args)
|
||||||
|
} else {
|
||||||
|
Cli::parse()
|
||||||
|
};
|
||||||
|
match &cli.command {
|
||||||
|
Commands::Info { file } => {
|
||||||
|
for f in file {
|
||||||
|
let view = View::<_, DynReader>::from_path(f)?.squeeze()?;
|
||||||
|
println!("{}", view.summary()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::ExtractOME { file } => {
|
||||||
|
for f in file {
|
||||||
|
let (path, dimensions) = Dimensions::parse_path(f)?;
|
||||||
|
let reader = DynReader::new(
|
||||||
|
&path,
|
||||||
|
dimensions.s.unwrap_or(0),
|
||||||
|
dimensions.p.unwrap_or(0),
|
||||||
|
)?;
|
||||||
|
let xml = reader.metadata()?.to_xml()?;
|
||||||
|
std::fs::write(path.with_extension("xml"), xml)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "tiffwrite")]
|
||||||
|
Commands::Tiff {
|
||||||
|
file,
|
||||||
|
colors,
|
||||||
|
overwrite,
|
||||||
|
operations,
|
||||||
|
output,
|
||||||
|
} => {
|
||||||
|
let options = crate::tiffwrite::TiffOptions::new(
|
||||||
|
Some(crate::utils::progress::get_bar(
|
||||||
|
Some(0),
|
||||||
|
Some("writing tiff file".to_string()),
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
colors.clone(),
|
||||||
|
*overwrite,
|
||||||
|
)?;
|
||||||
|
for f in file {
|
||||||
|
let mut view: View<_, DynReader> =
|
||||||
|
View::<_, DynReader>::from_path(f)?.into_dyn();
|
||||||
|
for operation in operations {
|
||||||
|
let a = operation.split(":").collect::<Vec<_>>();
|
||||||
|
if let Some(ax) = a.first()
|
||||||
|
&& let Some(op) = a.get(1)
|
||||||
|
{
|
||||||
|
view = view.operate(ax.parse::<Axis>()?, op.parse::<Operation>()?)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let out = output
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| file.len() == 1)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| f.with_extension("tiff"));
|
||||||
|
view.save_as_tiff(&out, &options)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "movie")]
|
||||||
|
Commands::Movie {
|
||||||
|
file,
|
||||||
|
velocity: speed,
|
||||||
|
brightness,
|
||||||
|
scale,
|
||||||
|
colors,
|
||||||
|
overwrite,
|
||||||
|
register,
|
||||||
|
channel,
|
||||||
|
zslice,
|
||||||
|
time,
|
||||||
|
no_scaling,
|
||||||
|
output,
|
||||||
|
} => {
|
||||||
|
let options = MovieOptions::new(
|
||||||
|
*speed,
|
||||||
|
brightness.to_vec(),
|
||||||
|
*scale,
|
||||||
|
colors.to_vec(),
|
||||||
|
*overwrite,
|
||||||
|
*register,
|
||||||
|
*no_scaling,
|
||||||
|
)?;
|
||||||
|
for f in file {
|
||||||
|
let view: View<_, DynReader> = View::from_path(f)?;
|
||||||
|
let mut s = [SliceInfoElem::Slice {
|
||||||
|
start: 0,
|
||||||
|
end: None,
|
||||||
|
step: 1,
|
||||||
|
}; 5];
|
||||||
|
if let Some(channel) = channel {
|
||||||
|
s[0] = SliceInfoElem::Index(*channel);
|
||||||
|
};
|
||||||
|
if let Some(zslice) = zslice {
|
||||||
|
s[1] = parse_slice(zslice)?;
|
||||||
|
}
|
||||||
|
if let Some(time) = time {
|
||||||
|
s[2] = parse_slice(time)?;
|
||||||
|
}
|
||||||
|
let out = output
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| file.len() == 1)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| f.with_extension("mp4"));
|
||||||
|
view.into_dyn()
|
||||||
|
.slice(s.as_slice())?
|
||||||
|
.save_as_movie(&out, &options)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::readers::{DynReader, Frame, Reader};
|
||||||
|
use ndarray::Array2;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
fn open(file: &str) -> Result<DynReader, Error> {
|
||||||
|
let path = std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file);
|
||||||
|
DynReader::new(&path, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pixel_type(file: &str) -> Result<String, Error> {
|
||||||
|
let reader = open(file)?;
|
||||||
|
Ok(format!(
|
||||||
|
"file: {}, pixel type: {:?}",
|
||||||
|
file,
|
||||||
|
reader.pixel_type()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_frame(file: &str) -> Result<Frame, Error> {
|
||||||
|
let reader = open(file)?;
|
||||||
|
reader.get_frame(0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_ser() -> Result<(), Error> {
|
||||||
|
let file = "czi/Experiment-2029.czi";
|
||||||
|
let reader = open(file)?;
|
||||||
|
println!("shape: {}", reader.shape());
|
||||||
|
let frame = reader.get_frame(0, 0, 0)?;
|
||||||
|
if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
|
||||||
|
println!("{:?}", arr);
|
||||||
|
} else {
|
||||||
|
println!("could not convert Frame to Array<i8>");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_par() -> Result<(), Error> {
|
||||||
|
let files = vec!["czi/Experiment-2029.czi", "tiff/test.tif"];
|
||||||
|
let pixel_type = files
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|file| get_pixel_type(file).unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
println!("{:?}", pixel_type);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_frame_par() -> Result<(), Error> {
|
||||||
|
let files = vec!["czi/Experiment-2029.czi", "tiff/test.tif"];
|
||||||
|
let frames = files
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|file| get_frame(file).unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
println!("{:?}", frames);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub(crate) fn main() -> Result<(), ndbioimage::error::Error> {
|
||||||
|
ndbioimage::main::main(None)
|
||||||
|
}
|
||||||
+398
@@ -0,0 +1,398 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ome_metadata::Ome;
|
||||||
|
use ome_metadata::ome::{
|
||||||
|
BinningType, Convert, Image, Instrument, Objective, Pixels, UnitsLength, UnitsTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Metadata for Ome {
|
||||||
|
fn get_instrument(&self) -> Option<&Instrument> {
|
||||||
|
let instrument_id = self.get_image()?.instrument_ref.as_ref()?.id.clone();
|
||||||
|
self.instrument.iter().find(|i| i.id == instrument_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_image(&self) -> Option<&Image> {
|
||||||
|
if let Some(image) = &self.image.first() {
|
||||||
|
Some(image)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper trait to extract useful information from ome metadata
|
||||||
|
pub trait Metadata {
|
||||||
|
/// the instrument used to acquire the image
|
||||||
|
fn get_instrument(&self) -> Option<&Instrument>;
|
||||||
|
/// the first image in the ome structure
|
||||||
|
fn get_image(&self) -> Option<&Image>;
|
||||||
|
|
||||||
|
/// the pixels of the image
|
||||||
|
fn get_pixels(&self) -> Option<&Pixels> {
|
||||||
|
if let Some(image) = self.get_image() {
|
||||||
|
Some(&image.pixels)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the objective used to acquire the image
|
||||||
|
fn get_objective(&self) -> Option<&Objective> {
|
||||||
|
let objective_id = self.get_image()?.objective_settings.as_ref()?.id.clone();
|
||||||
|
self.get_instrument()?
|
||||||
|
.objective
|
||||||
|
.iter()
|
||||||
|
.find(|o| o.id == objective_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the tube lens used to acquire the image
|
||||||
|
fn get_tube_lens(&self) -> Option<&Objective> {
|
||||||
|
self.get_instrument()?
|
||||||
|
.objective
|
||||||
|
.iter()
|
||||||
|
.find(|o| o.id.starts_with("Objective:Tubelens"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// shape of the data along cztyx axes
|
||||||
|
fn shape(&self) -> Result<(usize, usize, usize, usize, usize), Error> {
|
||||||
|
if let Some(pixels) = self.get_pixels() {
|
||||||
|
Ok((
|
||||||
|
pixels.size_c as usize,
|
||||||
|
pixels.size_z as usize,
|
||||||
|
pixels.size_t as usize,
|
||||||
|
pixels.size_y as usize,
|
||||||
|
pixels.size_x as usize,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(Error::NoImageOrPixels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// pixel size in nm
|
||||||
|
fn pixel_size(&self) -> Result<Option<f64>, Error> {
|
||||||
|
if let Some(pixels) = self.get_pixels() {
|
||||||
|
match (pixels.physical_size_x, pixels.physical_size_y) {
|
||||||
|
(Some(x), Some(y)) => Ok(Some(
|
||||||
|
(pixels
|
||||||
|
.physical_size_x_unit
|
||||||
|
.convert(&UnitsLength::nm, x as f64)?
|
||||||
|
+ pixels
|
||||||
|
.physical_size_y_unit
|
||||||
|
.convert(&UnitsLength::nm, y as f64)?)
|
||||||
|
/ 2f64,
|
||||||
|
)),
|
||||||
|
(Some(x), None) => Ok(Some(
|
||||||
|
pixels
|
||||||
|
.physical_size_x_unit
|
||||||
|
.convert(&UnitsLength::nm, x as f64)?
|
||||||
|
.powi(2),
|
||||||
|
)),
|
||||||
|
(None, Some(y)) => Ok(Some(
|
||||||
|
pixels
|
||||||
|
.physical_size_y_unit
|
||||||
|
.convert(&UnitsLength::nm, y as f64)?
|
||||||
|
.powi(2),
|
||||||
|
)),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// distance between planes in z-stack in nm
|
||||||
|
fn delta_z(&self) -> Result<Option<f64>, Error> {
|
||||||
|
Ok(
|
||||||
|
if let Some(pixels) = self.get_pixels()
|
||||||
|
&& let Some(z) = pixels.physical_size_z
|
||||||
|
{
|
||||||
|
Some(
|
||||||
|
pixels
|
||||||
|
.physical_size_z_unit
|
||||||
|
.convert(&UnitsLength::nm, z as f64)?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// time interval in seconds for time-lapse images
|
||||||
|
fn time_interval(&self) -> Result<Option<f64>, Error> {
|
||||||
|
if let Some(pixels) = self.get_pixels()
|
||||||
|
&& let Some(t) = pixels.plane.iter().filter_map(|p| p.the_t).max()
|
||||||
|
&& (t > 0)
|
||||||
|
{
|
||||||
|
let plane_a = pixels.plane.iter().find(|p| {
|
||||||
|
(p.the_c.is_none() || (p.the_c == Some(0)))
|
||||||
|
&& (p.the_z.is_none() || (p.the_z == Some(0)))
|
||||||
|
&& (p.the_t == Some(0))
|
||||||
|
});
|
||||||
|
let plane_b = pixels.plane.iter().find(|p| {
|
||||||
|
(p.the_c.is_none() || (p.the_c == Some(0)))
|
||||||
|
&& (p.the_z.is_none() || (p.the_z == Some(0)))
|
||||||
|
&& (p.the_t == Some(t))
|
||||||
|
});
|
||||||
|
if let (Some(a), Some(b)) = (plane_a, plane_b)
|
||||||
|
&& let (Some(a_t), Some(b_t)) = (a.delta_t, b.delta_t)
|
||||||
|
{
|
||||||
|
return Ok(Some(
|
||||||
|
(b.delta_t_unit.convert(&UnitsTime::s, b_t as f64)?
|
||||||
|
- a.delta_t_unit.convert(&UnitsTime::s, a_t as f64)?)
|
||||||
|
.abs()
|
||||||
|
/ (t as f64),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// exposure time for channel, z=0 and t=0
|
||||||
|
fn exposure_time(&self, channel: usize) -> Result<Option<f64>, Error> {
|
||||||
|
let c = channel as i32;
|
||||||
|
Ok(
|
||||||
|
if let Some(pixels) = self.get_pixels()
|
||||||
|
&& let Some(p) = pixels.plane.iter().find(|p| {
|
||||||
|
(p.the_c == Some(c))
|
||||||
|
&& (p.the_z.is_none() || (p.the_z == Some(0)))
|
||||||
|
&& (p.the_t.is_none() || (p.the_t == Some(0)))
|
||||||
|
})
|
||||||
|
&& let Some(t) = p.exposure_time
|
||||||
|
{
|
||||||
|
Some(p.exposure_time_unit.convert(&UnitsTime::s, t as f64)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the binning of the detector for a channel (1, 2, 4 or 8)
|
||||||
|
fn binning(&self, channel: usize) -> Option<usize> {
|
||||||
|
match self
|
||||||
|
.get_pixels()?
|
||||||
|
.channel
|
||||||
|
.get(channel)?
|
||||||
|
.detector_settings
|
||||||
|
.as_ref()?
|
||||||
|
.binning
|
||||||
|
.as_ref()?
|
||||||
|
{
|
||||||
|
BinningType::_1X1 => Some(1),
|
||||||
|
BinningType::_2X2 => Some(2),
|
||||||
|
BinningType::_4X4 => Some(4),
|
||||||
|
BinningType::_8X8 => Some(8),
|
||||||
|
BinningType::Other => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the excitation wavelength of the laser for a channel in nm
|
||||||
|
fn laser_wavelengths(&self, channel: usize) -> Result<Option<f64>, Error> {
|
||||||
|
Ok(
|
||||||
|
if let Some(pixels) = self.get_pixels()
|
||||||
|
&& let Some(channel) = pixels.channel.get(channel)
|
||||||
|
&& let Some(w) = channel.excitation_wavelength
|
||||||
|
{
|
||||||
|
Some(
|
||||||
|
channel
|
||||||
|
.excitation_wavelength_unit
|
||||||
|
.convert(&UnitsLength::nm, w as f64)?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the laser power (fraction of the maximum) for a channel
|
||||||
|
fn laser_powers(&self, channel: usize) -> Result<Option<f64>, Error> {
|
||||||
|
if let Some(pixels) = self.get_pixels()
|
||||||
|
&& let Some(channel) = pixels.channel.get(channel)
|
||||||
|
&& let Some(ls) = &channel.light_source_settings
|
||||||
|
&& let Some(a) = ls.attenuation
|
||||||
|
{
|
||||||
|
if (0. ..=1.).contains(&a) {
|
||||||
|
Ok(Some(1f64 - (a as f64)))
|
||||||
|
} else {
|
||||||
|
Err(Error::InvalidAttenuation(a.to_string()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the name of the objective
|
||||||
|
fn objective_name(&self) -> Option<String> {
|
||||||
|
Some(self.get_objective()?.model.as_ref()?.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the total magnification: objective magnification times tube lens magnification
|
||||||
|
fn magnification(&self) -> Option<f64> {
|
||||||
|
Some(
|
||||||
|
(self.get_objective()?.nominal_magnification? as f64)
|
||||||
|
* (self.get_tube_lens()?.nominal_magnification? as f64),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the name of the tube lens
|
||||||
|
fn tube_lens_name(&self) -> Option<String> {
|
||||||
|
self.get_tube_lens()?.model.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the name of the filter set for a channel
|
||||||
|
fn filter_set_name(&self, channel: usize) -> Option<String> {
|
||||||
|
let filter_set_id = self
|
||||||
|
.get_pixels()?
|
||||||
|
.channel
|
||||||
|
.get(channel)?
|
||||||
|
.filter_set_ref
|
||||||
|
.as_ref()?
|
||||||
|
.id
|
||||||
|
.clone();
|
||||||
|
self.get_instrument()
|
||||||
|
.as_ref()?
|
||||||
|
.filter_set
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.id == filter_set_id)?
|
||||||
|
.model
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// the gain of the detector for a channel
|
||||||
|
fn gain(&self, channel: usize) -> Option<f64> {
|
||||||
|
self.get_pixels()
|
||||||
|
.and_then(|p| p.channel.get(channel))
|
||||||
|
.and_then(|c| c.detector_settings.as_ref())
|
||||||
|
.map(|ds| ds.id.as_str())
|
||||||
|
.and_then(|detector_id| {
|
||||||
|
self.get_instrument()
|
||||||
|
.and_then(|i| i.detector.iter().find(|d| d.id == detector_id))
|
||||||
|
.and_then(|d| d.amplification_gain)
|
||||||
|
.map(|g| g as f64)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// whether the image is a z-stack (more than one z slice)
|
||||||
|
fn is_zstack(&self) -> Result<bool, Error> {
|
||||||
|
self.get_pixels()
|
||||||
|
.map(|p| Ok(p.size_z > 1))
|
||||||
|
.unwrap_or_else(|| Err(Error::NoImageOrPixels))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// whether the image is a time lapse (more than one time point)
|
||||||
|
fn is_time_lapse(&self) -> Result<bool, Error> {
|
||||||
|
self.get_pixels()
|
||||||
|
.map(|p| Ok(p.size_t > 1))
|
||||||
|
.unwrap_or_else(|| Err(Error::NoImageOrPixels))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a multi-line summary of the most relevant metadata, one field per line
|
||||||
|
fn summary(&self) -> Result<String, Error> {
|
||||||
|
let size_c = if let Some(pixels) = self.get_pixels() {
|
||||||
|
pixels.channel.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let mut s = "".to_string();
|
||||||
|
if let Ok(Some(pixel_size)) = self.pixel_size() {
|
||||||
|
s.push_str(&format!("pixel size: {pixel_size:.2} nm\n"));
|
||||||
|
}
|
||||||
|
if let Ok(Some(delta_z)) = self.delta_z()
|
||||||
|
&& self.is_zstack()?
|
||||||
|
{
|
||||||
|
s.push_str(&format!("z-interval: {delta_z:.2} nm\n"))
|
||||||
|
}
|
||||||
|
if let Ok(Some(time_interval)) = self.time_interval()
|
||||||
|
&& self.is_time_lapse()?
|
||||||
|
{
|
||||||
|
s.push_str(&format!("time interval: {time_interval:.2} s\n"))
|
||||||
|
}
|
||||||
|
let exposure_time = (0..size_c)
|
||||||
|
.map(|c| self.exposure_time(c))
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !exposure_time.is_empty() {
|
||||||
|
s.push_str(&format!(
|
||||||
|
"exposure time: {} s\n",
|
||||||
|
exposure_time
|
||||||
|
.iter()
|
||||||
|
.map(|e| format!("{:.2}", e))
|
||||||
|
.join(" | ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(magnification) = self.magnification() {
|
||||||
|
s.push_str(&format!("magnification: {magnification:.1}x\n"))
|
||||||
|
}
|
||||||
|
if let Some(objective_name) = self.objective_name() {
|
||||||
|
s.push_str(&format!("objective: {objective_name}\n"))
|
||||||
|
}
|
||||||
|
if let Some(tube_lens_name) = self.tube_lens_name() {
|
||||||
|
s.push_str(&format!("tube lens: {tube_lens_name}\n"))
|
||||||
|
}
|
||||||
|
let filter_set_name = (0..size_c)
|
||||||
|
.map(|c| self.filter_set_name(c))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !filter_set_name.is_empty() {
|
||||||
|
s.push_str(&format!(
|
||||||
|
"filter set: {}\n",
|
||||||
|
filter_set_name.into_iter().join(" | ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let gain = (0..size_c)
|
||||||
|
.map(|c| self.gain(c))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !gain.is_empty() {
|
||||||
|
s.push_str(&format!(
|
||||||
|
"gain: {}\n",
|
||||||
|
gain.into_iter().join(" | ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let laser_wavelengths = (0..size_c)
|
||||||
|
.map(|c| self.laser_wavelengths(c))
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !laser_wavelengths.is_empty() {
|
||||||
|
s.push_str(&format!(
|
||||||
|
"laser colors: {} nm\n",
|
||||||
|
laser_wavelengths.into_iter().join(" | ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let laser_powers = (0..size_c)
|
||||||
|
.map(|c| self.laser_powers(c))
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !laser_powers.is_empty() {
|
||||||
|
s.push_str(&format!(
|
||||||
|
"laser powers: {} %\n",
|
||||||
|
laser_powers
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| format!("{:.3}", 100.0 * p))
|
||||||
|
.join(" | ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let binning = (0..size_c)
|
||||||
|
.map(|c| self.binning(c))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !binning.is_empty() {
|
||||||
|
s.push_str(&format!(
|
||||||
|
"binning: {}\n",
|
||||||
|
binning.into_iter().join(" | ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
+317
@@ -0,0 +1,317 @@
|
|||||||
|
use crate::axes::Axis;
|
||||||
|
use crate::colors::Color;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::readers::{PixelType, Reader};
|
||||||
|
use crate::utils::progress::get_bar;
|
||||||
|
use crate::view::View;
|
||||||
|
use ffmpeg_sidecar::command::FfmpegCommand;
|
||||||
|
use ffmpeg_sidecar::download::auto_download;
|
||||||
|
use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ndarray::{Array2, Array3, Dimension, IxDyn, s, stack};
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// options for creating a movie from a view
|
||||||
|
pub struct MovieOptions {
|
||||||
|
velocity: f64,
|
||||||
|
brightness: Vec<f64>,
|
||||||
|
scale: f64,
|
||||||
|
colors: Option<Vec<Vec<u8>>>,
|
||||||
|
overwrite: bool,
|
||||||
|
register: bool,
|
||||||
|
no_scaling: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MovieOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
velocity: 3.6,
|
||||||
|
brightness: Vec::new(),
|
||||||
|
scale: 1.0,
|
||||||
|
colors: None,
|
||||||
|
overwrite: false,
|
||||||
|
register: false,
|
||||||
|
no_scaling: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MovieOptions {
|
||||||
|
/// create movie options with the given parameters
|
||||||
|
pub fn new(
|
||||||
|
velocity: f64,
|
||||||
|
brightness: Vec<f64>,
|
||||||
|
scale: f64,
|
||||||
|
colors: Vec<String>,
|
||||||
|
overwrite: bool,
|
||||||
|
register: bool,
|
||||||
|
no_scaling: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let colors = if colors.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let colors = colors
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.parse::<Color>())
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
Some(colors.into_iter().map(|c| c.to_rgb()).collect())
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
velocity,
|
||||||
|
brightness,
|
||||||
|
scale,
|
||||||
|
colors,
|
||||||
|
overwrite,
|
||||||
|
register,
|
||||||
|
no_scaling,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the frames per second of the movie
|
||||||
|
pub fn set_velocity(&mut self, velocity: f64) {
|
||||||
|
self.velocity = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the brightness scale factors for each channel
|
||||||
|
pub fn set_brightness(&mut self, brightness: Vec<f64>) {
|
||||||
|
self.brightness = brightness;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the display scale (zoom) factor for the movie
|
||||||
|
pub fn set_scale(&mut self, scale: f64) {
|
||||||
|
self.scale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the color lookup table for the movie
|
||||||
|
pub fn set_colors(&mut self, colors: &[String]) -> Result<(), Error> {
|
||||||
|
let colors = colors
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.parse::<Color>())
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
self.colors = Some(colors.into_iter().map(|c| c.to_rgb()).collect());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set whether an existing movie file should be overwritten
|
||||||
|
pub fn set_overwrite(&mut self, overwrite: bool) {
|
||||||
|
self.overwrite = overwrite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ab<R: Reader>(tyx: View<IxDyn, R>) -> Result<(f64, f64), Error> {
|
||||||
|
let s = tyx
|
||||||
|
.as_array::<f64>()?
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&i| {
|
||||||
|
if i == 0.0 || !i.is_finite() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(OrderedFloat::from(i))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sorted_unstable()
|
||||||
|
.map(f64::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let n = s.len();
|
||||||
|
let mut a = s[n / 100];
|
||||||
|
let mut b = s[n - n / 100 - 1];
|
||||||
|
if a == b {
|
||||||
|
a = s[0];
|
||||||
|
b = s[n - 1];
|
||||||
|
}
|
||||||
|
if a == b {
|
||||||
|
a = 0.0;
|
||||||
|
b = 1.0;
|
||||||
|
}
|
||||||
|
Ok((a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cframe(frame: Array2<f64>, color: &[u8], a: f64, b: f64) -> Array3<f64> {
|
||||||
|
let frame = ((frame - a) / (b - a)).clamp(0.0, 1.0);
|
||||||
|
let color = color
|
||||||
|
.iter()
|
||||||
|
.map(|&c| (c as f64) / 255.0)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let frame = color
|
||||||
|
.iter()
|
||||||
|
.map(|&c| (c * &frame).to_owned())
|
||||||
|
.collect::<Vec<Array2<f64>>>();
|
||||||
|
let view = frame.iter().map(|c| c.view()).collect::<Vec<_>>();
|
||||||
|
stack(ndarray::Axis(2), &view).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D, R> View<D, R>
|
||||||
|
where
|
||||||
|
D: Dimension,
|
||||||
|
R: Reader,
|
||||||
|
Self: 'static,
|
||||||
|
{
|
||||||
|
/// save the view as a movie file with the given options
|
||||||
|
pub fn save_as_movie<P>(&self, path: P, options: &MovieOptions) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
if options.register {
|
||||||
|
return Err(Error::NotImplemented("register".to_string()));
|
||||||
|
}
|
||||||
|
let path = path.as_ref().to_path_buf();
|
||||||
|
if path.exists() {
|
||||||
|
if options.overwrite {
|
||||||
|
std::fs::remove_file(&path)?;
|
||||||
|
} else {
|
||||||
|
return Err(Error::FileAlreadyExists(path.display().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let view = self.max_proj(Axis::Z)?.reset_axes()?;
|
||||||
|
let velocity = options.velocity;
|
||||||
|
let mut brightness = options.brightness.clone();
|
||||||
|
let scale = options.scale;
|
||||||
|
let shape = view.shape();
|
||||||
|
let size_c = shape[0];
|
||||||
|
let size_t = shape[2];
|
||||||
|
let size_y = shape[3];
|
||||||
|
let size_x = shape[4];
|
||||||
|
let shape_y = 2 * (((size_y as f64 * scale) / 2.).round() as usize);
|
||||||
|
let shape_x = 2 * (((size_x as f64 * scale) / 2.).round() as usize);
|
||||||
|
|
||||||
|
while brightness.len() < size_c {
|
||||||
|
brightness.push(1.0);
|
||||||
|
}
|
||||||
|
let mut colors = if let Some(colors) = options.colors.as_ref() {
|
||||||
|
colors.to_vec()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
while colors.len() < size_c {
|
||||||
|
colors.push(vec![255, 255, 255]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = auto_download() {
|
||||||
|
return Err(Error::Ffmpeg(e.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut movie = FfmpegCommand::new()
|
||||||
|
.args([
|
||||||
|
"-f",
|
||||||
|
"rawvideo",
|
||||||
|
"-pix_fmt",
|
||||||
|
"rgb24",
|
||||||
|
"-s",
|
||||||
|
&format!("{size_x}x{size_y}"),
|
||||||
|
])
|
||||||
|
.input("-")
|
||||||
|
.args([
|
||||||
|
"-vcodec",
|
||||||
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
"veryslow",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
"-r",
|
||||||
|
"7",
|
||||||
|
"-vf",
|
||||||
|
&format!("setpts={velocity}*PTS,scale={shape_x}:{shape_y}:flags=neighbor"),
|
||||||
|
])
|
||||||
|
.output(path.to_str().expect("path cannot be converted to string"))
|
||||||
|
.spawn()?;
|
||||||
|
let mut stdin = movie.take_stdin().unwrap();
|
||||||
|
|
||||||
|
let ab = if options.no_scaling {
|
||||||
|
vec![
|
||||||
|
match view.pixel_type() {
|
||||||
|
PixelType::I8 => (i8::MIN as f64, i8::MAX as f64),
|
||||||
|
PixelType::U8 => (u8::MIN as f64, u8::MAX as f64),
|
||||||
|
PixelType::I16 => (i16::MIN as f64, i16::MAX as f64),
|
||||||
|
PixelType::U16 => (u16::MIN as f64, u16::MAX as f64),
|
||||||
|
PixelType::I32 => (i32::MIN as f64, i32::MAX as f64),
|
||||||
|
PixelType::U32 => (u32::MIN as f64, u32::MAX as f64),
|
||||||
|
PixelType::I64 => (i64::MIN as f64, i64::MAX as f64),
|
||||||
|
PixelType::U64 => (u64::MIN as f64, u64::MAX as f64),
|
||||||
|
_ => (0.0, 1.0),
|
||||||
|
};
|
||||||
|
view.shape().c
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
(0..size_c)
|
||||||
|
.map(|c| match view.slice(s![c, .., .., .., ..]) {
|
||||||
|
Ok(slice) => get_ab(slice.into_dyn()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
|
let bar = get_bar(
|
||||||
|
Some(size_t),
|
||||||
|
Some("autoscaling movie brightness".to_string()),
|
||||||
|
);
|
||||||
|
let rt_bar = bar.clone();
|
||||||
|
let write_task = rt.spawn(async move {
|
||||||
|
for t in 0..size_t {
|
||||||
|
let mut frame = Array3::<f64>::zeros((size_y, size_x, 3));
|
||||||
|
for c in 0..size_c {
|
||||||
|
frame = frame
|
||||||
|
+ cframe(
|
||||||
|
view.get_frame(c, 0, t)?,
|
||||||
|
&colors[c],
|
||||||
|
ab[c].0,
|
||||||
|
ab[c].1 / brightness[c],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let frame = (frame.clamp(0.0, 1.0) * 255.0).round().mapv(|i| i as u8);
|
||||||
|
let bytes: Vec<_> = frame.flatten().into_iter().collect();
|
||||||
|
stdin.write_all(&bytes)?;
|
||||||
|
rt_bar.inc(1);
|
||||||
|
}
|
||||||
|
Ok::<(), Error>(())
|
||||||
|
});
|
||||||
|
bar.finish();
|
||||||
|
|
||||||
|
let bar = get_bar(Some(size_t), Some("saving movie".to_string()));
|
||||||
|
let rt_bar = bar.clone();
|
||||||
|
let progress_task = rt.spawn(async move {
|
||||||
|
for event in movie.iter().map_err(|e| Error::Ffmpeg(e.to_string()))? {
|
||||||
|
match event {
|
||||||
|
FfmpegEvent::Log(LogLevel::Error, e) => Err(Error::Ffmpeg(e))?,
|
||||||
|
FfmpegEvent::Progress(p) => rt_bar.set_position(p.frame as u64),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok::<(), Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
rt.block_on(progress_task)??;
|
||||||
|
rt.block_on(write_task)??;
|
||||||
|
bar.finish();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::readers::DynReader;
|
||||||
|
use crate::view::View;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "czi", feature = "bioformats_java"))]
|
||||||
|
#[test]
|
||||||
|
fn movie() -> Result<(), Error> {
|
||||||
|
let file = "czi/1xp53-01-AP1.czi";
|
||||||
|
let path = std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file);
|
||||||
|
let view: View<_, DynReader> = View::from_path(&path)?;
|
||||||
|
let mut options = MovieOptions::default();
|
||||||
|
options.set_overwrite(true);
|
||||||
|
view.save_as_movie(
|
||||||
|
std::env::home_dir().unwrap().join("tmp/movie.mp4"),
|
||||||
|
&options,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use pyo3::exceptions::PyRuntimeError;
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
use pyo3_stub_gen::derive::*;
|
||||||
|
use pyo3_stub_gen::{StubGenConfig, StubInfo};
|
||||||
|
use std::error::Error as StdError;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
mod imread;
|
||||||
|
#[cfg(feature = "transforms")]
|
||||||
|
mod transforms;
|
||||||
|
|
||||||
|
fn format_error_chain(err: &Error) -> String {
|
||||||
|
let mut msg = format!("[{}] {err}", err.variant_name());
|
||||||
|
let mut source = err.source();
|
||||||
|
while let Some(s) = source {
|
||||||
|
msg.push_str(&format!("\n |-> {s}"));
|
||||||
|
source = s.source();
|
||||||
|
}
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Error> for PyErr {
|
||||||
|
#[track_caller]
|
||||||
|
fn from(err: Error) -> PyErr {
|
||||||
|
let location = std::panic::Location::caller();
|
||||||
|
let msg = format!("{}: {}", location, format_error_chain(&err));
|
||||||
|
PyRuntimeError::new_err(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ndbioimage_file() -> PathBuf {
|
||||||
|
let file = Python::attach(|py| {
|
||||||
|
py.import("ndbioimage")
|
||||||
|
.unwrap()
|
||||||
|
.filename()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
});
|
||||||
|
PathBuf::from(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generates ndbioimage/__init__.pyi
|
||||||
|
#[gen_stub_pyfunction]
|
||||||
|
#[pyfunction]
|
||||||
|
pub fn generate_stub(dest_path: String) -> PyResult<()> {
|
||||||
|
Ok(StubInfo::from_project_root(
|
||||||
|
"ndbioimage".to_string(),
|
||||||
|
PathBuf::from(dest_path).join("py"),
|
||||||
|
true,
|
||||||
|
StubGenConfig::default(),
|
||||||
|
)?
|
||||||
|
.generate()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// main entry point for the command-line interface
|
||||||
|
#[gen_stub_pyfunction(module = "ndbioimage")]
|
||||||
|
#[pyfunction]
|
||||||
|
fn main() -> PyResult<()> {
|
||||||
|
Ok(crate::main::main(Some(
|
||||||
|
std::env::args()
|
||||||
|
.skip_while(|arg| !arg.ends_with("ndbioimage"))
|
||||||
|
.collect(),
|
||||||
|
))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
#[pyo3(name = "ndbioimage_rs")]
|
||||||
|
mod ndbioimage_rs {
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::main;
|
||||||
|
|
||||||
|
#[cfg(feature = "tiffwrite")]
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::imread::batch_to_tiff;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::generate_stub;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::imread::PyView;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::imread::PyShape;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::imread::PyFlags;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use ome_metadata::py::ome_metadata;
|
||||||
|
|
||||||
|
#[cfg(feature = "transforms")]
|
||||||
|
#[pymodule]
|
||||||
|
mod transforms {
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::super::transforms::PyTransform;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::super::transforms::PyTransforms;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::super::transforms::PyRegistrationStep;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::super::transforms::PySigma;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::super::transforms::PySamplingArg;
|
||||||
|
|
||||||
|
#[pymodule_export]
|
||||||
|
use super::super::transforms::PyOptimizer;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2376
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+821
@@ -0,0 +1,821 @@
|
|||||||
|
use crate::axes::{Axis, Shape};
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::view::View;
|
||||||
|
use ndarray::{Array, Dimension, Ix2, Ix5, s};
|
||||||
|
use num::{FromPrimitive, Zero};
|
||||||
|
use ome_metadata::Ome;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::any::type_name;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
/// czi file reader
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
pub mod czi;
|
||||||
|
|
||||||
|
/// bioformats reader (pure rust)
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
pub mod bioformats_rust;
|
||||||
|
|
||||||
|
/// bioformats reader (java bindings)
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
pub mod bioformats_java;
|
||||||
|
|
||||||
|
/// tiff sequence reader
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
pub mod tiffseq;
|
||||||
|
|
||||||
|
/// single tiff file reader
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
pub mod tiff;
|
||||||
|
|
||||||
|
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([CZTSP])\D+(\d+)$").unwrap());
|
||||||
|
|
||||||
|
/// dimensions of the image data
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct Dimensions {
|
||||||
|
/// number of channels
|
||||||
|
pub c: Option<usize>,
|
||||||
|
/// number of z slices
|
||||||
|
pub z: Option<usize>,
|
||||||
|
/// number of time points
|
||||||
|
pub t: Option<usize>,
|
||||||
|
/// series index
|
||||||
|
pub s: Option<usize>,
|
||||||
|
/// position index
|
||||||
|
pub p: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dimensions {
|
||||||
|
/// create dimensions with the given series and position
|
||||||
|
pub fn new(series: usize, position: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
c: None,
|
||||||
|
z: None,
|
||||||
|
t: None,
|
||||||
|
s: Some(series),
|
||||||
|
p: Some(position),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// parse a path and extract dimensions from the directory structure
|
||||||
|
pub fn parse_path<P>(path: P) -> Result<(PathBuf, Self), Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut path = path.as_ref();
|
||||||
|
let mut new = Self::default();
|
||||||
|
while !path.exists() {
|
||||||
|
let last = path
|
||||||
|
.file_name()
|
||||||
|
.ok_or(Error::InvalidFileName)?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
path = path.parent().ok_or(Error::NoParent)?;
|
||||||
|
let last_upper = last.to_uppercase();
|
||||||
|
let caps = RE.captures(&last_upper).ok_or(Error::FileDoesNotExist(
|
||||||
|
path.join(&last).display().to_string(),
|
||||||
|
))?;
|
||||||
|
let p = caps[2].parse()?;
|
||||||
|
match &caps[1] {
|
||||||
|
"C" => new.c = Some(p),
|
||||||
|
"Z" => new.z = Some(p),
|
||||||
|
"T" => new.t = Some(p),
|
||||||
|
"S" => new.s = Some(p),
|
||||||
|
"P" => new.p = Some(p),
|
||||||
|
_ => {
|
||||||
|
return Err(Error::FileDoesNotExist(
|
||||||
|
path.join(last).display().to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((path.to_path_buf(), new))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// pixel type enum
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum PixelType {
|
||||||
|
/// true / false
|
||||||
|
Bool,
|
||||||
|
/// signed 8-bit integer
|
||||||
|
I8,
|
||||||
|
/// unsigned 8-bit integer
|
||||||
|
U8,
|
||||||
|
/// signed 16-bit integer
|
||||||
|
I16,
|
||||||
|
/// unsigned 16-bit integer
|
||||||
|
U16,
|
||||||
|
/// signed 32-bit integer
|
||||||
|
I32,
|
||||||
|
/// unsigned 32-bit integer
|
||||||
|
U32,
|
||||||
|
/// 32-bit float
|
||||||
|
F32,
|
||||||
|
/// 64-bit float
|
||||||
|
F64,
|
||||||
|
/// signed 64-bit integer
|
||||||
|
I64,
|
||||||
|
/// unsigned 64-bit integer
|
||||||
|
U64,
|
||||||
|
/// signed 128-bit integer
|
||||||
|
I128,
|
||||||
|
/// unsigned 128-bit integer
|
||||||
|
U128,
|
||||||
|
/// 128-bit float (emulated)
|
||||||
|
F128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PixelType {
|
||||||
|
/// number of bytes per pixel for this type
|
||||||
|
pub fn bytes_per_pixel(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
PixelType::Bool | PixelType::I8 | PixelType::U8 => 1,
|
||||||
|
PixelType::I16 | PixelType::U16 => 2,
|
||||||
|
PixelType::I32 | PixelType::U32 | PixelType::F32 => 4,
|
||||||
|
PixelType::I64 | PixelType::U64 | PixelType::F64 => 8,
|
||||||
|
PixelType::I128 | PixelType::U128 | PixelType::F128 => 16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// array data with a specific pixel type
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ArrayT<D: Dimension> {
|
||||||
|
/// signed 8-bit integer array
|
||||||
|
I8(Array<i8, D>),
|
||||||
|
/// unsigned 8-bit integer array
|
||||||
|
U8(Array<u8, D>),
|
||||||
|
/// signed 16-bit integer array
|
||||||
|
I16(Array<i16, D>),
|
||||||
|
/// unsigned 16-bit integer array
|
||||||
|
U16(Array<u16, D>),
|
||||||
|
/// signed 32-bit integer array
|
||||||
|
I32(Array<i32, D>),
|
||||||
|
/// unsigned 32-bit integer array
|
||||||
|
U32(Array<u32, D>),
|
||||||
|
/// 32-bit float array
|
||||||
|
F32(Array<f32, D>),
|
||||||
|
/// 64-bit float array
|
||||||
|
F64(Array<f64, D>),
|
||||||
|
/// signed 64-bit integer array
|
||||||
|
I64(Array<i64, D>),
|
||||||
|
/// unsigned 64-bit integer array
|
||||||
|
U64(Array<u64, D>),
|
||||||
|
/// signed 128-bit integer array
|
||||||
|
I128(Array<i128, D>),
|
||||||
|
/// unsigned 128-bit integer array
|
||||||
|
U128(Array<u128, D>),
|
||||||
|
/// 128-bit float array (emulated as f64)
|
||||||
|
F128(Array<f64, D>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// type alias for a single frame (2D image)
|
||||||
|
pub type Frame = ArrayT<Ix2>;
|
||||||
|
|
||||||
|
/// trait for reading image files
|
||||||
|
pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
|
||||||
|
/// create a new reader for the given path, series, and position
|
||||||
|
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>;
|
||||||
|
|
||||||
|
/// the name of the reader type
|
||||||
|
fn reader_name(&self) -> &'static str {
|
||||||
|
type_name::<Self>()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: read from file if present
|
||||||
|
/// get the ome metadata for the image
|
||||||
|
fn metadata(&self) -> Result<Ome, Error>;
|
||||||
|
|
||||||
|
/// get a sliceable view on the image file
|
||||||
|
fn view(&self) -> View<Ix5, Self> {
|
||||||
|
let shape = self.shape();
|
||||||
|
let slice = s![0..shape.c, 0..shape.z, 0..shape.t, 0..shape.y, 0..shape.x,];
|
||||||
|
View::new(
|
||||||
|
self.clone(),
|
||||||
|
slice.as_ref().to_vec(),
|
||||||
|
vec![Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// retrieve frame at channel c, slice z and time t
|
||||||
|
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error>;
|
||||||
|
|
||||||
|
/// the path to the image file
|
||||||
|
fn path(&self) -> &Path;
|
||||||
|
/// the series index
|
||||||
|
fn series(&self) -> usize;
|
||||||
|
/// the position index
|
||||||
|
fn position(&self) -> usize;
|
||||||
|
/// the shape of the image data
|
||||||
|
fn shape(&self) -> &Shape;
|
||||||
|
/// the pixel type of the image data
|
||||||
|
fn pixel_type(&self) -> &PixelType;
|
||||||
|
/// get all available positions for a given series
|
||||||
|
fn get_available_positions<P>(path: P, series: usize) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>;
|
||||||
|
/// get all available series
|
||||||
|
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bioformats numbering
|
||||||
|
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(Error::UnknownPixelType(value.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PixelType {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"bool" | "bit" => Ok(PixelType::Bool),
|
||||||
|
"int8" | "i8" => Ok(PixelType::I8),
|
||||||
|
"uint8" | "u8" => Ok(PixelType::U8),
|
||||||
|
"int16" | "i16" => Ok(PixelType::I16),
|
||||||
|
"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(Error::UnknownPixelType(s.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_frame_cast {
|
||||||
|
($($t:tt: $s:ident $(,)?)*) => {
|
||||||
|
$(
|
||||||
|
impl<D: Dimension> From<Array<$t, D>> for ArrayT<D> {
|
||||||
|
fn from(value: Array<$t, D>) -> Self {
|
||||||
|
ArrayT::$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
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! try_into_array_body {
|
||||||
|
($self:expr, $map:ident) => {{
|
||||||
|
let mut err = Ok(());
|
||||||
|
let arr = match $self {
|
||||||
|
ArrayT::I8(v) => v.$map(|x| {
|
||||||
|
T::from_i8(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::U8(v) => v.$map(|x| {
|
||||||
|
T::from_u8(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::I16(v) => v.$map(|x| {
|
||||||
|
T::from_i16(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::U16(v) => v.$map(|x| {
|
||||||
|
T::from_u16(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::I32(v) => v.$map(|x| {
|
||||||
|
T::from_i32(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::U32(v) => v.$map(|x| {
|
||||||
|
T::from_u32(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::F32(v) => v.$map(|x| {
|
||||||
|
T::from_f32(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::F64(v) | ArrayT::F128(v) => v.$map(|x| {
|
||||||
|
T::from_f64(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::I64(v) => v.$map(|x| {
|
||||||
|
T::from_i64(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::U64(v) => v.$map(|x| {
|
||||||
|
T::from_u64(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::I128(v) => v.$map(|x| {
|
||||||
|
T::from_i128(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
ArrayT::U128(v) => v.$map(|x| {
|
||||||
|
T::from_u128(x).unwrap_or_else(|| {
|
||||||
|
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||||
|
T::zero()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
match err {
|
||||||
|
Err(err) => Err(err),
|
||||||
|
Ok(()) => Ok(arr),
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D, T> TryInto<Array<T, D>> for ArrayT<D>
|
||||||
|
where
|
||||||
|
D: Dimension,
|
||||||
|
T: FromPrimitive + Zero + 'static,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<Array<T, D>, Self::Error> {
|
||||||
|
try_into_array_body!(self, mapv_into_any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D, T> TryInto<Array<T, D>> for &ArrayT<D>
|
||||||
|
where
|
||||||
|
D: Dimension,
|
||||||
|
T: Clone + FromPrimitive + Zero + 'static,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_into(self) -> Result<Array<T, D>, Self::Error> {
|
||||||
|
try_into_array_body!(self, mapv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// dynamic reader that can handle multiple file formats
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||||
|
pub enum DynReader {
|
||||||
|
/// tiff file reader
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
Tiff(tiff::TiffReader),
|
||||||
|
/// tiff sequence reader
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
TiffSeq(tiffseq::TiffSeqReader),
|
||||||
|
/// czi file reader
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
Czi(czi::CziReader),
|
||||||
|
/// bioformats reader (pure rust)
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
BioFormatsRust(bioformats_rust::BioFormatsRustReader),
|
||||||
|
/// bioformats reader (java bindings)
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
BioFormatsJava(bioformats_java::BioFormatsJavaReader),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reader for DynReader {
|
||||||
|
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut errors = Vec::<String>::new();
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
match tiff::TiffReader::new(&path, series, position) {
|
||||||
|
Ok(reader) => return Ok(DynReader::Tiff(reader)),
|
||||||
|
Err(err) => errors.push(format!("TiffReader: {}", err)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
match tiffseq::TiffSeqReader::new(&path, series, position) {
|
||||||
|
Ok(reader) => return Ok(DynReader::TiffSeq(reader)),
|
||||||
|
Err(err) => errors.push(format!("TiffseqReader: {}", err)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
match czi::CziReader::new(&path, series, position) {
|
||||||
|
Ok(reader) => return Ok(DynReader::Czi(reader)),
|
||||||
|
Err(err) => errors.push(format!("CziReader: {}", err)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
match bioformats_rust::BioFormatsRustReader::new(&path, series, position) {
|
||||||
|
Ok(reader) => return Ok(DynReader::BioFormatsRust(reader)),
|
||||||
|
Err(err) => errors.push(format!("BioformatsRustReader: {}", err)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
match bioformats_java::BioFormatsJavaReader::new(&path, series, position) {
|
||||||
|
Ok(reader) => return Ok(DynReader::BioFormatsJava(reader)),
|
||||||
|
Err(err) => errors.push(format!("BioformatsReader: {}", err)),
|
||||||
|
}
|
||||||
|
Err(Error::NoReader(
|
||||||
|
path.as_ref().display().to_string(),
|
||||||
|
errors.join("\n"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reader_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
DynReader::Tiff(r) => r.reader_name(),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
DynReader::TiffSeq(r) => r.reader_name(),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
DynReader::Czi(r) => r.reader_name(),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
DynReader::BioFormatsRust(r) => r.reader_name(),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
DynReader::BioFormatsJava(r) => r.reader_name(),
|
||||||
|
#[expect(unreachable_patterns)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Result<Ome, Error> {
|
||||||
|
Ok(match self {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
DynReader::Tiff(r) => r.metadata()?,
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
DynReader::TiffSeq(r) => r.metadata()?,
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
DynReader::Czi(r) => r.metadata()?,
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
DynReader::BioFormatsRust(r) => r.metadata()?,
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
DynReader::BioFormatsJava(r) => r.metadata()?,
|
||||||
|
#[expect(unreachable_patterns)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
DynReader::Tiff(r) => r.get_frame(c, z, t),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
DynReader::TiffSeq(r) => r.get_frame(c, z, t),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
DynReader::Czi(r) => r.get_frame(c, z, t),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
DynReader::BioFormatsRust(r) => r.get_frame(c, z, t),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
DynReader::BioFormatsJava(r) => r.get_frame(c, z, t),
|
||||||
|
#[expect(unreachable_patterns)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
DynReader::Tiff(r) => r.path(),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
DynReader::TiffSeq(r) => r.path(),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
DynReader::Czi(r) => r.path(),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
DynReader::BioFormatsRust(r) => r.path(),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
DynReader::BioFormatsJava(r) => r.path(),
|
||||||
|
#[expect(unreachable_patterns)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn series(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
DynReader::Tiff(r) => r.series(),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
DynReader::TiffSeq(r) => r.series(),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
DynReader::Czi(r) => r.series(),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
DynReader::BioFormatsRust(r) => r.series(),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
DynReader::BioFormatsJava(r) => r.series(),
|
||||||
|
#[expect(unreachable_patterns)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
DynReader::Tiff(r) => r.position(),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
DynReader::TiffSeq(r) => r.position(),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
DynReader::Czi(r) => r.position(),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
DynReader::BioFormatsRust(r) => r.position(),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
DynReader::BioFormatsJava(r) => r.position(),
|
||||||
|
#[expect(unreachable_patterns)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape(&self) -> &Shape {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
DynReader::Tiff(r) => r.shape(),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
DynReader::TiffSeq(r) => r.shape(),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
DynReader::Czi(r) => r.shape(),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
DynReader::BioFormatsRust(r) => r.shape(),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
DynReader::BioFormatsJava(r) => r.shape(),
|
||||||
|
#[expect(unreachable_patterns)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel_type(&self) -> &PixelType {
|
||||||
|
match self {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
DynReader::Tiff(r) => r.pixel_type(),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
DynReader::TiffSeq(r) => r.pixel_type(),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
DynReader::Czi(r) => r.pixel_type(),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
DynReader::BioFormatsRust(r) => r.pixel_type(),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
DynReader::BioFormatsJava(r) => r.pixel_type(),
|
||||||
|
#[expect(unreachable_patterns)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_positions<P>(path: P, series: usize) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut errors = Vec::<String>::new();
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
match tiff::TiffReader::get_available_positions(&path, series) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("TiffReader: {}", e)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
match tiffseq::TiffSeqReader::get_available_positions(&path, series) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("TiffSeqReader: {}", e)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
match czi::CziReader::get_available_positions(&path, series) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("CziReader: {}", e)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
match bioformats_rust::BioFormatsRustReader::get_available_positions(&path, series) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("BioFormatsRustReader: {}", e)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
match bioformats_java::BioFormatsJavaReader::get_available_positions(&path, series) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("BioFormatsReader: {}", e)),
|
||||||
|
}
|
||||||
|
Err(Error::NoReader(
|
||||||
|
path.as_ref().display().to_string(),
|
||||||
|
errors.join("\n"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut errors = Vec::<String>::new();
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
match tiff::TiffReader::get_available_series(&path) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("TiffReader: {}", e)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
match tiffseq::TiffSeqReader::get_available_series(&path) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("TiffSeqReader: {}", e)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
match czi::CziReader::get_available_series(&path) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("CziReader: {}", e)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
match bioformats_rust::BioFormatsRustReader::get_available_series(&path) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("BioFormatsRustReader: {}", e)),
|
||||||
|
}
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
match bioformats_java::BioFormatsJavaReader::get_available_series(&path) {
|
||||||
|
Ok(positions) => return Ok(positions),
|
||||||
|
Err(e) => errors.push(format!("BioFormatsReader: {}", e)),
|
||||||
|
}
|
||||||
|
Err(Error::NoReader(
|
||||||
|
path.as_ref().display().to_string(),
|
||||||
|
errors.join("\n"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynReader {
|
||||||
|
/// list of enabled readers
|
||||||
|
pub fn available_readers() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
"tiff".to_string(),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
"tiffseq".to_string(),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
"czi".to_string(),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
"bioformats_rust".to_string(),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
"bioformats_java".to_string(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create a dynreader by selecting the reader type explicitly
|
||||||
|
pub fn from_path_select_reader<P, R>(path: P, reader: R) -> Result<DynReader, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
R: AsRef<str>,
|
||||||
|
{
|
||||||
|
let path = path.as_ref();
|
||||||
|
let (path, dimensions) = Dimensions::parse_path(path)?;
|
||||||
|
let reader = reader.as_ref();
|
||||||
|
let reader = match reader.to_lowercase().as_str() {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
"tiff" => Ok(DynReader::Tiff(tiff::TiffReader::new(
|
||||||
|
path,
|
||||||
|
dimensions.s.unwrap_or(0),
|
||||||
|
dimensions.p.unwrap_or(0),
|
||||||
|
)?)),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
"tiffseq" => Ok(DynReader::TiffSeq(tiffseq::TiffSeqReader::new(
|
||||||
|
path,
|
||||||
|
dimensions.s.unwrap_or(0),
|
||||||
|
dimensions.p.unwrap_or(0),
|
||||||
|
)?)),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
"czi" => Ok(DynReader::Czi(czi::CziReader::new(
|
||||||
|
path,
|
||||||
|
dimensions.s.unwrap_or(0),
|
||||||
|
dimensions.p.unwrap_or(0),
|
||||||
|
)?)),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
"bioformats_rust" => Ok(DynReader::BioFormatsRust(
|
||||||
|
bioformats_rust::BioFormatsRustReader::new(
|
||||||
|
path,
|
||||||
|
dimensions.s.unwrap_or(0),
|
||||||
|
dimensions.p.unwrap_or(0),
|
||||||
|
)?,
|
||||||
|
)),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
"bioformats_java" => Ok(DynReader::BioFormatsJava(
|
||||||
|
bioformats_java::BioFormatsJavaReader::new(
|
||||||
|
path,
|
||||||
|
dimensions.s.unwrap_or(0),
|
||||||
|
dimensions.p.unwrap_or(0),
|
||||||
|
)?,
|
||||||
|
)),
|
||||||
|
_ => Err(Error::Parse(reader.to_string())),
|
||||||
|
}?;
|
||||||
|
Ok(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get all available positions, optionally selecting a specific reader type
|
||||||
|
pub fn get_available_positions_select_reader<P, R>(
|
||||||
|
path: P,
|
||||||
|
series: usize,
|
||||||
|
reader: Option<R>,
|
||||||
|
) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
R: AsRef<str>,
|
||||||
|
{
|
||||||
|
Ok(if let Some(reader) = reader {
|
||||||
|
let reader = reader.as_ref();
|
||||||
|
match reader.to_lowercase().as_str() {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
"tiff" => Ok(tiff::TiffReader::get_available_positions(path, series)?),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
"tiffseq" => Ok(tiffseq::TiffSeqReader::get_available_positions(
|
||||||
|
path, series,
|
||||||
|
)?),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
"czi" => Ok(czi::CziReader::get_available_positions(path, series)?),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
"bioformats_rust" => Ok(
|
||||||
|
bioformats_rust::BioFormatsRustReader::get_available_positions(path, series)?,
|
||||||
|
),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
"bioformats_java" => Ok(
|
||||||
|
bioformats_java::BioFormatsJavaReader::get_available_positions(path, series)?,
|
||||||
|
),
|
||||||
|
_ => Err(Error::Parse(reader.to_string())),
|
||||||
|
}?
|
||||||
|
} else {
|
||||||
|
DynReader::get_available_positions(&path, series)?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get all available series, optionally selecting a specific reader type
|
||||||
|
pub fn get_available_series_select_reader<P, R>(
|
||||||
|
path: P,
|
||||||
|
reader: Option<R>,
|
||||||
|
) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
R: AsRef<str>,
|
||||||
|
{
|
||||||
|
Ok(if let Some(reader) = reader {
|
||||||
|
let reader = reader.as_ref();
|
||||||
|
match reader.to_lowercase().as_str() {
|
||||||
|
#[cfg(feature = "tiff")]
|
||||||
|
"tiff" => Ok(tiff::TiffReader::get_available_series(path)?),
|
||||||
|
#[cfg(feature = "tiffseq")]
|
||||||
|
"tiffseq" => Ok(tiffseq::TiffSeqReader::get_available_series(path)?),
|
||||||
|
#[cfg(feature = "czi")]
|
||||||
|
"czi" => Ok(czi::CziReader::get_available_series(path)?),
|
||||||
|
#[cfg(feature = "bioformats_rust")]
|
||||||
|
"bioformats_rust" => Ok(
|
||||||
|
bioformats_rust::BioFormatsRustReader::get_available_series(path)?,
|
||||||
|
),
|
||||||
|
#[cfg(feature = "bioformats_java")]
|
||||||
|
"bioformats_java" => Ok(
|
||||||
|
bioformats_java::BioFormatsJavaReader::get_available_series(path)?,
|
||||||
|
),
|
||||||
|
_ => Err(Error::Parse(reader.to_string())),
|
||||||
|
}?
|
||||||
|
} else {
|
||||||
|
DynReader::get_available_series(&path)?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,665 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use ndarray::Array2;
|
||||||
|
use ome_metadata::Ome;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use j4rs::{Instance, InvocationArg, JavaOpt, Jvm, JvmBuilder};
|
||||||
|
use std::cell::OnceCell;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use thread_local::ThreadLocal;
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/constants.rs"));
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static JVM: OnceCell<Rc<Jvm>> = const { OnceCell::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
static DOWNLOAD_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
static JVM_BUILT: Mutex<bool> = Mutex::new(false);
|
||||||
|
|
||||||
|
/// Ensure 1 jvm per thread
|
||||||
|
fn jvm() -> Rc<Jvm> {
|
||||||
|
JVM.with(|cell| {
|
||||||
|
cell.get_or_init(move || {
|
||||||
|
#[cfg(feature = "python")]
|
||||||
|
let path = crate::py::ndbioimage_file();
|
||||||
|
|
||||||
|
#[cfg(not(feature = "python"))]
|
||||||
|
let path = std::env::current_exe()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
let class_path = if path.join("jassets").exists() {
|
||||||
|
path.as_path()
|
||||||
|
} else {
|
||||||
|
path.parent().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// download jars if needed, but make sure only one thread will do this
|
||||||
|
{
|
||||||
|
let _guard = DOWNLOAD_LOCK.lock().unwrap();
|
||||||
|
let jassets = class_path.join("jassets");
|
||||||
|
if !jassets.exists() {
|
||||||
|
std::fs::create_dir_all(&jassets).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !jassets.join(format!("j4rs-{}-jar-with-dependencies.jar", J4RS_VERSION)).exists() {
|
||||||
|
println!("downloading j4rs-{}-jar-with-dependencies.jar into {}", J4RS_VERSION, jassets.display());
|
||||||
|
let download = downloader::Download::new(&format!(
|
||||||
|
"https://github.com/astonbitecode/j4rs/raw/v{}/rust/jassets/j4rs-{}-jar-with-dependencies.jar",
|
||||||
|
J4RS_VERSION, J4RS_VERSION
|
||||||
|
));
|
||||||
|
let mut downloader = downloader::Downloader::builder()
|
||||||
|
.download_folder(&jassets)
|
||||||
|
.build().unwrap();
|
||||||
|
downloader
|
||||||
|
.download(&[download]).unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<_>, _>>().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !jassets.join(format!("bioformats_package-{}.jar", BIOFORMATS_VERSION)).exists() {
|
||||||
|
println!("downloading bioformats_package-{}.jar into {}", BIOFORMATS_VERSION, jassets.display());
|
||||||
|
let download = downloader::Download::new(&format!(
|
||||||
|
"https://artifacts.openmicroscopy.org/artifactory/ome.releases/ome/bioformats_package/{}/bioformats_package-{}.jar",
|
||||||
|
BIOFORMATS_VERSION, BIOFORMATS_VERSION
|
||||||
|
));
|
||||||
|
let mut downloader = downloader::Downloader::builder()
|
||||||
|
.download_folder(&jassets)
|
||||||
|
.build().unwrap();
|
||||||
|
downloader
|
||||||
|
.download(&[download]).unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<_>, _>>().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "gpl-formats")]
|
||||||
|
if !jassets.join(format!("formats-gpl-{}.jar", BIOFORMATS_VERSION)).exists() {
|
||||||
|
println!("downloading formats-gpl-{}.jar into {}", BIOFORMATS_VERSION, jassets.display());
|
||||||
|
let download = downloader::Download::new(&format!(
|
||||||
|
"https://artifacts.openmicroscopy.org/artifactory/ome.releases/ome/formats-gpl/{}/formats-gpl-{}.jar",
|
||||||
|
BIOFORMATS_VERSION, BIOFORMATS_VERSION
|
||||||
|
));
|
||||||
|
let mut downloader = downloader::Downloader::builder()
|
||||||
|
.download_folder(&jassets)
|
||||||
|
.build().unwrap();
|
||||||
|
downloader
|
||||||
|
.download(&[download]).unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<_>, _>>().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut jvm_built = JVM_BUILT.lock().unwrap();
|
||||||
|
Rc::new(if *jvm_built {
|
||||||
|
Jvm::attach_thread().expect("Failed to attach to JVM")
|
||||||
|
} else {
|
||||||
|
*jvm_built = true;
|
||||||
|
let j = JvmBuilder::new()
|
||||||
|
.skip_setting_native_lib()
|
||||||
|
.with_base_path(class_path.to_str().unwrap())
|
||||||
|
.java_opt(JavaOpt::new("-XX:+IgnoreUnrecognizedVMOptions"))
|
||||||
|
.java_opt(JavaOpt::new("--enable-native-access=ALL-UNNAMED"))
|
||||||
|
.java_opt(JavaOpt::new("--sun-misc-unsafe-memory-access=allow"))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build JVM");
|
||||||
|
if let Ok(e) = InvocationArg::try_from("ERROR") {
|
||||||
|
let _ = j.invoke_static(
|
||||||
|
"loci.common.DebugTools",
|
||||||
|
"setRootLevel",
|
||||||
|
&[e],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
j
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! method_return {
|
||||||
|
($R:ty$(|c)?) => { Result<$R, Error> };
|
||||||
|
() => { Result<(), Error> };
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! method_arg {
|
||||||
|
($n:tt: $t:ty|p) => {
|
||||||
|
InvocationArg::try_from($n)?.into_primitive()?
|
||||||
|
};
|
||||||
|
($n:tt: $t:ty) => {
|
||||||
|
InvocationArg::try_from($n)?
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! method {
|
||||||
|
($name:ident, $method:expr $(,[$($n:tt: $t:ty$(|$p:tt)?),*])? $(=> $tt:ty$(|$c:tt)?)?) => {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn $name(&self, $($($n: $t),*)?) -> method_return!($($tt)?) {
|
||||||
|
let args: Vec<InvocationArg> = vec![$($( method_arg!($n:$t$(|$p)?) ),*)?];
|
||||||
|
let _result = jvm().invoke(&self.0, $method, &args)?;
|
||||||
|
|
||||||
|
macro_rules! method_result {
|
||||||
|
($R:ty|c) => {
|
||||||
|
Ok(jvm().to_rust(_result)?)
|
||||||
|
};
|
||||||
|
($R:ty|d) => {
|
||||||
|
Ok(jvm().to_rust_deserialized(_result)?)
|
||||||
|
};
|
||||||
|
($R:ty) => {
|
||||||
|
Ok(_result)
|
||||||
|
};
|
||||||
|
() => {
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
method_result!($($tt$(|$c)?)?)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transmute_vec<T, U>(vec: Vec<T>) -> Vec<U> {
|
||||||
|
unsafe {
|
||||||
|
// Ensure the original vector is not dropped.
|
||||||
|
let mut v_clone = std::mem::ManuallyDrop::new(vec);
|
||||||
|
Vec::from_raw_parts(
|
||||||
|
v_clone.as_mut_ptr() as *mut U,
|
||||||
|
v_clone.len(),
|
||||||
|
v_clone.capacity(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around bioformats java class loci.common.DebugTools
|
||||||
|
pub struct DebugTools;
|
||||||
|
|
||||||
|
impl DebugTools {
|
||||||
|
/// set debug root level: ERROR, DEBUG, TRACE, INFO, OFF
|
||||||
|
pub fn set_root_level(level: &str) -> Result<(), Error> {
|
||||||
|
jvm().invoke_static(
|
||||||
|
"loci.common.DebugTools",
|
||||||
|
"setRootLevel",
|
||||||
|
&[InvocationArg::try_from(level)?],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around bioformats java class loci.formats.ChannelSeparator
|
||||||
|
pub(crate) struct ChannelSeparator(Instance);
|
||||||
|
|
||||||
|
impl ChannelSeparator {
|
||||||
|
pub(crate) fn new(image_reader: &ImageReader) -> Result<Self, Error> {
|
||||||
|
let jvm = jvm();
|
||||||
|
let channel_separator = jvm.create_instance(
|
||||||
|
"loci.formats.ChannelSeparator",
|
||||||
|
&[InvocationArg::from(jvm.clone_instance(&image_reader.0)?)],
|
||||||
|
)?;
|
||||||
|
Ok(ChannelSeparator(channel_separator))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>, Error> {
|
||||||
|
Ok(transmute_vec(self.open_bi8(index)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
method!(open_bi8, "openBytes", [index: i32|p] => Vec<i8>|c);
|
||||||
|
method!(get_index, "getIndex", [z: i32|p, c: i32|p, t: i32|p] => i32|c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around bioformats java class loci.formats.ImageReader
|
||||||
|
pub struct ImageReader(Instance);
|
||||||
|
|
||||||
|
impl Drop for ImageReader {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.close().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageReader {
|
||||||
|
pub(crate) fn new() -> Result<Self, Error> {
|
||||||
|
let reader = jvm().create_instance("loci.formats.ImageReader", InvocationArg::empty())?;
|
||||||
|
Ok(ImageReader(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn open_bytes(&self, index: i32) -> Result<Vec<u8>, Error> {
|
||||||
|
Ok(transmute_vec(self.open_bi8(index)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ome_xml(&self) -> Result<String, Error> {
|
||||||
|
let mds = self.get_metadata_store()?;
|
||||||
|
Ok(jvm()
|
||||||
|
.chain(&mds)?
|
||||||
|
.cast("loci.formats.ome.OMEPyramidStore")?
|
||||||
|
.invoke("dumpXML", InvocationArg::empty())?
|
||||||
|
.to_rust()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
method!(close, "close");
|
||||||
|
method!(is_indexed, "isIndexed" => bool|c);
|
||||||
|
method!(is_interleaved, "isInterleaved" => bool|c);
|
||||||
|
method!(is_little_endian, "isLittleEndian" => bool|c);
|
||||||
|
method!(is_rgb, "isRGB" => bool|c);
|
||||||
|
method!(get_8bit_lookup_table, "get8BitLookupTable" => Instance);
|
||||||
|
method!(get_16bit_lookup_table, "get16BitLookupTable" => Instance);
|
||||||
|
method!(get_dimension_order, "getDimensionOrder" => String|c);
|
||||||
|
method!(set_id, "setId", [id: &str]);
|
||||||
|
method!(get_index, "getIndex", [z: i32|p, c: i32|p, t: i32|p] => i32|c);
|
||||||
|
method!(set_metadata_store, "setMetadataStore", [ome_data: Instance]);
|
||||||
|
method!(get_metadata_store, "getMetadataStore" => Instance);
|
||||||
|
method!(get_pixel_type, "getPixelType" => i32|c);
|
||||||
|
method!(get_rgb_channel_count, "getRGBChannelCount" => i32|c);
|
||||||
|
method!(get_series, "getSeries" => i32|c);
|
||||||
|
method!(set_series, "setSeries", [series: i32|p]);
|
||||||
|
method!(get_series_count, "getSeriesCount" => i32|c);
|
||||||
|
method!(get_size_x, "getSizeX" => i32|c);
|
||||||
|
method!(get_size_y, "getSizeY" => i32|c);
|
||||||
|
method!(get_size_c, "getSizeC" => i32|c);
|
||||||
|
method!(get_size_t, "getSizeT" => i32|c);
|
||||||
|
method!(get_size_z, "getSizeZ" => i32|c);
|
||||||
|
method!(open_bi8, "openBytes", [index: i32|p] => Vec<i8>|c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper around bioformats java class loci.formats.MetadataTools
|
||||||
|
pub(crate) struct MetadataTools(Instance);
|
||||||
|
|
||||||
|
impl MetadataTools {
|
||||||
|
pub(crate) fn new() -> Result<Self, Error> {
|
||||||
|
let meta_data_tools =
|
||||||
|
jvm().create_instance("loci.formats.MetadataTools", InvocationArg::empty())?;
|
||||||
|
Ok(MetadataTools(meta_data_tools))
|
||||||
|
}
|
||||||
|
|
||||||
|
method!(create_ome_xml_metadata, "createOMEXMLMetadata" => Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reader interface to file. Use get_frame to get data.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct BioFormatsJavaReader {
|
||||||
|
#[serde(skip)]
|
||||||
|
reader: ThreadLocal<ImageReader>,
|
||||||
|
/// path to file
|
||||||
|
path: PathBuf,
|
||||||
|
/// which (if more than 1) of the series in the file to open
|
||||||
|
series: usize,
|
||||||
|
shape: Shape,
|
||||||
|
pixel_type: PixelType,
|
||||||
|
little_endian: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BioFormatsJavaReader> for DynReader {
|
||||||
|
fn from(value: BioFormatsJavaReader) -> Self {
|
||||||
|
DynReader::BioFormatsJava(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for BioFormatsJavaReader {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.path.hash(state);
|
||||||
|
self.series.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BioFormatsJavaReader {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.path == other.path
|
||||||
|
&& self.series == other.series
|
||||||
|
&& self.shape == other.shape
|
||||||
|
&& self.pixel_type == other.pixel_type
|
||||||
|
&& self.little_endian == other.little_endian
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for BioFormatsJavaReader {}
|
||||||
|
|
||||||
|
impl Deref for BioFormatsJavaReader {
|
||||||
|
type Target = ImageReader;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.get_reader().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for BioFormatsJavaReader {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
// BioFormatsReader::new(&self.path, self.series, 0).unwrap()
|
||||||
|
Self {
|
||||||
|
reader: ThreadLocal::default(),
|
||||||
|
path: self.path.clone(),
|
||||||
|
series: self.series,
|
||||||
|
shape: self.shape.clone(),
|
||||||
|
pixel_type: self.pixel_type,
|
||||||
|
little_endian: self.little_endian,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for BioFormatsJavaReader {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("BioFormatsJavaReader")
|
||||||
|
.field("path", &self.path)
|
||||||
|
.field("series", &self.series)
|
||||||
|
.field("shape", &self.shape)
|
||||||
|
.field("pixel_type", &self.pixel_type)
|
||||||
|
.field("little_endian", &self.little_endian)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BioFormatsJavaReader {
|
||||||
|
fn get_reader(&self) -> Result<&ImageReader, Error> {
|
||||||
|
self.reader.get_or_try(|| {
|
||||||
|
let reader = ImageReader::new()?;
|
||||||
|
let meta_data_tools = MetadataTools::new()?;
|
||||||
|
let ome_meta = meta_data_tools.create_ome_xml_metadata()?;
|
||||||
|
reader.set_metadata_store(ome_meta)?;
|
||||||
|
reader.set_id(self.path.to_str().ok_or(Error::InvalidFileName)?)?;
|
||||||
|
reader.set_series(self.series as i32)?;
|
||||||
|
Ok(reader)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn set_reader(&self) -> Result<(), Error> {
|
||||||
|
// self.get_reader().map(|_| ())
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// Get ome metadata as ome structure
|
||||||
|
pub fn get_ome(&self) -> Result<Ome, Error> {
|
||||||
|
let mut ome = Ome::from_xml(self.ome_xml()?)?;
|
||||||
|
if ome.image.len() > 1 {
|
||||||
|
ome.image = vec![ome.image[self.series].clone()];
|
||||||
|
}
|
||||||
|
Ok(ome)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get ome metadata as xml string
|
||||||
|
pub fn get_ome_xml(&self) -> Result<String, Error> {
|
||||||
|
self.ome_xml()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>, Error> {
|
||||||
|
let chunk_size = match self.pixel_type {
|
||||||
|
PixelType::Bool => 1,
|
||||||
|
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.shape.c)
|
||||||
|
.flat_map(|a| a.to_vec())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame, Error> {
|
||||||
|
macro_rules! get_frame {
|
||||||
|
(bool, <$n:expr) => {
|
||||||
|
Ok(ArrayT::from(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
bytes
|
||||||
|
.iter()
|
||||||
|
.map(|x| [x & 128, x & 64, x & 32, x & 16, x & 8, x & 4, x & 2, x & 1])
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
)?))
|
||||||
|
};
|
||||||
|
(bool, >$n:expr) => {
|
||||||
|
Ok(ArrayT::from(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
bytes
|
||||||
|
.iter()
|
||||||
|
.map(|x| [x & 1, x & 2, x & 4, x & 8, x & 16, x & 32, x & 64, x & 128])
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
)?))
|
||||||
|
};
|
||||||
|
($t:tt, <$n:expr) => {
|
||||||
|
Ok(ArrayT::from(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
bytes
|
||||||
|
.chunks($n)
|
||||||
|
.map(|x| $t::from_le_bytes(x.try_into().unwrap()))
|
||||||
|
.collect(),
|
||||||
|
)?))
|
||||||
|
};
|
||||||
|
($t:tt, >$n:expr) => {
|
||||||
|
Ok(ArrayT::from(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
bytes
|
||||||
|
.chunks($n)
|
||||||
|
.map(|x| $t::from_be_bytes(x.try_into().unwrap()))
|
||||||
|
.collect(),
|
||||||
|
)?))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match (&self.pixel_type, self.little_endian) {
|
||||||
|
(PixelType::Bool, true) => get_frame!(bool, <1),
|
||||||
|
(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::Bool, false) => get_frame!(bool, >1),
|
||||||
|
(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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BioFormatsJavaReader {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Ok(reader) = self.get_reader() {
|
||||||
|
reader.close().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_tiff(path: &Path) -> Result<Option<PathBuf>, Error> {
|
||||||
|
if let Some(ext) = path.extension()
|
||||||
|
&& path.is_file()
|
||||||
|
&& (["tif", "tiff"].contains(&ext.to_string_lossy().to_lowercase().as_str()))
|
||||||
|
{
|
||||||
|
return Ok(Some(path.to_path_buf()));
|
||||||
|
} else if path.is_dir() {
|
||||||
|
for file in path.read_dir()?.flatten().sorted_by_key(|i| i.file_name()) {
|
||||||
|
if let Ok(Some(file)) = find_tiff(file.path().as_path()) {
|
||||||
|
return Ok(Some(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reader for BioFormatsJavaReader {
|
||||||
|
/// Create a new reader for the image file at a path, and open series #.
|
||||||
|
fn new<P>(path: P, series: usize, _position: usize) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut path = path.as_ref().to_path_buf();
|
||||||
|
if path.is_dir() {
|
||||||
|
let orig = path.clone();
|
||||||
|
path = find_tiff(&path)?.ok_or_else(|| {
|
||||||
|
Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
let mut new = Self {
|
||||||
|
reader: ThreadLocal::default(),
|
||||||
|
path,
|
||||||
|
series,
|
||||||
|
shape: Shape::default(),
|
||||||
|
pixel_type: PixelType::I8,
|
||||||
|
little_endian: false,
|
||||||
|
};
|
||||||
|
// new.set_reader()?;
|
||||||
|
new.shape.x = new.get_size_x()? as usize;
|
||||||
|
new.shape.y = new.get_size_y()? as usize;
|
||||||
|
new.shape.c = new.get_size_c()? as usize;
|
||||||
|
new.shape.z = new.get_size_z()? as usize;
|
||||||
|
new.shape.t = new.get_size_t()? as usize;
|
||||||
|
new.pixel_type = PixelType::try_from(new.get_pixel_type()?)?;
|
||||||
|
new.little_endian = new.is_little_endian()?;
|
||||||
|
Ok(new)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Result<Ome, Error> {
|
||||||
|
self.get_ome()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve fame at channel c, slize z and time t.
|
||||||
|
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
|
||||||
|
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 = ChannelSeparator::new(self)?;
|
||||||
|
let index = channel_separator.get_index(z as i32, c as i32, t as i32)?;
|
||||||
|
channel_separator.open_bytes(index)?
|
||||||
|
} 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 path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn series(&self) -> usize {
|
||||||
|
self.series
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(&self) -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape(&self) -> &Shape {
|
||||||
|
&self.shape
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel_type(&self) -> &PixelType {
|
||||||
|
&self.pixel_type
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_positions<P>(_path: P, _series: usize) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
Ok(HashSet::from([0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut path = path.as_ref().to_path_buf();
|
||||||
|
if path.is_dir() {
|
||||||
|
let orig = path.clone();
|
||||||
|
path = find_tiff(&path)?.ok_or_else(|| {
|
||||||
|
Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
let new = BioFormatsJavaReader {
|
||||||
|
reader: ThreadLocal::default(),
|
||||||
|
path,
|
||||||
|
series: 0,
|
||||||
|
shape: Shape::default(),
|
||||||
|
pixel_type: PixelType::I8,
|
||||||
|
little_endian: false,
|
||||||
|
};
|
||||||
|
Ok(HashSet::from_iter(0..(new.get_series_count()? as usize)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn open(file: &str) -> Result<BioFormatsJavaReader, Error> {
|
||||||
|
let path = std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file);
|
||||||
|
BioFormatsJavaReader::new(&path, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test_metadata {
|
||||||
|
($($name:ident: $file:expr $(,)?)*) => {
|
||||||
|
$(
|
||||||
|
#[test]
|
||||||
|
fn $name() -> Result<(), Error> {
|
||||||
|
let bf = open($file)?;
|
||||||
|
println!("{}", bf.view().squeeze()?.summary()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test_metadata! {
|
||||||
|
metadata_a: "czi/1xp53-01-AP1.czi",
|
||||||
|
metadata_b: "czi/beads_2023_05_04__19_00_22.czi",
|
||||||
|
metadata_c: "czi/Experiment-2029.czi",
|
||||||
|
metadata_d: "czi/MK022_cE9_1-01-Airyscan Processing-01-Scene-2-P1.czi",
|
||||||
|
metadata_e: "czi/YTL1849A131_2023_05_04__13_36_36.czi",
|
||||||
|
metadata_f: "czi/EU_UV_t=1-01.czi",
|
||||||
|
metadata_g: "tiffseq/4-Pos_001_002/img_000000000_Cy3-Cy3_filter_000.tif",
|
||||||
|
metadata_h: "tiffseq/20-Pos_005_005/img_000000000_Cy3-Cy3_filter_000.tif",
|
||||||
|
metadata_i: "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1",
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ome_xml() -> Result<(), Error> {
|
||||||
|
let file = "czi/Experiment-2029.czi";
|
||||||
|
let path = std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file);
|
||||||
|
let reader = BioFormatsJavaReader::new(&path, 0, 0)?;
|
||||||
|
let xml = reader.get_ome_xml()?;
|
||||||
|
println!("{}", xml);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use crate::readers::ArrayT;
|
||||||
|
use crate::readers::{DynReader, Frame, PixelType, Reader, Shape};
|
||||||
|
use bioformats::{DimensionOrder, ImageReader};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ndarray::Array2;
|
||||||
|
use ome_metadata::Ome;
|
||||||
|
use std::cell::{RefCell, RefMut};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fmt;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use thread_local::ThreadLocal;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct BioFormatsRustReader {
|
||||||
|
#[serde(skip)]
|
||||||
|
reader: ThreadLocal<RefCell<ImageReader>>,
|
||||||
|
path: PathBuf,
|
||||||
|
series: usize,
|
||||||
|
shape: Shape,
|
||||||
|
pixel_type: PixelType,
|
||||||
|
little_endian: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for BioFormatsRustReader {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("BioFormatsRustReader")
|
||||||
|
.field("path", &self.path)
|
||||||
|
.field("series", &self.series)
|
||||||
|
.field("shape", &self.shape)
|
||||||
|
.field("pixel_type", &self.pixel_type)
|
||||||
|
.field("little_endian", &self.little_endian)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BioFormatsRustReader> for DynReader {
|
||||||
|
fn from(value: BioFormatsRustReader) -> Self {
|
||||||
|
DynReader::BioFormatsRust(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for BioFormatsRustReader {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.path.hash(state);
|
||||||
|
self.series.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BioFormatsRustReader {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.path == other.path && self.series == other.series
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for BioFormatsRustReader {}
|
||||||
|
|
||||||
|
impl Clone for BioFormatsRustReader {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
BioFormatsRustReader {
|
||||||
|
reader: ThreadLocal::default(),
|
||||||
|
path: self.path.clone(),
|
||||||
|
series: self.series,
|
||||||
|
shape: self.shape.clone(),
|
||||||
|
pixel_type: self.pixel_type,
|
||||||
|
little_endian: self.little_endian,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for BioFormatsRustReader {
|
||||||
|
type Target = ThreadLocal<RefCell<ImageReader>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.reader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bioformats::PixelType> for PixelType {
|
||||||
|
fn from(bf: bioformats::PixelType) -> Self {
|
||||||
|
match bf {
|
||||||
|
bioformats::PixelType::Bit => PixelType::Bool,
|
||||||
|
bioformats::PixelType::Int8 => PixelType::I8,
|
||||||
|
bioformats::PixelType::Uint8 => PixelType::U8,
|
||||||
|
bioformats::PixelType::Int16 => PixelType::I16,
|
||||||
|
bioformats::PixelType::Uint16 => PixelType::U16,
|
||||||
|
bioformats::PixelType::Int32 => PixelType::I32,
|
||||||
|
bioformats::PixelType::Uint32 => PixelType::U32,
|
||||||
|
bioformats::PixelType::Float32 => PixelType::F32,
|
||||||
|
bioformats::PixelType::Float64 => PixelType::F64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BioFormatsRustReader {
|
||||||
|
fn get_reader(&self) -> Result<RefMut<'_, ImageReader>, Error> {
|
||||||
|
self.reader
|
||||||
|
.get_or_try(|| {
|
||||||
|
let mut reader = ImageReader::open(&self.path)?;
|
||||||
|
reader.set_series(self.series)?;
|
||||||
|
Ok(RefCell::new(reader))
|
||||||
|
})
|
||||||
|
.map(|i| i.borrow_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_index(order: DimensionOrder, shape: &Shape, c: usize, z: usize, t: usize) -> u32 {
|
||||||
|
let (s0, s1) = match order {
|
||||||
|
DimensionOrder::XYZCT => (shape.z, shape.c),
|
||||||
|
DimensionOrder::XYZTC => (shape.z, shape.t),
|
||||||
|
DimensionOrder::XYCZT => (shape.c, shape.z),
|
||||||
|
DimensionOrder::XYCTZ => (shape.c, shape.t),
|
||||||
|
DimensionOrder::XYTZC => (shape.t, shape.z),
|
||||||
|
DimensionOrder::XYTCZ => (shape.t, shape.c),
|
||||||
|
};
|
||||||
|
let (v0, v1, v2) = match order {
|
||||||
|
DimensionOrder::XYZCT => (z, c, t),
|
||||||
|
DimensionOrder::XYZTC => (z, t, c),
|
||||||
|
DimensionOrder::XYCZT => (c, z, t),
|
||||||
|
DimensionOrder::XYCTZ => (c, t, z),
|
||||||
|
DimensionOrder::XYTZC => (t, z, c),
|
||||||
|
DimensionOrder::XYTCZ => (t, c, z),
|
||||||
|
};
|
||||||
|
(v0 + v1 * s0 + v2 * s0 * s1) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinterleave(&self, bytes: Vec<u8>, channel: usize) -> Result<Vec<u8>, Error> {
|
||||||
|
let chunk_size = match self.pixel_type {
|
||||||
|
PixelType::Bool => 1,
|
||||||
|
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.shape.c)
|
||||||
|
.flat_map(|a| a.to_vec())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame, Error> {
|
||||||
|
macro_rules! get_frame {
|
||||||
|
(bool, <$n:expr) => {
|
||||||
|
Ok(ArrayT::from(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
bytes
|
||||||
|
.iter()
|
||||||
|
.map(|x| [x & 128, x & 64, x & 32, x & 16, x & 8, x & 4, x & 2, x & 1])
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
)?))
|
||||||
|
};
|
||||||
|
(bool, >$n:expr) => {
|
||||||
|
Ok(ArrayT::from(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
bytes
|
||||||
|
.iter()
|
||||||
|
.map(|x| [x & 1, x & 2, x & 4, x & 8, x & 16, x & 32, x & 64, x & 128])
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
)?))
|
||||||
|
};
|
||||||
|
($t:tt, <$n:expr) => {
|
||||||
|
Ok(ArrayT::from(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
bytes
|
||||||
|
.chunks($n)
|
||||||
|
.map(|x| $t::from_le_bytes(x.try_into().unwrap()))
|
||||||
|
.collect(),
|
||||||
|
)?))
|
||||||
|
};
|
||||||
|
($t:tt, >$n:expr) => {
|
||||||
|
Ok(ArrayT::from(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
bytes
|
||||||
|
.chunks($n)
|
||||||
|
.map(|x| $t::from_be_bytes(x.try_into().unwrap()))
|
||||||
|
.collect(),
|
||||||
|
)?))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
match (&self.pixel_type, self.little_endian) {
|
||||||
|
(PixelType::Bool, true) => get_frame!(bool, <1),
|
||||||
|
(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::Bool, false) => get_frame!(bool, >1),
|
||||||
|
(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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_tiff(path: &Path) -> Result<Option<PathBuf>, Error> {
|
||||||
|
if let Some(ext) = path.extension()
|
||||||
|
&& path.is_file()
|
||||||
|
&& (["tif", "tiff"].contains(&ext.to_string_lossy().to_lowercase().as_str()))
|
||||||
|
{
|
||||||
|
return Ok(Some(path.to_path_buf()));
|
||||||
|
} else if path.is_dir() {
|
||||||
|
for file in path.read_dir()?.flatten().sorted_by_key(|i| i.file_name()) {
|
||||||
|
if let Ok(Some(file)) = find_tiff(file.path().as_path()) {
|
||||||
|
return Ok(Some(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reader for BioFormatsRustReader {
|
||||||
|
fn new<P>(path: P, series: usize, _position: usize) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut path = path.as_ref().to_path_buf();
|
||||||
|
if path.is_dir() {
|
||||||
|
let orig = path.clone();
|
||||||
|
path = find_tiff(&path)?.ok_or_else(|| {
|
||||||
|
Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
let mut new = Self {
|
||||||
|
reader: ThreadLocal::default(),
|
||||||
|
path,
|
||||||
|
series,
|
||||||
|
shape: Shape::default(),
|
||||||
|
pixel_type: PixelType::I8,
|
||||||
|
little_endian: false,
|
||||||
|
};
|
||||||
|
let reader = new.get_reader()?;
|
||||||
|
let metadata = reader.metadata().clone();
|
||||||
|
drop(reader);
|
||||||
|
new.shape.c = metadata.size_c as usize;
|
||||||
|
new.shape.z = metadata.size_z as usize;
|
||||||
|
new.shape.t = metadata.size_t as usize;
|
||||||
|
new.shape.y = metadata.size_y as usize;
|
||||||
|
new.shape.x = metadata.size_x as usize;
|
||||||
|
new.little_endian = metadata.is_little_endian;
|
||||||
|
new.pixel_type = PixelType::from(metadata.pixel_type);
|
||||||
|
Ok(new)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Result<Ome, Error> {
|
||||||
|
let reader = self.get_reader()?;
|
||||||
|
let metadata = reader.metadata();
|
||||||
|
let ome_metadata = reader
|
||||||
|
.ome_metadata()
|
||||||
|
.ok_or_else(|| Error::Parse("no metadata".into()))?;
|
||||||
|
let xml = ome_metadata.to_ome_xml(metadata);
|
||||||
|
Ok(Ome::from_xml(xml)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
|
||||||
|
let mut reader = self.get_reader()?;
|
||||||
|
let metadata = reader.metadata();
|
||||||
|
let bytes = if metadata.is_rgb && metadata.is_interleaved {
|
||||||
|
let index = Self::get_index(metadata.dimension_order, &self.shape, 0, z, t);
|
||||||
|
self.deinterleave(reader.open_bytes(index)?, c)?
|
||||||
|
} else {
|
||||||
|
let index = Self::get_index(metadata.dimension_order, &self.shape, c, z, t);
|
||||||
|
reader.open_bytes(index)?
|
||||||
|
};
|
||||||
|
self.bytes_to_frame(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn series(&self) -> usize {
|
||||||
|
self.series
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(&self) -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape(&self) -> &Shape {
|
||||||
|
&self.shape
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel_type(&self) -> &PixelType {
|
||||||
|
&self.pixel_type
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_positions<P>(_path: P, _series: usize) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
Ok(HashSet::from([0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut path = path.as_ref().to_path_buf();
|
||||||
|
if path.is_dir() {
|
||||||
|
let orig = path.clone();
|
||||||
|
path = find_tiff(&path)?.ok_or_else(|| {
|
||||||
|
Error::FileDoesNotExist(orig.join("**").join("*.tif").display().to_string())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
let reader = ImageReader::open(path.as_ref())
|
||||||
|
.map_err(|e| Error::Parse(format!("bioformats failed to open: {}", e)))?;
|
||||||
|
let n = reader.series_count();
|
||||||
|
Ok(HashSet::from_iter(0..n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn open(file: &str) -> Result<BioFormatsRustReader, Error> {
|
||||||
|
let path = std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file);
|
||||||
|
BioFormatsRustReader::new(&path, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test_metadata {
|
||||||
|
($($name:ident: $file:expr $(,)?)*) => {
|
||||||
|
$(
|
||||||
|
#[test]
|
||||||
|
fn $name() -> Result<(), Error> {
|
||||||
|
let bf = open($file)?;
|
||||||
|
println!("{}", bf.view().squeeze()?.summary()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test_metadata! {
|
||||||
|
metadata_a: "czi/1xp53-01-AP1.czi",
|
||||||
|
metadata_b: "czi/beads_2023_05_04__19_00_22.czi",
|
||||||
|
metadata_c: "czi/Experiment-2029.czi",
|
||||||
|
metadata_d: "czi/MK022_cE9_1-01-Airyscan Processing-01-Scene-2-P1.czi",
|
||||||
|
metadata_e: "czi/YTL1849A131_2023_05_04__13_36_36.czi",
|
||||||
|
metadata_f: "czi/EU_UV_t=1-01.czi",
|
||||||
|
metadata_g: "tiffseq/4-Pos_001_002/img_000000000_Cy3-Cy3_filter_000.tif",
|
||||||
|
metadata_h: "tiffseq/20-Pos_005_005/img_000000000_Cy3-Cy3_filter_000.tif"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1292
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,497 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape};
|
||||||
|
use ndarray::Array2;
|
||||||
|
use ome_metadata::{Ome, ome};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cell::{RefCell, RefMut};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use thread_local::ThreadLocal;
|
||||||
|
use tiff::decoder::ifd::Value;
|
||||||
|
use tiff::decoder::{Decoder, DecodingResult};
|
||||||
|
use tiff::tags::Tag;
|
||||||
|
|
||||||
|
/// reader for single tiff image files
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TiffReader {
|
||||||
|
#[serde(skip)]
|
||||||
|
reader: ThreadLocal<RefCell<Decoder<File>>>,
|
||||||
|
path: PathBuf,
|
||||||
|
series: usize,
|
||||||
|
position: usize,
|
||||||
|
shape: Shape,
|
||||||
|
pixel_type: PixelType,
|
||||||
|
n_samples: usize,
|
||||||
|
p_ndim: u8,
|
||||||
|
planar: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TiffReader> for DynReader {
|
||||||
|
fn from(value: TiffReader) -> Self {
|
||||||
|
DynReader::Tiff(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for TiffReader {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.path.hash(state);
|
||||||
|
self.series.hash(state);
|
||||||
|
self.position.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for TiffReader {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
TiffReader {
|
||||||
|
reader: ThreadLocal::default(),
|
||||||
|
path: self.path.clone(),
|
||||||
|
series: self.series,
|
||||||
|
position: self.position,
|
||||||
|
shape: self.shape.clone(),
|
||||||
|
pixel_type: self.pixel_type,
|
||||||
|
n_samples: self.n_samples,
|
||||||
|
p_ndim: self.p_ndim,
|
||||||
|
planar: self.planar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for TiffReader {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.path == other.path && self.series == other.series && self.position == other.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for TiffReader {}
|
||||||
|
|
||||||
|
impl TiffReader {
|
||||||
|
fn get_reader(&self) -> Result<RefMut<'_, Decoder<File>>, Error> {
|
||||||
|
self.reader
|
||||||
|
.get_or_try(|| {
|
||||||
|
let file = File::open(&self.path)?;
|
||||||
|
Ok(RefCell::new(Decoder::new(file)?))
|
||||||
|
})
|
||||||
|
.map(|i| i.borrow_mut())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel_type_from_bits_sample(bits: u16, fmt: u16) -> Result<PixelType, Error> {
|
||||||
|
match (fmt, bits) {
|
||||||
|
(1, 8) => Ok(PixelType::U8),
|
||||||
|
(1, 16) => Ok(PixelType::U16),
|
||||||
|
(1, 32) => Ok(PixelType::U32),
|
||||||
|
(2, 8) => Ok(PixelType::I8),
|
||||||
|
(2, 16) => Ok(PixelType::I16),
|
||||||
|
(2, 32) => Ok(PixelType::I32),
|
||||||
|
(3, 32) => Ok(PixelType::F32),
|
||||||
|
(3, 64) => Ok(PixelType::F64),
|
||||||
|
_ => Err(Error::Parse(format!(
|
||||||
|
"unsupported pixel type: SampleFormat={}, BitsPerSample={}",
|
||||||
|
fmt, bits
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_imagej_metadata(s: &str) -> HashMap<String, String> {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for line in s.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(pos) = line.find('=') {
|
||||||
|
let key = line[..pos].trim().to_string();
|
||||||
|
let val = line[pos + 1..].trim().to_string();
|
||||||
|
map.insert(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reader for TiffReader {
|
||||||
|
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut new = Self {
|
||||||
|
reader: ThreadLocal::default(),
|
||||||
|
path: path.as_ref().to_path_buf(),
|
||||||
|
series,
|
||||||
|
position,
|
||||||
|
shape: Shape::default(),
|
||||||
|
pixel_type: PixelType::U16,
|
||||||
|
n_samples: 0,
|
||||||
|
p_ndim: 0,
|
||||||
|
planar: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut reader = new.get_reader()?;
|
||||||
|
let width = reader.get_tag(Tag::ImageWidth)?.into_u32()? as usize;
|
||||||
|
let height = reader.get_tag(Tag::ImageLength)?.into_u32()? as usize;
|
||||||
|
|
||||||
|
let n_samples = reader
|
||||||
|
.get_tag(Tag::SamplesPerPixel)
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| t.into_u16().ok())
|
||||||
|
.unwrap_or(1) as usize;
|
||||||
|
|
||||||
|
let bits = tag_first_u16(&mut reader, Tag::BitsPerSample).unwrap_or(8);
|
||||||
|
let sfmt = tag_first_u16(&mut reader, Tag::SampleFormat).unwrap_or(1);
|
||||||
|
let pixtype = pixel_type_from_bits_sample(bits, sfmt)?;
|
||||||
|
|
||||||
|
let planar = reader
|
||||||
|
.get_tag(Tag::PlanarConfiguration)
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| t.into_u16().ok())
|
||||||
|
.unwrap_or(1)
|
||||||
|
== 2;
|
||||||
|
|
||||||
|
let imagej_bytes = reader
|
||||||
|
.get_tag(Tag::Unknown(50839))
|
||||||
|
.or_else(|_| reader.get_tag(Tag::ImageDescription))?;
|
||||||
|
let imagej_bytes = match imagej_bytes {
|
||||||
|
Value::Ascii(s) => s.into_bytes(),
|
||||||
|
other => other.into_u8_vec()?,
|
||||||
|
};
|
||||||
|
let metadata_str = String::from_utf8_lossy(&imagej_bytes);
|
||||||
|
let metadata_map = parse_imagej_metadata(&metadata_str);
|
||||||
|
let p_ndim = if n_samples > 1 { 3u8 } else { 2u8 };
|
||||||
|
let size_c = if p_ndim == 3 {
|
||||||
|
n_samples
|
||||||
|
} else {
|
||||||
|
metadata_map
|
||||||
|
.get("channels")
|
||||||
|
.and_then(|v| v.parse::<usize>().ok())
|
||||||
|
.unwrap_or(1)
|
||||||
|
};
|
||||||
|
let size_z = metadata_map
|
||||||
|
.get("slices")
|
||||||
|
.and_then(|v| v.parse::<usize>().ok())
|
||||||
|
.unwrap_or(1);
|
||||||
|
let size_t = metadata_map
|
||||||
|
.get("frames")
|
||||||
|
.and_then(|v| v.parse::<usize>().ok())
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
|
let mult = if p_ndim == 3 { n_samples } else { 1 };
|
||||||
|
let expected = size_c * size_z * size_t / mult;
|
||||||
|
|
||||||
|
let mut n_ifds = 1;
|
||||||
|
while reader.more_images() {
|
||||||
|
reader.next_image()?;
|
||||||
|
n_ifds += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size_z = if n_ifds != expected && n_ifds > 0 {
|
||||||
|
n_ifds / (size_c * size_t / mult).max(1)
|
||||||
|
} else {
|
||||||
|
size_z
|
||||||
|
};
|
||||||
|
drop(reader);
|
||||||
|
|
||||||
|
new.shape = Shape {
|
||||||
|
c: size_c,
|
||||||
|
z: size_z,
|
||||||
|
t: size_t,
|
||||||
|
y: height,
|
||||||
|
x: width,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
new.pixel_type = pixtype;
|
||||||
|
new.n_samples = n_samples;
|
||||||
|
new.p_ndim = p_ndim;
|
||||||
|
new.planar = planar;
|
||||||
|
|
||||||
|
// test reading first frame, if error another reader (bioformats) can take over
|
||||||
|
new.get_frame(0, 0, 0)?;
|
||||||
|
Ok(new)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Result<Ome, Error> {
|
||||||
|
let mut reader = self.get_reader()?;
|
||||||
|
let imagej_bytes = reader
|
||||||
|
.get_tag(Tag::Unknown(50839))
|
||||||
|
.or_else(|_| reader.get_tag(Tag::ImageDescription))?;
|
||||||
|
let imagej_bytes = match imagej_bytes {
|
||||||
|
Value::Ascii(s) => s.into_bytes(),
|
||||||
|
other => other.into_u8_vec()?,
|
||||||
|
};
|
||||||
|
let metadata_str = String::from_utf8_lossy(&imagej_bytes);
|
||||||
|
let metadata_map = parse_imagej_metadata(&metadata_str);
|
||||||
|
|
||||||
|
let interval_t = metadata_map
|
||||||
|
.get("interval")
|
||||||
|
.and_then(|v| v.parse::<f64>().ok());
|
||||||
|
let ru = reader
|
||||||
|
.get_tag(Tag::ResolutionUnit)
|
||||||
|
.ok()
|
||||||
|
.and_then(|i| i.into_u16().ok())
|
||||||
|
.unwrap_or(2);
|
||||||
|
let pixel_size = reader
|
||||||
|
.get_tag(Tag::XResolution)
|
||||||
|
.ok()
|
||||||
|
.and_then(|i| i.into_u64_vec().ok())
|
||||||
|
.and_then(|i| {
|
||||||
|
i.first()
|
||||||
|
.and_then(|&n| i.get(1).map(|&d| (n as f64) / (d as f64)))
|
||||||
|
})
|
||||||
|
.and_then(|i| {
|
||||||
|
if i <= 0.0 {
|
||||||
|
None
|
||||||
|
} else if ru == 3 {
|
||||||
|
Some(1e4 / i)
|
||||||
|
} else if ru == 2 {
|
||||||
|
Some(25400.0 / i)
|
||||||
|
} else {
|
||||||
|
Some(1.0 / i)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let interval_z = metadata_map
|
||||||
|
.get("spacing")
|
||||||
|
.and_then(|v| v.parse::<f64>().ok());
|
||||||
|
|
||||||
|
let ome_pixel_type = match self.pixel_type {
|
||||||
|
PixelType::I8 => ome::PixelType::Int8,
|
||||||
|
PixelType::U8 => ome::PixelType::Uint8,
|
||||||
|
PixelType::I16 => ome::PixelType::Int16,
|
||||||
|
PixelType::U16 => ome::PixelType::Uint16,
|
||||||
|
PixelType::I32 => ome::PixelType::Int32,
|
||||||
|
PixelType::U32 => ome::PixelType::Uint32,
|
||||||
|
PixelType::F32 => ome::PixelType::Float,
|
||||||
|
PixelType::F64 => ome::PixelType::Double,
|
||||||
|
_ => ome::PixelType::Uint16,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pixels = ome::Pixels {
|
||||||
|
id: "Pixels:0".to_string(),
|
||||||
|
size_x: self.shape.x as i32,
|
||||||
|
size_y: self.shape.y as i32,
|
||||||
|
size_z: self.shape.z as i32,
|
||||||
|
size_c: self.shape.c as i32,
|
||||||
|
size_t: self.shape.t as i32,
|
||||||
|
dimension_order: ome::PixelsDimensionOrderType::Xyczt,
|
||||||
|
r#type: ome_pixel_type,
|
||||||
|
physical_size_x: pixel_size.map(|v| v as f32),
|
||||||
|
physical_size_x_unit: ome::UnitsLength::um,
|
||||||
|
physical_size_y: pixel_size.map(|v| v as f32),
|
||||||
|
physical_size_y_unit: ome::UnitsLength::um,
|
||||||
|
physical_size_z: interval_z.map(|v| v as f32),
|
||||||
|
physical_size_z_unit: ome::UnitsLength::um,
|
||||||
|
time_increment: interval_t.map(|v| v as f32),
|
||||||
|
time_increment_unit: ome::UnitsTime::s,
|
||||||
|
significant_bits: None,
|
||||||
|
interleaved: None,
|
||||||
|
big_endian: None,
|
||||||
|
channel: Vec::new(),
|
||||||
|
bin_data: Vec::new(),
|
||||||
|
tiff_data: Vec::new(),
|
||||||
|
metadata_only: None,
|
||||||
|
plane: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for c in 0..self.shape.c {
|
||||||
|
pixels.channel.push(ome::Channel {
|
||||||
|
id: format!("Channel:{}", c),
|
||||||
|
name: Some(format!("Channel {}", c)),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in 0..self.shape.c {
|
||||||
|
for z in 0..self.shape.z {
|
||||||
|
for t in 0..self.shape.t {
|
||||||
|
pixels.plane.push(ome::Plane {
|
||||||
|
the_c: Some(c as i32),
|
||||||
|
the_z: Some(z as i32),
|
||||||
|
the_t: Some(t as i32),
|
||||||
|
delta_t: interval_t.and_then(|v| {
|
||||||
|
if v > 0.0 {
|
||||||
|
Some((t as f64 * v) as f32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ome = Ome::default();
|
||||||
|
ome.image.push(ome::Image {
|
||||||
|
id: "Image:0".to_string(),
|
||||||
|
name: self
|
||||||
|
.path
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().to_string()),
|
||||||
|
pixels,
|
||||||
|
instrument_ref: None,
|
||||||
|
objective_settings: None,
|
||||||
|
acquisition_date: None,
|
||||||
|
description: None,
|
||||||
|
experimenter_ref: None,
|
||||||
|
experiment_ref: None,
|
||||||
|
experimenter_group_ref: None,
|
||||||
|
imaging_environment: None,
|
||||||
|
stage_label: None,
|
||||||
|
roi_ref: Vec::new(),
|
||||||
|
microbeam_manipulation_ref: Vec::new(),
|
||||||
|
annotation_ref: Vec::new(),
|
||||||
|
});
|
||||||
|
Ok(ome)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
|
||||||
|
let (page_idx, offset, stride) = if self.p_ndim == 3 {
|
||||||
|
(t * self.shape.z + z, c, self.n_samples)
|
||||||
|
} else {
|
||||||
|
(c + z * self.shape.c + t * self.shape.c * self.shape.z, 0, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut reader = self.get_reader()?;
|
||||||
|
reader.seek_to_image(page_idx)?;
|
||||||
|
|
||||||
|
let data: DecodingResult = if self.planar {
|
||||||
|
let mut result = DecodingResult::U16(Vec::new());
|
||||||
|
reader.read_image_to_buffer(&mut result)?;
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
reader.read_image()?
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! to_frame {
|
||||||
|
($var:ident, $ty:ty) => {{
|
||||||
|
let v: Vec<$ty> = match data {
|
||||||
|
DecodingResult::$var(v) => v,
|
||||||
|
_ => {
|
||||||
|
return Err(Error::Parse(format!(
|
||||||
|
"type mismatch: expected {}",
|
||||||
|
stringify!($var)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let yx = self.shape.y * self.shape.x;
|
||||||
|
if self.planar {
|
||||||
|
let chan: Vec<$ty> = v.into_iter().skip(c * yx).take(yx).collect();
|
||||||
|
Ok(ArrayT::$var(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
chan,
|
||||||
|
)?))
|
||||||
|
} else if stride > 1 {
|
||||||
|
let chan: Vec<$ty> = v
|
||||||
|
.into_iter()
|
||||||
|
.skip(offset)
|
||||||
|
.step_by(stride)
|
||||||
|
.take(yx)
|
||||||
|
.collect();
|
||||||
|
Ok(ArrayT::$var(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
chan,
|
||||||
|
)?))
|
||||||
|
} else {
|
||||||
|
Ok(ArrayT::$var(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
v,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.pixel_type {
|
||||||
|
PixelType::U8 => to_frame!(U8, u8),
|
||||||
|
PixelType::U16 => to_frame!(U16, u16),
|
||||||
|
PixelType::U32 => to_frame!(U32, u32),
|
||||||
|
PixelType::U64 => to_frame!(U64, u64),
|
||||||
|
PixelType::I8 => to_frame!(I8, i8),
|
||||||
|
PixelType::I16 => to_frame!(I16, i16),
|
||||||
|
PixelType::I32 => to_frame!(I32, i32),
|
||||||
|
PixelType::I64 => to_frame!(I64, i64),
|
||||||
|
PixelType::F32 => to_frame!(F32, f32),
|
||||||
|
PixelType::F64 => to_frame!(F64, f64),
|
||||||
|
_ => Err(Error::NotImplemented(format!(
|
||||||
|
"unsupported TIFF pixel type: {:?}",
|
||||||
|
self.pixel_type
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
fn series(&self) -> usize {
|
||||||
|
self.series
|
||||||
|
}
|
||||||
|
fn position(&self) -> usize {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
fn shape(&self) -> &Shape {
|
||||||
|
&self.shape
|
||||||
|
}
|
||||||
|
fn pixel_type(&self) -> &PixelType {
|
||||||
|
&self.pixel_type
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_positions<P>(_path: P, _series: usize) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
Ok(HashSet::from([0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_series<P>(_path: P) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
Ok(HashSet::from([0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag_first_u16(reader: &mut RefMut<Decoder<File>>, tag: Tag) -> Option<u16> {
|
||||||
|
let val = reader.get_tag(tag).ok()?;
|
||||||
|
use tiff::decoder::ifd::Value;
|
||||||
|
match val {
|
||||||
|
Value::Short(v) => Some(v),
|
||||||
|
Value::List(list) => list.first().and_then(|v| match v {
|
||||||
|
Value::Short(v) => Some(*v),
|
||||||
|
_ => None,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn open(file: &str) -> Result<TiffReader, Error> {
|
||||||
|
let path = std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file);
|
||||||
|
TiffReader::new(&path, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test_metadata {
|
||||||
|
($($name:ident: $file:expr $(,)?)*) => {
|
||||||
|
$(
|
||||||
|
#[test]
|
||||||
|
fn $name() -> Result<(), Error> {
|
||||||
|
let ts = open($file)?;
|
||||||
|
println!("{}", ts.view().squeeze()?.summary()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test_metadata! {
|
||||||
|
// metadata_a: "tiff/20251014_20-Pos_000_000_mask.tif",
|
||||||
|
metadata_b: "tiff/20251014_20-Pos_000_000_max.tif",
|
||||||
|
metadata_c: "tiff/20251014_20-Pos_000_000_loc_results_Cy3.tif",
|
||||||
|
metadata_e: "tiff/1xp53-01-AP1.tiff",
|
||||||
|
metadata_f: "tiff/test.tif",
|
||||||
|
// metadata_g: "tiff/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,636 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape};
|
||||||
|
use ndarray::Array2;
|
||||||
|
use ome_metadata::{Ome, ome};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tiff::decoder::{Decoder, DecodingResult};
|
||||||
|
use tiff::tags::Tag;
|
||||||
|
|
||||||
|
/// reader for sequences of tiff files (one file per z/time plane)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TiffSeqReader {
|
||||||
|
path: PathBuf,
|
||||||
|
series: usize,
|
||||||
|
position: usize,
|
||||||
|
shape: Shape,
|
||||||
|
pixel_type: PixelType,
|
||||||
|
filedict: HashMap<(usize, usize, usize), PathBuf>,
|
||||||
|
cnamelist: Vec<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
metadata_map: HashMap<String, yaml_serde::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TiffSeqReader> for DynReader {
|
||||||
|
fn from(value: TiffSeqReader) -> Self {
|
||||||
|
DynReader::TiffSeq(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for TiffSeqReader {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.path.hash(state);
|
||||||
|
self.series.hash(state);
|
||||||
|
self.position.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TiffSeqReader {
|
||||||
|
fn find_pos_dir<P: AsRef<Path>>(path: P, series: usize) -> Result<PathBuf, Error> {
|
||||||
|
let pat = Regex::new(&format!("(?i)^(?:\\d+-)?Pos0*{}", series))?;
|
||||||
|
let pos_dir = path.as_ref().to_path_buf();
|
||||||
|
if pos_dir
|
||||||
|
.file_name()
|
||||||
|
.map(|n| pat.is_match(&n.to_string_lossy()))
|
||||||
|
== Some(true)
|
||||||
|
{
|
||||||
|
return Ok(pos_dir);
|
||||||
|
}
|
||||||
|
for file in pos_dir.read_dir()?.flatten() {
|
||||||
|
let p = file.path();
|
||||||
|
if p.file_name().map(|n| pat.is_match(&n.to_string_lossy())) == Some(true) {
|
||||||
|
return Ok(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(pos_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_tiff_files<P: AsRef<Path>>(path: P) -> Result<Vec<PathBuf>, Error> {
|
||||||
|
let pat = Regex::new(r"(?i)^img_\d{3,}.*\d{3,}\.tif$")?;
|
||||||
|
let mut files = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(path.as_ref())? {
|
||||||
|
let entry = entry?;
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if pat.is_match(&name) {
|
||||||
|
files.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_metadata_from_file(dir: &Path) -> Result<HashMap<String, yaml_serde::Value>, Error> {
|
||||||
|
let md_path = dir.join("metadata.txt");
|
||||||
|
let text = std::fs::read_to_string(&md_path)?;
|
||||||
|
let parsed: yaml_serde::Value = yaml_serde::from_str(&text)?;
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
map.insert("Info".to_string(), parsed);
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_tiff_dimensions(path: &Path) -> Result<(usize, usize), Error> {
|
||||||
|
let file = std::fs::File::open(path)?;
|
||||||
|
let mut decoder = Decoder::new(file)?;
|
||||||
|
let width = decoder.get_tag(Tag::ImageWidth)?.into_u32()? as usize;
|
||||||
|
let height = decoder.get_tag(Tag::ImageLength)?.into_u32()? as usize;
|
||||||
|
Ok((width, height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for TiffSeqReader {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.path == other.path
|
||||||
|
&& self.series == other.series
|
||||||
|
&& self.position == other.position
|
||||||
|
&& self.shape == other.shape
|
||||||
|
&& self.pixel_type == other.pixel_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for TiffSeqReader {}
|
||||||
|
|
||||||
|
impl Reader for TiffSeqReader {
|
||||||
|
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let path = path.as_ref();
|
||||||
|
let pos_path = Self::find_pos_dir(path, series)?;
|
||||||
|
let filelist = Self::list_tiff_files(&pos_path)?;
|
||||||
|
if filelist.is_empty() {
|
||||||
|
return Err(Error::InvalidReader(
|
||||||
|
"TiffSeqReader".to_string(),
|
||||||
|
pos_path.display().to_string(),
|
||||||
|
"no tiff files found".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = &filelist[0];
|
||||||
|
let (width, height) = Self::read_tiff_dimensions(first)?;
|
||||||
|
let metadata = Self::read_metadata_from_file(&pos_path)?;
|
||||||
|
|
||||||
|
let info = metadata
|
||||||
|
.get("Info")
|
||||||
|
.and_then(|v| v.as_mapping())
|
||||||
|
.ok_or_else(|| Error::Parse("missing Info key in tag 50839".to_string()))?;
|
||||||
|
|
||||||
|
let lookup = |key: &str| {
|
||||||
|
info.get(yaml_serde::Value::String(key.to_string()))
|
||||||
|
.or_else(|| {
|
||||||
|
info.get(yaml_serde::Value::String("Summary".to_string()))
|
||||||
|
.and_then(|s| s.as_mapping())
|
||||||
|
.and_then(|s| s.get(yaml_serde::Value::String(key.to_string())))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let pixel_type_str = lookup("PixelType")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("gray16");
|
||||||
|
let pixel_type =
|
||||||
|
PixelType::from_str(&pixel_type_str.to_lowercase().replace("gray", "uint"))
|
||||||
|
.unwrap_or(PixelType::U16);
|
||||||
|
|
||||||
|
let cnamelist: Vec<String> = lookup("Summary")
|
||||||
|
.and_then(|v| v.get("ChNames"))
|
||||||
|
.and_then(|v| v.as_sequence())
|
||||||
|
.map(|seq| {
|
||||||
|
seq.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(String::from))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let pattern_c = Regex::new(r"(?i)img_\d{3,}_(.*)_\d{3,}$")?;
|
||||||
|
let pattern_z = Regex::new(r"(\d{3,})$")?;
|
||||||
|
let pattern_t = Regex::new(r"(?i)img_(\d{3,})")?;
|
||||||
|
|
||||||
|
let cnamelist: Vec<String> = if cnamelist.is_empty() {
|
||||||
|
let mut names: Vec<String> = filelist
|
||||||
|
.iter()
|
||||||
|
.filter_map(|f| {
|
||||||
|
let stem = f.file_stem()?;
|
||||||
|
let stem = stem.to_string_lossy();
|
||||||
|
pattern_c
|
||||||
|
.captures(&stem)
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.map(|m| m.as_str().to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
names.sort();
|
||||||
|
names.dedup();
|
||||||
|
names
|
||||||
|
} else {
|
||||||
|
cnamelist
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| filelist.iter().any(|f| f.to_string_lossy().contains(c)))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut filedict = HashMap::new();
|
||||||
|
for f in &filelist {
|
||||||
|
let stem = f
|
||||||
|
.file_stem()
|
||||||
|
.ok_or_else(|| Error::Parse("no stem".to_string()))?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let chan = pattern_c
|
||||||
|
.captures(&stem)
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.map(|m| m.as_str().to_string())
|
||||||
|
.ok_or_else(|| Error::Parse(format!("could not parse channel from {}", stem)))?;
|
||||||
|
let z: usize = pattern_z
|
||||||
|
.captures(&stem)
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.map(|m| m.as_str().parse().unwrap_or(0))
|
||||||
|
.ok_or_else(|| Error::Parse(format!("could not parse z from {}", stem)))?;
|
||||||
|
let t: usize = pattern_t
|
||||||
|
.captures(&stem)
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.map(|m| m.as_str().parse().unwrap_or(0))
|
||||||
|
.ok_or_else(|| Error::Parse(format!("could not parse t from {}", stem)))?;
|
||||||
|
|
||||||
|
let c_idx = cnamelist
|
||||||
|
.iter()
|
||||||
|
.position(|cn| cn == &chan)
|
||||||
|
.ok_or_else(|| Error::Parse(format!("channel '{}' not in cnamelist", chan)))?;
|
||||||
|
|
||||||
|
filedict.insert((c_idx, z, t), f.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let size_c = filedict.keys().map(|(c, _, _)| c).max().unwrap_or(&0) + 1;
|
||||||
|
let size_z = filedict.keys().map(|(_, z, _)| z).max().unwrap_or(&0) + 1;
|
||||||
|
let size_t = filedict.keys().map(|(_, _, t)| t).max().unwrap_or(&0) + 1;
|
||||||
|
|
||||||
|
Ok(TiffSeqReader {
|
||||||
|
path: pos_path,
|
||||||
|
series,
|
||||||
|
position,
|
||||||
|
shape: Shape {
|
||||||
|
c: size_c,
|
||||||
|
z: size_z,
|
||||||
|
t: size_t,
|
||||||
|
y: height,
|
||||||
|
x: width,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
pixel_type,
|
||||||
|
filedict,
|
||||||
|
cnamelist,
|
||||||
|
metadata_map: metadata,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata(&self) -> Result<Ome, Error> {
|
||||||
|
let mut ome = Ome::default();
|
||||||
|
let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping());
|
||||||
|
|
||||||
|
let slookup =
|
||||||
|
|key: &str| info.and_then(|m| m.get(yaml_serde::Value::String(key.to_string())));
|
||||||
|
|
||||||
|
let summary = slookup("Summary").and_then(|v| v.as_mapping());
|
||||||
|
|
||||||
|
let summary_lookup =
|
||||||
|
|key: &str| summary.and_then(|m| m.get(yaml_serde::Value::String(key.to_string())));
|
||||||
|
|
||||||
|
let first_frame = info.and_then(|m| {
|
||||||
|
m.iter()
|
||||||
|
.find(|(k, _)| k.as_str().is_some_and(|s| s.starts_with("FrameKey-")))
|
||||||
|
.and_then(|(_, v)| v.as_mapping())
|
||||||
|
});
|
||||||
|
|
||||||
|
let frame_lookup =
|
||||||
|
|key: &str| first_frame.and_then(|m| m.get(yaml_serde::Value::String(key.to_string())));
|
||||||
|
|
||||||
|
let ome_pixel_type = match self.pixel_type {
|
||||||
|
PixelType::I8 => ome::PixelType::Int8,
|
||||||
|
PixelType::U8 => ome::PixelType::Uint8,
|
||||||
|
PixelType::I16 => ome::PixelType::Int16,
|
||||||
|
PixelType::U16 => ome::PixelType::Uint16,
|
||||||
|
PixelType::I32 => ome::PixelType::Int32,
|
||||||
|
PixelType::U32 => ome::PixelType::Uint32,
|
||||||
|
PixelType::F32 => ome::PixelType::Float,
|
||||||
|
PixelType::F64 => ome::PixelType::Double,
|
||||||
|
_ => ome::PixelType::Bit,
|
||||||
|
};
|
||||||
|
|
||||||
|
let zstep = summary_lookup("z-step_um").and_then(|v| v.as_f64());
|
||||||
|
|
||||||
|
let exposure = frame_lookup("Exposure-ms")
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.map(|v| v as f32 / 1000.0);
|
||||||
|
|
||||||
|
let objective_str = frame_lookup("ZeissObjectiveTurret-Label")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let tubelens_str = frame_lookup("ZeissOptovar-Label")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let filter_set_str = frame_lookup("ZeissReflectorTurret-Label")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let binning_str = frame_lookup("Hamamatsu_sCMOS-Binning")
|
||||||
|
.or_else(|| frame_lookup("Binning"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let user_name = summary_lookup("UserName").and_then(|v| v.as_str());
|
||||||
|
|
||||||
|
let pxsize = frame_lookup("PixelSizeUm")
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.or_else(|| summary_lookup("PixelSize_um").and_then(|v| v.as_f64()));
|
||||||
|
|
||||||
|
let pxsize = pxsize.map(|v| {
|
||||||
|
if v == 0.0 {
|
||||||
|
let camera_str = frame_lookup("Core-Camera")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
let cam_px_um = if camera_str.to_lowercase().contains("hamamatsu") {
|
||||||
|
Some(6.5_f64)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let bin_factor = binning_str
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.split('x').next().and_then(|n| n.parse::<f64>().ok()))
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
let obj_mag = objective_str.as_ref().and_then(|s| {
|
||||||
|
Regex::new(r"(\d+(?:\.\d+)?)x")
|
||||||
|
.ok()
|
||||||
|
.and_then(|re| re.captures(s))
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.and_then(|m| m.as_str().parse::<f64>().ok())
|
||||||
|
});
|
||||||
|
let tube_mag = tubelens_str.as_ref().and_then(|s| {
|
||||||
|
Regex::new(r"(\d+(?:[,.]\d+)?)x$")
|
||||||
|
.ok()
|
||||||
|
.and_then(|re| re.captures(s))
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.and_then(|m| m.as_str().replace(",", ".").parse::<f64>().ok())
|
||||||
|
});
|
||||||
|
match (cam_px_um, obj_mag, tube_mag) {
|
||||||
|
(Some(cam), Some(obj), Some(tube)) => cam * bin_factor / (obj * tube),
|
||||||
|
(Some(cam), Some(obj), None) => cam * bin_factor / obj,
|
||||||
|
_ => v,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let objective = objective_str.as_ref().map(|s| {
|
||||||
|
let mag = Regex::new(r"(\d+(?:\.\d+)?)x")
|
||||||
|
.ok()
|
||||||
|
.and_then(|re| re.captures(s))
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.and_then(|m| m.as_str().parse::<f32>().ok());
|
||||||
|
|
||||||
|
let na = Regex::new(r"/(\d+\.\d+)")
|
||||||
|
.ok()
|
||||||
|
.and_then(|re| re.captures(s))
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.and_then(|m| m.as_str().parse::<f32>().ok());
|
||||||
|
|
||||||
|
let immersion = if s.to_lowercase().contains("oil") {
|
||||||
|
Some(ome::ObjectiveImmersionType::Oil)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
ome::Objective {
|
||||||
|
id: "Objective:0".to_string(),
|
||||||
|
manufacturer: Some("Zeiss".to_string()),
|
||||||
|
model: Some(s.clone()),
|
||||||
|
lens_na: na,
|
||||||
|
nominal_magnification: mag,
|
||||||
|
immersion,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tubelens = tubelens_str.as_ref().map(|s| {
|
||||||
|
let mag = Regex::new(r"(\d+(?:[,.]\d+)?)x$")
|
||||||
|
.ok()
|
||||||
|
.and_then(|re| re.captures(s))
|
||||||
|
.and_then(|c| c.get(1))
|
||||||
|
.and_then(|m| m.as_str().replace(",", ".").parse::<f32>().ok());
|
||||||
|
|
||||||
|
ome::Objective {
|
||||||
|
id: "Objective:Tubelens:0".to_string(),
|
||||||
|
manufacturer: Some("Zeiss".to_string()),
|
||||||
|
model: Some(s.clone()),
|
||||||
|
nominal_magnification: mag,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let filter_set = filter_set_str.as_ref().map(|s| ome::FilterSet {
|
||||||
|
id: "FilterSet:0".to_string(),
|
||||||
|
model: Some(s.clone()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut instrument = ome::Instrument {
|
||||||
|
id: "Instrument:0".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
if let Some(o) = objective {
|
||||||
|
instrument.objective.push(o);
|
||||||
|
}
|
||||||
|
if let Some(t) = tubelens {
|
||||||
|
instrument.objective.push(t);
|
||||||
|
}
|
||||||
|
instrument.detector.push(ome::Detector {
|
||||||
|
id: "Detector:0".to_string(),
|
||||||
|
manufacturer: Some("Hamamatsu".to_string()),
|
||||||
|
amplification_gain: Some(100.0),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
if let Some(f) = filter_set {
|
||||||
|
instrument.filter_set.push(f);
|
||||||
|
}
|
||||||
|
ome.instrument.push(instrument);
|
||||||
|
|
||||||
|
let mut pixels = ome::Pixels {
|
||||||
|
id: "Pixels:0".to_string(),
|
||||||
|
size_x: self.shape.x as i32,
|
||||||
|
size_y: self.shape.y as i32,
|
||||||
|
size_z: self.shape.z as i32,
|
||||||
|
size_c: self.shape.c as i32,
|
||||||
|
size_t: self.shape.t as i32,
|
||||||
|
dimension_order: ome::PixelsDimensionOrderType::Xyczt,
|
||||||
|
r#type: ome_pixel_type,
|
||||||
|
physical_size_x: pxsize.map(|v| v as f32),
|
||||||
|
physical_size_x_unit: ome::UnitsLength::um,
|
||||||
|
physical_size_y: pxsize.map(|v| v as f32),
|
||||||
|
physical_size_y_unit: ome::UnitsLength::um,
|
||||||
|
physical_size_z: zstep.map(|v| v as f32),
|
||||||
|
physical_size_z_unit: ome::UnitsLength::um,
|
||||||
|
time_increment: None,
|
||||||
|
time_increment_unit: ome::UnitsTime::s,
|
||||||
|
significant_bits: None,
|
||||||
|
interleaved: None,
|
||||||
|
big_endian: None,
|
||||||
|
channel: Vec::new(),
|
||||||
|
bin_data: Vec::new(),
|
||||||
|
tiff_data: Vec::new(),
|
||||||
|
metadata_only: None,
|
||||||
|
plane: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (c_idx, cname) in self.cnamelist.iter().enumerate() {
|
||||||
|
pixels.channel.push(ome::Channel {
|
||||||
|
id: format!("Channel:{}", c_idx),
|
||||||
|
name: Some(cname.clone()),
|
||||||
|
detector_settings: Some(ome::DetectorSettings {
|
||||||
|
id: "Detector:0".to_string(),
|
||||||
|
binning: binning_str.as_ref().map(|b| b.parse()).transpose()?,
|
||||||
|
gain: Some(100.0),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
filter_set_ref: filter_set_str.as_ref().map(|_| ome::AnnotationRef {
|
||||||
|
id: "FilterSet:0".to_string(),
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in 0..self.shape.c {
|
||||||
|
for z in 0..self.shape.z {
|
||||||
|
for t in 0..self.shape.t {
|
||||||
|
pixels.plane.push(ome::Plane {
|
||||||
|
the_c: Some(c as i32),
|
||||||
|
the_z: Some(z as i32),
|
||||||
|
the_t: Some(t as i32),
|
||||||
|
exposure_time: exposure,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut image = ome::Image {
|
||||||
|
id: "Image:0".to_string(),
|
||||||
|
name: self
|
||||||
|
.path
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().to_string()),
|
||||||
|
pixels,
|
||||||
|
instrument_ref: Some(ome::AnnotationRef {
|
||||||
|
id: "Instrument:0".to_string(),
|
||||||
|
}),
|
||||||
|
objective_settings: objective_str.as_ref().map(|_| ome::ObjectiveSettings {
|
||||||
|
id: "Objective:0".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
acquisition_date: None,
|
||||||
|
description: None,
|
||||||
|
experimenter_ref: None,
|
||||||
|
experiment_ref: None,
|
||||||
|
experimenter_group_ref: None,
|
||||||
|
imaging_environment: None,
|
||||||
|
stage_label: None,
|
||||||
|
roi_ref: Vec::new(),
|
||||||
|
microbeam_manipulation_ref: Vec::new(),
|
||||||
|
annotation_ref: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(uname) = user_name {
|
||||||
|
ome.experimenter.push(ome::Experimenter {
|
||||||
|
id: "Experimenter:0".to_string(),
|
||||||
|
user_name: Some(uname.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
image.experimenter_ref = Some(ome::AnnotationRef {
|
||||||
|
id: "Experimenter:0".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ome.image.push(image);
|
||||||
|
Ok(ome)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
|
||||||
|
let file = self
|
||||||
|
.filedict
|
||||||
|
.get(&(c, z, t))
|
||||||
|
.ok_or_else(|| Error::OutOfBounds(c.max(z).max(t) as isize, 0))?;
|
||||||
|
|
||||||
|
let rdr = std::fs::File::open(file)?;
|
||||||
|
let mut decoder = Decoder::new(rdr)?;
|
||||||
|
|
||||||
|
match decoder.read_image()? {
|
||||||
|
DecodingResult::U8(d) => Ok(ArrayT::U8(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
d,
|
||||||
|
)?)),
|
||||||
|
DecodingResult::U16(d) => Ok(ArrayT::U16(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
d,
|
||||||
|
)?)),
|
||||||
|
DecodingResult::U32(d) => Ok(ArrayT::U32(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
d,
|
||||||
|
)?)),
|
||||||
|
DecodingResult::I8(d) => Ok(ArrayT::I8(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
d,
|
||||||
|
)?)),
|
||||||
|
DecodingResult::I16(d) => Ok(ArrayT::I16(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
d,
|
||||||
|
)?)),
|
||||||
|
DecodingResult::I32(d) => Ok(ArrayT::I32(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
d,
|
||||||
|
)?)),
|
||||||
|
DecodingResult::F32(d) => Ok(ArrayT::F32(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
d,
|
||||||
|
)?)),
|
||||||
|
DecodingResult::F64(d) => Ok(ArrayT::F64(Array2::from_shape_vec(
|
||||||
|
(self.shape.y, self.shape.x),
|
||||||
|
d,
|
||||||
|
)?)),
|
||||||
|
_ => Err(Error::NotImplemented(
|
||||||
|
"unsupported TIFF pixel type".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn series(&self) -> usize {
|
||||||
|
self.series
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(&self) -> usize {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape(&self) -> &Shape {
|
||||||
|
&self.shape
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel_type(&self) -> &PixelType {
|
||||||
|
&self.pixel_type
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_positions<P>(_path: P, _series: usize) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
Ok(HashSet::from([0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let pat = Regex::new(r"(?i)^(?:\d+-)?Pos(\d+)$")?;
|
||||||
|
let mut series = HashSet::new();
|
||||||
|
for entry in std::fs::read_dir(path.as_ref())? {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type()?.is_dir() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if let Some(caps) = pat.captures(&name)
|
||||||
|
&& let Ok(s) = caps[1].parse::<usize>()
|
||||||
|
{
|
||||||
|
series.insert(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(series)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn open(file: &str) -> Result<TiffSeqReader, Error> {
|
||||||
|
let path = std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file);
|
||||||
|
TiffSeqReader::new(&path, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test_metadata {
|
||||||
|
($($name:ident: $file:expr $(,)?)*) => {
|
||||||
|
$(
|
||||||
|
#[test]
|
||||||
|
fn $name() -> Result<(), Error> {
|
||||||
|
let ts = open($file)?;
|
||||||
|
println!("{}", ts.view().squeeze()?.summary()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test_metadata! {
|
||||||
|
metadata_a: "tiffseq/4-Pos_001_002",
|
||||||
|
metadata_b: "tiffseq/20-Pos_005_005",
|
||||||
|
metadata_c: "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1",
|
||||||
|
}
|
||||||
|
}
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
use crate::error::Error;
|
||||||
|
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 {
|
||||||
|
/// the type of the result after reducing one axis
|
||||||
|
type Output;
|
||||||
|
|
||||||
|
/// the max of the array along `axis`
|
||||||
|
fn max(self, axis: usize) -> Result<Self::Output, Error>;
|
||||||
|
/// the min of the array along `axis`
|
||||||
|
fn min(self, axis: usize) -> Result<Self::Output, Error>;
|
||||||
|
/// the sum of the array along `axis`
|
||||||
|
fn sum(self, axis: usize) -> Result<Self::Output, Error>;
|
||||||
|
/// the mean of the array along `axis`
|
||||||
|
fn mean(self, axis: usize) -> Result<Self::Output, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
Ok(self.sum_axis(Axis(axis)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mean(self, axis: usize) -> Result<Self::Output, Error> {
|
||||||
|
self.mean_axis(Axis(axis)).ok_or(Error::NoMean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
Ok(self.sum_axis(Axis(axis)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mean(self, axis: usize) -> Result<Self::Output, Error> {
|
||||||
|
self.mean_axis(Axis(axis)).ok_or(Error::NoMean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
Ok(self.sum_axis(Axis(axis)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mean(self, axis: usize) -> Result<Self::Output, Error> {
|
||||||
|
self.mean_axis(Axis(axis)).ok_or(Error::NoMean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
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, Error> {
|
||||||
|
Ok(self.sum_axis(Axis(axis)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mean(self, axis: usize) -> Result<Self::Output, Error> {
|
||||||
|
self.mean_axis(Axis(axis)).ok_or(Error::NoMean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
use crate::axes::Axis;
|
||||||
|
use crate::colors::Color;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::metadata::Metadata;
|
||||||
|
use crate::readers::{DynReader, PixelType, Reader};
|
||||||
|
use crate::stats::MinMax;
|
||||||
|
use crate::utils::progress::get_bar;
|
||||||
|
use crate::view::{Number, View};
|
||||||
|
use indicatif::ProgressBar;
|
||||||
|
use itertools::iproduct;
|
||||||
|
use ndarray::{Array0, Array1, Array2, ArrayD, Dimension};
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Condvar, Mutex};
|
||||||
|
use tiffwrite::{Bytes, Colors, Compression, IJTiffFile};
|
||||||
|
|
||||||
|
/// options for saving tiff files
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TiffOptions {
|
||||||
|
bar: Option<ProgressBar>,
|
||||||
|
compression: Compression,
|
||||||
|
colors: Option<Vec<Vec<u8>>>,
|
||||||
|
overwrite: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TiffOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bar: None,
|
||||||
|
compression: Compression::Zstd(10),
|
||||||
|
colors: None,
|
||||||
|
overwrite: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TiffOptions {
|
||||||
|
/// create tiff options with the given parameters
|
||||||
|
pub fn new(
|
||||||
|
bar: Option<ProgressBar>,
|
||||||
|
compression: Option<Compression>,
|
||||||
|
colors: Vec<String>,
|
||||||
|
overwrite: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let mut options = Self {
|
||||||
|
bar,
|
||||||
|
compression: compression.unwrap_or(Compression::Zstd(10)),
|
||||||
|
colors: None,
|
||||||
|
overwrite,
|
||||||
|
};
|
||||||
|
if !colors.is_empty() {
|
||||||
|
options.set_colors(&colors)?;
|
||||||
|
}
|
||||||
|
Ok(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// show a progress bar while saving tiff
|
||||||
|
pub fn enable_bar(&mut self, message: Option<String>) {
|
||||||
|
self.bar = Some(get_bar(Some(0), message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// do not show a progress bar while saving tiff
|
||||||
|
pub fn disable_bar(&mut self) {
|
||||||
|
self.bar = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save tiff with zstd compression (default)
|
||||||
|
pub fn set_zstd_compression(&mut self) {
|
||||||
|
self.compression = Compression::Zstd(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save tiff with zstd compression, choose a level between 7..=22
|
||||||
|
pub fn set_zstd_compression_level(&mut self, level: i32) {
|
||||||
|
self.compression = Compression::Zstd(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save tiff with deflate compression
|
||||||
|
pub fn set_deflate_compression(&mut self) {
|
||||||
|
self.compression = Compression::Deflate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set the color lookup table for the tiff
|
||||||
|
pub fn set_colors(&mut self, colors: &[String]) -> Result<(), Error> {
|
||||||
|
let colors = colors
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.parse::<Color>())
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
self.colors = Some(colors.into_iter().map(|c| c.to_rgb()).collect());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set whether an existing tiff file should be overwritten
|
||||||
|
pub fn set_overwrite(&mut self, overwrite: bool) {
|
||||||
|
self.overwrite = overwrite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TiffOptions {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(bar) = self.bar.take() {
|
||||||
|
bar.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> View<D>
|
||||||
|
where
|
||||||
|
D: Dimension,
|
||||||
|
{
|
||||||
|
/// save as tiff with a certain type
|
||||||
|
pub fn save_as_tiff_with_type<T, P>(&self, path: P, options: &TiffOptions) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
T: Bytes + Number + Send + Sync,
|
||||||
|
ArrayD<T>: MinMax<Output = ArrayD<T>>,
|
||||||
|
Array1<T>: MinMax<Output = Array0<T>>,
|
||||||
|
Array2<T>: MinMax<Output = Array1<T>>,
|
||||||
|
{
|
||||||
|
let path = path.as_ref().to_path_buf();
|
||||||
|
if path.exists() {
|
||||||
|
if options.overwrite {
|
||||||
|
std::fs::remove_file(&path)?;
|
||||||
|
} else {
|
||||||
|
return Err(Error::FileAlreadyExists(path.display().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let shape = self.shape();
|
||||||
|
let mut tiff = IJTiffFile::new(path)?;
|
||||||
|
tiff.set_compression(options.compression);
|
||||||
|
let metadata = self.metadata()?;
|
||||||
|
tiff.px_size = metadata.pixel_size()?.map(|i| i / 1e3);
|
||||||
|
tiff.time_interval = metadata.time_interval()?.map(|i| i / 1e3);
|
||||||
|
tiff.delta_z = metadata.delta_z()?.map(|i| i / 1e3);
|
||||||
|
tiff.comment = Some(metadata.summary()?);
|
||||||
|
if let Some(mut colors) = options.colors.clone() {
|
||||||
|
while colors.len() < shape.c {
|
||||||
|
colors.push(vec![255, 255, 255]);
|
||||||
|
}
|
||||||
|
tiff.colors = Colors::Colors(colors);
|
||||||
|
}
|
||||||
|
let tiff = Arc::new(Mutex::new(tiff));
|
||||||
|
if let Some(bar) = options.bar.as_ref() {
|
||||||
|
bar.inc_length((shape.c * shape.z * shape.t) as u64);
|
||||||
|
}
|
||||||
|
iproduct!(0..shape.c, 0..shape.z, 0..shape.t)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.try_for_each(|(c, z, t)| {
|
||||||
|
if let Ok(mut tiff) = tiff.lock() {
|
||||||
|
tiff.save(&self.get_frame::<T, _>(c, z, t)?, c, z, t)?;
|
||||||
|
if let Some(bar) = options.bar.as_ref() {
|
||||||
|
bar.inc(1);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::TiffLock)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save as tiff with whatever pixel type the view has
|
||||||
|
pub fn save_as_tiff<P>(&self, path: P, options: &TiffOptions) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
match self.pixel_type() {
|
||||||
|
PixelType::Bool => self.save_as_tiff_with_type::<u8, P>(path, options)?,
|
||||||
|
PixelType::I8 => self.save_as_tiff_with_type::<i8, P>(path, options)?,
|
||||||
|
PixelType::U8 => self.save_as_tiff_with_type::<u8, P>(path, options)?,
|
||||||
|
PixelType::I16 => self.save_as_tiff_with_type::<i16, P>(path, options)?,
|
||||||
|
PixelType::U16 => self.save_as_tiff_with_type::<u16, P>(path, options)?,
|
||||||
|
PixelType::I32 => self.save_as_tiff_with_type::<i32, P>(path, options)?,
|
||||||
|
PixelType::U32 => self.save_as_tiff_with_type::<u32, P>(path, options)?,
|
||||||
|
PixelType::F32 => self.save_as_tiff_with_type::<f32, P>(path, options)?,
|
||||||
|
PixelType::F64 => self.save_as_tiff_with_type::<f64, P>(path, options)?,
|
||||||
|
PixelType::I64 => self.save_as_tiff_with_type::<i64, P>(path, options)?,
|
||||||
|
PixelType::U64 => self.save_as_tiff_with_type::<u64, P>(path, options)?,
|
||||||
|
PixelType::I128 => self.save_as_tiff_with_type::<i64, P>(path, options)?,
|
||||||
|
PixelType::U128 => self.save_as_tiff_with_type::<u64, P>(path, options)?,
|
||||||
|
PixelType::F128 => self.save_as_tiff_with_type::<f64, P>(path, options)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// batch convert multiple files to tiff format
|
||||||
|
pub fn batch_to_tiff(
|
||||||
|
files_in: &[PathBuf],
|
||||||
|
files_out: &[PathBuf],
|
||||||
|
operations: Option<Vec<(String, String)>>,
|
||||||
|
colors: Option<Vec<String>>,
|
||||||
|
overwrite: bool,
|
||||||
|
bar: bool,
|
||||||
|
message: Option<String>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let bar = if bar {
|
||||||
|
Some(get_bar(
|
||||||
|
Some(0),
|
||||||
|
Some(message.unwrap_or("writing tiff files".to_string())),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let options = TiffOptions::new(bar, None, colors.unwrap_or_default().clone(), overwrite)?;
|
||||||
|
let semaphore = Arc::new((Mutex::new(0usize), Condvar::new()));
|
||||||
|
files_in
|
||||||
|
.iter()
|
||||||
|
.zip(files_out)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|(file_in, file_out)| {
|
||||||
|
let (lock, cvar) = &*semaphore;
|
||||||
|
{
|
||||||
|
let mut count = lock.lock().unwrap();
|
||||||
|
while *count >= 10 {
|
||||||
|
count = cvar.wait(count).unwrap();
|
||||||
|
}
|
||||||
|
*count += 1;
|
||||||
|
}
|
||||||
|
let mut view = View::<_, DynReader>::from_path(file_in)?.into_dyn();
|
||||||
|
if let Some(operations) = operations.as_ref() {
|
||||||
|
for (ax, op) in operations {
|
||||||
|
view = view.operate(ax.parse::<Axis>()?, op.parse()?)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.save_as_tiff(file_out, &options)?;
|
||||||
|
{
|
||||||
|
let mut count = lock.lock().unwrap();
|
||||||
|
*count -= 1;
|
||||||
|
cvar.notify_one();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<()>, Error>>()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::readers::DynReader;
|
||||||
|
use crate::tiffwrite::{TiffOptions, batch_to_tiff, get_bar};
|
||||||
|
use crate::view::View;
|
||||||
|
use std::fs::create_dir_all;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[cfg(any(
|
||||||
|
feature = "czi",
|
||||||
|
feature = "tiffseq",
|
||||||
|
feature = "tiff",
|
||||||
|
feature = "bioformats_java"
|
||||||
|
))]
|
||||||
|
#[test]
|
||||||
|
fn tiff() -> Result<(), Error> {
|
||||||
|
let file = if cfg!(any(feature = "czi", feature = "bioformats_java")) {
|
||||||
|
"czi/1xp53-01-AP1.czi"
|
||||||
|
} else if cfg!(feature = "tiff") {
|
||||||
|
"tiff/20251014_20-Pos_000_000_loc_results_Cy3.tif"
|
||||||
|
} else if cfg!(feature = "tiffseq") {
|
||||||
|
"tiffseq/20-Pos_005_005"
|
||||||
|
} else {
|
||||||
|
unreachable!(
|
||||||
|
"need to enable one of these features: czi, bioformats_java, tiff, tiffseq"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let path = std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file);
|
||||||
|
let view: View<_, DynReader> = View::from_path(&path)?;
|
||||||
|
println!("{}", view.summary()?);
|
||||||
|
let bar = Some(get_bar(Some(0), Some("writing tiff file".to_string())));
|
||||||
|
let options = TiffOptions::new(bar, None, Vec::new(), true)?;
|
||||||
|
view.save_as_tiff(
|
||||||
|
std::env::home_dir().unwrap().join("tmp/movie.tif"),
|
||||||
|
&options,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "tiffseq", feature = "tiff", feature = "bioformats_java"))]
|
||||||
|
#[test]
|
||||||
|
fn tiff_parallel() -> Result<(), Error> {
|
||||||
|
let files = [
|
||||||
|
#[cfg(any(feature = "tiffseq", feature = "bioformats_java"))]
|
||||||
|
"tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1",
|
||||||
|
#[cfg(any(feature = "tiff", feature = "bioformats_java"))]
|
||||||
|
"tiff/20251014_20-Pos_000_000_max.tif",
|
||||||
|
#[cfg(any(feature = "czi", feature = "bioformats_java"))]
|
||||||
|
"czi/1xp53-01-AP1.czi",
|
||||||
|
#[cfg(any(feature = "czi", feature = "bioformats_java"))]
|
||||||
|
"czi/beads_2023_05_04__19_00_22.czi",
|
||||||
|
#[cfg(any(feature = "czi", feature = "bioformats_java"))]
|
||||||
|
"czi/YTL1849A131_2023_05_04__13_36_36.czi",
|
||||||
|
#[cfg(any(feature = "czi", feature = "bioformats_java"))]
|
||||||
|
"czi/p53_2x_3-pos_20s_SR-8Y-01_AP-Scene-3-P2.czi",
|
||||||
|
];
|
||||||
|
let files_in = files
|
||||||
|
.iter()
|
||||||
|
.map(|file| {
|
||||||
|
Ok(std::env::current_dir()?
|
||||||
|
.join("tests")
|
||||||
|
.join("files")
|
||||||
|
.join(file))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
let files_out = files
|
||||||
|
.iter()
|
||||||
|
.map(|file| {
|
||||||
|
std::env::home_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join("tmp")
|
||||||
|
.join(PathBuf::from(file).with_extension("tif"))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for file in &files_out {
|
||||||
|
create_dir_all(file.parent().unwrap())?;
|
||||||
|
}
|
||||||
|
batch_to_tiff(&files_in, &files_out, None, None, true, true, None)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
use crate::axes::Axis;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::readers::{DynReader, Reader};
|
||||||
|
use crate::view::View;
|
||||||
|
pub use image_registration::transform::Transform;
|
||||||
|
use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, s};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::io::Seek;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||||
|
struct TransformOld {
|
||||||
|
#[serde(alias = "CenterOfRotationPoint")]
|
||||||
|
center_of_rotation_point: Vec<f64>,
|
||||||
|
#[serde(alias = "Size")]
|
||||||
|
size: Vec<f64>,
|
||||||
|
#[serde(alias = "TransformParameters")]
|
||||||
|
transform_parameters: Vec<f64>,
|
||||||
|
#[serde(alias = "dTransformParameters")]
|
||||||
|
dtransform_parameters: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<HashMap<String, TransformOld>> for Transforms {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(t: HashMap<String, TransformOld>) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
channel: t
|
||||||
|
.into_iter()
|
||||||
|
.map(|(n, i)| {
|
||||||
|
Ok((
|
||||||
|
n,
|
||||||
|
Transform::new_with_center(
|
||||||
|
i.transform_parameters,
|
||||||
|
i.center_of_rotation_point,
|
||||||
|
i.size.into_iter().map(|j| j as usize).collect(),
|
||||||
|
)
|
||||||
|
.with_dparameters(i.dtransform_parameters)
|
||||||
|
.inverse()?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?,
|
||||||
|
drift: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Transforms {
|
||||||
|
#[serde(skip_serializing_if = "HashMap::is_empty", default = "HashMap::new")]
|
||||||
|
pub channel: HashMap<String, Transform<IxDyn>>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty", default = "Vec::new")]
|
||||||
|
pub drift: Vec<Transform<IxDyn>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transforms {
|
||||||
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
|
||||||
|
let mut file = std::fs::File::open(&path)?;
|
||||||
|
Ok(
|
||||||
|
if let Ok(t) = yaml_serde::from_reader::<_, Self>(&file)
|
||||||
|
&& !(t.channel.is_empty() && t.drift.is_empty())
|
||||||
|
{
|
||||||
|
t
|
||||||
|
} else {
|
||||||
|
file.seek(std::io::SeekFrom::Start(0))?;
|
||||||
|
yaml_serde::from_reader::<_, HashMap<String, TransformOld>>(&file)?.try_into()?
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
|
||||||
|
let file = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(path)?;
|
||||||
|
Ok(yaml_serde::to_writer(file, self)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_channel_transforms_2d(
|
||||||
|
bead_files: &[&Path],
|
||||||
|
main_channel: usize,
|
||||||
|
default_transform: Option<Transform<Ix2>>,
|
||||||
|
) -> Result<HashMap<String, Transform<Ix2>>, Error> {
|
||||||
|
let mut transforms = Vec::new();
|
||||||
|
let default_transform = default_transform.unwrap_or_default();
|
||||||
|
for file in bead_files {
|
||||||
|
let view = View::<_, DynReader>::from_path(file)?;
|
||||||
|
transforms.push(view.calculate_channel_transforms_2d(main_channel)?)
|
||||||
|
}
|
||||||
|
let channels = transforms
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| t.keys())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let mut average_transforms = HashMap::new();
|
||||||
|
for channel in channels {
|
||||||
|
let (s, n) = transforms
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| {
|
||||||
|
if t.contains_key(channel) {
|
||||||
|
Some((&t[channel] * &default_transform).matrix())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fold((Array2::zeros((3, 3)), 0), |(s, n), t| (s + t, n + 1));
|
||||||
|
let matrix = s / (n as f64);
|
||||||
|
let (s, n) = transforms
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| {
|
||||||
|
if t.contains_key(channel) {
|
||||||
|
Some(((&t[channel] * &default_transform).matrix() - &matrix).powi(2))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fold((Array2::zeros((3, 3)), 0), |(s, n), t| (s + t, n + 1));
|
||||||
|
let dmatrix = (s / n as f64).sqrt();
|
||||||
|
average_transforms.insert(
|
||||||
|
channel.clone(),
|
||||||
|
Transform::default()
|
||||||
|
.with_matrix(matrix.view())
|
||||||
|
.with_dmatrix(dmatrix.view()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(average_transforms)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_channel_transforms_3d(
|
||||||
|
bead_files: &[&Path],
|
||||||
|
main_channel: usize,
|
||||||
|
default_transform: Option<Transform<Ix3>>,
|
||||||
|
) -> Result<HashMap<String, Transform<Ix3>>, Error> {
|
||||||
|
let mut transforms = Vec::new();
|
||||||
|
let default_transform = default_transform.unwrap_or_default();
|
||||||
|
for file in bead_files {
|
||||||
|
let view = View::<_, DynReader>::from_path(file)?;
|
||||||
|
transforms.push(view.calculate_channel_transforms_3d(main_channel)?)
|
||||||
|
}
|
||||||
|
let channels = transforms
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| t.keys())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let mut average_transforms = HashMap::new();
|
||||||
|
for channel in channels {
|
||||||
|
let (s, n) = transforms
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| {
|
||||||
|
if t.contains_key(channel) {
|
||||||
|
Some((&t[channel] * &default_transform).matrix())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fold((Array2::zeros((4, 4)), 0), |(s, n), t| (s + t, n + 1));
|
||||||
|
let matrix = s / (n as f64);
|
||||||
|
let (s, n) = transforms
|
||||||
|
.iter()
|
||||||
|
.filter_map(|t| {
|
||||||
|
if t.contains_key(channel) {
|
||||||
|
Some(((&t[channel] * &default_transform).matrix() - &matrix).powi(2))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fold((Array2::zeros((4, 4)), 0), |(s, n), t| (s + t, n + 1));
|
||||||
|
let dmatrix = (s / n as f64).sqrt();
|
||||||
|
average_transforms.insert(
|
||||||
|
channel.clone(),
|
||||||
|
Transform::default()
|
||||||
|
.with_matrix(matrix.view())
|
||||||
|
.with_dmatrix(dmatrix.view()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(average_transforms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Dimension, R: Reader> View<D, R> {
|
||||||
|
#[cfg(feature = "transforms")]
|
||||||
|
pub(crate) fn get_transforms(&self) -> &Transforms {
|
||||||
|
&self.transforms
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "transforms")]
|
||||||
|
pub(crate) fn set_transforms(&mut self, transforms: Transforms) {
|
||||||
|
self.transforms = transforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_transforms_from_yaml(mut self, path: &Path) -> Result<Self, Error> {
|
||||||
|
self.set_transforms(Transforms::load(path)?);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_transforms_from_yaml(&mut self, path: &Path) -> Result<(), Error> {
|
||||||
|
self.set_transforms(Transforms::load(path)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_channel_transforms_2d(
|
||||||
|
&self,
|
||||||
|
main_channel: usize,
|
||||||
|
) -> Result<HashMap<String, Transform<Ix2>>, Error> {
|
||||||
|
let main_max = self
|
||||||
|
.slice_cztyx(s![main_channel, .., 0, .., ..])?
|
||||||
|
.max_proj(Axis::Z)?
|
||||||
|
.as_array::<f64>()?;
|
||||||
|
let mut transforms = HashMap::new();
|
||||||
|
let shape = vec![self.shape().y, self.shape().x];
|
||||||
|
let center = shape
|
||||||
|
.iter()
|
||||||
|
.map(|&i| (i as f64 - 1.0) / 2.0)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() {
|
||||||
|
let name = metadata
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("channel{}", channel));
|
||||||
|
if channel == main_channel {
|
||||||
|
transforms.insert(
|
||||||
|
name,
|
||||||
|
Transform::default()
|
||||||
|
.with_shape(shape.clone())
|
||||||
|
.with_center(center.clone()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let max = self
|
||||||
|
.slice_cztyx(s![channel, .., 0, .., ..])?
|
||||||
|
.max_proj(Axis::Z)?
|
||||||
|
.as_array::<f64>()?;
|
||||||
|
transforms.insert(
|
||||||
|
name,
|
||||||
|
Transform::register_affine(main_max.view(), max.view())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(transforms)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_channel_transforms_3d(
|
||||||
|
&self,
|
||||||
|
main_channel: usize,
|
||||||
|
) -> Result<HashMap<String, Transform<Ix3>>, Error> {
|
||||||
|
let main_max = self
|
||||||
|
.slice_cztyx(s![main_channel, .., 0, .., ..])?
|
||||||
|
.as_array::<f64>()?;
|
||||||
|
let mut transforms = HashMap::new();
|
||||||
|
let shape = vec![self.shape().z, self.shape().y, self.shape().x];
|
||||||
|
let center = shape
|
||||||
|
.iter()
|
||||||
|
.map(|&i| (i as f64 - 1.0) / 2.0)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() {
|
||||||
|
let name = metadata
|
||||||
|
.name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("channel{}", channel));
|
||||||
|
if channel == main_channel {
|
||||||
|
transforms.insert(
|
||||||
|
name,
|
||||||
|
Transform::default()
|
||||||
|
.with_shape(shape.clone())
|
||||||
|
.with_center(center.clone()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let max = self
|
||||||
|
.slice_cztyx(s![channel, .., 0, .., ..])?
|
||||||
|
.as_array::<f64>()?;
|
||||||
|
transforms.insert(
|
||||||
|
name,
|
||||||
|
Transform::register_affine(main_max.view(), max.view())?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(transforms)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_drift_transforms_2d(&self) -> Result<Vec<Transform<Ix2>>, Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_drift_transforms_3d(&self) -> Result<Vec<Transform<Ix3>>, Error> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::transforms::Transforms;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transforms() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let _t = Transforms::default();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_transforms() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let t = Transforms::load("tests/files/transforms.yml")?;
|
||||||
|
println!("t: {:?}", t);
|
||||||
|
assert_eq!(t.channel["TV1"].ndim(), 2);
|
||||||
|
assert_eq!(t.channel["TV2"].ndim(), 2);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_old_transforms() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let t = Transforms::load("tests/files/transform_red.yml")?;
|
||||||
|
println!("t: {:?}", t);
|
||||||
|
assert_eq!(t.channel["TV1"].ndim(), 2);
|
||||||
|
assert_eq!(t.channel["TV2"].ndim(), 2);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#[cfg(any(feature = "tiffwrite", feature = "movie"))]
|
||||||
|
pub(crate) mod progress {
|
||||||
|
use console::Term;
|
||||||
|
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressState, ProgressStyle};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// a progress bar with an ok style that when py::detach is used also works in jupyter
|
||||||
|
pub fn get_bar(count: Option<usize>, message: Option<String>) -> ProgressBar {
|
||||||
|
let style = ProgressStyle::with_template(
|
||||||
|
"{spinner:.green} {msg} {percent}% [{wide_bar:.green/lime}] {pos:>7}/{len:7} [{elapsed}/{eta}, {rate}]",
|
||||||
|
)
|
||||||
|
.expect("could not build progress bar style")
|
||||||
|
.with_key("rate", |state: &ProgressState, w: &mut dyn std::fmt::Write| {
|
||||||
|
if state.per_sec() < 1.0 {
|
||||||
|
write!(w, "{:>4.2} s", 1.0 / state.per_sec()).expect("could not write to progress bar");
|
||||||
|
} else {
|
||||||
|
write!(w, "{:>4.2}/s", state.per_sec()).expect("could not write to progress bar");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.progress_chars("#>-");
|
||||||
|
let bar = ProgressBar::with_draw_target(
|
||||||
|
count.map(|i| i as u64),
|
||||||
|
ProgressDrawTarget::term_like_with_hz(Box::new(Term::buffered_stdout()), 20),
|
||||||
|
)
|
||||||
|
.with_style(style);
|
||||||
|
if let Some(message) = message {
|
||||||
|
bar.set_message(message);
|
||||||
|
}
|
||||||
|
bar.enable_steady_tick(Duration::from_millis(100));
|
||||||
|
bar
|
||||||
|
}
|
||||||
|
}
|
||||||
+1607
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+19
-19
@@ -1,27 +1,27 @@
|
|||||||
import pickle
|
import pickle
|
||||||
from multiprocessing import active_children
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ndbioimage import Imread, ReaderNotFoundError
|
from ndbioimage import Imread
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('file', (Path(__file__).parent / 'files').iterdir())
|
@pytest.mark.parametrize(
|
||||||
|
"file",
|
||||||
|
[
|
||||||
|
file
|
||||||
|
for path in (Path(__file__).parent / "files").iterdir()
|
||||||
|
if path.is_dir() and path.name != "czi_xml"
|
||||||
|
for file in path.iterdir()
|
||||||
|
if file.suffix.lower() not in [".pzl", ".xml", "", ".txt"]
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_open(file):
|
def test_open(file):
|
||||||
try:
|
with Imread(file, axes="cztyx") as im:
|
||||||
with Imread(file) as im:
|
mean = im[0, 0, 0].mean()
|
||||||
mean = im[dict(c=0, z=0, t=0)].mean()
|
b = pickle.dumps(im)
|
||||||
b = pickle.dumps(im)
|
jm = pickle.loads(b)
|
||||||
jm = pickle.loads(b)
|
assert jm.get_frame(0, 0, 0).mean() == mean
|
||||||
assert jm[dict(c=0, z=0, t=0)].mean() == mean
|
b = pickle.dumps(im)
|
||||||
v = im.view()
|
jm = pickle.loads(b)
|
||||||
assert v[dict(c=0, z=0, t=0)].mean() == mean
|
assert jm[0, 0, 0].mean() == mean
|
||||||
b = pickle.dumps(v)
|
|
||||||
w = pickle.loads(b)
|
|
||||||
assert w[dict(c=0, z=0, t=0)].mean() == mean
|
|
||||||
except ReaderNotFoundError:
|
|
||||||
assert len(Imread.__subclasses__()), 'No subclasses for Imread found.'
|
|
||||||
|
|
||||||
for child in active_children():
|
|
||||||
child.kill()
|
|
||||||
+25
-8
@@ -1,24 +1,41 @@
|
|||||||
|
import tempfile
|
||||||
from itertools import combinations_with_replacement
|
from itertools import combinations_with_replacement
|
||||||
from numbers import Number
|
from numbers import Number
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
from tiffwrite import tiffwrite
|
||||||
|
|
||||||
from ndbioimage import Imread
|
from ndbioimage import Imread
|
||||||
|
|
||||||
r = np.random.randint(0, 255, (64, 64, 2, 3, 4))
|
|
||||||
im = Imread(r)
|
@pytest.fixture
|
||||||
a = np.array(im)
|
def array():
|
||||||
|
return np.random.randint(0, 255, (64, 64, 2, 3, 4), "uint8")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('s', combinations_with_replacement(
|
@pytest.fixture()
|
||||||
(0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5))
|
def image(array):
|
||||||
def test_slicing(s):
|
with tempfile.TemporaryDirectory() as folder:
|
||||||
s_im, s_a = im[s], a[s]
|
file = Path(folder) / "test.tif"
|
||||||
|
tiffwrite(file, array, "yxczt")
|
||||||
|
with Imread(file, axes="yxczt") as im:
|
||||||
|
yield im
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"s",
|
||||||
|
combinations_with_replacement(
|
||||||
|
(0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_slicing(s, image, array):
|
||||||
|
s_im, s_a = image[s], array[s]
|
||||||
if isinstance(s_a, Number):
|
if isinstance(s_a, Number):
|
||||||
assert isinstance(s_im, Number)
|
assert isinstance(s_im, Number)
|
||||||
assert s_im == s_a
|
assert s_im == s_a
|
||||||
else:
|
else:
|
||||||
assert isinstance(s_im, Imread)
|
assert isinstance(s_im, Imread)
|
||||||
assert s_im.shape == s_a.shape
|
assert tuple(s_im.shape) == s_a.shape
|
||||||
assert np.all(s_im == s_a)
|
assert np.all(s_im == s_a)
|
||||||
+43
-9
@@ -1,19 +1,53 @@
|
|||||||
|
import tempfile
|
||||||
from itertools import product
|
from itertools import product
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
from tiffwrite import tiffwrite
|
||||||
|
|
||||||
from ndbioimage import Imread
|
from ndbioimage import Imread
|
||||||
|
|
||||||
r = np.random.randint(0, 255, (64, 64, 2, 3, 4))
|
|
||||||
im = Imread(r)
|
@pytest.fixture
|
||||||
a = np.array(im)
|
def array():
|
||||||
|
return np.random.randint(0, 255, (64, 64, 2, 3, 4), "uint16")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('fun_and_axis', product(
|
@pytest.fixture
|
||||||
(np.sum, np.nansum, np.min, np.nanmin, np.max, np.nanmax, np.argmin, np.argmax,
|
def image(array):
|
||||||
np.mean, np.nanmean, np.var, np.nanvar, np.std, np.nanstd), (None, 0, 1, 2, 3, 4)))
|
with tempfile.TemporaryDirectory() as folder:
|
||||||
def test_ufuncs(fun_and_axis):
|
file = Path(folder) / "tiff" / "test.tif"
|
||||||
|
file.parent.mkdir(parents=True)
|
||||||
|
tiffwrite(file, array, "yxczt")
|
||||||
|
with Imread(file, axes="yxczt") as im:
|
||||||
|
yield im
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"fun_and_axis",
|
||||||
|
product(
|
||||||
|
(
|
||||||
|
np.sum,
|
||||||
|
np.nansum,
|
||||||
|
np.min,
|
||||||
|
np.nanmin,
|
||||||
|
np.max,
|
||||||
|
np.nanmax,
|
||||||
|
np.argmin,
|
||||||
|
np.argmax,
|
||||||
|
np.mean,
|
||||||
|
np.nanmean,
|
||||||
|
np.var,
|
||||||
|
np.nanvar,
|
||||||
|
np.std,
|
||||||
|
np.nanstd,
|
||||||
|
),
|
||||||
|
(None, 0, 1, 2, 3, 4),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_ufuncs(fun_and_axis, image, array):
|
||||||
fun, axis = fun_and_axis
|
fun, axis = fun_and_axis
|
||||||
assert np.all(np.isclose(fun(im, axis), fun(a, 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