- implement shape in Rust
- implement more readers - fix downloading of bioformats jar - (mostly) compatible with python version
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
name: Publish
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
crates_io_publish:
|
||||
name: Publish (crates.io)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
key: cache-ubuntu-cargo-publish
|
||||
|
||||
- name: Install Rust
|
||||
run: |-
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
if ! command -v rustc >/dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
else
|
||||
rustup update
|
||||
fi
|
||||
cargo install cargo-release
|
||||
shell: bash
|
||||
|
||||
# allow-branch HEAD is because GitHub actions switches
|
||||
# to the tag while building, which is a detached head
|
||||
|
||||
# Publishing is currently messy, because:
|
||||
#
|
||||
# * `peace_rt_model_core` exports `NativeError` or `WebError` depending on the target.
|
||||
# * `peace_rt_model_web` fails to build when publishing the workspace for a native target.
|
||||
# * `peace_rt_model_web` still needs its dependencies to be published before it can be
|
||||
# published.
|
||||
# * `peace_rt_model_hack` needs `peace_rt_model_web` to be published before it can be
|
||||
# published.
|
||||
#
|
||||
# We *could* pass through `--no-verify` so `cargo` doesn't build the crate before publishing,
|
||||
# which is reasonable, since this job only runs after the Linux, Windows, and WASM builds
|
||||
# have passed.
|
||||
- name: "cargo release publish"
|
||||
run: |-
|
||||
export PATH="$HOME/.osxcross/bin:$PATH"
|
||||
cargo login ${{ secrets.CRATES_IO_API_TOKEN }}
|
||||
cargo release \
|
||||
publish \
|
||||
--workspace \
|
||||
--all-features \
|
||||
--allow-branch "main" \
|
||||
--no-confirm \
|
||||
--no-verify \
|
||||
--execute
|
||||
|
||||
- name: Store cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
key: cache-ubuntu-cargo-publish
|
||||
@@ -0,0 +1,114 @@
|
||||
name: CI
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pypi_publish:
|
||||
name: Publish (pypi.org)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.cache/pip-wheel
|
||||
~/.cache/sccache
|
||||
~/.cache/cargo-xwin
|
||||
~/.cargo
|
||||
~/.osxcross
|
||||
key: cache-ubuntu-maturin-cross-compile
|
||||
|
||||
- name: Install llvm
|
||||
run: |
|
||||
if ! command -v llvm-dlltool >/dev/null 2>&1; then
|
||||
sudo apt update
|
||||
sudo apt install -y llvm
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
if ! command -v rustc >/dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
else
|
||||
rustup update
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install sccache and maturin
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
python -m pip install --upgrade pip
|
||||
pip install maturin ziglang
|
||||
if ! command -v sccache >/dev/null 2>&1; then
|
||||
cargo install sccache || pip install sccache
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install xwin
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
if ! command -v cargo-xwin >/dev/null 2>&1; then
|
||||
cargo install cargo-xwin || pip install cargo-xwin
|
||||
cargo xwin cache xwin
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install osxcross
|
||||
run: |
|
||||
export PATH="$HOME/.osxcross/bin:$PATH"
|
||||
if ! command -v osxcross >/dev/null 2>&1; then
|
||||
wget ${{ secrets.OSXCROSS_LINK }} -O osxcross.tar.gz
|
||||
tar -xzf osxcross.tar.gz -C ~/
|
||||
mv ~/osxcross ~/.osxcross
|
||||
fi
|
||||
|
||||
- name: Build wheels
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$HOME/.osxcross/bin:$PATH"
|
||||
maturin sdist --out dist
|
||||
rustup default nightly
|
||||
|
||||
rustup target add x86_64-unknown-linux-gnu --toolchain nightly
|
||||
maturin build --release --out dist --target x86_64-unknown-linux-gnu
|
||||
rustup target add aarch64-unknown-linux-gnu --toolchain nightly
|
||||
maturin build --release --out dist --target aarch64-unknown-linux-gnu --zig
|
||||
|
||||
rustup target add x86_64-pc-windows-msvc --toolchain nightly
|
||||
maturin build --release --out dist --target x86_64-pc-windows-msvc
|
||||
rustup target add aarch64-pc-windows-msvc --toolchain nightly
|
||||
maturin build --release --out dist --target aarch64-pc-windows-msvc
|
||||
|
||||
rustup target add x86_64-apple-darwin --toolchain nightly
|
||||
maturin build --release --out dist --target x86_64-apple-darwin --zig
|
||||
rustup target add aarch64-apple-darwin --toolchain nightly
|
||||
maturin build --release --out dist --target aarch64-apple-darwin --zig
|
||||
|
||||
- name: Store cache
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
~/.cache/pip-wheel
|
||||
~/.cache/sccache
|
||||
~/.cache/cargo-xwin
|
||||
~/.cargo
|
||||
~/.osxcross
|
||||
key: cache-ubuntu-maturin-cross-compile
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
env:
|
||||
GITHUB_WORKFLOW_REF: 1.10.1
|
||||
@@ -73,3 +73,5 @@ docs/_build/
|
||||
.python-version
|
||||
|
||||
/tests/files/*
|
||||
AGENTS.md
|
||||
.agentbridge
|
||||
+47
-33
@@ -1,20 +1,19 @@
|
||||
[package]
|
||||
name = "ndbioimage"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.87.0"
|
||||
authors = ["Wim Pomp <w.pomp@nki.nl>"]
|
||||
license = "MIT"
|
||||
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.wimpomp.nl/wim/ndbioimage/src/branch/rs"
|
||||
repository = "https://git.wimpomp.nl/wim/ndbioimage/src/branch/rs"
|
||||
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"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
name = "ndbioimage"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
@@ -22,50 +21,65 @@ 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.14"
|
||||
itertools = "0.15"
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
indicatif = { version = "0.18", features = ["rayon"], optional = true }
|
||||
j4rs = "0.24"
|
||||
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.28", optional = true }
|
||||
ordered-float = "5"
|
||||
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 }
|
||||
serde = { version = "1", features = ["rc"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
regex = "1"
|
||||
serde = { version = "1", features = ["rc", "derive"] }
|
||||
serde_yaml = { version = "0.9", optional = true }
|
||||
serde_with = "3"
|
||||
tiffwrite = { version = ">=2026.1.1, <2026.2.0", optional = true}
|
||||
thread_local = "1"
|
||||
ome-metadata = "0.4"
|
||||
lazy_static = "1"
|
||||
strum = { version = "0.28", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
|
||||
[dependencies.pyo3]
|
||||
version = "0.28"
|
||||
features = ["extension-module", "abi3-py310", "eyre", "generate-import-lib"]
|
||||
optional = true
|
||||
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]
|
||||
downloader = "0.2"
|
||||
rayon = "1"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.13", features = ["blocking"] }
|
||||
|
||||
[build-dependencies]
|
||||
j4rs = "0.24"
|
||||
j4rs = "0.25"
|
||||
ffmpeg-sidecar = "2"
|
||||
retry = "2"
|
||||
toml = "1"
|
||||
|
||||
[features]
|
||||
# Enables formats for which code in bioformats with a GPL license is needed
|
||||
default = []
|
||||
all = ["bioformats_java", "bioformats_rust", "czi", "gpl-formats", "movie", "tiffseq", "tiffwrite", "tiff"]
|
||||
gpl-formats = []
|
||||
# Enables python ffi using pyO3
|
||||
python = ["dep:pyo3", "dep:numpy", "dep:serde_json", "dep:color-eyre"]
|
||||
# Enables writing as tiff
|
||||
tiff = ["dep:tiffwrite", "dep:indicatif", "dep:rayon"]
|
||||
# Enables writing as mp4 using ffmpeg
|
||||
movie = ["dep:ffmpeg-sidecar"]
|
||||
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 = ["gpl-formats", "tiff", "movie"]
|
||||
features = ["bioformats", "gpl-formats", "czi", "tiff", "movie"]
|
||||
|
||||
[profile.test]
|
||||
inherits = "release"
|
||||
|
||||
[profile.dev.package.backtrace]
|
||||
opt-level = 3
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2025 Wim Pomp
|
||||
|
||||
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.
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2015 - 2021 Ulrik Sverdrup "bluss",
|
||||
Jim Turner,
|
||||
and ndarray developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,5 +1,5 @@
|
||||
# ndbioimage
|
||||
[](https://github.com/wimpomp/ndbioimage/actions/workflows/pytest.yml)
|
||||
[](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml)
|
||||
|
||||
## Work in progress
|
||||
Rust rewrite of python version. Read bio image formats using the bio-formats java package.
|
||||
|
||||
@@ -1,103 +1,111 @@
|
||||
#[cfg(not(feature = "python"))]
|
||||
use j4rs::{JvmBuilder, MavenArtifact, MavenArtifactRepo, MavenSettings, errors::J4RsError};
|
||||
#[cfg(not(feature = "python"))]
|
||||
use retry::{delay, delay::Exponential, retry};
|
||||
use std::error::Error;
|
||||
#[cfg(not(feature = "python"))]
|
||||
use std::fmt::Display;
|
||||
#[cfg(not(feature = "python"))]
|
||||
use std::fmt::Formatter;
|
||||
#[cfg(not(feature = "python"))]
|
||||
use std::path::PathBuf;
|
||||
#[cfg(not(feature = "python"))]
|
||||
use std::{env, fs};
|
||||
#[cfg(feature = "bioformats_java")]
|
||||
const BIOFORMATS_VERSION: &str = "8.5.0";
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
use j4rs::Jvm;
|
||||
#[cfg(feature = "bioformats_java")]
|
||||
mod bioformats {
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[cfg(feature = "movie")]
|
||||
use ffmpeg_sidecar::download::auto_download;
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum BuildError {
|
||||
J4rsVersionNotFound,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "python"))]
|
||||
#[derive(Clone, Debug)]
|
||||
enum BuildError {
|
||||
BioFormatsNotDownloaded,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "python"))]
|
||||
impl Display for BuildError {
|
||||
impl Display for BuildError {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||
write!(fmt, "Bioformats package not downloaded")
|
||||
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(not(feature = "python"))]
|
||||
impl Error for BuildError {}
|
||||
#[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};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
println!("cargo::rerun-if-changed=build.rs");
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum BuildError {
|
||||
BioFormatsNotDownloaded,
|
||||
}
|
||||
|
||||
if std::env::var("DOCS_RS").is_err() {
|
||||
#[cfg(feature = "movie")]
|
||||
auto_download()?;
|
||||
impl Display for BuildError {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> {
|
||||
write!(fmt, "Bioformats package not downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "python"))]
|
||||
{
|
||||
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.3.0.jar").exists() {
|
||||
if !path.join("bioformats_package-8.5.0.jar").exists() {
|
||||
Err(BuildError::BioFormatsNotDownloaded)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
{
|
||||
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)?;
|
||||
}
|
||||
|
||||
Jvm::copy_j4rs_libs_under(py_src_path.to_str().unwrap())?;
|
||||
|
||||
// rename else maturin will ignore them
|
||||
for file in std::fs::read_dir(&py_deps_path)? {
|
||||
let f = file?.path().to_str().unwrap().to_string();
|
||||
if !f.ends_with("_") {
|
||||
std::fs::rename(&f, std::format!("{f}_"))?;
|
||||
}
|
||||
}
|
||||
|
||||
// remove so we don't include too much accidentally
|
||||
for file in std::fs::read_dir(&py_jassets_path)? {
|
||||
let f = file?.path();
|
||||
if !f.file_name().unwrap().to_str().unwrap().starts_with("j4rs") {
|
||||
std::fs::remove_file(&f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "python"))]
|
||||
fn default_jassets_path() -> Result<PathBuf, J4RsError> {
|
||||
let is_build_script = env::var("OUT_DIR").is_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 {
|
||||
PathBuf::from(env::var("OUT_DIR")?)
|
||||
std::path::PathBuf::from(std::env::var("OUT_DIR")?)
|
||||
} else {
|
||||
env::current_exe()?
|
||||
std::env::current_exe()?
|
||||
};
|
||||
start_path = fs::canonicalize(start_path)?;
|
||||
start_path = std::fs::canonicalize(start_path)?;
|
||||
|
||||
while start_path.pop() {
|
||||
for entry in std::fs::read_dir(&start_path)? {
|
||||
@@ -109,12 +117,11 @@ fn default_jassets_path() -> Result<PathBuf, J4RsError> {
|
||||
}
|
||||
|
||||
Err(J4RsError::GeneralError(
|
||||
"Can not find jassets directory".to_owned(),
|
||||
"Can not find jassets directory".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "python"))]
|
||||
fn deploy_java_artifacts() -> Result<(), J4RsError> {
|
||||
fn deploy_java_artifacts() -> Result<(), J4RsError> {
|
||||
let jvm = JvmBuilder::new()
|
||||
.skip_setting_native_lib()
|
||||
.with_maven_settings(MavenSettings::new(vec![MavenArtifactRepo::from(
|
||||
@@ -122,10 +129,49 @@ fn deploy_java_artifacts() -> Result<(), J4RsError> {
|
||||
)]))
|
||||
.build()?;
|
||||
|
||||
jvm.deploy_artifact(&MavenArtifact::from("ome:bioformats_package:8.3.0"))?;
|
||||
jvm.deploy_artifact(&MavenArtifact::from("ome:bioformats_package:8.5.0"))?;
|
||||
|
||||
#[cfg(feature = "gpl-formats")]
|
||||
jvm.deploy_artifact(&MavenArtifact::from("ome:formats-gpl:8.3.0"))?;
|
||||
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(())
|
||||
}
|
||||
|
||||
+36
-578
@@ -1,26 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from abc import ABC
|
||||
from argparse import ArgumentParser
|
||||
from collections import OrderedDict
|
||||
from functools import cached_property, wraps
|
||||
from importlib.metadata import version
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional, Sequence, TypeVar
|
||||
from typing import TypeVar
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike, DTypeLike
|
||||
from tiffwrite import FrameInfo, IJTiffParallel
|
||||
from tqdm.auto import tqdm
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
os.environ["RUST_BACKTRACE"] = "full"
|
||||
os.environ["COLORBT_SHOW_HIDDEN"] = "1"
|
||||
|
||||
from ome_metadata import Ome
|
||||
from ome_metadata.ome_metadata_rs import Length # noqa
|
||||
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
|
||||
@@ -38,587 +40,43 @@ warnings.filterwarnings("ignore", "Reference to unknown ID")
|
||||
Number = int | float | np.integer | np.floating
|
||||
|
||||
|
||||
for dep_file in (Path(__file__).parent / "deps").glob("*_"):
|
||||
dep_file.rename(str(dep_file)[:-1])
|
||||
|
||||
if not list((Path(__file__).parent / "jassets").glob("bioformats*.jar")):
|
||||
rs.download_bioformats(True)
|
||||
# backwards compatibility
|
||||
class JVMException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# backwards compatibility
|
||||
class ReaderNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TransformTiff(IJTiffParallel):
|
||||
"""transform frames in a parallel process to speed up saving"""
|
||||
|
||||
def __init__(self, image: Imread, *args: Any, **kwargs: Any) -> None:
|
||||
self.image = image
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def parallel(self, frame: tuple[int, int, int]) -> tuple[FrameInfo]:
|
||||
return ((np.asarray(self.image(*frame)), 0, 0, 0),)
|
||||
|
||||
|
||||
class DequeDict(OrderedDict):
|
||||
def __init__(self, maxlen: int = None, *args: Any, **kwargs: Any) -> None:
|
||||
self.maxlen = maxlen
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __setitem__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__setitem__(*args, **kwargs)
|
||||
self.truncate()
|
||||
|
||||
def truncate(self) -> None:
|
||||
if self.maxlen is not None:
|
||||
while len(self) > self.maxlen:
|
||||
self.popitem(False)
|
||||
|
||||
def update(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().update(*args, **kwargs) # type: ignore
|
||||
self.truncate()
|
||||
|
||||
|
||||
def find(obj: Sequence[Any], **kwargs: Any) -> Any:
|
||||
for item in obj:
|
||||
try:
|
||||
if all([getattr(item, key) == value for key, value in kwargs.items()]):
|
||||
return item
|
||||
except AttributeError:
|
||||
pass
|
||||
return None
|
||||
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 try_default(fun: Callable[..., R], default: Any, *args: Any, **kwargs: Any) -> R:
|
||||
try:
|
||||
return fun(*args, **kwargs)
|
||||
except Exception: # noqa
|
||||
return default
|
||||
def get_positions(path: str | Path) -> set[int]:
|
||||
return Imread.get_available_series(path)
|
||||
|
||||
|
||||
class Shape(tuple):
|
||||
def __new__(cls, shape: Sequence[int] | Shape, axes: str = "yxczt") -> Shape:
|
||||
if isinstance(shape, Shape):
|
||||
axes = shape.axes # type: ignore
|
||||
new = super().__new__(cls, shape)
|
||||
new.axes = axes.lower()
|
||||
return new # type: ignore
|
||||
|
||||
def __getitem__(self, n: int | str) -> int | tuple[int]:
|
||||
if isinstance(n, str):
|
||||
if len(n) == 1:
|
||||
return self[self.axes.find(n.lower())] if n.lower() in self.axes else 1
|
||||
else:
|
||||
return tuple(self[i] for i in n) # type: ignore
|
||||
return super().__getitem__(n)
|
||||
|
||||
@cached_property
|
||||
def yxczt(self) -> tuple[int, int, int, int, int]:
|
||||
return tuple(self[i] for i in "yxczt") # type: ignore
|
||||
|
||||
|
||||
class OmeCache(DequeDict):
|
||||
"""prevent (potentially expensive) rereading of ome data by caching"""
|
||||
|
||||
instance = None
|
||||
|
||||
def __new__(cls) -> OmeCache:
|
||||
if cls.instance is None:
|
||||
cls.instance = super().__new__(cls)
|
||||
return cls.instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(64)
|
||||
|
||||
def __reduce__(self) -> tuple[type, tuple]:
|
||||
return self.__class__, ()
|
||||
|
||||
def __getitem__(self, path: Path | str | tuple) -> Ome:
|
||||
if isinstance(path, tuple):
|
||||
return super().__getitem__(path)
|
||||
else:
|
||||
return super().__getitem__(self.path_and_lstat(path))
|
||||
|
||||
def __setitem__(self, path: Path | str | tuple, value: Ome) -> None:
|
||||
if isinstance(path, tuple):
|
||||
super().__setitem__(path, value)
|
||||
else:
|
||||
super().__setitem__(self.path_and_lstat(path), value)
|
||||
|
||||
def __contains__(self, path: Path | str | tuple) -> bool:
|
||||
if isinstance(path, tuple):
|
||||
return super().__contains__(path)
|
||||
else:
|
||||
return super().__contains__(self.path_and_lstat(path))
|
||||
|
||||
@staticmethod
|
||||
def path_and_lstat(
|
||||
path: str | Path,
|
||||
) -> tuple[Path, Optional[os.stat_result], Optional[os.stat_result]]:
|
||||
path = Path(path)
|
||||
return (
|
||||
path,
|
||||
(path.lstat() if path.exists() else None),
|
||||
(
|
||||
path.with_suffix(".ome.xml").lstat()
|
||||
if path.with_suffix(".ome.xml").exists()
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_positions(path: str | Path) -> Optional[list[int]]: # noqa
|
||||
# TODO
|
||||
return None
|
||||
|
||||
|
||||
class Imread(rs.View, np.lib.mixins.NDArrayOperatorsMixin, ABC):
|
||||
"""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 %
|
||||
|
||||
See __init__ and other functions for more ideas.
|
||||
|
||||
# TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var, squeeze
|
||||
"""
|
||||
|
||||
def __getitem__(self, item):
|
||||
new = super().__getitem__(item)
|
||||
return Imread(new) if isinstance(new, rs.View) else new
|
||||
|
||||
def __copy__(self):
|
||||
Imread(super().__copy__())
|
||||
|
||||
def copy(self):
|
||||
Imread(super().copy())
|
||||
|
||||
def astype(self):
|
||||
Imread(super().astype())
|
||||
|
||||
def squeeze(self):
|
||||
new = super().squeeze()
|
||||
return Imread(new) if isinstance(new, rs.View) else new
|
||||
|
||||
def min(self, *args, **kwargs) -> Imread | float:
|
||||
new = super().min(*args, **kwargs)
|
||||
return Imread(new) if isinstance(new, rs.View) else new
|
||||
|
||||
def max(self, *args, **kwargs) -> Imread | float:
|
||||
new = super().max(*args, **kwargs)
|
||||
return Imread(new) if isinstance(new, rs.View) else new
|
||||
|
||||
def mean(self, *args, **kwargs) -> Imread | float:
|
||||
new = super().mean(*args, **kwargs)
|
||||
return Imread(new) if isinstance(new, rs.View) else new
|
||||
|
||||
def sum(self, *args, **kwargs) -> Imread | float:
|
||||
new = super().sum(*args, **kwargs)
|
||||
return Imread(new) if isinstance(new, rs.View) else new
|
||||
|
||||
def transpose(self, *args, **kwargs) -> Imread | float:
|
||||
new = super().transpose(*args, **kwargs)
|
||||
return Imread(new) if isinstance(new, rs.View) else new
|
||||
|
||||
def swap_axes(self, *args, **kwargs) -> Imread | float:
|
||||
new = super().swap_axes(*args, **kwargs)
|
||||
return Imread(new) if isinstance(new, rs.View) else new
|
||||
|
||||
@property
|
||||
def T(self) -> Imread | float:
|
||||
return Imread(super().T)
|
||||
|
||||
@staticmethod
|
||||
def get_positions(path: str | Path) -> Optional[list[int]]: # noqa
|
||||
# TODO
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def as_axis(axis):
|
||||
if axis is None:
|
||||
return None
|
||||
elif isinstance(axis, int):
|
||||
return axis
|
||||
else:
|
||||
return str(axis)
|
||||
|
||||
@wraps(np.moveaxis)
|
||||
def moveaxis(self, source, destination):
|
||||
raise NotImplementedError("moveaxis is not implemented")
|
||||
|
||||
@wraps(np.ndarray.flatten)
|
||||
def flatten(self, *args, **kwargs) -> np.ndarray:
|
||||
return np.asarray(self).flatten(*args, **kwargs)
|
||||
|
||||
@wraps(np.ndarray.reshape)
|
||||
def reshape(self, *args, **kwargs) -> np.ndarray:
|
||||
return np.asarray(self).reshape(*args, **kwargs) # noqa
|
||||
|
||||
def as_array(self) -> np.ndarray:
|
||||
return self.__array__()
|
||||
|
||||
@wraps(np.ndarray.astype)
|
||||
def astype(self, dtype: DTypeLike, *_, **__) -> Imread:
|
||||
return Imread(super().astype(str(np.dtype(dtype))))
|
||||
|
||||
@staticmethod
|
||||
def fix_ome(ome: Ome) -> Ome:
|
||||
# fix ome if necessary
|
||||
for image in ome.image:
|
||||
try:
|
||||
if (
|
||||
image.pixels.physical_size_z is None
|
||||
and len(set([plane.the_z for plane in image.pixels.planes])) > 1
|
||||
):
|
||||
z = np.array(
|
||||
[
|
||||
(
|
||||
plane.position_z_unit.convert("um", plane.position_z),
|
||||
plane.the_z,
|
||||
)
|
||||
for plane in image.pixels.planes
|
||||
if plane.the_c == 0 and plane.the_t == 0
|
||||
]
|
||||
)
|
||||
i = np.argsort(z[:, 1])
|
||||
image.pixels.physical_size_z = (
|
||||
np.nanmean(np.true_divide(*np.diff(z[i], axis=0).T)) * 1e6
|
||||
)
|
||||
image.pixels.physical_size_z_unit = Length("um") # type: ignore
|
||||
except Exception: # noqa
|
||||
pass
|
||||
return ome
|
||||
|
||||
@staticmethod
|
||||
def read_ome(path: str | Path) -> Optional[Ome]:
|
||||
path = Path(path) # type: ignore
|
||||
if path.with_suffix(".ome.xml").exists():
|
||||
return Ome.from_xml(path.with_suffix(".ome.xml").read_text())
|
||||
return None
|
||||
|
||||
def get_ome(self) -> Ome:
|
||||
"""OME metadata structure"""
|
||||
return Ome.from_xml(self.get_ome_xml())
|
||||
|
||||
@cached_property
|
||||
def ome(self) -> Ome:
|
||||
cache = OmeCache()
|
||||
if self.path not in cache:
|
||||
ome = self.read_ome(self.path)
|
||||
if ome is None:
|
||||
ome = self.get_ome()
|
||||
cache[self.path] = self.fix_ome(ome)
|
||||
return cache[self.path]
|
||||
|
||||
def is_noise(self, volume: ArrayLike = None) -> bool:
|
||||
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))
|
||||
return 1 - corr[tuple([0] * corr.ndim)] < 0.0067
|
||||
|
||||
@staticmethod
|
||||
def kill_vm() -> None:
|
||||
pass
|
||||
|
||||
def save_as_movie(
|
||||
self,
|
||||
fname: Path | str = None,
|
||||
c: int | Sequence[int] = None, # noqa
|
||||
z: int | Sequence[int] = None, # noqa
|
||||
t: str | int | Sequence[int] = None, # noqa
|
||||
colors: tuple[str] = None,
|
||||
brightnesses: tuple[float] = None,
|
||||
scale: int = None,
|
||||
bar: bool = True,
|
||||
) -> None:
|
||||
"""saves the image as a mp4 or mkv file"""
|
||||
from matplotlib.colors import to_rgb
|
||||
from skvideo.io import FFmpegWriter
|
||||
|
||||
if t is None:
|
||||
t = np.arange(self.shape["t"])
|
||||
elif isinstance(t, str):
|
||||
t = eval(f"np.arange(self.shape['t'])[{t}]")
|
||||
elif np.isscalar(t):
|
||||
t = (t,)
|
||||
|
||||
def get_ab(
|
||||
tyx: Imread, p: tuple[float, float] = (1, 99)
|
||||
) -> tuple[float, float]:
|
||||
s = tyx.flatten()
|
||||
s = s[s > 0]
|
||||
a, b = np.percentile(s, p)
|
||||
if a == b:
|
||||
a, b = np.min(s), np.max(s)
|
||||
if a == b:
|
||||
a, b = 0, 1
|
||||
return a, b
|
||||
|
||||
def cframe(
|
||||
frame: ArrayLike,
|
||||
color: str,
|
||||
a: float,
|
||||
b: float,
|
||||
scale: float = 1, # noqa
|
||||
) -> np.ndarray:
|
||||
color = to_rgb(color)
|
||||
frame = (frame - a) / (b - a)
|
||||
frame = np.dstack([255 * frame * i for i in color])
|
||||
return np.clip(np.round(frame), 0, 255).astype("uint8")
|
||||
|
||||
ab = list(zip(*[get_ab(i) for i in self.transpose("cztyx")])) # type: ignore
|
||||
colors = colors or ("r", "g", "b")[: self.shape["c"]] + max(
|
||||
0, self.shape["c"] - 3
|
||||
) * ("w",)
|
||||
brightnesses = brightnesses or (1,) * self.shape["c"]
|
||||
scale = scale or 1
|
||||
shape_x = 2 * ((self.shape["x"] * scale + 1) // 2)
|
||||
shape_y = 2 * ((self.shape["y"] * scale + 1) // 2)
|
||||
|
||||
with FFmpegWriter(
|
||||
str(fname).format(name=self.path.stem, path=str(self.path.parent)),
|
||||
outputdict={
|
||||
"-vcodec": "libx264",
|
||||
"-preset": "veryslow",
|
||||
"-pix_fmt": "yuv420p",
|
||||
"-r": "7",
|
||||
"-vf": f"setpts={25 / 7}*PTS,scale={shape_x}:{shape_y}:flags=neighbor",
|
||||
},
|
||||
) as movie:
|
||||
im = self.transpose("tzcyx") # type: ignore
|
||||
for ti in tqdm(t, desc="Saving movie", disable=not bar):
|
||||
movie.writeFrame(
|
||||
np.max(
|
||||
[
|
||||
cframe(yx, c, a, b / s, scale)
|
||||
for yx, a, b, c, s in zip(
|
||||
im[ti].max("z"), *ab, colors, brightnesses
|
||||
)
|
||||
],
|
||||
0,
|
||||
)
|
||||
)
|
||||
|
||||
def save_as_tiff(
|
||||
self,
|
||||
fname: Path | str = None,
|
||||
c: int | Sequence[int] = None,
|
||||
z: int | Sequence[int] = None,
|
||||
t: int | Sequence[int] = None,
|
||||
split: bool = False,
|
||||
bar: bool = True,
|
||||
pixel_type: str = "uint16",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""saves the image as a tif file
|
||||
split: split channels into different files"""
|
||||
fname = Path(str(fname).format(name=self.path.stem, path=str(self.path.parent)))
|
||||
if fname is None:
|
||||
fname = self.path.with_suffix(".tif")
|
||||
if fname == self.path:
|
||||
raise FileExistsError(f"File {fname} exists already.")
|
||||
if not isinstance(fname, Path):
|
||||
fname = Path(fname)
|
||||
if split:
|
||||
for i in range(self.shape["c"]):
|
||||
if self.timeseries:
|
||||
self.save_as_tiff(
|
||||
fname.with_name(f"{fname.stem}_C{i:01d}").with_suffix(".tif"),
|
||||
i,
|
||||
0,
|
||||
None,
|
||||
False,
|
||||
bar,
|
||||
pixel_type,
|
||||
)
|
||||
else:
|
||||
self.save_as_tiff(
|
||||
fname.with_name(f"{fname.stem}_C{i:01d}").with_suffix(".tif"),
|
||||
i,
|
||||
None,
|
||||
0,
|
||||
False,
|
||||
bar,
|
||||
pixel_type,
|
||||
)
|
||||
else:
|
||||
n = [c, z, t]
|
||||
for i, ax in enumerate("czt"):
|
||||
if n[i] is None:
|
||||
n[i] = range(self.shape[ax])
|
||||
elif not isinstance(n[i], (tuple, list)):
|
||||
n[i] = (n[i],)
|
||||
|
||||
shape = [len(i) for i in n]
|
||||
with TransformTiff(
|
||||
self,
|
||||
fname.with_suffix(".tif"),
|
||||
dtype=pixel_type,
|
||||
pxsize=self.pxsize,
|
||||
deltaz=self.deltaz,
|
||||
**kwargs,
|
||||
) as tif:
|
||||
for i, m in tqdm( # noqa
|
||||
zip(product(*[range(s) for s in shape]), product(*n)), # noqa
|
||||
total=np.prod(shape),
|
||||
desc="Saving tiff",
|
||||
disable=not bar,
|
||||
):
|
||||
tif.save(m, *i) # type: ignore
|
||||
|
||||
def with_transform(
|
||||
self,
|
||||
channels: bool = True,
|
||||
drift: bool = False,
|
||||
file: Path | str = None,
|
||||
bead_files: Sequence[Path | str] = (),
|
||||
) -> Imread:
|
||||
"""returns a view where channels and/or frames are registered with an affine transformation
|
||||
channels: True/False register channels using bead_files
|
||||
drift: True/False register frames to correct drift
|
||||
file: load registration from file with name file, default: transform.yml in self.path.parent
|
||||
bead_files: files used to register channels, default: files in self.path.parent,
|
||||
with names starting with 'beads'
|
||||
"""
|
||||
raise NotImplementedError("transforms are not yet implemented")
|
||||
# view = self.copy()
|
||||
# if file is None:
|
||||
# file = Path(view.path.parent) / 'transform.yml'
|
||||
# else:
|
||||
# file = Path(file)
|
||||
# if not bead_files:
|
||||
# try:
|
||||
# bead_files = Transforms.get_bead_files(view.path.parent)
|
||||
# except Exception: # noqa
|
||||
# if not file.exists():
|
||||
# raise Exception('No transform file and no bead file found.')
|
||||
# bead_files = ()
|
||||
#
|
||||
# if channels:
|
||||
# try:
|
||||
# view.transform = Transforms.from_file(file, T=drift)
|
||||
# except Exception: # noqa
|
||||
# view.transform = Transforms().with_beads(view.cyllens, bead_files)
|
||||
# if drift:
|
||||
# view.transform = view.transform.with_drift(view)
|
||||
# view.transform.save(file.with_suffix('.yml'))
|
||||
# view.transform.save_channel_transform_tiff(bead_files, file.with_suffix('.tif'))
|
||||
# elif drift:
|
||||
# try:
|
||||
# view.transform = Transforms.from_file(file, C=False)
|
||||
# except Exception: # noqa
|
||||
# view.transform = Transforms().with_drift(self)
|
||||
# view.transform.adapt(view.frameoffset, view.shape.yxczt, view.channel_names)
|
||||
# return view
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = ArgumentParser(description="Display info and save as tif")
|
||||
parser.add_argument("-v", "--version", action="version", version=__version__)
|
||||
parser.add_argument("file", help="image_file", type=str, nargs="*")
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--write",
|
||||
help="path to tif/movie out, {folder}, {name} and {ext} take this from file in",
|
||||
type=str,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--extract_ome", help="extract ome to xml file", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--register", help="register channels", action="store_true"
|
||||
)
|
||||
parser.add_argument("-c", "--channel", help="channel", type=int, default=None)
|
||||
parser.add_argument("-z", "--zslice", help="z-slice", type=int, default=None)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--time",
|
||||
help="time (frames) in python slicing notation",
|
||||
type=str,
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument("-s", "--split", help="split channels", action="store_true")
|
||||
parser.add_argument("-f", "--force", help="force overwrite", action="store_true")
|
||||
parser.add_argument(
|
||||
"-C", "--movie-colors", help="colors for channels in movie", type=str, nargs="*"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-B",
|
||||
"--movie-brightnesses",
|
||||
help="scale brightness of each channel",
|
||||
type=float,
|
||||
nargs="*",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-S", "--movie-scale", help="upscale movie xy size, int", type=float
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
for file in tqdm(args.file, desc="operating on files", disable=len(args.file) == 1):
|
||||
file = Path(file)
|
||||
with Imread(file) as im: # noqa
|
||||
if args.register:
|
||||
im = im.with_transform() # noqa
|
||||
if args.write:
|
||||
write = Path(
|
||||
args.write.format(
|
||||
folder=str(file.parent), name=file.stem, ext=file.suffix
|
||||
)
|
||||
).absolute() # noqa
|
||||
write.parent.mkdir(parents=True, exist_ok=True)
|
||||
if write.exists() and not args.force:
|
||||
print(
|
||||
f"File {args.write} exists already, add the -f flag if you want to overwrite it."
|
||||
)
|
||||
elif write.suffix in (".mkv", ".mp4"):
|
||||
im.save_as_movie(
|
||||
write,
|
||||
args.channel,
|
||||
args.zslice,
|
||||
args.time,
|
||||
args.movie_colors,
|
||||
args.movie_brightnesses,
|
||||
args.movie_scale,
|
||||
bar=len(args.file) == 1,
|
||||
)
|
||||
else:
|
||||
im.save_as_tiff(
|
||||
write,
|
||||
args.channel,
|
||||
args.zslice,
|
||||
args.time,
|
||||
args.split,
|
||||
bar=len(args.file) == 1,
|
||||
)
|
||||
if args.extract_ome:
|
||||
with open(im.path.with_suffix(".ome.xml"), "w") as f:
|
||||
f.write(im.ome.to_xml())
|
||||
if len(args.file) == 1:
|
||||
print(im.summary)
|
||||
corr = np.fft.fftshift(np.fft.ifftn(fft * fft.conj()).real / np.sum(volume**2)) # type: ignore
|
||||
return 1 - corr[tuple([0] * corr.ndim)] < 0.006 # type: ignore
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
# 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: ...
|
||||
@@ -1,39 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ome_metadata import ome_metadata_rs as rs # noqa
|
||||
from collections import UserDict, UserList
|
||||
|
||||
|
||||
class Ome(UserDict):
|
||||
@staticmethod
|
||||
def from_xml(xml: str) -> Ome:
|
||||
"""Create the OME structure from an XML string"""
|
||||
new = Ome()
|
||||
new.update(rs.ome(str(xml)))
|
||||
return new
|
||||
|
||||
def __dir__(self) -> list[str]:
|
||||
return list(self.keys()) + list(super().__dir__())
|
||||
|
||||
def __getattr__(self, key: str) -> Ome | OmeList | int | float | str:
|
||||
try:
|
||||
new = self.__getitem__(key)
|
||||
except KeyError:
|
||||
raise AttributeError(f"'Ome' object has no attribute '{key}'")
|
||||
if isinstance(new, dict):
|
||||
return Ome(**new)
|
||||
elif isinstance(new, list):
|
||||
return OmeList(new)
|
||||
else:
|
||||
return new
|
||||
|
||||
|
||||
class OmeList(UserList):
|
||||
def __getitem__(self, item: int) -> Ome | OmeList | int | float | str:
|
||||
new = super().__getitem__(item)
|
||||
if isinstance(new, dict):
|
||||
return Ome(**new)
|
||||
elif isinstance(new, list):
|
||||
return OmeList(new)
|
||||
else:
|
||||
return new
|
||||
+17
-29
@@ -1,55 +1,43 @@
|
||||
[build-system]
|
||||
requires = ["maturin>=1.8,<2.0"]
|
||||
requires = ["maturin>=1.9.4,<2.0"]
|
||||
build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ndbioimage"
|
||||
description = "Bio image reading, metadata and some affine registration."
|
||||
authors = [
|
||||
{ name = "W. Pomp", email = "w.pomp@nki.nl" }
|
||||
]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
keywords = ["bioformats", "imread", "numpy", "metadata"]
|
||||
version = "2027.0.0"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Rust",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dynamic = [
|
||||
"description",
|
||||
"readme",
|
||||
"license",
|
||||
"license-files",
|
||||
"authors",
|
||||
"maintainers",
|
||||
"keywords",
|
||||
"urls",
|
||||
]
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"tiffwrite",
|
||||
"ome-metadata >= 0.2.1",
|
||||
"tqdm",
|
||||
"numpy >= 1.16.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = ["pytest"]
|
||||
write = ["matplotlib", "scikit-video"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/wimpomp/ndbioimage/tree/rs"
|
||||
|
||||
[project.scripts]
|
||||
ndbioimage = "ndbioimage:main"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
filterwarnings = ["ignore:::(colorcet)"]
|
||||
ndbioimage = "ndbioimage:ndbioimage_rs.main"
|
||||
ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub"
|
||||
|
||||
[tool.maturin]
|
||||
python-source = "py"
|
||||
features = ["pyo3/extension-module", "python", "gpl-formats"]
|
||||
features = ["python", "bioformats_java", "tiffwrite", "czi"]
|
||||
module-name = "ndbioimage.ndbioimage_rs"
|
||||
exclude = ["py/ndbioimage/jassets/*", "py/ndbioimage/deps/*"]
|
||||
include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"]
|
||||
strip = true
|
||||
|
||||
[tool.isort]
|
||||
|
||||
+205
-34
@@ -3,9 +3,11 @@ 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::str::FromStr;
|
||||
use std::ops::Index;
|
||||
use strum::{AsRefStr, Display, EnumString};
|
||||
|
||||
/// a trait to find axis indices from any object
|
||||
pub trait Ax {
|
||||
@@ -25,13 +27,17 @@ pub trait Ax {
|
||||
}
|
||||
|
||||
/// Enum for CZTYX axes or a new axis
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
#[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,
|
||||
}
|
||||
|
||||
@@ -41,36 +47,6 @@ impl Hash for Axis {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Axis {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_uppercase().as_str() {
|
||||
"C" => Ok(Axis::C),
|
||||
"Z" => Ok(Axis::Z),
|
||||
"T" => Ok(Axis::T),
|
||||
"Y" => Ok(Axis::Y),
|
||||
"X" => Ok(Axis::X),
|
||||
"NEW" => Ok(Axis::New),
|
||||
_ => Err(Error::InvalidAxis(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Axis {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
Axis::C => "C",
|
||||
Axis::Z => "Z",
|
||||
Axis::T => "T",
|
||||
Axis::Y => "Y",
|
||||
Axis::X => "X",
|
||||
Axis::New => "N",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ax for Axis {
|
||||
fn n(&self) -> usize {
|
||||
*self as usize
|
||||
@@ -134,8 +110,11 @@ impl Ax for usize {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum Operation {
|
||||
#[derive(
|
||||
Clone, Debug, Serialize, Deserialize, EnumString, AsRefStr, Display, PartialEq, Eq, Hash,
|
||||
)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
pub enum Operation {
|
||||
Max,
|
||||
Min,
|
||||
Sum,
|
||||
@@ -249,3 +228,195 @@ impl IntoIterator for &Slice {
|
||||
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 => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
use crate::error::Error;
|
||||
use j4rs::{Instance, InvocationArg, Jvm, JvmBuilder};
|
||||
use std::cell::OnceCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
thread_local! {
|
||||
static JVM: OnceCell<Rc<Jvm>> = const { OnceCell::new() }
|
||||
}
|
||||
|
||||
/// 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()
|
||||
};
|
||||
if !class_path.join("jassets").exists() {
|
||||
panic!(
|
||||
"jassets directory does not exist in {}",
|
||||
class_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Rc::new(
|
||||
JvmBuilder::new()
|
||||
.skip_setting_native_lib()
|
||||
.with_base_path(class_path.to_str().unwrap())
|
||||
.build()
|
||||
.expect("Failed to build JVM"),
|
||||
)
|
||||
})
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn download_bioformats(gpl_formats: bool) -> Result<(), Error> {
|
||||
#[cfg(feature = "python")]
|
||||
let path = crate::py::ndbioimage_file();
|
||||
|
||||
#[cfg(not(feature = "python"))]
|
||||
let path = std::env::current_exe()?.parent().unwrap().to_path_buf();
|
||||
|
||||
let class_path = path.parent().unwrap();
|
||||
let jassets = class_path.join("jassets");
|
||||
if !jassets.exists() {
|
||||
std::fs::create_dir_all(jassets)?;
|
||||
}
|
||||
println!("installing jassets in {}", class_path.display());
|
||||
let jvm = JvmBuilder::new()
|
||||
.skip_setting_native_lib()
|
||||
.with_base_path(class_path.to_str().unwrap())
|
||||
.with_maven_settings(j4rs::MavenSettings::new(vec![
|
||||
j4rs::MavenArtifactRepo::from(
|
||||
"openmicroscopy::https://artifacts.openmicroscopy.org/artifactory/ome.releases",
|
||||
),
|
||||
]))
|
||||
.build()?;
|
||||
|
||||
jvm.deploy_artifact(&j4rs::MavenArtifact::from("ome:bioformats_package:8.3.0"))?;
|
||||
|
||||
if gpl_formats {
|
||||
jvm.deploy_artifact(&j4rs::MavenArtifact::from("ome:formats-gpl:8.3.0"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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!(set_metadata_store, "setMetadataStore", [ome_data: Instance]);
|
||||
method!(get_metadata_store, "getMetadataStore" => Instance);
|
||||
method!(set_id, "setId", [id: &str]);
|
||||
method!(set_series, "setSeries", [series: i32|p]);
|
||||
method!(open_bi8, "openBytes", [index: i32|p] => Vec<i8>|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!(get_pixel_type, "getPixelType" => i32|c);
|
||||
method!(is_little_endian, "isLittleEndian" => bool|c);
|
||||
method!(is_rgb, "isRGB" => bool|c);
|
||||
method!(is_interleaved, "isInterleaved" => bool|c);
|
||||
method!(get_index, "getIndex", [z: i32|p, c: i32|p, t: i32|p] => i32|c);
|
||||
method!(get_rgb_channel_count, "getRGBChannelCount" => i32|c);
|
||||
method!(is_indexed, "isIndexed" => bool|c);
|
||||
method!(get_8bit_lookup_table, "get8BitLookupTable" => Instance);
|
||||
method!(get_16bit_lookup_table, "get16BitLookupTable" => Instance);
|
||||
method!(close, "close");
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
+159
-164
@@ -1,171 +1,166 @@
|
||||
use crate::error::Error;
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
use phf::phf_map;
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
lazy_static! {
|
||||
static ref COLORS: HashMap<String, String> = {
|
||||
HashMap::from([
|
||||
("b".to_string(), "#0000FF".to_string()),
|
||||
("g".to_string(), "#008000".to_string()),
|
||||
("r".to_string(), "#FF0000".to_string()),
|
||||
("c".to_string(), "#00BFBF".to_string()),
|
||||
("m".to_string(), "#BF00BF".to_string()),
|
||||
("y".to_string(), "#BFBF00".to_string()),
|
||||
("k".to_string(), "#000000".to_string()),
|
||||
("w".to_string(), "#FFFFFF".to_string()),
|
||||
("aliceblue".to_string(), "#F0F8FF".to_string()),
|
||||
("antiquewhite".to_string(), "#FAEBD7".to_string()),
|
||||
("aqua".to_string(), "#00FFFF".to_string()),
|
||||
("aquamarine".to_string(), "#7FFFD4".to_string()),
|
||||
("azure".to_string(), "#F0FFFF".to_string()),
|
||||
("beige".to_string(), "#F5F5DC".to_string()),
|
||||
("bisque".to_string(), "#FFE4C4".to_string()),
|
||||
("black".to_string(), "#000000".to_string()),
|
||||
("blanchedalmond".to_string(), "#FFEBCD".to_string()),
|
||||
("blue".to_string(), "#0000FF".to_string()),
|
||||
("blueviolet".to_string(), "#8A2BE2".to_string()),
|
||||
("brown".to_string(), "#A52A2A".to_string()),
|
||||
("burlywood".to_string(), "#DEB887".to_string()),
|
||||
("cadetblue".to_string(), "#5F9EA0".to_string()),
|
||||
("chartreuse".to_string(), "#7FFF00".to_string()),
|
||||
("chocolate".to_string(), "#D2691E".to_string()),
|
||||
("coral".to_string(), "#FF7F50".to_string()),
|
||||
("cornflowerblue".to_string(), "#6495ED".to_string()),
|
||||
("cornsilk".to_string(), "#FFF8DC".to_string()),
|
||||
("crimson".to_string(), "#DC143C".to_string()),
|
||||
("cyan".to_string(), "#00FFFF".to_string()),
|
||||
("darkblue".to_string(), "#00008B".to_string()),
|
||||
("darkcyan".to_string(), "#008B8B".to_string()),
|
||||
("darkgoldenrod".to_string(), "#B8860B".to_string()),
|
||||
("darkgray".to_string(), "#A9A9A9".to_string()),
|
||||
("darkgreen".to_string(), "#006400".to_string()),
|
||||
("darkgrey".to_string(), "#A9A9A9".to_string()),
|
||||
("darkkhaki".to_string(), "#BDB76B".to_string()),
|
||||
("darkmagenta".to_string(), "#8B008B".to_string()),
|
||||
("darkolivegreen".to_string(), "#556B2F".to_string()),
|
||||
("darkorange".to_string(), "#FF8C00".to_string()),
|
||||
("darkorchid".to_string(), "#9932CC".to_string()),
|
||||
("darkred".to_string(), "#8B0000".to_string()),
|
||||
("darksalmon".to_string(), "#E9967A".to_string()),
|
||||
("darkseagreen".to_string(), "#8FBC8F".to_string()),
|
||||
("darkslateblue".to_string(), "#483D8B".to_string()),
|
||||
("darkslategray".to_string(), "#2F4F4F".to_string()),
|
||||
("darkslategrey".to_string(), "#2F4F4F".to_string()),
|
||||
("darkturquoise".to_string(), "#00CED1".to_string()),
|
||||
("darkviolet".to_string(), "#9400D3".to_string()),
|
||||
("deeppink".to_string(), "#FF1493".to_string()),
|
||||
("deepskyblue".to_string(), "#00BFFF".to_string()),
|
||||
("dimgray".to_string(), "#696969".to_string()),
|
||||
("dimgrey".to_string(), "#696969".to_string()),
|
||||
("dodgerblue".to_string(), "#1E90FF".to_string()),
|
||||
("firebrick".to_string(), "#B22222".to_string()),
|
||||
("floralwhite".to_string(), "#FFFAF0".to_string()),
|
||||
("forestgreen".to_string(), "#228B22".to_string()),
|
||||
("fuchsia".to_string(), "#FF00FF".to_string()),
|
||||
("gainsboro".to_string(), "#DCDCDC".to_string()),
|
||||
("ghostwhite".to_string(), "#F8F8FF".to_string()),
|
||||
("gold".to_string(), "#FFD700".to_string()),
|
||||
("goldenrod".to_string(), "#DAA520".to_string()),
|
||||
("gray".to_string(), "#808080".to_string()),
|
||||
("green".to_string(), "#008000".to_string()),
|
||||
("greenyellow".to_string(), "#ADFF2F".to_string()),
|
||||
("grey".to_string(), "#808080".to_string()),
|
||||
("honeydew".to_string(), "#F0FFF0".to_string()),
|
||||
("hotpink".to_string(), "#FF69B4".to_string()),
|
||||
("indianred".to_string(), "#CD5C5C".to_string()),
|
||||
("indigo".to_string(), "#4B0082".to_string()),
|
||||
("ivory".to_string(), "#FFFFF0".to_string()),
|
||||
("khaki".to_string(), "#F0E68C".to_string()),
|
||||
("lavender".to_string(), "#E6E6FA".to_string()),
|
||||
("lavenderblush".to_string(), "#FFF0F5".to_string()),
|
||||
("lawngreen".to_string(), "#7CFC00".to_string()),
|
||||
("lemonchiffon".to_string(), "#FFFACD".to_string()),
|
||||
("lightblue".to_string(), "#ADD8E6".to_string()),
|
||||
("lightcoral".to_string(), "#F08080".to_string()),
|
||||
("lightcyan".to_string(), "#E0FFFF".to_string()),
|
||||
("lightgoldenrodyellow".to_string(), "#FAFAD2".to_string()),
|
||||
("lightgray".to_string(), "#D3D3D3".to_string()),
|
||||
("lightgreen".to_string(), "#90EE90".to_string()),
|
||||
("lightgrey".to_string(), "#D3D3D3".to_string()),
|
||||
("lightpink".to_string(), "#FFB6C1".to_string()),
|
||||
("lightsalmon".to_string(), "#FFA07A".to_string()),
|
||||
("lightseagreen".to_string(), "#20B2AA".to_string()),
|
||||
("lightskyblue".to_string(), "#87CEFA".to_string()),
|
||||
("lightslategray".to_string(), "#778899".to_string()),
|
||||
("lightslategrey".to_string(), "#778899".to_string()),
|
||||
("lightsteelblue".to_string(), "#B0C4DE".to_string()),
|
||||
("lightyellow".to_string(), "#FFFFE0".to_string()),
|
||||
("lime".to_string(), "#00FF00".to_string()),
|
||||
("limegreen".to_string(), "#32CD32".to_string()),
|
||||
("linen".to_string(), "#FAF0E6".to_string()),
|
||||
("magenta".to_string(), "#FF00FF".to_string()),
|
||||
("maroon".to_string(), "#800000".to_string()),
|
||||
("mediumaquamarine".to_string(), "#66CDAA".to_string()),
|
||||
("mediumblue".to_string(), "#0000CD".to_string()),
|
||||
("mediumorchid".to_string(), "#BA55D3".to_string()),
|
||||
("mediumpurple".to_string(), "#9370DB".to_string()),
|
||||
("mediumseagreen".to_string(), "#3CB371".to_string()),
|
||||
("mediumslateblue".to_string(), "#7B68EE".to_string()),
|
||||
("mediumspringgreen".to_string(), "#00FA9A".to_string()),
|
||||
("mediumturquoise".to_string(), "#48D1CC".to_string()),
|
||||
("mediumvioletred".to_string(), "#C71585".to_string()),
|
||||
("midnightblue".to_string(), "#191970".to_string()),
|
||||
("mintcream".to_string(), "#F5FFFA".to_string()),
|
||||
("mistyrose".to_string(), "#FFE4E1".to_string()),
|
||||
("moccasin".to_string(), "#FFE4B5".to_string()),
|
||||
("navajowhite".to_string(), "#FFDEAD".to_string()),
|
||||
("navy".to_string(), "#000080".to_string()),
|
||||
("oldlace".to_string(), "#FDF5E6".to_string()),
|
||||
("olive".to_string(), "#808000".to_string()),
|
||||
("olivedrab".to_string(), "#6B8E23".to_string()),
|
||||
("orange".to_string(), "#FFA500".to_string()),
|
||||
("orangered".to_string(), "#FF4500".to_string()),
|
||||
("orchid".to_string(), "#DA70D6".to_string()),
|
||||
("palegoldenrod".to_string(), "#EEE8AA".to_string()),
|
||||
("palegreen".to_string(), "#98FB98".to_string()),
|
||||
("paleturquoise".to_string(), "#AFEEEE".to_string()),
|
||||
("palevioletred".to_string(), "#DB7093".to_string()),
|
||||
("papayawhip".to_string(), "#FFEFD5".to_string()),
|
||||
("peachpuff".to_string(), "#FFDAB9".to_string()),
|
||||
("peru".to_string(), "#CD853F".to_string()),
|
||||
("pink".to_string(), "#FFC0CB".to_string()),
|
||||
("plum".to_string(), "#DDA0DD".to_string()),
|
||||
("powderblue".to_string(), "#B0E0E6".to_string()),
|
||||
("purple".to_string(), "#800080".to_string()),
|
||||
("rebeccapurple".to_string(), "#663399".to_string()),
|
||||
("red".to_string(), "#FF0000".to_string()),
|
||||
("rosybrown".to_string(), "#BC8F8F".to_string()),
|
||||
("royalblue".to_string(), "#4169E1".to_string()),
|
||||
("saddlebrown".to_string(), "#8B4513".to_string()),
|
||||
("salmon".to_string(), "#FA8072".to_string()),
|
||||
("sandybrown".to_string(), "#F4A460".to_string()),
|
||||
("seagreen".to_string(), "#2E8B57".to_string()),
|
||||
("seashell".to_string(), "#FFF5EE".to_string()),
|
||||
("sienna".to_string(), "#A0522D".to_string()),
|
||||
("silver".to_string(), "#C0C0C0".to_string()),
|
||||
("skyblue".to_string(), "#87CEEB".to_string()),
|
||||
("slateblue".to_string(), "#6A5ACD".to_string()),
|
||||
("slategray".to_string(), "#708090".to_string()),
|
||||
("slategrey".to_string(), "#708090".to_string()),
|
||||
("snow".to_string(), "#FFFAFA".to_string()),
|
||||
("springgreen".to_string(), "#00FF7F".to_string()),
|
||||
("steelblue".to_string(), "#4682B4".to_string()),
|
||||
("tan".to_string(), "#D2B48C".to_string()),
|
||||
("teal".to_string(), "#008080".to_string()),
|
||||
("thistle".to_string(), "#D8BFD8".to_string()),
|
||||
("tomato".to_string(), "#FF6347".to_string()),
|
||||
("turquoise".to_string(), "#40E0D0".to_string()),
|
||||
("violet".to_string(), "#EE82EE".to_string()),
|
||||
("wheat".to_string(), "#F5DEB3".to_string()),
|
||||
("white".to_string(), "#FFFFFF".to_string()),
|
||||
("whitesmoke".to_string(), "#F5F5F5".to_string()),
|
||||
("yellow".to_string(), "#FFFF00".to_string()),
|
||||
("yellowgreen".to_string(), "#9ACD32".to_string()),
|
||||
])
|
||||
};
|
||||
}
|
||||
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 {
|
||||
|
||||
+52
-2
@@ -6,6 +6,7 @@ pub enum Error {
|
||||
IO(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Shape(#[from] ndarray::ShapeError),
|
||||
#[cfg(feature = "bioformats_java")]
|
||||
#[error(transparent)]
|
||||
J4rs(#[from] j4rs::errors::J4RsError),
|
||||
#[error(transparent)]
|
||||
@@ -14,12 +15,47 @@ pub enum Error {
|
||||
ParseIntError(#[from] std::num::ParseIntError),
|
||||
#[error(transparent)]
|
||||
Ome(#[from] ome_metadata::error::Error),
|
||||
#[cfg(feature = "tiff")]
|
||||
#[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 = "tiff")]
|
||||
#[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}")]
|
||||
@@ -29,6 +65,8 @@ pub enum Error {
|
||||
#[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),
|
||||
@@ -52,6 +90,8 @@ pub enum Error {
|
||||
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")]
|
||||
@@ -62,4 +102,14 @@ pub enum Error {
|
||||
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),
|
||||
}
|
||||
|
||||
+243
-345
@@ -1,89 +1,271 @@
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod bioformats;
|
||||
|
||||
pub mod axes;
|
||||
pub mod metadata;
|
||||
#[cfg(feature = "python")]
|
||||
mod py;
|
||||
pub mod reader;
|
||||
pub mod stats;
|
||||
pub mod view;
|
||||
|
||||
pub mod colors;
|
||||
pub mod error;
|
||||
pub mod metadata;
|
||||
#[cfg(feature = "movie")]
|
||||
pub mod movie;
|
||||
#[cfg(feature = "tiff")]
|
||||
pub mod tiff;
|
||||
pub mod readers;
|
||||
#[cfg(feature = "tiffwrite")]
|
||||
pub mod tiffwrite;
|
||||
// mod cache;
|
||||
|
||||
pub use bioformats::download_bioformats;
|
||||
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::axes::Axis;
|
||||
use crate::error::Error;
|
||||
use crate::reader::{Frame, Reader};
|
||||
use crate::stats::MinMax;
|
||||
use crate::view::Item;
|
||||
use downloader::{Download, Downloader};
|
||||
use ndarray::{Array, Array4, Array5, NewAxis};
|
||||
use ndarray::{Array2, s};
|
||||
use crate::readers::{DynReader, Frame, Reader};
|
||||
use ndarray::Array2;
|
||||
use rayon::prelude::*;
|
||||
|
||||
fn open(file: &str) -> Result<Reader, Error> {
|
||||
fn open(file: &str) -> Result<DynReader, Error> {
|
||||
let path = std::env::current_dir()?
|
||||
.join("tests")
|
||||
.join("files")
|
||||
.join(file);
|
||||
Reader::new(&path, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_ome() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = std::env::current_dir()?.join("tests/files/ome");
|
||||
std::fs::create_dir_all(&path)?;
|
||||
let url =
|
||||
"https://downloads.openmicroscopy.org/images/OME-TIFF/2016-06/bioformats-artificial/";
|
||||
let page = reqwest::blocking::get(url)?.text()?;
|
||||
let pat = regex::Regex::new(
|
||||
r#"<a\s+href\s*=\s*"([^"<>]+)">[^<>]+</a>\s+\d{2}-\w{3}-\d{4}\s+\d{2}:\d{2}\s+(\d+)"#,
|
||||
)?;
|
||||
let mut downloads = Vec::new();
|
||||
let mut files = Vec::new();
|
||||
for line in page.lines() {
|
||||
if let Some(cap) = pat.captures(line) {
|
||||
let link = cap[1].trim().to_string();
|
||||
let size = cap[2].trim().parse::<usize>()?;
|
||||
if size < 10 * 1024usize.pow(2) {
|
||||
if !path.join(&link).exists() {
|
||||
downloads.push(Download::new(&format!("{}{}", url, link)));
|
||||
}
|
||||
files.push(path.join(link));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !downloads.is_empty() {
|
||||
let mut downloader = Downloader::builder().download_folder(&path).build()?;
|
||||
downloader.download(&downloads)?;
|
||||
}
|
||||
let mut count = 0;
|
||||
for file in files {
|
||||
if let Ok(reader) = Reader::new(&file, 0) {
|
||||
let _ome = reader.get_ome()?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
println!("count: {}", count);
|
||||
assert!(count > 30);
|
||||
Ok(())
|
||||
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
|
||||
file,
|
||||
reader.pixel_type()
|
||||
))
|
||||
}
|
||||
|
||||
@@ -94,9 +276,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_ser() -> Result<(), Error> {
|
||||
let file = "Experiment-2029.czi";
|
||||
let file = "czi/Experiment-2029.czi";
|
||||
let reader = open(file)?;
|
||||
println!("size: {}, {}", reader.size_y, reader.size_y);
|
||||
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);
|
||||
@@ -108,7 +290,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_par() -> Result<(), Error> {
|
||||
let files = vec!["Experiment-2029.czi", "test.tif"];
|
||||
let files = vec!["czi/Experiment-2029.czi", "tiff/test.tif"];
|
||||
let pixel_type = files
|
||||
.into_par_iter()
|
||||
.map(|file| get_pixel_type(file).unwrap())
|
||||
@@ -119,7 +301,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn read_frame_par() -> Result<(), Error> {
|
||||
let files = vec!["Experiment-2029.czi", "test.tif"];
|
||||
let files = vec!["czi/Experiment-2029.czi", "tiff/test.tif"];
|
||||
let frames = files
|
||||
.into_par_iter()
|
||||
.map(|file| get_frame(file).unwrap())
|
||||
@@ -127,288 +309,4 @@ mod tests {
|
||||
println!("{:?}", frames);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_sequence() -> Result<(), Error> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
println!("reader: {:?}", reader);
|
||||
let frame = reader.get_frame(0, 4, 0)?;
|
||||
println!("frame: {:?}", frame);
|
||||
let frame = reader.get_frame(0, 2, 0)?;
|
||||
println!("frame: {:?}", frame);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_sequence1() -> Result<(), Error> {
|
||||
let file = "4-Pos_001_002/img_000000000_Cy3-Cy3_filter_000.tif";
|
||||
let reader = open(file)?;
|
||||
println!("reader: {:?}", reader);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ome_xml() -> Result<(), Error> {
|
||||
let file = "Experiment-2029.czi";
|
||||
let reader = open(file)?;
|
||||
let xml = reader.get_ome_xml()?;
|
||||
println!("{}", xml);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view() -> Result<(), Error> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = view.slice(s![0, 5, 0, .., ..])?;
|
||||
let b = reader.get_frame(0, 5, 0)?;
|
||||
let c: Array2<isize> = a.try_into()?;
|
||||
let d: Array2<isize> = b.try_into()?;
|
||||
assert_eq!(c, d);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_shape() -> Result<(), Error> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = view.slice(s![0, ..5, 0, .., 100..200])?;
|
||||
let shape = a.shape();
|
||||
assert_eq!(shape, vec![5, 1024, 100]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_new_axis() -> Result<(), Error> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = Array5::<u8>::zeros((1, 9, 1, 1024, 1024));
|
||||
let a = a.slice(s![0, ..5, 0, NewAxis, 100..200, ..]);
|
||||
let v = view.slice(s![0, ..5, 0, NewAxis, 100..200, ..])?;
|
||||
assert_eq!(v.shape(), a.shape());
|
||||
let a = a.slice(s![NewAxis, .., .., NewAxis, .., .., NewAxis]);
|
||||
let v = v.slice(s![NewAxis, .., .., NewAxis, .., .., NewAxis])?;
|
||||
assert_eq!(v.shape(), a.shape());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_permute_axes() -> Result<(), Error> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let s = view.shape();
|
||||
let mut a = Array5::<u8>::zeros((s[0], s[1], s[2], s[3], s[4]));
|
||||
assert_eq!(view.shape(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
|
||||
let view = view.swap_axes(Axis::C, Axis::Z)?;
|
||||
a.swap_axes(0, 1);
|
||||
assert_eq!(view.shape(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
let view = view.permute_axes(&[Axis::X, Axis::Z, Axis::Y])?;
|
||||
let a = a.permuted_axes([4, 1, 2, 0, 3]);
|
||||
assert_eq!(view.shape(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! test_max {
|
||||
($($name:ident: $b:expr $(,)?)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() -> Result<(), Error> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let array: Array5<usize> = view.clone().try_into()?;
|
||||
let view = view.max_proj($b)?;
|
||||
let a: Array4<usize> = view.clone().try_into()?;
|
||||
let b = array.max($b)?;
|
||||
assert_eq!(a.shape(), b.shape());
|
||||
assert_eq!(a, b);
|
||||
Ok(())
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_max! {
|
||||
max_c: 0
|
||||
max_z: 1
|
||||
max_t: 2
|
||||
max_y: 3
|
||||
max_x: 4
|
||||
}
|
||||
|
||||
macro_rules! test_index {
|
||||
($($name:ident: $b:expr $(,)?)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() -> Result<(), Error> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let v4: Array<usize, _> = view.slice($b)?.try_into()?;
|
||||
let a5: Array5<usize> = reader.view().try_into()?;
|
||||
let a4 = a5.slice($b).to_owned();
|
||||
assert_eq!(a4, v4);
|
||||
Ok(())
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_index! {
|
||||
index_0: s![.., .., .., .., ..]
|
||||
index_1: s![0, .., .., .., ..]
|
||||
index_2: s![.., 0, .., .., ..]
|
||||
index_3: s![.., .., 0, .., ..]
|
||||
index_4: s![.., .., .., 0, ..]
|
||||
index_5: s![.., .., .., .., 0]
|
||||
index_6: s![0, 0, .., .., ..]
|
||||
index_7: s![0, .., 0, .., ..]
|
||||
index_8: s![0, .., .., 0, ..]
|
||||
index_9: s![0, .., .., .., 0]
|
||||
index_a: s![.., 0, 0, .., ..]
|
||||
index_b: s![.., 0, .., 0, ..]
|
||||
index_c: s![.., 0, .., .., 0]
|
||||
index_d: s![.., .., 0, 0, ..]
|
||||
index_e: s![.., .., 0, .., 0]
|
||||
index_f: s![.., .., .., 0, 0]
|
||||
index_g: s![0, 0, 0, .., ..]
|
||||
index_h: s![0, 0, .., 0, ..]
|
||||
index_i: s![0, 0, .., .., 0]
|
||||
index_j: s![0, .., 0, 0, ..]
|
||||
index_k: s![0, .., 0, .., 0]
|
||||
index_l: s![0, .., .., 0, 0]
|
||||
index_m: s![0, 0, 0, 0, ..]
|
||||
index_n: s![0, 0, 0, .., 0]
|
||||
index_o: s![0, 0, .., 0, 0]
|
||||
index_p: s![0, .., 0, 0, 0]
|
||||
index_q: s![.., 0, 0, 0, 0]
|
||||
index_r: s![0, 0, 0, 0, 0]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dyn_view() -> Result<(), Error> {
|
||||
let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif";
|
||||
let reader = open(file)?;
|
||||
let a = reader.view().into_dyn();
|
||||
let b = a.max_proj(1)?;
|
||||
let c = b.slice(s![0, 0, .., ..])?;
|
||||
let d = c.as_array::<usize>()?;
|
||||
assert_eq!(d.shape(), [1024, 1024]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item() -> Result<(), Error> {
|
||||
let file = "1xp53-01-AP1.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = view.slice(s![.., 0, 0, 0, 0])?;
|
||||
let b = a.slice(s![0])?;
|
||||
let item = b.item::<usize>()?;
|
||||
assert_eq!(item, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slice_cztyx() -> Result<(), Error> {
|
||||
let file = "1xp53-01-AP1.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view().max_proj(Axis::Z)?.into_dyn();
|
||||
println!("view.axes: {:?}", view.get_axes());
|
||||
println!("view.slice: {:?}", view.get_slice());
|
||||
let r = view.reset_axes()?;
|
||||
println!("r.axes: {:?}", r.get_axes());
|
||||
println!("r.slice: {:?}", r.get_slice());
|
||||
let a = view.slice_cztyx(s![0, 0, 0, .., ..])?;
|
||||
println!("a.axes: {:?}", a.get_axes());
|
||||
println!("a.slice: {:?}", a.get_slice());
|
||||
assert_eq!(a.axes(), [Axis::Y, Axis::X]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_axes() -> Result<(), Error> {
|
||||
let file = "1xp53-01-AP1.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view().max_proj(Axis::Z)?;
|
||||
let view = view.reset_axes()?;
|
||||
assert_eq!(view.axes(), [Axis::C, Axis::New, Axis::T, Axis::Y, Axis::X]);
|
||||
let a = view.as_array::<f64>()?;
|
||||
assert_eq!(a.ndim(), 5);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_axes2() -> Result<(), Error> {
|
||||
let file = "Experiment-2029.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view().squeeze()?;
|
||||
let a = view.reset_axes()?;
|
||||
assert_eq!(a.axes(), [Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_axes3() -> Result<(), Error> {
|
||||
let file = "Experiment-2029.czi";
|
||||
let reader = open(file)?;
|
||||
let view4 = reader.view().squeeze()?;
|
||||
let view = view4.max_proj(Axis::Z)?.into_dyn();
|
||||
let slice = view.slice_cztyx(s![0, .., .., .., ..])?.into_dyn();
|
||||
let a = slice.as_array::<u16>()?;
|
||||
assert_eq!(slice.shape(), [1, 10, 1280, 1280]);
|
||||
assert_eq!(a.shape(), [1, 10, 1280, 1280]);
|
||||
let r = slice.reset_axes()?;
|
||||
let b = r.as_array::<u16>()?;
|
||||
assert_eq!(r.shape(), [1, 1, 10, 1280, 1280]);
|
||||
assert_eq!(b.shape(), [1, 1, 10, 1280, 1280]);
|
||||
let q = slice.max_proj(Axis::C)?.max_proj(Axis::T)?;
|
||||
let c = q.as_array::<f64>()?;
|
||||
assert_eq!(q.shape(), [1, 1280, 1280]);
|
||||
assert_eq!(c.shape(), [1, 1280, 1280]);
|
||||
let p = q.reset_axes()?;
|
||||
let d = p.as_array::<u16>()?;
|
||||
println!("axes: {:?}", p.get_axes());
|
||||
println!("operations: {:?}", p.get_operations());
|
||||
println!("slice: {:?}", p.get_slice());
|
||||
assert_eq!(p.shape(), [1, 1, 1, 1280, 1280]);
|
||||
assert_eq!(d.shape(), [1, 1, 1, 1280, 1280]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max() -> Result<(), Error> {
|
||||
let file = "Experiment-2029.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let m = view.max_proj(Axis::T)?;
|
||||
let a = m.as_array::<u16>()?;
|
||||
assert_eq!(m.shape(), [2, 1, 1280, 1280]);
|
||||
assert_eq!(a.shape(), [2, 1, 1280, 1280]);
|
||||
let mc = view.max_proj(Axis::C)?;
|
||||
let a = mc.as_array::<u16>()?;
|
||||
assert_eq!(mc.shape(), [1, 10, 1280, 1280]);
|
||||
assert_eq!(a.shape(), [1, 10, 1280, 1280]);
|
||||
let mz = mc.max_proj(Axis::Z)?;
|
||||
let a = mz.as_array::<u16>()?;
|
||||
assert_eq!(mz.shape(), [10, 1280, 1280]);
|
||||
assert_eq!(a.shape(), [10, 1280, 1280]);
|
||||
let mt = mz.max_proj(Axis::T)?;
|
||||
let a = mt.as_array::<u16>()?;
|
||||
assert_eq!(mt.shape(), [1280, 1280]);
|
||||
assert_eq!(a.shape(), [1280, 1280]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-193
@@ -1,194 +1,3 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
#[cfg(feature = "movie")]
|
||||
use ndarray::SliceInfoElem;
|
||||
use ndbioimage::error::Error;
|
||||
#[cfg(feature = "movie")]
|
||||
use ndbioimage::movie::MovieOptions;
|
||||
use ndbioimage::reader::{split_path_and_series, Reader};
|
||||
#[cfg(feature = "tiff")]
|
||||
use ndbioimage::tiff::TiffOptions;
|
||||
use ndbioimage::view::View;
|
||||
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 = "tiff")]
|
||||
Tiff {
|
||||
#[arg(value_name = "FILE", num_args(1..))]
|
||||
file: Vec<PathBuf>,
|
||||
#[arg(short, long, value_name = "COLOR", num_args(1..))]
|
||||
colors: Vec<String>,
|
||||
#[arg(short, long, value_name = "OVERWRITE")]
|
||||
overwrite: bool,
|
||||
},
|
||||
/// 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, 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,
|
||||
},
|
||||
/// Download the BioFormats jar into the correct folder
|
||||
DownloadBioFormats {
|
||||
#[arg(short, long, value_name = "GPL_FORMATS")]
|
||||
gpl_formats: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[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(crate) fn main() -> Result<(), Error> {
|
||||
let cli = Cli::parse();
|
||||
match &cli.command {
|
||||
Commands::Info { file } => {
|
||||
for f in file {
|
||||
let (path, series) = split_path_and_series(f)?;
|
||||
let view = View::from_path(path, series.unwrap_or(0))?.squeeze()?;
|
||||
println!("{}", view.summary()?);
|
||||
}
|
||||
}
|
||||
Commands::ExtractOME { file } => {
|
||||
for f in file {
|
||||
let (path, series) = split_path_and_series(f)?;
|
||||
let reader = Reader::new(&path, series.unwrap_or(0))?;
|
||||
let xml = reader.get_ome_xml()?;
|
||||
std::fs::write(path.with_extension("xml"), xml)?;
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tiff")]
|
||||
Commands::Tiff {
|
||||
file,
|
||||
colors,
|
||||
overwrite,
|
||||
} => {
|
||||
let mut options = TiffOptions::new(true, None, colors.clone(), *overwrite)?;
|
||||
options.enable_bar()?;
|
||||
for f in file {
|
||||
let (path, series) = split_path_and_series(f)?;
|
||||
let view = View::from_path(path, series.unwrap_or(0))?;
|
||||
view.save_as_tiff(f.with_extension("tiff"), &options)?;
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "movie")]
|
||||
Commands::Movie {
|
||||
file,
|
||||
velocity: speed,
|
||||
brightness,
|
||||
scale,
|
||||
colors,
|
||||
overwrite,
|
||||
register,
|
||||
channel,
|
||||
zslice,
|
||||
time,
|
||||
no_scaling,
|
||||
} => {
|
||||
let options = MovieOptions::new(
|
||||
*speed,
|
||||
brightness.to_vec(),
|
||||
*scale,
|
||||
colors.to_vec(),
|
||||
*overwrite,
|
||||
*register,
|
||||
*no_scaling,
|
||||
)?;
|
||||
for f in file {
|
||||
let (path, series) = split_path_and_series(f)?;
|
||||
let view = View::from_path(path, series.unwrap_or(0))?;
|
||||
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)?;
|
||||
}
|
||||
view.into_dyn()
|
||||
.slice(s.as_slice())?
|
||||
.save_as_movie(f.with_extension("mp4"), &options)?;
|
||||
}
|
||||
}
|
||||
Commands::DownloadBioFormats { gpl_formats } => {
|
||||
ndbioimage::download_bioformats(*gpl_formats)?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub(crate) fn main() -> Result<(), ndbioimage::error::Error> {
|
||||
ndbioimage::main::main(None)
|
||||
}
|
||||
|
||||
+59
-22
@@ -1,17 +1,19 @@
|
||||
use crate::axes::Axis;
|
||||
use crate::colors::Color;
|
||||
use crate::error::Error;
|
||||
use crate::reader::PixelType;
|
||||
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::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct MovieOptions {
|
||||
velocity: f64,
|
||||
@@ -93,7 +95,7 @@ impl MovieOptions {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ab(tyx: View<IxDyn>) -> Result<(f64, f64), Error> {
|
||||
fn get_ab<R: Reader>(tyx: View<IxDyn, R>) -> Result<(f64, f64), Error> {
|
||||
let s = tyx
|
||||
.as_array::<f64>()?
|
||||
.iter()
|
||||
@@ -136,9 +138,25 @@ fn cframe(frame: Array2<f64>, color: &[u8], a: f64, b: f64) -> Array3<f64> {
|
||||
stack(ndarray::Axis(2), &view).unwrap()
|
||||
}
|
||||
|
||||
impl<D> View<D>
|
||||
/// 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
|
||||
@@ -211,7 +229,7 @@ where
|
||||
|
||||
let ab = if options.no_scaling {
|
||||
vec![
|
||||
match view.pixel_type {
|
||||
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),
|
||||
@@ -222,7 +240,7 @@ where
|
||||
PixelType::U64 => (u64::MIN as f64, u64::MAX as f64),
|
||||
_ => (0.0, 1.0),
|
||||
};
|
||||
view.size_c
|
||||
view.shape().c
|
||||
]
|
||||
} else {
|
||||
(0..size_c)
|
||||
@@ -233,13 +251,16 @@ where
|
||||
.collect::<Result<Vec<_>, Error>>()?
|
||||
};
|
||||
|
||||
thread::spawn(move || {
|
||||
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).unwrap(),
|
||||
view.get_frame(c, 0, t)?,
|
||||
&colors[c],
|
||||
ab[c].0,
|
||||
ab[c].1 / brightness[c],
|
||||
@@ -247,18 +268,30 @@ where
|
||||
}
|
||||
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).unwrap();
|
||||
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>(())
|
||||
});
|
||||
|
||||
movie
|
||||
.iter()
|
||||
.map_err(|e| Error::Ffmpeg(e.to_string()))?
|
||||
.for_each(|e| match e {
|
||||
FfmpegEvent::Log(LogLevel::Error, e) => println!("Error: {}", e),
|
||||
FfmpegEvent::Progress(p) => println!("Progress: {} / 00:00:15", p.time),
|
||||
_ => {}
|
||||
});
|
||||
rt.block_on(progress_task)??;
|
||||
rt.block_on(write_task)??;
|
||||
bar.finish();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -266,20 +299,24 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::reader::Reader;
|
||||
use crate::readers::DynReader;
|
||||
use crate::view::View;
|
||||
|
||||
#[cfg(any(feature = "czi", feature = "bioformats_java"))]
|
||||
#[test]
|
||||
fn movie() -> Result<(), Error> {
|
||||
let file = "1xp53-01-AP1.czi";
|
||||
let file = "czi/1xp53-01-AP1.czi";
|
||||
let path = std::env::current_dir()?
|
||||
.join("tests")
|
||||
.join("files")
|
||||
.join(file);
|
||||
let reader = Reader::new(&path, 0)?;
|
||||
let view = reader.view();
|
||||
let view: View<_, DynReader> = View::from_path(&path)?;
|
||||
let mut options = MovieOptions::default();
|
||||
options.set_overwrite(true);
|
||||
view.save_as_movie("/home/wim/tmp/movie.mp4", &options)?;
|
||||
view.save_as_movie(
|
||||
std::env::home_dir().unwrap().join("tmp/movie.mp4"),
|
||||
&options,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
-487
@@ -1,487 +0,0 @@
|
||||
use crate::axes::Axis;
|
||||
use crate::bioformats;
|
||||
use crate::bioformats::{DebugTools, ImageReader, MetadataTools};
|
||||
use crate::error::Error;
|
||||
use crate::view::View;
|
||||
use ndarray::{Array2, Ix5, s};
|
||||
use num::{FromPrimitive, Zero};
|
||||
use ome_metadata::Ome;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::any::type_name;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use thread_local::ThreadLocal;
|
||||
|
||||
pub fn split_path_and_series<P>(path: P) -> Result<(PathBuf, Option<usize>), Error>
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
let path = path.into();
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.ok_or(Error::InvalidFileName)?
|
||||
.to_str()
|
||||
.ok_or(Error::InvalidFileName)?;
|
||||
if file_name.to_lowercase().starts_with("pos") {
|
||||
if let Some(series) = file_name.get(3..) {
|
||||
if let Ok(series) = series.parse::<usize>() {
|
||||
return Ok((path, Some(series)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((path, None))
|
||||
}
|
||||
|
||||
/// Pixel types (u)int(8/16/32) or float(32/64), (u/i)(64/128) are not included in bioformats
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum PixelType {
|
||||
I8,
|
||||
U8,
|
||||
I16,
|
||||
U16,
|
||||
I32,
|
||||
U32,
|
||||
F32,
|
||||
F64,
|
||||
I64,
|
||||
U64,
|
||||
I128,
|
||||
U128,
|
||||
F128,
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for PixelType {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(PixelType::I8),
|
||||
1 => Ok(PixelType::U8),
|
||||
2 => Ok(PixelType::I16),
|
||||
3 => Ok(PixelType::U16),
|
||||
4 => Ok(PixelType::I32),
|
||||
5 => Ok(PixelType::U32),
|
||||
6 => Ok(PixelType::F32),
|
||||
7 => Ok(PixelType::F64),
|
||||
8 => Ok(PixelType::I64),
|
||||
9 => Ok(PixelType::U64),
|
||||
10 => Ok(PixelType::I128),
|
||||
11 => Ok(PixelType::U128),
|
||||
12 => Ok(PixelType::F128),
|
||||
_ => Err(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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct containing frame data in one of eight pixel types. Cast to `Array2<T>` using try_into.
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Frame {
|
||||
I8(Array2<i8>),
|
||||
U8(Array2<u8>),
|
||||
I16(Array2<i16>),
|
||||
U16(Array2<u16>),
|
||||
I32(Array2<i32>),
|
||||
U32(Array2<u32>),
|
||||
F32(Array2<f32>),
|
||||
F64(Array2<f64>),
|
||||
I64(Array2<i64>),
|
||||
U64(Array2<u64>),
|
||||
I128(Array2<i128>),
|
||||
U128(Array2<u128>),
|
||||
F128(Array2<f64>), // f128 is nightly
|
||||
}
|
||||
|
||||
macro_rules! impl_frame_cast {
|
||||
($($t:tt: $s:ident $(,)?)*) => {
|
||||
$(
|
||||
impl From<Array2<$t>> for Frame {
|
||||
fn from(value: Array2<$t>) -> Self {
|
||||
Frame::$s(value)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_frame_cast! {
|
||||
u8: U8
|
||||
i8: I8
|
||||
i16: I16
|
||||
u16: U16
|
||||
i32: I32
|
||||
u32: U32
|
||||
f32: F32
|
||||
f64: F64
|
||||
i64: I64
|
||||
u64: U64
|
||||
i128: I128
|
||||
u128: U128
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
impl_frame_cast! {
|
||||
usize: UINT32
|
||||
isize: INT32
|
||||
}
|
||||
|
||||
impl<T> TryInto<Array2<T>> for Frame
|
||||
where
|
||||
T: FromPrimitive + Zero + 'static,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<Array2<T>, Self::Error> {
|
||||
let mut err = Ok(());
|
||||
let arr = match self {
|
||||
Frame::I8(v) => v.mapv_into_any(|x| {
|
||||
T::from_i8(x).unwrap_or_else(|| {
|
||||
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
|
||||
T::zero()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::F64(v) | Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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()
|
||||
})
|
||||
}),
|
||||
Frame::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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reader interface to file. Use get_frame to get data.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Reader {
|
||||
#[serde(skip)]
|
||||
image_reader: ThreadLocal<ImageReader>,
|
||||
/// path to file
|
||||
pub path: PathBuf,
|
||||
/// which (if more than 1) of the series in the file to open
|
||||
pub series: usize,
|
||||
/// size x (horizontal)
|
||||
pub size_x: usize,
|
||||
/// size y (vertical)
|
||||
pub size_y: usize,
|
||||
/// size c (# channels)
|
||||
pub size_c: usize,
|
||||
/// size z (# slices)
|
||||
pub size_z: usize,
|
||||
/// size t (# time/frames)
|
||||
pub size_t: usize,
|
||||
/// pixel type ((u)int(8/16/32) or float(32/64))
|
||||
pub pixel_type: PixelType,
|
||||
little_endian: bool,
|
||||
}
|
||||
|
||||
impl Deref for Reader {
|
||||
type Target = ImageReader;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.get_reader().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Reader {
|
||||
fn clone(&self) -> Self {
|
||||
Reader::new(&self.path, self.series).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Reader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Reader")
|
||||
.field("path", &self.path)
|
||||
.field("series", &self.series)
|
||||
.field("size_x", &self.size_x)
|
||||
.field("size_y", &self.size_y)
|
||||
.field("size_c", &self.size_c)
|
||||
.field("size_z", &self.size_z)
|
||||
.field("size_t", &self.size_t)
|
||||
.field("pixel_type", &self.pixel_type)
|
||||
.field("little_endian", &self.little_endian)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader {
|
||||
/// Create a new reader for the image file at a path, and open series #.
|
||||
pub fn new<P>(path: P, series: usize) -> Result<Self, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
DebugTools::set_root_level("ERROR")?;
|
||||
let mut reader = Reader {
|
||||
image_reader: ThreadLocal::default(),
|
||||
path: path.as_ref().to_path_buf(),
|
||||
series,
|
||||
size_x: 0,
|
||||
size_y: 0,
|
||||
size_c: 0,
|
||||
size_z: 0,
|
||||
size_t: 0,
|
||||
pixel_type: PixelType::I8,
|
||||
little_endian: false,
|
||||
};
|
||||
reader.set_reader()?;
|
||||
reader.size_x = reader.get_size_x()? as usize;
|
||||
reader.size_y = reader.get_size_y()? as usize;
|
||||
reader.size_c = reader.get_size_c()? as usize;
|
||||
reader.size_z = reader.get_size_z()? as usize;
|
||||
reader.size_t = reader.get_size_t()? as usize;
|
||||
reader.pixel_type = PixelType::try_from(reader.get_pixel_type()?)?;
|
||||
reader.little_endian = reader.is_little_endian()?;
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
fn get_reader(&self) -> Result<&ImageReader, Error> {
|
||||
self.image_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 = self.ome_xml()?.parse::<Ome>()?;
|
||||
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.size_c)
|
||||
.flat_map(|a| a.to_vec())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Retrieve fame at channel c, slize z and time t.
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
pub 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 = bioformats::ChannelSeparator::new(self)?;
|
||||
let index = channel_separator.get_index(z as i32, c as i32, t as i32)?;
|
||||
channel_separator.open_bytes(index)?
|
||||
} else if self.is_indexed()? {
|
||||
let index = self.get_index(z as i32, c as i32, t as i32)?;
|
||||
self.open_bytes(index)?
|
||||
// TODO: apply LUT
|
||||
// let _bytes_lut = match self.pixel_type {
|
||||
// PixelType::INT8 | PixelType::UINT8 => {
|
||||
// let _lut = self.image_reader.get_8bit_lookup_table()?;
|
||||
// }
|
||||
// PixelType::INT16 | PixelType::UINT16 => {
|
||||
// let _lut = self.image_reader.get_16bit_lookup_table()?;
|
||||
// }
|
||||
// _ => {}
|
||||
// };
|
||||
} else {
|
||||
let index = self.get_index(z as i32, c as i32, t as i32)?;
|
||||
self.open_bytes(index)?
|
||||
};
|
||||
self.bytes_to_frame(bytes)
|
||||
}
|
||||
|
||||
fn bytes_to_frame(&self, bytes: Vec<u8>) -> Result<Frame, Error> {
|
||||
macro_rules! get_frame {
|
||||
($t:tt, <$n:expr) => {
|
||||
Ok(Frame::from(Array2::from_shape_vec(
|
||||
(self.size_y, self.size_x),
|
||||
bytes
|
||||
.chunks($n)
|
||||
.map(|x| $t::from_le_bytes(x.try_into().unwrap()))
|
||||
.collect(),
|
||||
)?))
|
||||
};
|
||||
($t:tt, >$n:expr) => {
|
||||
Ok(Frame::from(Array2::from_shape_vec(
|
||||
(self.size_y, self.size_x),
|
||||
bytes
|
||||
.chunks($n)
|
||||
.map(|x| $t::from_be_bytes(x.try_into().unwrap()))
|
||||
.collect(),
|
||||
)?))
|
||||
};
|
||||
}
|
||||
|
||||
match (&self.pixel_type, self.little_endian) {
|
||||
(PixelType::I8, true) => get_frame!(i8, <1),
|
||||
(PixelType::U8, true) => get_frame!(u8, <1),
|
||||
(PixelType::I16, true) => get_frame!(i16, <2),
|
||||
(PixelType::U16, true) => get_frame!(u16, <2),
|
||||
(PixelType::I32, true) => get_frame!(i32, <4),
|
||||
(PixelType::U32, true) => get_frame!(u32, <4),
|
||||
(PixelType::F32, true) => get_frame!(f32, <4),
|
||||
(PixelType::F64, true) => get_frame!(f64, <8),
|
||||
(PixelType::I64, true) => get_frame!(i64, <8),
|
||||
(PixelType::U64, true) => get_frame!(u64, <8),
|
||||
(PixelType::I128, true) => get_frame!(i128, <16),
|
||||
(PixelType::U128, true) => get_frame!(u128, <16),
|
||||
(PixelType::F128, true) => get_frame!(f64, <8),
|
||||
(PixelType::I8, false) => get_frame!(i8, >1),
|
||||
(PixelType::U8, false) => get_frame!(u8, >1),
|
||||
(PixelType::I16, false) => get_frame!(i16, >2),
|
||||
(PixelType::U16, false) => get_frame!(u16, >2),
|
||||
(PixelType::I32, false) => get_frame!(i32, >4),
|
||||
(PixelType::U32, false) => get_frame!(u32, >4),
|
||||
(PixelType::F32, false) => get_frame!(f32, >4),
|
||||
(PixelType::F64, false) => get_frame!(f64, >8),
|
||||
(PixelType::I64, false) => get_frame!(i64, >8),
|
||||
(PixelType::U64, false) => get_frame!(u64, >8),
|
||||
(PixelType::I128, false) => get_frame!(i128, >16),
|
||||
(PixelType::U128, false) => get_frame!(u128, >16),
|
||||
(PixelType::F128, false) => get_frame!(f64, >8),
|
||||
}
|
||||
}
|
||||
|
||||
/// get a sliceable view on the image file
|
||||
pub fn view(&self) -> View<Ix5> {
|
||||
let slice = s![
|
||||
0..self.size_c,
|
||||
0..self.size_z,
|
||||
0..self.size_t,
|
||||
0..self.size_y,
|
||||
0..self.size_x
|
||||
];
|
||||
View::new(
|
||||
Arc::new(self.clone()),
|
||||
slice.as_ref().to_vec(),
|
||||
vec![Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Reader {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(reader) = self.get_reader() {
|
||||
reader.close().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
+726
@@ -0,0 +1,726 @@
|
||||
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)?
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
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 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;
|
||||
JvmBuilder::new()
|
||||
.skip_setting_native_lib()
|
||||
.with_base_path(class_path.to_str().unwrap())
|
||||
.build()
|
||||
.expect("Failed to build JVM")
|
||||
})
|
||||
}
|
||||
})
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
{
|
||||
DebugTools::set_root_level("ERROR")?;
|
||||
let mut path = path.as_ref().to_path_buf();
|
||||
if path.is_dir() {
|
||||
for file in path.read_dir()?.flatten() {
|
||||
let p = file.path();
|
||||
if file.path().is_file() && (p.extension() == Some("tif".as_ref())) {
|
||||
path = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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>,
|
||||
{
|
||||
DebugTools::set_root_level("ERROR")?;
|
||||
let new = BioFormatsJavaReader {
|
||||
reader: ThreadLocal::default(),
|
||||
path: path.as_ref().to_path_buf(),
|
||||
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"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ome_xml() -> Result<(), Error> {
|
||||
let file = "czi/Experiment-2029.czi";
|
||||
let path = std::env::current_dir()?
|
||||
.join("tests")
|
||||
.join("files")
|
||||
.join(file);
|
||||
let reader = BioFormatsJavaReader::new(&path, 0, 0)?;
|
||||
let xml = reader.get_ome_xml()?;
|
||||
println!("{}", xml);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,483 @@
|
||||
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",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
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",
|
||||
}
|
||||
}
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
use crate::colors::Color;
|
||||
use crate::error::Error;
|
||||
use crate::metadata::Metadata;
|
||||
use crate::reader::PixelType;
|
||||
use crate::stats::MinMax;
|
||||
use crate::view::{Number, View};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use itertools::iproduct;
|
||||
use ndarray::{Array0, Array1, Array2, ArrayD, Dimension};
|
||||
use rayon::prelude::*;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tiffwrite::{Bytes, Colors, Compression, IJTiffFile};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TiffOptions {
|
||||
bar: Option<ProgressStyle>,
|
||||
compression: Compression,
|
||||
colors: Option<Vec<Vec<u8>>>,
|
||||
overwrite: bool,
|
||||
}
|
||||
|
||||
impl Default for TiffOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bar: None,
|
||||
compression: Compression::Zstd(10),
|
||||
colors: None,
|
||||
overwrite: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TiffOptions {
|
||||
pub fn new(
|
||||
bar: bool,
|
||||
compression: Option<Compression>,
|
||||
colors: Vec<String>,
|
||||
overwrite: bool,
|
||||
) -> Result<Self, Error> {
|
||||
let mut options = Self {
|
||||
bar: None,
|
||||
compression: compression.unwrap_or(Compression::Zstd(10)),
|
||||
colors: None,
|
||||
overwrite,
|
||||
};
|
||||
if bar {
|
||||
options.enable_bar()?;
|
||||
}
|
||||
if !colors.is_empty() {
|
||||
options.set_colors(&colors)?;
|
||||
}
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
/// show a progress bar while saving tiff
|
||||
pub fn enable_bar(&mut self) -> Result<(), Error> {
|
||||
self.bar = Some(ProgressStyle::with_template(
|
||||
"{spinner:.green} [{elapsed_precise}, {percent}%] [{wide_bar:.green/lime}] {pos:>7}/{len:7} ({eta_precise}, {per_sec:<5})",
|
||||
)?.progress_chars("▰▱▱"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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<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 size_c = self.size_c();
|
||||
let size_z = self.size_z();
|
||||
let size_t = self.size_t();
|
||||
let mut tiff = IJTiffFile::new(path)?;
|
||||
tiff.set_compression(options.compression.clone());
|
||||
let ome = self.get_ome()?;
|
||||
tiff.px_size = ome.pixel_size()?.map(|i| i / 1e3);
|
||||
tiff.time_interval = ome.time_interval()?.map(|i| i / 1e3);
|
||||
tiff.delta_z = ome.delta_z()?.map(|i| i / 1e3);
|
||||
tiff.comment = Some(self.ome_xml()?);
|
||||
if let Some(mut colors) = options.colors.clone() {
|
||||
while colors.len() < self.size_c {
|
||||
colors.push(vec![255, 255, 255]);
|
||||
}
|
||||
tiff.colors = Colors::Colors(colors);
|
||||
}
|
||||
let tiff = Arc::new(Mutex::new(tiff));
|
||||
if let Some(style) = &options.bar {
|
||||
let bar = ProgressBar::new((size_c as u64) * (size_z as u64) * (size_t as u64))
|
||||
.with_style(style.clone());
|
||||
iproduct!(0..size_c, 0..size_z, 0..size_t)
|
||||
.collect::<Vec<_>>()
|
||||
.into_par_iter()
|
||||
.map(|(c, z, t)| {
|
||||
if let Ok(mut tiff) = tiff.lock() {
|
||||
tiff.save(&self.get_frame::<T, _>(c, z, t)?, c, z, t)?;
|
||||
bar.inc(1);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::TiffLock)
|
||||
}
|
||||
})
|
||||
.collect::<Result<(), Error>>()?;
|
||||
bar.finish();
|
||||
} else {
|
||||
iproduct!(0..size_c, 0..size_z, 0..size_t)
|
||||
.collect::<Vec<_>>()
|
||||
.into_par_iter()
|
||||
.map(|(c, z, t)| {
|
||||
if let Ok(mut tiff) = tiff.lock() {
|
||||
tiff.save(&self.get_frame::<T, _>(c, z, t)?, c, z, t)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::TiffLock)
|
||||
}
|
||||
})
|
||||
.collect::<Result<(), Error>>()?;
|
||||
};
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
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(())
|
||||
}
|
||||
}
|
||||
+414
-156
@@ -1,7 +1,7 @@
|
||||
use crate::axes::{Ax, Axis, Operation, Slice, SliceInfoElemDef, slice_info};
|
||||
use crate::axes::{Ax, Axis, Operation, Shape, Slice, SliceInfoElemDef, slice_info};
|
||||
use crate::error::Error;
|
||||
use crate::metadata::Metadata;
|
||||
use crate::reader::Reader;
|
||||
use crate::readers::{Dimensions, DynReader, Reader};
|
||||
use crate::stats::MinMax;
|
||||
use indexmap::IndexMap;
|
||||
use itertools::{Itertools, iproduct};
|
||||
@@ -14,13 +14,13 @@ use num::{Bounded, FromPrimitive, ToPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::serde_as;
|
||||
use std::any::type_name;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter::Sum;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::{AddAssign, Deref, Div};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::path::Path;
|
||||
|
||||
fn idx_bnd(idx: isize, bnd: isize) -> Result<isize, Error> {
|
||||
if idx < -bnd {
|
||||
@@ -64,9 +64,9 @@ impl<T> Number for T where
|
||||
|
||||
/// sliceable view on an image file
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct View<D: Dimension> {
|
||||
reader: Arc<Reader>,
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct View<D: Dimension, R: Reader = DynReader> {
|
||||
reader: R,
|
||||
/// same order as axes
|
||||
#[serde_as(as = "Vec<SliceInfoElemDef>")]
|
||||
slice: Vec<SliceInfoElem>,
|
||||
@@ -76,8 +76,24 @@ pub struct View<D: Dimension> {
|
||||
dimensionality: PhantomData<D>,
|
||||
}
|
||||
|
||||
impl<D: Dimension> View<D> {
|
||||
pub(crate) fn new(reader: Arc<Reader>, slice: Vec<SliceInfoElem>, axes: Vec<Axis>) -> Self {
|
||||
impl<D, R> Hash for View<D, R>
|
||||
where
|
||||
D: Dimension,
|
||||
R: Reader,
|
||||
{
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.reader.hash(state);
|
||||
self.slice.hash(state);
|
||||
self.axes.hash(state);
|
||||
for (ax, op) in self.operations.iter() {
|
||||
ax.hash(state);
|
||||
op.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Dimension, R: Reader> View<D, R> {
|
||||
pub(crate) fn new(reader: R, slice: Vec<SliceInfoElem>, axes: Vec<Axis>) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
slice,
|
||||
@@ -88,33 +104,34 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new_with_axes(reader: Arc<Reader>, axes: Vec<Axis>) -> Result<Self, Error> {
|
||||
pub(crate) fn new_with_axes(reader: R, axes: Vec<Axis>) -> Result<Self, Error> {
|
||||
let mut slice = Vec::new();
|
||||
let shape = reader.shape();
|
||||
for axis in axes.iter() {
|
||||
match axis {
|
||||
Axis::C => slice.push(SliceInfoElem::Slice {
|
||||
start: 0,
|
||||
end: Some(reader.size_c as isize),
|
||||
end: Some(shape.c as isize),
|
||||
step: 1,
|
||||
}),
|
||||
Axis::Z => slice.push(SliceInfoElem::Slice {
|
||||
start: 0,
|
||||
end: Some(reader.size_z as isize),
|
||||
end: Some(shape.z as isize),
|
||||
step: 1,
|
||||
}),
|
||||
Axis::T => slice.push(SliceInfoElem::Slice {
|
||||
start: 0,
|
||||
end: Some(reader.size_t as isize),
|
||||
end: Some(shape.t as isize),
|
||||
step: 1,
|
||||
}),
|
||||
Axis::Y => slice.push(SliceInfoElem::Slice {
|
||||
start: 0,
|
||||
end: Some(reader.size_y as isize),
|
||||
end: Some(shape.y as isize),
|
||||
step: 1,
|
||||
}),
|
||||
Axis::X => slice.push(SliceInfoElem::Slice {
|
||||
start: 0,
|
||||
end: Some(reader.size_x as isize),
|
||||
end: Some(shape.x as isize),
|
||||
step: 1,
|
||||
}),
|
||||
Axis::New => {
|
||||
@@ -126,11 +143,11 @@ impl<D: Dimension> View<D> {
|
||||
for axis in [Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X] {
|
||||
if !axes.contains(&axis) {
|
||||
let size = match axis {
|
||||
Axis::C => reader.size_c,
|
||||
Axis::Z => reader.size_z,
|
||||
Axis::T => reader.size_t,
|
||||
Axis::Y => reader.size_y,
|
||||
Axis::X => reader.size_x,
|
||||
Axis::C => shape.c,
|
||||
Axis::Z => shape.z,
|
||||
Axis::T => shape.t,
|
||||
Axis::Y => shape.y,
|
||||
Axis::X => shape.x,
|
||||
Axis::New => 1,
|
||||
};
|
||||
if size > 1 {
|
||||
@@ -150,13 +167,13 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
|
||||
/// the file path
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
&self.reader.path
|
||||
pub fn path(&self) -> &Path {
|
||||
self.reader.path()
|
||||
}
|
||||
|
||||
/// the series in the file
|
||||
pub fn series(&self) -> usize {
|
||||
self.reader.series
|
||||
self.reader.series()
|
||||
}
|
||||
|
||||
fn with_operations(mut self, operations: IndexMap<Axis, Operation>) -> Self {
|
||||
@@ -165,7 +182,7 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
|
||||
/// change the dimension into a dynamic dimension
|
||||
pub fn into_dyn(self) -> View<IxDyn> {
|
||||
pub fn into_dyn(self) -> View<IxDyn, R> {
|
||||
View {
|
||||
reader: self.reader,
|
||||
slice: self.slice,
|
||||
@@ -176,7 +193,7 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
|
||||
/// change the dimension into a concrete dimension
|
||||
pub fn into_dimensionality<D2: Dimension>(self) -> Result<View<D2>, Error> {
|
||||
pub fn into_dimensionality<D2: Dimension>(self) -> Result<View<D2, R>, Error> {
|
||||
if let Some(d) = D2::NDIM {
|
||||
if d == self.ndim() {
|
||||
Ok(View {
|
||||
@@ -231,7 +248,7 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
|
||||
/// remove axes of size 1
|
||||
pub fn squeeze(&self) -> Result<View<IxDyn>, Error> {
|
||||
pub fn squeeze(&self) -> Result<View<IxDyn, R>, Error> {
|
||||
let view = self.clone().into_dyn();
|
||||
let slice: Vec<_> = self
|
||||
.shape()
|
||||
@@ -286,14 +303,15 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
|
||||
/// the shape of the view
|
||||
pub fn shape(&self) -> Vec<usize> {
|
||||
let mut shape = Vec::<usize>::new();
|
||||
pub fn shape(&self) -> Shape {
|
||||
let mut shape = Shape::new();
|
||||
for (ax, s) in self.axes.iter().zip(self.slice.iter()) {
|
||||
match s {
|
||||
SliceInfoElem::Slice { start, end, step } => {
|
||||
if !self.operations.contains_key(ax) {
|
||||
if let Some(e) = end {
|
||||
shape.push(((e - start).max(0) / step) as usize);
|
||||
shape.order.push(*ax);
|
||||
shape.set_axis(ax, ((e - start).max(0) / step) as usize);
|
||||
} else {
|
||||
panic!("slice has no end")
|
||||
}
|
||||
@@ -302,7 +320,7 @@ impl<D: Dimension> View<D> {
|
||||
SliceInfoElem::Index(_) => {}
|
||||
SliceInfoElem::NewAxis => {
|
||||
if !self.operations.contains_key(ax) {
|
||||
shape.push(1);
|
||||
shape.order.push(*ax);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,60 +328,6 @@ impl<D: Dimension> View<D> {
|
||||
shape
|
||||
}
|
||||
|
||||
pub fn size_of(&self, axis: Axis) -> usize {
|
||||
if let Some(axis_position) = self.axes.iter().position(|a| *a == axis) {
|
||||
match self.slice[axis_position] {
|
||||
SliceInfoElem::Slice { start, end, step } => {
|
||||
if let Some(e) = end {
|
||||
((e - start).max(0) / step) as usize
|
||||
} else {
|
||||
panic!("slice has no end")
|
||||
}
|
||||
}
|
||||
_ => 1,
|
||||
}
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size_c(&self) -> usize {
|
||||
self.size_of(Axis::C)
|
||||
}
|
||||
|
||||
pub fn size_z(&self) -> usize {
|
||||
self.size_of(Axis::Z)
|
||||
}
|
||||
|
||||
pub fn size_t(&self) -> usize {
|
||||
self.size_of(Axis::T)
|
||||
}
|
||||
|
||||
pub fn size_y(&self) -> usize {
|
||||
self.size_of(Axis::Y)
|
||||
}
|
||||
|
||||
pub fn size_x(&self) -> usize {
|
||||
self.size_of(Axis::X)
|
||||
}
|
||||
|
||||
fn shape_all(&self) -> Vec<usize> {
|
||||
let mut shape = Vec::<usize>::new();
|
||||
for s in self.slice.iter() {
|
||||
match s {
|
||||
SliceInfoElem::Slice { start, end, step } => {
|
||||
if let Some(e) = end {
|
||||
shape.push(((e - start).max(0) / step) as usize);
|
||||
} else {
|
||||
panic!("slice has no end")
|
||||
}
|
||||
}
|
||||
_ => shape.push(1),
|
||||
}
|
||||
}
|
||||
shape
|
||||
}
|
||||
|
||||
/// swap two axes
|
||||
pub fn swap_axes<A: Ax>(&self, axis0: A, axis1: A) -> Result<Self, Error> {
|
||||
let idx0 = axis0.pos_op(&self.axes, &self.slice, &self.op_axes())?;
|
||||
@@ -392,6 +356,37 @@ impl<D: Dimension> View<D> {
|
||||
Ok(View::new(self.reader.clone(), slice, axes).with_operations(self.operations.clone()))
|
||||
}
|
||||
|
||||
/// subset of gives axes will be reordered in given order, only axes in axes will be returned
|
||||
pub fn permute_axes_dyn<A: Ax>(&self, axes: &[A]) -> Result<View<IxDyn, R>, Error> {
|
||||
let idx: Vec<usize> = axes
|
||||
.iter()
|
||||
.map(|a| a.pos_op(&self.axes, &self.slice, &self.op_axes()).unwrap())
|
||||
.collect();
|
||||
let mut jdx = idx.clone();
|
||||
jdx.sort();
|
||||
let mut new_slice = self.slice.to_vec();
|
||||
let mut new_axes = self.axes.clone();
|
||||
let axes = axes.iter().map(|a| a.n()).collect::<HashSet<_>>();
|
||||
for (&i, j) in idx.iter().zip(jdx) {
|
||||
new_slice[j] = self.slice[i];
|
||||
new_axes[j] = self.axes[i];
|
||||
}
|
||||
for (ax, s) in new_axes.iter().zip(new_slice.iter_mut()) {
|
||||
if let SliceInfoElem::Slice { start, end, step } = s
|
||||
&& !axes.contains(&ax.n())
|
||||
{
|
||||
let size = ((end.expect("slice has no end") - *start).max(0) / *step) as usize;
|
||||
if size != 1 {
|
||||
return Err(Error::SizeMismatch(ax.to_string(), size));
|
||||
}
|
||||
*s = SliceInfoElem::Index(*start)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(View::new(self.reader.clone(), new_slice, new_axes)
|
||||
.with_operations(self.operations.clone()))
|
||||
}
|
||||
|
||||
/// reverse the order of the axes
|
||||
pub fn transpose(&self) -> Result<Self, Error> {
|
||||
Ok(View::new(
|
||||
@@ -402,7 +397,11 @@ impl<D: Dimension> View<D> {
|
||||
.with_operations(self.operations.clone()))
|
||||
}
|
||||
|
||||
fn operate<A: Ax>(&self, axis: A, operation: Operation) -> Result<View<D::Smaller>, Error> {
|
||||
pub fn operate<A: Ax>(
|
||||
&self,
|
||||
axis: A,
|
||||
operation: Operation,
|
||||
) -> Result<View<D::Smaller, R>, Error> {
|
||||
let pos = axis.pos_op(&self.axes, &self.slice, &self.op_axes())?;
|
||||
let ax = self.axes[pos];
|
||||
let (axes, slice, operations) = if Axis::New == ax {
|
||||
@@ -430,27 +429,27 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
|
||||
/// maximum along axis
|
||||
pub fn max_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>, Error> {
|
||||
pub fn max_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller, R>, Error> {
|
||||
self.operate(axis, Operation::Max)
|
||||
}
|
||||
|
||||
/// minimum along axis
|
||||
pub fn min_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>, Error> {
|
||||
pub fn min_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller, R>, Error> {
|
||||
self.operate(axis, Operation::Min)
|
||||
}
|
||||
|
||||
/// sum along axis
|
||||
pub fn sum_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>, Error> {
|
||||
pub fn sum_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller, R>, Error> {
|
||||
self.operate(axis, Operation::Sum)
|
||||
}
|
||||
|
||||
/// mean along axis
|
||||
pub fn mean_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller>, Error> {
|
||||
pub fn mean_proj<A: Ax>(&self, axis: A) -> Result<View<D::Smaller, R>, Error> {
|
||||
self.operate(axis, Operation::Mean)
|
||||
}
|
||||
|
||||
/// created a new sliced view
|
||||
pub fn slice<I>(&self, info: I) -> Result<View<I::OutDim>, Error>
|
||||
pub fn slice<I>(&self, info: I) -> Result<View<I::OutDim, R>, Error>
|
||||
where
|
||||
I: SliceArg<D>,
|
||||
{
|
||||
@@ -554,11 +553,7 @@ impl<D: Dimension> View<D> {
|
||||
new_axes.push(*a.expect("axis should exist when slice exists"));
|
||||
r_idx += 1;
|
||||
}
|
||||
_ => {
|
||||
panic!("unreachable");
|
||||
// n_idx += 1;
|
||||
// r_idx += 1;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -575,7 +570,7 @@ impl<D: Dimension> View<D> {
|
||||
|
||||
/// resets axes to cztyx order, with all 5 axes present,
|
||||
/// inserts new axes in place of axes under operation (max_proj etc.)
|
||||
pub fn reset_axes(&self) -> Result<View<Ix5>, Error> {
|
||||
pub fn reset_axes(&self) -> Result<View<Ix5, R>, Error> {
|
||||
let mut axes = Vec::new();
|
||||
let mut slice = Vec::new();
|
||||
|
||||
@@ -603,7 +598,7 @@ impl<D: Dimension> View<D> {
|
||||
|
||||
/// slice, but slice elements are in cztyx order, all cztyx must be given,
|
||||
/// but axes not present in view will be ignored, view axes are reordered in cztyx order
|
||||
pub fn slice_cztyx<I>(&self, info: I) -> Result<View<I::OutDim>, Error>
|
||||
pub fn slice_cztyx<I>(&self, info: I) -> Result<View<I::OutDim, R>, Error>
|
||||
where
|
||||
I: SliceArg<Ix5>,
|
||||
{
|
||||
@@ -644,16 +639,16 @@ impl<D: Dimension> View<D> {
|
||||
Array2<T>: MinMax<Output = Array1<T>>,
|
||||
{
|
||||
let mut op_xy = IndexMap::new();
|
||||
if let Some((&ax, op)) = self.operations.first() {
|
||||
if (ax == Axis::X) || (ax == Axis::Y) {
|
||||
if let Some((&ax, op)) = self.operations.first()
|
||||
&& ((ax == Axis::X) || (ax == Axis::Y))
|
||||
{
|
||||
op_xy.insert(ax, op.clone());
|
||||
if let Some((&ax2, op2)) = self.operations.get_index(1) {
|
||||
if (ax2 == Axis::X) || (ax2 == Axis::Y) {
|
||||
if let Some((&ax2, op2)) = self.operations.get_index(1)
|
||||
&& ((ax2 == Axis::X) || (ax2 == Axis::Y))
|
||||
{
|
||||
op_xy.insert(ax2, op2.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let op_czt = if let Some((&ax, op)) = self.operations.get_index(op_xy.len()) {
|
||||
IndexMap::from([(ax, op.clone())])
|
||||
} else {
|
||||
@@ -686,33 +681,27 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
let mut slice_reader = vec![Slice::empty(); 5];
|
||||
let mut xy_dim = 0usize;
|
||||
let shape = [
|
||||
self.size_c as isize,
|
||||
self.size_z as isize,
|
||||
self.size_t as isize,
|
||||
self.size_y as isize,
|
||||
self.size_x as isize,
|
||||
];
|
||||
let shape_reader = self.reader.shape();
|
||||
for (s, &axis) in self.slice.iter().zip(&self.axes) {
|
||||
match axis {
|
||||
Axis::New => {}
|
||||
_ => match s {
|
||||
SliceInfoElem::Slice { start, end, step } => {
|
||||
if let Axis::X | Axis::Y = axis {
|
||||
if !op_xy.contains_key(&axis) {
|
||||
if let Axis::X | Axis::Y = axis
|
||||
&& !op_xy.contains_key(&axis)
|
||||
{
|
||||
xy_dim += 1;
|
||||
}
|
||||
}
|
||||
slice_reader[axis as usize] = Slice::new(
|
||||
idx_bnd(*start, shape[axis as usize])?,
|
||||
slc_bnd(end.unwrap(), shape[axis as usize])?,
|
||||
idx_bnd(*start, shape_reader[axis] as isize)?,
|
||||
slc_bnd(end.unwrap(), shape_reader[axis] as isize)?,
|
||||
*step,
|
||||
);
|
||||
}
|
||||
SliceInfoElem::Index(j) => {
|
||||
slice_reader[axis as usize] = Slice::new(
|
||||
idx_bnd(*j, shape[axis as usize])?,
|
||||
slc_bnd(*j + 1, shape[axis as usize])?,
|
||||
idx_bnd(*j, shape_reader[axis] as isize)?,
|
||||
slc_bnd(*j + 1, shape_reader[axis] as isize)?,
|
||||
1,
|
||||
);
|
||||
}
|
||||
@@ -737,10 +726,7 @@ impl<D: Dimension> View<D> {
|
||||
} else {
|
||||
ArrayD::<T>::zeros(shape_out.into_dimension())
|
||||
};
|
||||
let size_c = self.reader.size_c as isize;
|
||||
let size_z = self.reader.size_z as isize;
|
||||
let size_t = self.reader.size_t as isize;
|
||||
|
||||
let shape = self.reader.shape();
|
||||
let mut axes_out_idx = [None; 5];
|
||||
for (i, ax) in ax_out.iter().enumerate() {
|
||||
if *ax < Axis::New {
|
||||
@@ -759,9 +745,9 @@ impl<D: Dimension> View<D> {
|
||||
slice[i] = SliceInfoElem::Index(t)
|
||||
};
|
||||
let frame = self.reader.get_frame(
|
||||
(c % size_c) as usize,
|
||||
(z % size_z) as usize,
|
||||
(t % size_t) as usize,
|
||||
(c % shape.c as isize) as usize,
|
||||
(z % shape.z as isize) as usize,
|
||||
(t % shape.t as isize) as usize,
|
||||
)?;
|
||||
|
||||
let arr_frame: Array2<T> = frame.try_into()?;
|
||||
@@ -860,13 +846,13 @@ impl<D: Dimension> View<D> {
|
||||
}
|
||||
}
|
||||
let mut n = 1;
|
||||
for (&ax, size) in self.axes.iter().zip(self.shape_all().iter()) {
|
||||
if let Some(Operation::Mean) = self.operations.get(&ax) {
|
||||
if (ax == Axis::C) || (ax == Axis::Z) || (ax == Axis::T) {
|
||||
for (ax, size) in self.shape().to_hashmap().into_iter() {
|
||||
if ((ax == Axis::C) || (ax == Axis::Z) || (ax == Axis::T))
|
||||
&& let Some(Operation::Mean) = self.operations.get(&ax)
|
||||
{
|
||||
n *= size;
|
||||
}
|
||||
}
|
||||
}
|
||||
let array = if n == 1 {
|
||||
out.take().unwrap()
|
||||
} else {
|
||||
@@ -999,9 +985,10 @@ impl<D: Dimension> View<D> {
|
||||
/// gives a helpful summary of the recorded experiment
|
||||
pub fn summary(&self) -> Result<String, Error> {
|
||||
let mut s = "".to_string();
|
||||
s.push_str(&format!("path/filename: {}\n", self.path.display()));
|
||||
s.push_str(&format!("series/pos: {}\n", self.series));
|
||||
s.push_str(&format!("dtype: {:?}\n", self.pixel_type));
|
||||
s.push_str(&format!("path/filename: {}\n", self.path().display()));
|
||||
s.push_str(&format!("series/pos: {}\n", self.series()));
|
||||
s.push_str(&format!("reader: {}\n", self.reader_name()));
|
||||
s.push_str(&format!("dtype: {:?}\n", self.pixel_type()));
|
||||
let axes = self
|
||||
.axes()
|
||||
.into_iter()
|
||||
@@ -1015,45 +1002,47 @@ impl<D: Dimension> View<D> {
|
||||
.join(" x ");
|
||||
let space = " ".repeat(6usize.saturating_sub(axes.len()));
|
||||
s.push_str(&format!("shape ({}):{}{}\n", axes, space, shape));
|
||||
s.push_str(&self.get_ome()?.summary()?);
|
||||
s.push_str(&self.metadata()?.summary()?);
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Dimension> Deref for View<D> {
|
||||
type Target = Reader;
|
||||
impl<D: Dimension, R: Reader> Deref for View<D, R> {
|
||||
type Target = R;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.reader.as_ref()
|
||||
&self.reader
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, D> TryFrom<View<D>> for Array<T, D>
|
||||
impl<T, D, R> TryFrom<View<D, R>> for Array<T, D>
|
||||
where
|
||||
T: Number,
|
||||
D: Dimension,
|
||||
R: Reader,
|
||||
ArrayD<T>: MinMax<Output = ArrayD<T>>,
|
||||
Array1<T>: MinMax<Output = Array0<T>>,
|
||||
Array2<T>: MinMax<Output = Array1<T>>,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(view: View<D>) -> Result<Self, Self::Error> {
|
||||
fn try_from(view: View<D, R>) -> Result<Self, Self::Error> {
|
||||
view.as_array()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, D> TryFrom<&View<D>> for Array<T, D>
|
||||
impl<T, D, R> TryFrom<&View<D, R>> for Array<T, D>
|
||||
where
|
||||
T: Number,
|
||||
D: Dimension,
|
||||
R: Reader,
|
||||
ArrayD<T>: MinMax<Output = ArrayD<T>>,
|
||||
Array1<T>: MinMax<Output = Array0<T>>,
|
||||
Array2<T>: MinMax<Output = Array1<T>>,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(view: &View<D>) -> Result<Self, Self::Error> {
|
||||
fn try_from(view: &View<D, R>) -> Result<Self, Self::Error> {
|
||||
view.as_array()
|
||||
}
|
||||
}
|
||||
@@ -1068,26 +1057,17 @@ pub trait Item {
|
||||
Array2<T>: MinMax<Output = Array1<T>>;
|
||||
}
|
||||
|
||||
impl View<Ix5> {
|
||||
pub fn from_path<P>(path: P, series: usize) -> Result<Self, Error>
|
||||
impl<R: Reader> View<Ix5, R> {
|
||||
pub fn from_path<P>(path: P) -> Result<Self, Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut path = path.as_ref().to_path_buf();
|
||||
if path.is_dir() {
|
||||
for file in path.read_dir()?.flatten() {
|
||||
let p = file.path();
|
||||
if file.path().is_file() && (p.extension() == Some("tif".as_ref())) {
|
||||
path = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Reader::new(path, series)?.view())
|
||||
let (path, dimensions) = Dimensions::parse_path(path)?;
|
||||
Ok(R::new(path, dimensions.s.unwrap_or(0), dimensions.p.unwrap_or(0))?.view())
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for View<Ix0> {
|
||||
impl<R: Reader> Item for View<Ix0, R> {
|
||||
fn item<T>(&self) -> Result<T, Error>
|
||||
where
|
||||
T: Number,
|
||||
@@ -1099,12 +1079,12 @@ impl Item for View<Ix0> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Dimension> Display for View<D> {
|
||||
impl<D: Dimension, R: Reader> Display for View<D, R> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if let Ok(summary) = self.summary() {
|
||||
write!(f, "{}", summary)
|
||||
} else {
|
||||
write!(f, "{}", self.path.display())
|
||||
write!(f, "{}", self.path().display())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1131,3 +1111,281 @@ macro_rules! to_bytes_vec_impl {
|
||||
to_bytes_vec_impl!(
|
||||
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::axes::Axis;
|
||||
use crate::error::Error;
|
||||
use crate::readers::{DynReader, Reader};
|
||||
use crate::stats::MinMax;
|
||||
use crate::view::Item;
|
||||
use ndarray::{Array, Array4, Array5, NewAxis};
|
||||
use ndarray::{Array2, s};
|
||||
|
||||
fn open(file: &str) -> Result<DynReader, Error> {
|
||||
let path = std::env::current_dir()?
|
||||
.join("tests")
|
||||
.join("files")
|
||||
.join(file);
|
||||
DynReader::new(&path, 0, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view() -> Result<(), Error> {
|
||||
let file = "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
println!("view: {:?}", view);
|
||||
let a = view.slice(s![0, 5, 0, .., ..])?;
|
||||
println!("a: {:?}", a);
|
||||
let b = reader.get_frame(0, 5, 0)?;
|
||||
let c: Array2<isize> = a.try_into()?;
|
||||
println!("c {:?}", c);
|
||||
let d: Array2<isize> = b.try_into()?;
|
||||
println!("d {:?}", d);
|
||||
assert_eq!(c, d);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_shape() -> Result<(), Error> {
|
||||
let file = "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = view.slice(s![0, ..5, 0, .., 100..200])?;
|
||||
let shape = a.shape();
|
||||
assert_eq!(shape.to_vec(), vec![5, 1024, 100]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_new_axis() -> Result<(), Error> {
|
||||
let file = "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = Array5::<u8>::zeros((1, 9, 1, 1024, 1024));
|
||||
let a = a.slice(s![0, ..5, 0, NewAxis, 100..200, ..]);
|
||||
let v = view.slice(s![0, ..5, 0, NewAxis, 100..200, ..])?;
|
||||
assert_eq!(v.shape().to_vec(), a.shape());
|
||||
let a = a.slice(s![NewAxis, .., .., NewAxis, .., .., NewAxis]);
|
||||
let v = v.slice(s![NewAxis, .., .., NewAxis, .., .., NewAxis])?;
|
||||
assert_eq!(v.shape().to_vec(), a.shape());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_permute_axes() -> Result<(), Error> {
|
||||
let file = "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let s = view.shape();
|
||||
let mut a = Array5::<u8>::zeros((s[0], s[1], s[2], s[3], s[4]));
|
||||
assert_eq!(view.shape().to_vec(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
|
||||
let view = view.swap_axes(Axis::C, Axis::Z)?;
|
||||
a.swap_axes(0, 1);
|
||||
assert_eq!(view.shape().to_vec(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
let view = view.permute_axes(&[Axis::X, Axis::Z, Axis::Y])?;
|
||||
let a = a.permuted_axes([4, 1, 2, 0, 3]);
|
||||
assert_eq!(view.shape().to_vec(), a.shape());
|
||||
let b: Array5<usize> = view.clone().try_into()?;
|
||||
assert_eq!(b.shape(), a.shape());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! test_max {
|
||||
($($name:ident: $b:expr $(,)?)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() -> Result<(), Error> {
|
||||
let file = "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let array: Array5<usize> = view.clone().try_into()?;
|
||||
let view = view.max_proj($b)?;
|
||||
let a: Array4<usize> = view.clone().try_into()?;
|
||||
let b = array.max($b)?;
|
||||
assert_eq!(a.shape(), b.shape());
|
||||
assert_eq!(a, b);
|
||||
Ok(())
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_max! {
|
||||
max_c: 0
|
||||
max_z: 1
|
||||
max_t: 2
|
||||
max_y: 3
|
||||
max_x: 4
|
||||
}
|
||||
|
||||
macro_rules! test_index {
|
||||
($($name:ident: $b:expr $(,)?)*) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() -> Result<(), Error> {
|
||||
let file = "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let v4: Array<usize, _> = view.slice($b)?.try_into()?;
|
||||
let a5: Array5<usize> = reader.view().try_into()?;
|
||||
let a4 = a5.slice($b).to_owned();
|
||||
assert_eq!(a4, v4);
|
||||
Ok(())
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
test_index! {
|
||||
index_0: s![.., .., .., .., ..]
|
||||
index_1: s![0, .., .., .., ..]
|
||||
index_2: s![.., 0, .., .., ..]
|
||||
index_3: s![.., .., 0, .., ..]
|
||||
index_4: s![.., .., .., 0, ..]
|
||||
index_5: s![.., .., .., .., 0]
|
||||
index_6: s![0, 0, .., .., ..]
|
||||
index_7: s![0, .., 0, .., ..]
|
||||
index_8: s![0, .., .., 0, ..]
|
||||
index_9: s![0, .., .., .., 0]
|
||||
index_a: s![.., 0, 0, .., ..]
|
||||
index_b: s![.., 0, .., 0, ..]
|
||||
index_c: s![.., 0, .., .., 0]
|
||||
index_d: s![.., .., 0, 0, ..]
|
||||
index_e: s![.., .., 0, .., 0]
|
||||
index_f: s![.., .., .., 0, 0]
|
||||
index_g: s![0, 0, 0, .., ..]
|
||||
index_h: s![0, 0, .., 0, ..]
|
||||
index_i: s![0, 0, .., .., 0]
|
||||
index_j: s![0, .., 0, 0, ..]
|
||||
index_k: s![0, .., 0, .., 0]
|
||||
index_l: s![0, .., .., 0, 0]
|
||||
index_m: s![0, 0, 0, 0, ..]
|
||||
index_n: s![0, 0, 0, .., 0]
|
||||
index_o: s![0, 0, .., 0, 0]
|
||||
index_p: s![0, .., 0, 0, 0]
|
||||
index_q: s![.., 0, 0, 0, 0]
|
||||
index_r: s![0, 0, 0, 0, 0]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dyn_view() -> Result<(), Error> {
|
||||
let file = "tiffseq/YTL1841B2-2-1_1hr_DMSO_galinduction_1";
|
||||
let reader = open(file)?;
|
||||
let a = reader.view().into_dyn();
|
||||
let b = a.max_proj(1)?;
|
||||
let c = b.slice(s![0, 0, .., ..])?;
|
||||
let d = c.as_array::<usize>()?;
|
||||
assert_eq!(d.shape(), [1024, 1024]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item() -> Result<(), Error> {
|
||||
let file = "czi/1xp53-01-AP1.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let a = view.slice(s![.., 0, 0, 0, 0])?;
|
||||
let b = a.slice(s![0])?;
|
||||
let item = b.item::<usize>()?;
|
||||
assert_eq!(item, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slice_cztyx() -> Result<(), Error> {
|
||||
let file = "czi/1xp53-01-AP1.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view().max_proj(Axis::Z)?.into_dyn();
|
||||
println!("view.axes: {:?}", view.get_axes());
|
||||
println!("view.slice: {:?}", view.get_slice());
|
||||
let r = view.reset_axes()?;
|
||||
println!("r.axes: {:?}", r.get_axes());
|
||||
println!("r.slice: {:?}", r.get_slice());
|
||||
let a = view.slice_cztyx(s![0, 0, 0, .., ..])?;
|
||||
println!("a.axes: {:?}", a.get_axes());
|
||||
println!("a.slice: {:?}", a.get_slice());
|
||||
assert_eq!(a.axes(), [Axis::Y, Axis::X]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_axes() -> Result<(), Error> {
|
||||
let file = "czi/1xp53-01-AP1.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view().max_proj(Axis::Z)?;
|
||||
let view = view.reset_axes()?;
|
||||
assert_eq!(view.axes(), [Axis::C, Axis::New, Axis::T, Axis::Y, Axis::X]);
|
||||
let a = view.as_array::<f64>()?;
|
||||
assert_eq!(a.ndim(), 5);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_axes2() -> Result<(), Error> {
|
||||
let file = "czi/Experiment-2029.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view().squeeze()?;
|
||||
let a = view.reset_axes()?;
|
||||
assert_eq!(a.axes(), [Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_axes3() -> Result<(), Error> {
|
||||
let file = "czi/Experiment-2029.czi";
|
||||
let reader = open(file)?;
|
||||
let view4 = reader.view().squeeze()?;
|
||||
let view = view4.max_proj(Axis::Z)?.into_dyn();
|
||||
let slice = view.slice_cztyx(s![0, .., .., .., ..])?.into_dyn();
|
||||
let a = slice.as_array::<u16>()?;
|
||||
assert_eq!(slice.shape().to_vec(), [1, 10, 1280, 1280]);
|
||||
assert_eq!(a.shape(), [1, 10, 1280, 1280]);
|
||||
let r = slice.reset_axes()?;
|
||||
let b = r.as_array::<u16>()?;
|
||||
assert_eq!(r.shape().to_vec(), [1, 1, 10, 1280, 1280]);
|
||||
assert_eq!(b.shape(), [1, 1, 10, 1280, 1280]);
|
||||
let q = slice.max_proj(Axis::C)?.max_proj(Axis::T)?;
|
||||
let c = q.as_array::<f64>()?;
|
||||
assert_eq!(q.shape().to_vec(), [1, 1280, 1280]);
|
||||
assert_eq!(c.shape().to_vec(), [1, 1280, 1280]);
|
||||
let p = q.reset_axes()?;
|
||||
let d = p.as_array::<u16>()?;
|
||||
println!("axes: {:?}", p.get_axes());
|
||||
println!("operations: {:?}", p.get_operations());
|
||||
println!("slice: {:?}", p.get_slice());
|
||||
assert_eq!(p.shape().to_vec(), [1, 1, 1, 1280, 1280]);
|
||||
assert_eq!(d.shape(), [1, 1, 1, 1280, 1280]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max() -> Result<(), Error> {
|
||||
let file = "czi/Experiment-2029.czi";
|
||||
let reader = open(file)?;
|
||||
let view = reader.view();
|
||||
let m = view.max_proj(Axis::T)?;
|
||||
let a = m.as_array::<u16>()?;
|
||||
assert_eq!(m.shape().to_vec(), [2, 1, 1280, 1280]);
|
||||
assert_eq!(a.shape(), [2, 1, 1280, 1280]);
|
||||
let mc = view.max_proj(Axis::C)?;
|
||||
let a = mc.as_array::<u16>()?;
|
||||
assert_eq!(mc.shape().to_vec(), [1, 10, 1280, 1280]);
|
||||
assert_eq!(a.shape(), [1, 10, 1280, 1280]);
|
||||
let mz = mc.max_proj(Axis::Z)?;
|
||||
let a = mz.as_array::<u16>()?;
|
||||
assert_eq!(mz.shape().to_vec(), [10, 1280, 1280]);
|
||||
assert_eq!(a.shape(), [10, 1280, 1280]);
|
||||
let mt = mz.max_proj(Axis::T)?;
|
||||
let a = mt.as_array::<u16>()?;
|
||||
assert_eq!(mt.shape().to_vec(), [1280, 1280]);
|
||||
assert_eq!(a.shape(), [1280, 1280]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user