Compare commits

..

21 Commits

Author SHA1 Message Date
Wim Pomp 46c511dd49 - let tifread raise FileNotFoundError when series != 0
PyTest / pytest (3.10) (push) Has been cancelled
PyTest / pytest (3.11) (push) Has been cancelled
PyTest / pytest (3.12) (push) Has been cancelled
PyTest / pytest (3.13) (push) Has been cancelled
2026-04-01 10:13:43 +02:00
w.pomp 0ee456a064 - improve support for tiled czi's 2026-03-30 11:35:25 +02:00
w.pomp 6123eeee8b - add support for tiled czi's: bugfix 2 2026-03-26 17:05:37 +01:00
w.pomp e56ef334f4 - add support for tiled czi's: bugfix 2026-03-26 16:52:18 +01:00
w.pomp bba24f2156 - add support for tiled czi's 2026-03-26 16:39:16 +01:00
w.pomp 7e9cf46d55 - limit imagecodecs version 2026-03-11 21:00:53 +01:00
w.pomp 351f563867 - cziread: fallback for when tile indices do not work 2026-03-11 20:51:32 +01:00
w.pomp 4563908254 - make paths absolute if possible
- seqfind: read some metadata from display_and_comments.txt if needed and present
2026-03-03 15:56:27 +01:00
w.pomp e5eac07b7b - bugfix 2026-01-26 17:22:11 +01:00
w.pomp 8ff52f5af5 - main_channel and default_transform arguments for Imread.with_transform 2026-01-26 17:03:27 +01:00
w.pomp 066a39719a - match ome.tif sequence metadata based on position number, with backup by taking any metadata found (with warning) 2026-01-16 11:32:00 +01:00
w.pomp 776b5204c4 - remove python 3.14 test as SimpleITK-SimpleElastix is not yet available 2025-11-27 15:45:55 +01:00
w.pomp e27a0f2657 - cziread: deal with missing exposure times 2025-11-26 17:24:48 +01:00
Wim Pomp 7fe1d189e5 - fix reading time interval when defined not in s in ome
- search all ome.tif for metadata in order
2025-10-15 20:29:07 +02:00
w.pomp 1b5febc35b - add option to skip autoscaling brightness when saving as mp4
- let coords_pandas also deal with polars dataframes
2025-09-25 15:20:00 +02:00
Wim Pomp 1fe3b3c824 - Make bioformats optional because jpype can cause problems
- Ruff format
2025-08-06 11:03:03 +02:00
Wim Pomp 3346ed3a48 - Can pycharm please stop adding wrong imports please? 2025-03-19 16:14:28 +01:00
Wim Pomp b7dadb645e - read metadata from first file in ome tiff sequence 2025-03-19 16:06:07 +01:00
Wim Pomp 0ac22aff87 - pyproject towards PEP621
- catch errors reading metadata in tifread
- read dimension order from ome
2025-03-19 15:04:57 +01:00
Wim Pomp 6daa372ccf - allow None in Transform.from_dict
- None transform parameters -> unit parameters/matrix
2025-02-14 15:45:43 +01:00
Wim Pomp cb52e77c34 - allow None in Transform.from_dict 2025-02-14 15:00:55 +01:00
48 changed files with 4153 additions and 11435 deletions
-66
View File
@@ -1,66 +0,0 @@
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
-114
View File
@@ -1,114 +0,0 @@
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
+22
View File
@@ -0,0 +1,22 @@
name: PyTest
on: [workflow_call, push, pull_request]
jobs:
pytest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
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
+12 -76
View File
@@ -1,77 +1,13 @@
/target
/Cargo.lock
# Byte-compiled / optimized / DLL files
__pycache__/
.pytest_cache/
*.py[cod]
# 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
._*
*.pyc
/build/
*.egg-info
/venv/
.idea
/.pytest_cache/
/ndbioimage/_version.py
/ndbioimage/jars
/tests/files/*
AGENTS.md
.agentbridge
/poetry.lock
/dist/
/uv.lock
-85
View File
@@ -1,85 +0,0 @@
[package]
name = "ndbioimage"
version = "0.2.0"
edition = "2024"
rust-version = "1.94.0"
authors = ["Wim Pomp <w.pomp@nki.nl>", "opencode"]
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]
clap = { version = "4", features = ["derive"] }
color-eyre = { version = "0.6", optional = true }
console = { version = "0.16", optional = true }
downloader = { version = "0.2", optional = true }
ffmpeg-sidecar = { version = "2", optional = true }
itertools = "0.15"
indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.18", features = ["rayon"], optional = true }
j4rs = { version = "0.25", optional = true }
libczirw-sys = { path = "../libczirw-sys", optional = true }
ndarray = { version = "0.17", features = ["serde"] }
num = "0.4"
numpy = { version = "0.29", optional = true }
ome-metadata = { path = "../ome-metadata/ome-metadata" }
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", "eyre", "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_yaml = { version = "0.9", optional = true }
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 }
bioformats = { version = "0.1", optional = true }
[dev-dependencies]
rayon = "1"
regex = "1"
[build-dependencies]
j4rs = "0.25"
ffmpeg-sidecar = "2"
retry = "2"
toml = "1"
[features]
default = []
all = ["bioformats_java", "bioformats_rust", "czi", "gpl-formats", "movie", "tiffseq", "tiffwrite", "tiff"]
gpl-formats = []
python = ["dep:pyo3", "dep:numpy", "dep:color-eyre", "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:serde_yaml"]
tiff = ["dep:tiff", "dep:thread_local"]
movie = ["dep:ffmpeg-sidecar", "dep:tokio", "dep:ordered-float", "dep:indicatif", "dep:console"]
[package.metadata.docs.rs]
features = ["bioformats", "gpl-formats", "czi", "tiff", "movie"]
[profile.test]
inherits = "release"
[profile.dev.package.backtrace]
opt-level = 3
+674
View File
@@ -0,0 +1,674 @@
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
View File
@@ -1,201 +0,0 @@
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
View File
@@ -1,27 +0,0 @@
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.
+34 -44
View File
@@ -1,9 +1,6 @@
# ndbioimage
[![Pytest](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml/badge.svg)](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml)
[![Pytest](https://github.com/wimpomp/ndbioimage/actions/workflows/pytest.yml/badge.svg)](https://github.com/wimpomp/ndbioimage/actions/workflows/pytest.yml)
## 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/)
# ndbioimage
Exposes (bio) images as a numpy ndarray-like object, but without loading the whole
image into memory, reading from the file only when needed. Some metadata is read
@@ -12,23 +9,23 @@ Additionally, it can automatically calculate an affine transform that corrects f
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.
To transition to semver, versions before 0.1.0 were yanked from crates.io.
[bioformats](https://www.openmicroscopy.org/bio-formats/) can handle.
## Installation
One of:
```
pip install ndbioimage
```
### Installation with option to write mp4 or mkv:
Work in progress! Make sure ffmpeg is installed.
```
pip install ndbioimage[bioformats]
pip install ndbioimage[write]
pip install ndbioimage[bioformats, write]
```
- bioformats: use [bio-formats](https://www.openmicroscopy.org/bio-formats/)
as fallback when other readers cannot open a file.
- write: write an image file into a mp4 or mkv file. Work in progress! Make sure ffmpeg is installed.
## Usage
### Python
@@ -40,7 +37,7 @@ from ndbioimage import Imread
with Imread('image_file.tif', axes='ctyx', dtype=int) as im:
plt.imshow(im[2, 1])
```
- Showing some image metadata
```
@@ -70,37 +67,30 @@ with Imread('image_file.tif', axes='cztyx') as im:
array = np.asarray(im[0, 0])
```
### Rust
```
use ndarray::Array2;
use ndbioimage::Reader;
let path = "/path/to/file";
let reader = Reader::new(&path, 0)?;
println!("size: {}, {}", reader.size_y, reader.size_y);
let frame = reader.get_frame(0, 0, 0).unwrap();
if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
println!("{:?}", arr);
} else {
println!("could not convert Frame to Array<i8>");
}
let xml = reader.get_ome_xml().unwrap();
println!("{}", xml);
```
```
use ndarray::Array2;
use ndbioimage::Reader;
let path = "/path/to/file";
let reader = Reader::new(&path, 0)?;
let view = reader.view();
let view = view.max_proj(3)?;
let array = view.as_array::<u16>()?
```
### Command line
```ndbioimage --help```: show help
```ndbioimage image```: show metadata about image
```ndbioimage image -w {name}.tif -r```: copy image into image.tif (replacing {name} with image), while registering channels
```ndbioimage image -w image.mp4 -C cyan lime red``` copy image into image.mp4 (z will be max projected), make channel colors cyan lime and red
## Adding more formats
Readers for image formats subclass AbstractReader. When an image reader is imported, Imread will
automatically recognize it and use it to open the appropriate file format. Image readers
are required to implement the following methods:
- staticmethod _can_open(path): return True if path can be opened by this reader
- \_\_frame__(self, c, z, t): return the frame at channel=c, z-slice=z, time=t from the file
Optional methods:
- get_ome: reads metadata from file and adds them to an OME object imported
from the ome-types library
- open(self): maybe open some file handle
- close(self): close any file handles
Optional fields:
- priority (int): Imread will try readers with a lower number first, default: 99
- do_not_pickle (strings): any attributes that should not be included when the object is pickled,
for example: any file handles
# TODO
- more image formats
-177
View File
@@ -1,177 +0,0 @@
#[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");
if std::env::var("DOCS_RS").is_err() {
#[cfg(feature = "movie")]
ffmpeg_sidecar::download::auto_download()?;
#[cfg(feature = "bioformats_java")]
bioformats::build()?;
#[cfg(all(not(feature = "python"), feature = "bioformats_java"))]
no_python_bioformats::build()?;
#[cfg(all(feature = "python", feature = "bioformats_java"))]
python_bioformats::build()?;
}
Ok(())
}
+1621
View File
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
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
+6
View File
@@ -0,0 +1,6 @@
from .. import JVM
if JVM is None:
__all__ = "cziread", "fijiread", "ndread", "seqread", "tifread", "metaseriesread"
else:
__all__ = "bfread", "cziread", "fijiread", "ndread", "seqread", "tifread", "metaseriesread"
+213
View File
@@ -0,0 +1,213 @@
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()
+761
View File
@@ -0,0 +1,761 @@
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, ureg
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
def xml_walk(tree, elements):
element, *elements = elements
if elements:
return [j for i in tree.findall(element) for j in xml_walk(i, elements)]
else:
return tree.findall(element)
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 = {}
syx = set()
si = self.reader.axes.index("S") if "S" in self.reader.axes else None
ci = self.reader.axes.index("C") if "C" in self.reader.axes else None
zi = self.reader.axes.index("Z") if "Z" in self.reader.axes else None
ti = self.reader.axes.index("T") if "T" in self.reader.axes else None
yi = self.reader.axes.index("Y") if "Y" in self.reader.axes else None
xi = self.reader.axes.index("X") if "X" in self.reader.axes else None
if si is None and self.series > 0:
raise FileNotFoundError(f"Series {self.series} not found in {self.path}.")
for directory_entry in self.reader.filtered_subblock_directory:
idx = self.get_index(directory_entry, self.reader.start)
syx.add((0 if si is None else idx[si], idx[yi], idx[xi]))
if self.tiles != (1, 1):
assert len({s for s, *_ in list(syx)}) == 1, "multiple tiled series not supported"
x, y = np.array(list(syx))[:, 1:, 0].T
a, b = np.min(x), np.max(x)
n = self.tiles[0]
bx = np.linspace(a - (b - a) / (n - 1) / 2, b + (b - a) / (n - 1) / 2, n + 1)
a, b = np.min(y), np.max(y)
n = self.tiles[1]
by = np.linspace(a - (b - a) / (n - 1) / 2, b + (b - a) / (n - 1) / 2, n + 1)
b = list(product([(i, j) for i, j in zip(by, by[1:])], [(i, j) for i, j in zip(bx, bx[1:])]))
if self.series < len(b):
by, bx = b[self.series]
else:
raise FileNotFoundError(f"Series {self.series} not found in {self.path}.")
for directory_entry in self.reader.filtered_subblock_directory:
idx = self.get_index(directory_entry, self.reader.start)
if bx[0] < idx[xi][0] < bx[1] and by[0] < idx[yi][0] < by[1]:
for cj in (0,) if ci is None else range(*idx[ci]):
for zj in (0,) if zi is None else range(*idx[zi]):
for tj in (0,) if ti is None else range(*idx[ti]):
if (cj, zj, tj) in filedict:
filedict[cj, zj, tj].append(directory_entry)
else:
filedict[cj, zj, tj] = [directory_entry]
else:
for directory_entry in self.reader.filtered_subblock_directory:
idx = self.get_index(directory_entry, self.reader.start)
if si is None or self.series == idx[si][0]:
for cj in (0,) if ci is None else range(*idx[ci]):
for zj in (0,) if zi is None else range(*idx[zi]):
for tj in (0,) if ti is None else range(*idx[ti]):
if (cj, zj, tj) in filedict:
filedict[cj, zj, tj].append(directory_entry)
else:
filedict[cj, zj, tj] = [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]
start = np.min([directory_entry.start for directory_entry in directory_entries], 0)
for directory_entry in directory_entries:
subblock = directory_entry.data_segment()
tile = subblock.data(resize=True, order=0)
index = [slice(i - j, i - j + k) for i, j, k in zip(directory_entry.start, start, tile.shape)]
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)]
@cached_property
def tiles(self):
columns = 1
rows = 1
xml = self.reader.metadata()
tree = etree.fromstring(xml)
tile_regions = xml_walk(
tree,
(
"Metadata",
"Experiment",
"ExperimentBlocks",
"AcquisitionBlock",
"SubDimensionSetups",
"RegionsSetup",
"SampleHolder",
"TileRegions",
"TileRegion",
),
)
for tile_region in tile_regions:
used = tile_region.find("IsUsedForAcquisition")
if used is not None and used.text.lower() == "true":
c = tile_region.find("Columns")
if c is not None:
columns = int(c.text)
r = tile_region.find("Rows")
if r is not None:
rows = int(r.text)
break
return columns, rows
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(axis)] if axis in self.reader.axes else 1 for axis 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
else:
raise NotImplementedError(f"unknown czi version: {self.version}")
@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}")
pxsize_x = self.ome.images[0].pixels.physical_size_x_quantity.to(ureg.um).magnitude
pxsize_y = self.ome.images[0].pixels.physical_size_y_quantity.to(ureg.um).magnitude
for t, z, c in product(range(self.size_t), range(self.size_z), range(self.size_c)):
x_min = (
min([f.start[f.axes.index("X")] for f in self.filedict[c, z, t]]) if (c, z, t) in self.filedict else 0
)
y_min = (
min([f.start[f.axes.index("Y")] for f in self.filedict[c, z, t]]) if (c, z, t) in self.filedict else 0
)
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[min(c, len(exposure_times) - 1)] if len(exposure_times) > 0 else None,
position_x=self.positions[0] + x_min * pxsize_x,
position_x_unit=self.um,
position_y=self.positions[1] + y_min * pxsize_y,
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
+68
View File
@@ -0,0 +1,68 @@
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
+88
View File
@@ -0,0 +1,88 @@
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")
+62
View File
@@ -0,0 +1,62 @@
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
+195
View File
@@ -0,0 +1,195 @@
import re
import warnings
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()}
if "Summary" in metadata["Info"] and "UserName" in metadata["Info"]["Summary"]:
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"]
if "Summary" in metadata["Info"]
else None,
),
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
if "Summary" in metadata["Info"] and "ChNames" in metadata["Info"]["Summary"]:
cnamelist = metadata["Info"]["Summary"]["ChNames"]
elif (self.path.parent / "display_and_comments.txt").exists():
warnings.warn(f"{self.path} is missing some metadata")
with open(self.path.parent / "display_and_comments.txt") as f:
cnamelist = [channel["Name"] for channel in yaml.safe_load(f)["Channels"]]
else:
raise ValueError("could not find metadata describing the order of the channels")
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)])
+172
View File
@@ -0,0 +1,172 @@
import re
import warnings
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 from_xml, model
from .. import AbstractReader, try_default
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: try_default(yaml.safe_load, value, value) if isinstance(value, str) else value
for key, value in self.reader.imagej_metadata.items()
}
def get_ome(self):
if self.reader.is_ome:
pos_number_pat = re.compile(r"\d+")
def get_pos_number(s):
return [int(i) for i in pos_number_pat.findall(s)]
match = re.match(r"^(.*)(pos[\d_]+)(.*)$", self.path.name, flags=re.IGNORECASE)
if match is not None and len(match.groups()) == 3:
a, b, c = match.groups()
pat = re.compile(f"^{re.escape(a)}" + re.sub(r"\d+", r"\\d+", b) + f"{re.escape(c)}$")
backup_ome = []
backup_backup_ome = []
pos_number = get_pos_number(b)
for file in sorted(self.path.parent.iterdir(), key=lambda i: (len(i.name), i.name)):
if pat.match(file.name):
with tifffile.TiffFile(file) as tif:
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=UserWarning)
ome = from_xml(tif.ome_metadata)
backup_backup_ome.extend(ome.images)
try:
backup_ome.extend(
[
image
for image in ome.images
if pos_number == get_pos_number(image.stage_label.name)
]
)
except ValueError:
pass
ome.images = [image for image in ome.images if b == image.stage_label.name]
if ome.images:
return ome
if backup_ome:
ome.images = [backup_ome[0]]
warnings.warn(
"could not find the ome.tif file containing the metadata with an exact match, "
f"matched {ome.images[0].stage_label.name} with {b} instead, "
"did you rename the file?"
)
return ome
if backup_backup_ome:
ome.images = [backup_backup_ome[0]]
warnings.warn(
"could not find the ome.tif file containing the metadata, "
f"used metadata from {ome.images[0].name} instead, "
"did you rename the file"
)
return ome
warnings.warn("could not find the ome.tif file containing the metadata")
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):
if self.series != 0:
raise FileNotFoundError(f"Series {self.series} not found in {self.path}. Tifread only supports one series.")
self.reader = tifffile.TiffFile(self.path)
page = self.reader.pages.first
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
else:
self.p_transpose = [i for i in [page.axes.find(j) for j in "YX"] if i >= 0] # noqa
def close(self):
self.reader.close()
def __frame__(self, c: int, z: int, t: int):
dimension_order = self.ome.images[0].pixels.dimension_order.value
if self.p_ndim == 3:
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "zt"])
ct = {"z": z, "t": t}
n = sum([ct[ax] * np.prod(self.base_shape[axes[:i]]) for i, ax in enumerate(axes)])
return np.transpose(self.reader.asarray(int(n)), self.p_transpose)[int(c)]
else:
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "czt"])
czt = {"c": c, "z": z, "t": t}
n = sum([czt[ax] * np.prod(self.base_shape[axes[:i]]) for i, ax in enumerate(axes)])
return np.transpose(self.reader.asarray(int(n)), self.p_transpose)
@@ -1,6 +1,4 @@
#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
@@ -16,9 +16,14 @@ except ImportError:
sitk = None
try:
from pandas import DataFrame, Series, concat
import pandas as pd
except ImportError:
DataFrame, Series, concat = None, None, None
pd = None
try:
import polars as pl
except ImportError:
pl = None
if hasattr(yaml, "full_load"):
@@ -42,9 +47,7 @@ class Transforms(dict):
new = cls()
for key, value in d.items():
if isinstance(key, str) and C:
new[key.replace(r"\:", ":").replace("\\\\", "\\")] = (
Transform.from_dict(value)
)
new[key.replace(r"\:", ":").replace("\\\\", "\\")] = Transform.from_dict(value)
elif T:
new[key] = Transform.from_dict(value)
return new
@@ -72,18 +75,12 @@ class Transforms(dict):
def asdict(self):
return {
key.replace("\\", "\\\\").replace(":", r"\:")
if isinstance(key, str)
else key: value.asdict()
key.replace("\\", "\\\\").replace(":", r"\:") if isinstance(key, str) else key: value.asdict()
for key, value in self.items()
}
def __getitem__(self, item):
return (
np.prod([self[i] for i in item[::-1]])
if isinstance(item, tuple)
else super().__getitem__(item)
)
return np.prod([self[i] for i in item[::-1]]) if isinstance(item, tuple) else super().__getitem__(item)
def __missing__(self, key):
return self.default
@@ -120,8 +117,7 @@ class Transforms(dict):
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}"
f"The image file and the transform do not have the same channels, creating a mapping: {mapping}"
)
for key_im, key_t in mapping.items():
self[key_im] = self[key_t]
@@ -135,15 +131,13 @@ class Transforms(dict):
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,
if pd is None:
raise ImportError("pandas is not available")
if isinstance(array, pd.DataFrame):
return pd.concat(
[self.coords_pandas(row, channel_names, columns) for _, row in array.iterrows()], axis=1
).T
elif isinstance(array, Series):
elif isinstance(array, pd.Series):
key = []
if "C" in array:
key.append(channel_names[int(array["C"])])
@@ -153,27 +147,20 @@ class Transforms(dict):
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."
)
def with_beads(self, cyllens, bead_files, main_channel=None, default_transform=None):
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
self.calculate_channel_transforms(file, cyllens, main_channel, default_transform) for file in bead_files
]
for key in {key for transform in transforms for key in transform.keys()}:
new_transforms = [
transform[key] for transform in transforms if key in transform
]
new_transforms = [transform[key] for transform in transforms if key in transform]
if len(new_transforms) == 1:
self[key] = new_transforms[0]
else:
self[key] = Transform()
self[key].parameters = np.mean(
[t.parameters for t in new_transforms], 0
)
self[key].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))
np.std([t.parameters for t in new_transforms], 0) / np.sqrt(len(new_transforms))
).tolist()
return self
@@ -206,7 +193,7 @@ class Transforms(dict):
return checked_files
@staticmethod
def calculate_channel_transforms(bead_file, cyllens):
def calculate_channel_transforms(bead_file, cyllens, main_channel=None, default_transform=None):
"""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
@@ -216,34 +203,32 @@ class Transforms(dict):
goodch = [c for c, max_im in enumerate(max_ims) if not im.is_noise(max_im)]
if not goodch:
goodch = list(range(len(max_ims)))
untransformed = [
c
for c in range(im.shape["c"])
if cyllens[im.detector[c]].lower() == "none"
]
untransformed = [c for c in range(im.shape["c"]) if cyllens[im.detector[c]].lower() == "none"]
good_and_untrans = sorted(set(goodch) & set(untransformed))
if good_and_untrans:
masterch = good_and_untrans[0]
else:
masterch = goodch[0]
if main_channel is None:
if good_and_untrans:
main_channel = good_and_untrans[0]
else:
main_channel = goodch[0]
transform = Transform()
if not good_and_untrans:
matrix = transform.matrix
matrix = transform.matrix
if default_transform is None:
matrix[0, 0] = 0.86
transform.matrix = matrix
else:
for i, t in zip(((0, 0), (0, 1), (1, 0), (1, 1), (0, 2), (1, 2)), default_transform):
matrix[i] = t
transform.matrix = matrix
transforms = Transforms()
for c in tqdm(goodch, desc="Calculating channel transforms"): # noqa
if c == masterch:
if c == main_channel:
transforms[im.channel_names[c]] = transform
else:
transforms[im.channel_names[c]] = (
Transform.register(max_ims[masterch], max_ims[c]) * transform
)
transforms[im.channel_names[c]] = Transform.register(max_ims[main_channel], max_ims[c]) * transform
return transforms
@staticmethod
def save_channel_transform_tiff(bead_files, tiffile):
def save_channel_transform_tiff(bead_files, tiffile, default_transform=None):
from . import Imread
n_channels = 0
@@ -253,16 +238,9 @@ class Transforms(dict):
with IJTiffFile(tiffile) as tif:
for t, file in enumerate(bead_files):
with Imread(file) as im:
with Imread(file).with_transform() as jm:
with Imread(file).with_transform(default_transform=default_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,
)
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
@@ -270,22 +248,11 @@ class Transforms(dict):
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_groups = [list(chunk) for chunk in Chunks(range(im.shape["t"]), size=round(np.sqrt(im.shape["t"])))]
t_keys = [int(np.round(np.mean(t_group))) for t_group in t_groups]
t_pairs = [
(int(np.round(np.mean(t_group))), frame)
for t_group in t_groups
for frame in t_group
]
t_pairs = [(int(np.round(np.mean(t_group))), frame) for t_group in t_groups for frame in t_group]
t_pairs.extend(zip(t_keys, t_keys[1:]))
fmaxz_keys = {
t_key: filters.gaussian(im[t_key].max("z"), 5) for t_key in t_keys
}
fmaxz_keys = {t_key: filters.gaussian(im[t_key].max("z"), 5) for t_key in t_keys}
def fun(t_key_t, im, fmaxz_keys):
t_key, t = t_key_t
@@ -293,17 +260,11 @@ class Transforms(dict):
return 0, 0
else:
fmaxz = filters.gaussian(im[t].max("z"), 5)
return Transform.register(
fmaxz_keys[t_key], fmaxz, "translation"
).parameters[4:]
return Transform.register(fmaxz_keys[t_key], fmaxz, "translation").parameters[4:]
shifts = np.array(
pmap(fun, t_pairs, (im, fmaxz_keys), desc="Calculating image shifts.")
)
shifts = np.array(pmap(fun, t_pairs, (im, fmaxz_keys), desc="Calculating image shifts."))
shift_keys_cum = np.zeros(2)
for shift_keys, t_group in zip(
np.vstack((-shifts[0], shifts[im.shape["t"] :])), t_groups
):
for shift_keys, t_group in zip(np.vstack((-shifts[0], shifts[im.shape["t"] :])), t_groups):
shift_keys_cum += shift_keys
shifts[t_group] += shift_keys_cum
@@ -317,9 +278,7 @@ class Transform:
if sitk is None:
self.transform = None
else:
self.transform = sitk.ReadTransform(
str(Path(__file__).parent / "transform.txt")
)
self.transform = sitk.ReadTransform(str(Path(__file__).parent / "transform.txt"))
self.dparameters = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
self.shape = [512.0, 512.0]
self.origin = [255.5, 255.5]
@@ -339,8 +298,7 @@ class Transform:
"""kind: 'affine', 'translation', 'rigid'"""
if sitk is None:
raise ImportError(
"SimpleElastix is not installed: "
"https://simpleelastix.readthedocs.io/GettingStarted.html"
"SimpleElastix is not installed: https://simpleelastix.readthedocs.io/GettingStarted.html"
)
new = cls()
kind = kind or "affine"
@@ -359,13 +317,11 @@ class Transform:
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.parameters = [1.0, 0.0, 0.0, 1.0] + [float(t) for t in transform["TransformParameters"]]
new.shape = [float(t) for t in transform["Size"]]
new.origin = [(t - 1) / 2 for t in new.shape]
else:
raise NotImplementedError(f"{kind} tranforms not implemented (yet)")
raise NotImplementedError(f"{kind} transforms not implemented (yet)")
new.dparameters = 6 * [np.nan]
return new
@@ -387,29 +343,18 @@ class Transform:
@classmethod
def from_dict(cls, d):
new = cls()
new.origin = (
None
if d["CenterOfRotationPoint"] is None
else [float(i) for i in d["CenterOfRotationPoint"]]
)
new.origin = None if d["CenterOfRotationPoint"] is None else [float(i) for i in d["CenterOfRotationPoint"]]
new.parameters = (
(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
if d["TransformParameters"] is None
else [float(i) for i in d["TransformParameters"]]
)
new.dparameters = (
[
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0) if i is None else float(i)
for i in d["dTransformParameters"]
]
[(0.0, 0.0, 0.0, 0.0, 0.0, 0.0) if i is None else float(i) for i in d["dTransformParameters"]]
if "dTransformParameters" in d
else 6 * [np.nan] and d["dTransformParameters"] is not None
)
new.shape = (
None
if d["Size"] is None
else [None if i is None else float(i) for i in d["Size"]]
)
new.shape = None if d["Size"] is None else [None if i is None else float(i) for i in d["Size"]]
return new
def __mul__(self, other): # TODO: take care of dmatrix
@@ -443,11 +388,7 @@ class Transform:
@property
def matrix(self):
return np.array(
(
(*self.parameters[:2], self.parameters[4]),
(*self.parameters[2:4], self.parameters[5]),
(0, 0, 1),
)
((*self.parameters[:2], self.parameters[4]), (*self.parameters[2:4], self.parameters[5]), (0, 0, 1))
)
@matrix.setter
@@ -458,11 +399,7 @@ class Transform:
@property
def dmatrix(self):
return np.array(
(
(*self.dparameters[:2], self.dparameters[4]),
(*self.dparameters[2:4], self.dparameters[5]),
(0, 0, 0),
)
((*self.dparameters[:2], self.dparameters[4]), (*self.dparameters[2:4], self.dparameters[5]), (0, 0, 0))
)
@dmatrix.setter
@@ -524,19 +461,12 @@ class Transform:
else:
if sitk is None:
raise ImportError(
"SimpleElastix is not installed: "
"https://simpleelastix.readthedocs.io/GettingStarted.html"
"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)
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,
@@ -544,23 +474,23 @@ class Transform:
"""
if self.is_unity():
return array.copy()
elif DataFrame is not None and isinstance(array, (DataFrame, Series)):
elif pd is not None and isinstance(array, (pd.DataFrame, pd.Series)):
columns = columns or ["x", "y"]
array = array.copy()
if isinstance(array, DataFrame):
if isinstance(array, pd.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
]
elif isinstance(array, pd.Series):
array[columns] = self.coords(np.atleast_2d(array[columns].to_numpy()))[0]
return array
elif pl is not None and isinstance(array, (pl.DataFrame, pl.LazyFrame)):
columns = columns or ["x", "y"]
if isinstance(array, pl.DataFrame):
xy = self.coords(np.atleast_2d(array.select(columns).to_numpy()))
elif isinstance(array, pl.LazyFrame):
xy = self.coords(np.atleast_2d(array.select(columns).collect().to_numpy()))
return array.with_columns(**{c: i for c, i in zip(columns, xy.T)})
else: # somehow we need to use the inverse here to get the same effect as when using self.frame
return np.array(
[
self.inverse.transform.TransformPoint(i.tolist())
for i in np.asarray(array)
]
)
return np.array([self.inverse.transform.TransformPoint(i.tolist()) for i in np.asarray(array)])
def save(self, file):
"""save the parameters of the transform calculated
-82
View File
@@ -1,82 +0,0 @@
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 . import ndbioimage_rs as rs # noqa
from .ndbioimage_rs import View as Imread
from .ndbioimage_rs import batch_to_tiff
from .transforms import Transform, Transforms # noqa: F401
try:
import ome_metadata # noqa
except ModuleNotFoundError:
sys.modules["ome_metadata"] = rs.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()
if (path / "py" / "ndbioimage" / "__init__.py").exists():
rs.generate_stub(str(path)) # noqa
else:
raise ModuleNotFoundError(str(path / "py" / "ndbioimage" / "__init__.py"))
(path / "py" / "ndbioimage" / "__init__.pyi").unlink(missing_ok=True)
(path / "py" / "ndbioimage" / "ndbioimage_rs" / "__init__.pyi").rename(
path / "py" / "ndbioimage" / "ndbioimage_rs.pyi"
)
(path / "py" / "ndbioimage" / "ndbioimage_rs").rmdir()
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
-419
View File
@@ -1,419 +0,0 @@
# 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__ = [
"Shape",
"View",
"batch_to_tiff",
"main",
]
class Shape:
@property
def c(self) -> builtins.int: ...
@property
def z(self) -> builtins.int: ...
@property
def t(self) -> builtins.int: ...
@property
def y(self) -> builtins.int: ...
@property
def x(self) -> builtins.int: ...
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: ...
def __str__(self) -> builtins.str: ...
def __repr__(self) -> builtins.str: ...
def __getnewargs__(
self,
) -> tuple[
builtins.str,
builtins.int,
builtins.int,
builtins.int,
builtins.int,
builtins.int,
]: ...
def __getitem__(
self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int]
) -> typing.Optional[int | list[int]]: ...
def to_list(self) -> builtins.list[builtins.int]: ...
class View:
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
"""
@property
def reader_name(self) -> builtins.str: ...
@property
def transform(self) -> None: ...
@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]: ...
@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) -> View: ...
@property
def dtype(self) -> numpy.dtype: ...
@property
def z_stack(self) -> builtins.bool: ...
@property
def zstack(self) -> builtins.bool:
r"""
backwards compatibility
"""
@property
def time_series(self) -> builtins.bool: ...
@property
def timeseries(self) -> builtins.bool:
r"""
backwards compatibility
"""
@property
def pixel_size(self) -> typing.Optional[builtins.float]: ...
@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]: ...
@property
def time_interval(self) -> typing.Optional[builtins.float]: ...
@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]: ...
@property
def magnification(self) -> typing.Optional[builtins.float]: ...
@property
def tube_lens_name(self) -> typing.Optional[builtins.str]: ...
def __new__(
cls,
path: str | pathlib.Path | View | bytes,
dtype: numpy.typing.DTypeLike = None,
axes: builtins.str = "cztyx",
reader: typing.Optional[builtins.str] = None,
) -> View:
r"""
new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64)
"""
@staticmethod
def get_positions(
path: str | pathlib.Path | View | bytes,
) -> builtins.set[builtins.int]: ...
@staticmethod
def kill_vm() -> None:
r"""
only remains for backwards compatibility
"""
def reshape(self, order: builtins.str, copy: builtins.bool) -> typing.Any: ...
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,
) -> View: ...
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) -> View:
r"""
change the data type of the view: (u)int(8/16/32) or float(32/64)
"""
def astype(self, dtype: numpy.typing.DTypeLike) -> View:
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: numpy.typing.DTypeLike = None,
copy: typing.Optional[builtins.bool] = None,
) -> typing.Any: ...
def __contains__(self, _item: typing.Any) -> builtins.bool: ...
def __lt__(self, other: typing.Any) -> typing.Any: ...
def __le__(self, other: typing.Any) -> typing.Any: ...
def __eq__(self, other: typing.Any) -> typing.Any: ...
def __ne__(self, other: typing.Any) -> typing.Any: ...
def __gt__(self, other: typing.Any) -> typing.Any: ...
def __ge__(self, other: typing.Any) -> typing.Any: ...
def __add__(self, other: typing.Any) -> typing.Any: ...
def __radd__(self, other: typing.Any) -> typing.Any: ...
def __sub__(self, other: typing.Any) -> typing.Any: ...
def __rsub__(self, other: typing.Any) -> typing.Any: ...
def __mul__(self, other: typing.Any) -> typing.Any: ...
def __rmul__(self, other: typing.Any) -> typing.Any: ...
def __truediv__(self, other: typing.Any) -> typing.Any: ...
def __rtruediv__(self, other: typing.Any) -> typing.Any: ...
def __floordiv__(self, other: typing.Any) -> typing.Any: ...
def __rfloordiv__(self, other: typing.Any) -> typing.Any: ...
def __mod__(self, other: typing.Any) -> typing.Any: ...
def __rmod__(self, other: typing.Any) -> typing.Any: ...
def __matmul__(self, other: typing.Any) -> typing.Any: ...
def __rmatmul__(self, other: typing.Any) -> typing.Any: ...
def __and__(self, other: typing.Any) -> typing.Any: ...
def __rand__(self, other: typing.Any) -> typing.Any: ...
def __or__(self, other: typing.Any) -> typing.Any: ...
def __ror__(self, other: typing.Any) -> typing.Any: ...
def __xor__(self, other: typing.Any) -> typing.Any: ...
def __rxor__(self, other: typing.Any) -> typing.Any: ...
def __lshift__(self, other: typing.Any) -> typing.Any: ...
def __rlshift__(self, other: typing.Any) -> typing.Any: ...
def __rshift__(self, other: typing.Any) -> typing.Any: ...
def __rrshift__(self, other: typing.Any) -> typing.Any: ...
def __neg__(self) -> typing.Any: ...
def __pos__(self) -> typing.Any: ...
def __abs__(self) -> typing.Any: ...
def __invert__(self) -> typing.Any: ...
def __enter__(self) -> View: ...
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: ...
def __getnewargs__(self) -> tuple[builtins.list[builtins.int]]: ...
def __copy__(self) -> View: ...
def __deepcopy__(self) -> View: ...
def copy(self) -> View: ...
def __iter__(self) -> View: ...
def __next__(self) -> typing.Optional[typing.Any]: ...
def __len__(self) -> builtins.int: ...
def __repr__(self) -> builtins.str: ...
def __str__(self) -> builtins.str: ...
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: ...
def to_bytes(self) -> builtins.list[builtins.int]: ...
def tobytes(self) -> builtins.list[builtins.int]: ...
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) -> View:
r"""
swap two axes
"""
def transpose(
self, axes: typing.Optional[typing.Sequence[int | str]] = None
) -> View:
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]: ...
def binning(self, channel: builtins.int) -> typing.Optional[builtins.int]: ...
def laser_wavelengths(
self, channel: builtins.int
) -> typing.Optional[builtins.float]: ...
def laser_power(self, channel: builtins.int) -> typing.Optional[builtins.float]: ...
def filter_set_name(
self, channel: builtins.int
) -> typing.Optional[builtins.str]: ...
def gain(self, channel: builtins.int) -> typing.Optional[builtins.float]: ...
def summary(self) -> builtins.str:
r"""
gives a helpful summary of the recorded experiment
"""
@staticmethod
def get_available_series(
path: str | pathlib.Path | View, 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 | View,
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: ...
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: ...
def __pow__(
self,
other: View | numpy.typing.NDArray | int | float,
mod: typing.Any = None,
/,
) -> numpy.typing.NDArray: ...
def __rpow__(
self,
other: View | numpy.typing.NDArray | int | float,
mod: typing.Any = None,
/,
) -> numpy.typing.NDArray: ...
def max(
self,
axis: int | str = None,
dtype: numpy.typing.DTypeLike = None,
out: typing.Any = None,
keepdims: bool = False,
initial: int | float = None,
where: bool = True,
) -> View | 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: numpy.typing.DTypeLike = None,
out: typing.Any = None,
keepdims: bool = False,
initial: int | float = None,
where: bool = True,
) -> View | 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: numpy.typing.DTypeLike = None,
out: typing.Any = None,
keepdims: bool = False,
) -> View | 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: numpy.typing.DTypeLike = None,
out: typing.Any = None,
keepdims: bool = False,
initial: int | float = None,
where: bool = True,
) -> View | numpy.typing.NDArray | int | float:
r"""
Return the sum along a given axis. Arguments beyond axis are not implemented
"""
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: ...
def main() -> None: ...
View File
+42 -30
View File
@@ -1,43 +1,55 @@
[build-system]
requires = ["maturin>=1.9.4,<2.0"]
build-backend = "maturin"
[project]
name = "ndbioimage"
version = "2027.0.1"
version = "2026.4.0"
description = "Bio image reading, metadata and some affine registration."
authors = [
{ name = "W. Pomp", email = "w.pomp@nki.nl" }
]
license = { text = "GPL-3.0-or-later"}
readme = "README.md"
keywords = ["bioformats", "imread", "numpy", "metadata"]
include = ["transform.txt"]
requires-python = ">=3.10"
classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Rust",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
]
dynamic = [
"description",
"readme",
"license",
"license-files",
"authors",
"maintainers",
"keywords",
"urls",
]
exclude = ["ndbioimage/jars"]
dependencies = [
"numpy >= 1.16.0",
"czifile == 2019.7.2",
"imagecodecs <= 2026.1.14",
"lxml",
"numpy >= 1.20",
"ome-types",
"pandas",
"parfor >= 2025.1.0",
"pint",
"pyyaml",
"SimpleITK-SimpleElastix; sys_platform != 'darwin'",
"scikit-image",
"tifffile <= 2025.1.10",
"tiffwrite >= 2024.12.1",
"tqdm",
]
[project.optional-dependencies]
test = ["pytest", "tiffwrite"]
test = ["pytest"]
write = ["matplotlib", "scikit-video"]
bioformats = ["JPype1"]
[project.urls]
repository = "https://git.wimpomp.nl/wim/focusfeedbackgui"
[project.scripts]
ndbioimage = "ndbioimage:ndbioimage_rs.main"
ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub"
ndbioimage = "ndbioimage:main"
[tool.maturin]
python-source = "py"
features = ["python", "bioformats_java", "tiffwrite", "czi"]
module-name = "ndbioimage.ndbioimage_rs"
include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"]
[tool.pytest.ini_options]
filterwarnings = ["ignore:::(colorcet)"]
[tool.isort]
line_length = 119
[tool.ruff]
line-length = 119
indent-width = 4
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
-422
View File
@@ -1,422 +0,0 @@
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 {
C,
Z,
T,
Y,
X,
#[strum(serialize = "N")]
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)]
pub enum Operation {
Max,
Min,
Sum,
Mean,
}
impl Operation {
pub(crate) fn operate<T, D>(
&self,
array: Array<T, D>,
axis: usize,
) -> Result<<Array<T, D> as MinMax>::Output, 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()
}
}
#[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,
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()
}
}
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)
}
}
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 {
pub fn new() -> Self {
Self {
c: 1,
z: 1,
t: 1,
y: 1,
x: 1,
order: vec![],
}
}
pub fn iter(&self) -> ShapeIterBorrow<'_> {
ShapeIterBorrow {
shape: self,
index: 0,
}
}
pub fn len(&self) -> usize {
self.order.len()
}
pub fn is_empty(&self) -> bool {
self.order.is_empty()
}
pub fn to_vec(&self) -> Vec<usize> {
self.order.iter().map(|axis| self[axis]).collect()
}
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
}
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 => (),
}
}
}
-94
View File
@@ -1,94 +0,0 @@
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, RwLock};
use indexmap::{Equivalent, IndexMap};
use ndarray::IxDyn;
use crate::error::Error;
use crate::readers::ArrayT;
static CACHE: LazyLock<RwLock<IndexMap<ViewHash, ArrayT<IxDyn>>>> =
LazyLock::new(|| RwLock::new(IndexMap::new()));
#[derive(Debug, Clone, Eq, PartialEq)]
struct ViewHash {
reader: String,
path: PathBuf,
series: usize,
position: usize,
c: isize,
z: isize,
t: isize,
}
impl Hash for ViewHash {
fn hash<H: Hasher>(&self, state: &mut H) {
self.reader.as_str().hash(state);
self.path.as_path().hash(state);
self.series.hash(state);
self.position.hash(state);
self.c.hash(state);
self.z.hash(state);
self.t.hash(state);
}
}
impl ViewHash {
fn new(reader: String, path: PathBuf, series: usize, position: usize, c: isize, z: isize, t: isize) -> Self {
Self { reader, path, series, position, c, z, t }
}
}
struct ViewEquiv<'a> {
reader: &'a str,
path: &'a Path,
series: usize,
position: usize,
c: isize,
z: isize,
t: isize,
}
impl Hash for ViewEquiv<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.reader.hash(state);
self.path.hash(state);
self.series.hash(state);
self.position.hash(state);
self.c.hash(state);
self.z.hash(state);
self.t.hash(state);
}
}
impl<'a> ViewEquiv<'a> {
fn new(reader: &'a str, path: &'a Path, series: usize, position: usize, c: isize, z: isize, t: isize) -> ViewEquiv<'a> {
Self { reader, path, series, position, c, z, t }
}
}
impl Equivalent<ViewEquiv<'_>> for ViewHash {
fn equivalent(&self, key: &ViewEquiv) -> bool {
(self.reader == key.reader) && (self.path == key.path) && (self.series == key.series) && (self.position == key.position) && (self.c == key.c) && (self.z == key.z) && (self.t == key.t)
}
}
impl Equivalent<ViewHash> for ViewEquiv<'_> {
fn equivalent(&self, key: &ViewHash) -> bool {
key.equivalent(self)
}
}
pub fn cache_get_or_insert(f: &dyn Fn() -> ArrayT<IxDyn>, reader: &str, path: &Path, series: usize, position: usize, c: isize, z: isize, t: isize) -> Result<ArrayT<IxDyn>, Error> {
if let Some(frame) = CACHE.read().unwrap().get(&ViewEquiv::new(reader, path, series, position, c, z, t)) {
Ok(frame.clone())
} else {
let a = f();
let mut cache = CACHE.write().unwrap();
cache.insert(ViewHash::new(reader.to_string(), path.to_path_buf(), series, position, c, z, t), a.clone());
// TODO: find an IndexMapDeque to pop efficiently at the other end
while cache.len() > 1024 {
cache.pop();
}
Ok(a)
}
}
-202
View File
@@ -1,202 +0,0 @@
use crate::error::Error;
use phf::phf_map;
use std::fmt::Display;
use std::str::FromStr;
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",
};
#[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 {
pub fn to_rgb(&self) -> Vec<u8> {
vec![self.r, self.g, self.b]
}
}
-122
View File
@@ -1,122 +0,0 @@
use strum::IntoStaticStr;
use thiserror::Error;
#[derive(Debug, Error, IntoStaticStr)]
pub enum Error {
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
Shape(#[from] ndarray::ShapeError),
#[cfg(feature = "bioformats_java")]
#[error(transparent)]
J4rs(#[from] j4rs::errors::J4RsError),
#[error(transparent)]
Infallible(#[from] std::convert::Infallible),
#[error(transparent)]
ParseIntError(#[from] std::num::ParseIntError),
#[error(transparent)]
Ome(#[from] ome_metadata::error::Error),
#[cfg(feature = "bioformats_java")]
#[error(transparent)]
Downloader(#[from] downloader::Error),
#[error(transparent)]
Strum(#[from] strum::ParseError),
#[cfg(feature = "tiffwrite")]
#[error(transparent)]
TemplateError(#[from] indicatif::style::TemplateError),
#[cfg(feature = "tiffwrite")]
#[error(transparent)]
TiffWrite(#[from] tiffwrite::error::Error),
#[cfg(feature = "tiffseq")]
#[error(transparent)]
SerdeYaml(#[from] serde_yaml::Error),
#[cfg(any(feature = "tiffseq", feature = "tiff"))]
#[error(transparent)]
Tiff(#[from] tiff::TiffError),
#[cfg(feature = "python")]
#[error(transparent)]
PostCard(#[from] postcard::Error),
#[cfg(feature = "czi")]
#[error(transparent)]
LibCzi(#[from] libczirw_sys::error::Error),
#[error(transparent)]
RegexError(#[from] regex::Error),
#[cfg(feature = "czi")]
#[error(transparent)]
XmlTree(#[from] xmltree::Error),
#[cfg(feature = "czi")]
#[error(transparent)]
XmlTreeParse(#[from] xmltree::ParseError),
#[cfg(feature = "czi")]
#[error(transparent)]
Czi(#[from] crate::readers::czi::CziError),
#[cfg(feature = "movie")]
#[error(transparent)]
TokioJoin(#[from] tokio::task::JoinError),
#[cfg(feature = "bioformats_rust")]
#[error(transparent)]
BioFormats(#[from] bioformats::error::BioFormatsError),
#[error("invalid axis: {0}")]
InvalidAxis(String),
#[error("axis {0} not found in axes {1}")]
AxisNotFound(String, String),
#[error("conversion error: {0}")]
TryInto(String),
#[error("file already exists {0}")]
FileAlreadyExists(String),
#[error("could not download ffmpeg: {0}")]
FfmpegDownload(String),
#[error("FFmpeg error: {0}")]
Ffmpeg(String),
#[error("index {0} out of bounds {1}")]
OutOfBounds(isize, isize),
#[error("axis {0} has length {1}, but was not included")]
OutOfBoundsAxis(String, usize),
#[error("dimensionality mismatch: {0} != {0}")]
DimensionalityMismatch(usize, usize),
#[error("axis {0}: {1} is already operated on!")]
AxisAlreadyOperated(usize, String),
#[error("not enough free dimensions")]
NotEnoughFreeDimensions,
#[error("cannot cast {0} to {1}")]
Cast(String, String),
#[error("empty view")]
EmptyView,
#[error("invalid color: {0}")]
InvalidColor(String),
#[error("no image or pixels found")]
NoImageOrPixels,
#[error("invalid attenuation value: {0}")]
InvalidAttenuation(String),
#[error("not a valid file name")]
InvalidFileName,
#[error("file has no parent")]
NoParent,
#[error("unknown pixel type {0}")]
UnknownPixelType(String),
#[error("no mean")]
NoMean,
#[error("tiff is locked")]
TiffLock,
#[error("not implemented: {0}")]
NotImplemented(String),
#[error("cannot parse: {0}")]
Parse(String),
#[error("cannot convert libczi pixel type: {0}")]
Conversion(String),
#[error("no reader found for {0}, tried: {1}")]
NoReader(String, String),
#[error("reader {0} cannot open file {1} because {2}")]
InvalidReader(String, String, String),
#[error("file does not exist: {0}")]
FileDoesNotExist(String),
#[error("cannot remove axes {0}, size {1} != 1")]
SizeMismatch(String, usize),
}
impl Error {
pub fn variant_name(&self) -> &'static str {
self.into()
}
}
-312
View File
@@ -1,312 +0,0 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod axes;
#[cfg(feature = "python")]
mod py;
pub mod stats;
pub mod view;
pub mod colors;
pub mod error;
pub mod metadata;
#[cfg(feature = "movie")]
pub mod movie;
pub mod readers;
#[cfg(feature = "tiffwrite")]
pub mod tiffwrite;
// mod cache;
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())),
}
}
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::tiffwrite::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(())
}
}
-3
View File
@@ -1,3 +0,0 @@
pub(crate) fn main() -> Result<(), ndbioimage::error::Error> {
ndbioimage::main::main(None)
}
-381
View File
@@ -1,381 +0,0 @@
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
}
}
}
pub trait Metadata {
fn get_instrument(&self) -> Option<&Instrument>;
fn get_image(&self) -> Option<&Image>;
fn get_pixels(&self) -> Option<&Pixels> {
if let Some(image) = self.get_image() {
Some(&image.pixels)
} else {
None
}
}
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)
}
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
},
)
}
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,
}
}
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
},
)
}
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)
}
}
fn objective_name(&self) -> Option<String> {
Some(self.get_objective()?.model.as_ref()?.clone())
}
fn magnification(&self) -> Option<f64> {
Some(
(self.get_objective()?.nominal_magnification? as f64)
* (self.get_tube_lens()?.nominal_magnification? as f64),
)
}
fn tube_lens_name(&self) -> Option<String> {
self.get_tube_lens()?.model.clone()
}
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()
}
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)
})
}
fn is_zstack(&self) -> Result<bool, Error> {
self.get_pixels()
.map(|p| Ok(p.size_z > 1))
.unwrap_or_else(|| Err(Error::NoImageOrPixels))
}
fn is_time_lapse(&self) -> Result<bool, Error> {
self.get_pixels()
.map(|p| Ok(p.size_t > 1))
.unwrap_or_else(|| Err(Error::NoImageOrPixels))
}
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())
}
}
-322
View File
@@ -1,322 +0,0 @@
use crate::axes::Axis;
use crate::colors::Color;
use crate::error::Error;
use crate::readers::{PixelType, Reader};
use crate::view::View;
use console::Term;
use ffmpeg_sidecar::command::FfmpegCommand;
use ffmpeg_sidecar::download::auto_download;
use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel};
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use itertools::Itertools;
use ndarray::{Array2, Array3, Dimension, IxDyn, s, stack};
use ordered_float::OrderedFloat;
use std::io::Write;
use std::path::Path;
use std::time::Duration;
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 {
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,
})
}
pub fn set_velocity(&mut self, velocity: f64) {
self.velocity = velocity;
}
pub fn set_brightness(&mut self, brightness: Vec<f64>) {
self.brightness = brightness;
}
pub fn set_scale(&mut self, scale: f64) {
self.scale = scale;
}
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(())
}
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()
}
/// a progress bar with an ok style that when py::detach is used also works in jupyter
pub fn get_bar(count: Option<usize>) -> ProgressBar {
let style = ProgressStyle::with_template(
"{spinner:.green} {percent}% [{wide_bar:.green/lime}] {pos:>7}/{len:7} [{elapsed}/{eta}, {per_sec:<5}]",
).expect("template should be working").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);
bar.enable_steady_tick(Duration::from_millis(100));
bar
}
impl<D, R> View<D, R>
where
D: Dimension,
R: Reader,
Self: 'static,
{
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));
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));
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(())
}
}
-2073
View File
File diff suppressed because it is too large Load Diff
-726
View File
@@ -1,726 +0,0 @@
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;
#[cfg(feature = "czi")]
pub mod czi;
#[cfg(feature = "bioformats_rust")]
pub mod bioformats_rust;
#[cfg(feature = "bioformats_java")]
pub mod bioformats_java;
#[cfg(feature = "tiffseq")]
pub mod tiffseq;
#[cfg(feature = "tiff")]
pub mod tiff;
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([CZTSP])\D+(\d+)$").unwrap());
#[derive(Debug, Clone, Copy, Default)]
pub struct Dimensions {
pub c: Option<usize>,
pub z: Option<usize>,
pub t: Option<usize>,
pub s: Option<usize>,
pub p: Option<usize>,
}
impl Dimensions {
pub fn new(series: usize, position: usize) -> Self {
Self {
c: None,
z: None,
t: None,
s: Some(series),
p: Some(position),
}
}
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 types (u)int(8/16/32) or float(32/64), (u/i)(64/128) are not included in bioformats
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub enum PixelType {
I8,
U8,
I16,
U16,
I32,
U32,
F32,
F64,
I64,
U64,
I128,
U128,
F128,
}
impl PixelType {
pub fn bytes_per_pixel(&self) -> usize {
match self {
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,
}
}
}
/// Struct containing frame data in one of eight pixel types. Cast to `Array2<T>` using try_into.
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug)]
pub enum ArrayT<D: Dimension> {
I8(Array<i8, D>),
U8(Array<u8, D>),
I16(Array<i16, D>),
U16(Array<u16, D>),
I32(Array<i32, D>),
U32(Array<u32, D>),
F32(Array<f32, D>),
F64(Array<f64, D>),
I64(Array<i64, D>),
U64(Array<u64, D>),
I128(Array<i128, D>),
U128(Array<u128, D>),
F128(Array<f64, D>), // f128 is nightly
}
pub(crate) type Frame = ArrayT<Ix2>;
pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
where
P: AsRef<Path>;
fn reader_name(&self) -> String {
type_name::<Self>().to_string()
}
// TODO: read from file if present
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 fame at channel c, slize z and time t.
#[allow(clippy::if_same_then_else)]
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error>;
fn path(&self) -> &Path;
fn series(&self) -> usize;
fn position(&self) -> usize;
fn shape(&self) -> &Shape;
fn pixel_type(&self) -> &PixelType;
fn get_available_positions<P>(path: P, series: usize) -> Result<HashSet<usize>, Error>
where
P: AsRef<Path>;
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
where
P: AsRef<Path>;
}
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() {
"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
}
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> {
let mut err = Ok(());
let arr = match self {
ArrayT::I8(v) => v.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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.mapv_into_any(|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),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum DynReader {
#[cfg(feature = "tiff")]
Tiff(tiff::TiffReader),
#[cfg(feature = "tiffseq")]
TiffSeq(tiffseq::TiffSeqReader),
#[cfg(feature = "czi")]
Czi(czi::CziReader),
#[cfg(feature = "bioformats_rust")]
BioFormatsRust(bioformats_rust::BioFormatsRustReader),
#[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) -> String {
let name = 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(),
#[allow(unreachable_patterns)]
_ => unreachable!(),
};
format!("DynReader<{}>", name)
}
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()?,
#[allow(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),
#[allow(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(),
#[allow(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(),
#[allow(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(),
#[allow(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(),
#[allow(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(),
#[allow(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 {
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")]
"tif" => 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)
}
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")]
"tif" => 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)?
})
}
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")]
"tif" => 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)?
})
}
}
-640
View File
@@ -1,640 +0,0 @@
use crate::error::Error;
use ndarray::Array2;
use ome_metadata::Ome;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
pub use crate::readers::{ArrayT, PixelType, Reader};
use crate::readers::{DynReader, Frame, Shape};
use itertools::Itertools;
use j4rs::{Instance, InvocationArg, 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())
.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::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 {
($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::I8, true) => get_frame!(i8, <1),
(PixelType::U8, true) => get_frame!(u8, <1),
(PixelType::I16, true) => get_frame!(i16, <2),
(PixelType::U16, true) => get_frame!(u16, <2),
(PixelType::I32, true) => get_frame!(i32, <4),
(PixelType::U32, true) => get_frame!(u32, <4),
(PixelType::F32, true) => get_frame!(f32, <4),
(PixelType::F64, true) => get_frame!(f64, <8),
(PixelType::I64, true) => get_frame!(i64, <8),
(PixelType::U64, true) => get_frame!(u64, <8),
(PixelType::I128, true) => get_frame!(i128, <16),
(PixelType::U128, true) => get_frame!(u128, <16),
(PixelType::F128, true) => get_frame!(f64, <8),
(PixelType::I8, false) => get_frame!(i8, >1),
(PixelType::U8, false) => get_frame!(u8, >1),
(PixelType::I16, false) => get_frame!(i16, >2),
(PixelType::U16, false) => get_frame!(u16, >2),
(PixelType::I32, false) => get_frame!(i32, >4),
(PixelType::U32, false) => get_frame!(u32, >4),
(PixelType::F32, false) => get_frame!(f32, >4),
(PixelType::F64, false) => get_frame!(f64, >8),
(PixelType::I64, false) => get_frame!(i64, >8),
(PixelType::U64, false) => get_frame!(u64, >8),
(PixelType::I128, false) => get_frame!(i128, >16),
(PixelType::U128, false) => get_frame!(u128, >16),
(PixelType::F128, false) => get_frame!(f64, >8),
}
}
}
impl Drop for BioFormatsJavaReader {
fn drop(&mut self) {
if let Ok(reader) = self.get_reader() {
reader.close().unwrap();
}
}
}
fn find_tiff(path: PathBuf) -> 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));
} 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()) {
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 = BioFormatsJavaReader {
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(())
}
}
-325
View File
@@ -1,325 +0,0 @@
use crate::error::Error;
use crate::readers::ArrayT;
use crate::readers::{DynReader, Frame, PixelType, Reader, Shape};
use bioformats::{DimensionOrder, ImageReader};
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
}
}
fn map_pixel_type(bf: bioformats::PixelType) -> Result<PixelType, Error> {
use bioformats::PixelType as Bf;
Ok(match bf {
Bf::Int8 => PixelType::I8,
Bf::Uint8 => PixelType::U8,
Bf::Int16 => PixelType::I16,
Bf::Uint16 => PixelType::U16,
Bf::Int32 => PixelType::I32,
Bf::Uint32 => PixelType::U32,
Bf::Float32 => PixelType::F32,
Bf::Float64 => PixelType::F64,
Bf::Bit => PixelType::U8,
})
}
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::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 {
($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::I8, true) => get_frame!(i8, <1),
(PixelType::U8, true) => get_frame!(u8, <1),
(PixelType::I16, true) => get_frame!(i16, <2),
(PixelType::U16, true) => get_frame!(u16, <2),
(PixelType::I32, true) => get_frame!(i32, <4),
(PixelType::U32, true) => get_frame!(u32, <4),
(PixelType::F32, true) => get_frame!(f32, <4),
(PixelType::F64, true) => get_frame!(f64, <8),
(PixelType::I64, true) => get_frame!(i64, <8),
(PixelType::U64, true) => get_frame!(u64, <8),
(PixelType::I128, true) => get_frame!(i128, <16),
(PixelType::U128, true) => get_frame!(u128, <16),
(PixelType::F128, true) => get_frame!(f64, <8),
(PixelType::I8, false) => get_frame!(i8, >1),
(PixelType::U8, false) => get_frame!(u8, >1),
(PixelType::I16, false) => get_frame!(i16, >2),
(PixelType::U16, false) => get_frame!(u16, >2),
(PixelType::I32, false) => get_frame!(i32, >4),
(PixelType::U32, false) => get_frame!(u32, >4),
(PixelType::F32, false) => get_frame!(f32, >4),
(PixelType::F64, false) => get_frame!(f64, >8),
(PixelType::I64, false) => get_frame!(i64, >8),
(PixelType::U64, false) => get_frame!(u64, >8),
(PixelType::I128, false) => get_frame!(i128, >16),
(PixelType::U128, false) => get_frame!(u128, >16),
(PixelType::F128, false) => get_frame!(f64, >8),
}
}
}
impl Reader for BioFormatsRustReader {
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,
shape: Shape::default(),
pixel_type: PixelType::U16,
little_endian: true,
};
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 = map_pixel_type(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 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"
}
}
-1250
View File
File diff suppressed because it is too large Load Diff
-483
View File
@@ -1,483 +0,0 @@
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::{Decoder, DecodingResult};
use tiff::tags::Tag;
#[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,
interval_t: f64,
pixel_size: Option<f64>,
}
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,
interval_t: self.interval_t,
pixel_size: self.pixel_size,
}
}
}
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,
interval_t: 0.0,
pixel_size: None,
};
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 {
tiff::decoder::ifd::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 interval_t = metadata_map
.get("interval")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
let ru = reader
.get_tag(Tag::ResolutionUnit)
.ok()
.and_then(|i| i.into_u16().ok())
.unwrap_or(2);
let pixel_size = reader
.get_tag(Tag::XResolution)
.ok()
.and_then(|i| i.into_f64().ok())
.and_then(|i| {
if i <= 0.0 {
None
} else if ru == 3 {
Some(1e4 / i)
} else if ru == 2 {
Some(25400.0 / i)
} else {
Some(1.0 / i)
}
});
let mult = if p_ndim == 3 { n_samples } else { 1 };
let expected = size_c * size_z * size_t / mult;
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.interval_t = interval_t;
new.pixel_size = pixel_size;
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 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: self.pixel_size.map(|v| v as f32),
physical_size_x_unit: ome::UnitsLength::um,
physical_size_y: self.pixel_size.map(|v| v as f32),
physical_size_y_unit: ome::UnitsLength::um,
physical_size_z: None,
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 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: if self.interval_t > 0.0 {
Some(t as f32 * self.interval_t 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 {
(z * self.shape.t + t, 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![]);
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",
}
}
-635
View File
@@ -1,635 +0,0 @@
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;
#[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, serde_yaml::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, serde_yaml::Value>, Error> {
let md_path = dir.join("metadata.txt");
let text = std::fs::read_to_string(&md_path)?;
let parsed: serde_yaml::Value = serde_yaml::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(serde_yaml::Value::String(key.to_string()))
.or_else(|| {
info.get(serde_yaml::Value::String("Summary".to_string()))
.and_then(|s| s.as_mapping())
.and_then(|s| s.get(serde_yaml::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(serde_yaml::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(serde_yaml::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(serde_yaml::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",
}
}
-205
View File
@@ -1,205 +0,0 @@
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 {
type Output;
fn max(self, axis: usize) -> Result<Self::Output, Error>;
fn min(self, axis: usize) -> Result<Self::Output, Error>;
fn sum(self, axis: usize) -> Result<Self::Output, Error>;
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
);
-325
View File
@@ -1,325 +0,0 @@
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::view::{Number, View};
use console::Term;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
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 std::time::Duration;
use tiffwrite::{Bytes, Colors, Compression, IJTiffFile};
#[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,
}
}
}
/// 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} {percent}% [{wide_bar:.green/lime}] {pos:>7}/{len:7} [{elapsed}/{eta}, {per_sec:<5}]",
).expect("template should be working").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
}
impl TiffOptions {
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
}
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(())
}
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::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(())
}
}
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;
#[test]
fn tiff() -> Result<(), Error> {
#[cfg(any(feature = "czi", feature = "bioformats_java"))]
let file = "czi/1xp53-01-AP1.czi";
#[cfg(feature = "tiff")]
let file = "tiff/20251014_20-Pos_000_000_loc_results_Cy3.tif";
#[cfg(feature = "tiffseq")]
let file = "tiffseq/20-Pos_005_005";
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(())
}
}
-1391
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -1,27 +1,27 @@
import pickle
from multiprocessing import active_children
from pathlib import Path
import pytest
from ndbioimage import Imread
from ndbioimage import Imread, ReaderNotFoundError
@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"]
],
)
@pytest.mark.parametrize("file", (Path(__file__).parent / "files").iterdir())
def test_open(file):
with Imread(file, axes="cztyx") as im:
mean = im[0, 0, 0].mean()
b = pickle.dumps(im)
jm = pickle.loads(b)
assert jm.get_frame(0, 0, 0).mean() == mean
b = pickle.dumps(im)
jm = pickle.loads(b)
assert jm[0, 0, 0].mean() == mean
try:
with Imread(file) as im:
mean = im[dict(c=0, z=0, t=0)].mean()
b = pickle.dumps(im)
jm = pickle.loads(b)
assert jm[dict(c=0, z=0, t=0)].mean() == mean
v = im.view()
assert v[dict(c=0, z=0, t=0)].mean() == mean
b = pickle.dumps(v)
w = pickle.loads(b)
assert w[dict(c=0, z=0, t=0)].mean() == mean
except ReaderNotFoundError:
assert len(Imread.__subclasses__()), "No subclasses for Imread found."
for child in active_children():
child.kill()
+7 -23
View File
@@ -1,41 +1,25 @@
import tempfile
from itertools import combinations_with_replacement
from numbers import Number
from pathlib import Path
import numpy as np
import pytest
from tiffwrite import tiffwrite
from ndbioimage import Imread
@pytest.fixture
def array():
return np.random.randint(0, 255, (64, 64, 2, 3, 4), "uint8")
@pytest.fixture()
def image(array):
with tempfile.TemporaryDirectory() as folder:
file = Path(folder) / "test.tif"
tiffwrite(file, array, "yxczt")
with Imread(file, axes="yxczt") as im:
yield im
r = np.random.randint(0, 255, (64, 64, 2, 3, 4))
im = Imread(r)
a = np.array(im)
@pytest.mark.parametrize(
"s",
combinations_with_replacement(
(0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5
),
"s", combinations_with_replacement((0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5)
)
def test_slicing(s, image, array):
s_im, s_a = image[s], array[s]
def test_slicing(s):
s_im, s_a = im[s], a[s]
if isinstance(s_a, Number):
assert isinstance(s_im, Number)
assert s_im == s_a
else:
assert isinstance(s_im, Imread)
assert tuple(s_im.shape) == s_a.shape
assert s_im.shape == s_a.shape
assert np.all(s_im == s_a)
+5 -18
View File
@@ -1,26 +1,13 @@
import tempfile
from itertools import product
from pathlib import Path
import numpy as np
import pytest
from tiffwrite import tiffwrite
from ndbioimage import Imread
@pytest.fixture
def array():
return np.random.randint(0, 255, (64, 64, 2, 3, 4), "uint16")
@pytest.fixture()
def image(array):
with tempfile.TemporaryDirectory() as folder:
file = Path(folder) / "tiff" / "test.tif"
tiffwrite(file, array, "yxczt")
with Imread(file, axes="yxczt") as im:
yield im
r = np.random.randint(0, 255, (64, 64, 2, 3, 4))
im = Imread(r)
a = np.array(im)
@pytest.mark.parametrize(
@@ -45,8 +32,8 @@ def image(array):
(None, 0, 1, 2, 3, 4),
),
)
def test_ufuncs(fun_and_axis, image, array):
def test_ufuncs(fun_and_axis):
fun, axis = fun_and_axis
assert np.all(np.isclose(np.asarray(fun(image, axis)), fun(array, axis))), (
assert np.all(np.isclose(fun(im, axis), fun(a, axis))), (
f"function {fun.__name__} over axis {axis} does not give the correct result"
)