- implement shape in Rust

- implement more readers
- fix downloading of bioformats jar
- (mostly) compatible with python version
This commit is contained in:
w.pomp
2026-07-13 13:40:34 +02:00
parent 705ca16379
commit ff7cd562af
36 changed files with 7946 additions and 2843 deletions
+66
View File
@@ -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
+114
View File
@@ -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
+2
View File
@@ -73,3 +73,5 @@ docs/_build/
.python-version .python-version
/tests/files/* /tests/files/*
AGENTS.md
.agentbridge
+47 -33
View File
@@ -1,20 +1,19 @@
[package] [package]
name = "ndbioimage" name = "ndbioimage"
version = "0.1.0" version = "0.2.0"
edition = "2024" edition = "2024"
rust-version = "1.87.0" rust-version = "1.94.0"
authors = ["Wim Pomp <w.pomp@nki.nl>"] authors = ["Wim Pomp <w.pomp@nki.nl>", "opencode"]
license = "MIT" license = "MIT OR Apache-2.0"
description = "Read bio image formats using the bio-formats java package." description = "Read bio image formats using the bio-formats java package."
homepage = "https://git.wimpomp.nl/wim/ndbioimage/src/branch/rs" homepage = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs"
repository = "https://git.wimpomp.nl/wim/ndbioimage/src/branch/rs" repository = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs"
documentation = "https://docs.rs/ndbioimage" documentation = "https://docs.rs/ndbioimage"
readme = "README.md" readme = "README.md"
keywords = ["bioformats", "imread", "ndarray", "metadata"] keywords = ["bioformats", "imread", "ndarray", "metadata"]
categories = ["multimedia::images", "science"] categories = ["multimedia::images", "science"]
exclude = ["/tests"] exclude = ["/tests"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]
name = "ndbioimage" name = "ndbioimage"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
@@ -22,50 +21,65 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
color-eyre = { version = "0.6", optional = true } 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 } ffmpeg-sidecar = { version = "2", optional = true }
itertools = "0.14" itertools = "0.15"
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.18", features = ["rayon"], optional = true } 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"] } ndarray = { version = "0.17", features = ["serde"] }
num = "0.4" num = "0.4"
numpy = { version = "0.28", optional = true } numpy = { version = "0.29", optional = true }
ordered-float = "5" 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 } rayon = { version = "1", optional = true }
serde = { version = "1", features = ["rc"] } regex = "1"
serde_json = { version = "1", optional = true } serde = { version = "1", features = ["rc", "derive"] }
serde_yaml = { version = "0.9", optional = true }
serde_with = "3" serde_with = "3"
tiffwrite = { version = ">=2026.1.1, <2026.2.0", optional = true} strum = { version = "0.28", features = ["derive"] }
thread_local = "1"
ome-metadata = "0.4"
lazy_static = "1"
thiserror = "2" thiserror = "2"
tiff = { version = "0.11", features = ["zstd"], optional = true }
[dependencies.pyo3] tiffwrite = { version = "2026.6.0", optional = true }
version = "0.28" tokio = { version = "1", features = ["rt", "rt-multi-thread"], optional = true }
features = ["extension-module", "abi3-py310", "eyre", "generate-import-lib"] thread_local = { version = "1", optional = true }
optional = true xmltree = { version = "0.12", optional = true }
bioformats = { version = "0.1", optional = true }
[dev-dependencies] [dev-dependencies]
downloader = "0.2"
rayon = "1" rayon = "1"
regex = "1" regex = "1"
reqwest = { version = "0.13", features = ["blocking"] }
[build-dependencies] [build-dependencies]
j4rs = "0.24" j4rs = "0.25"
ffmpeg-sidecar = "2" ffmpeg-sidecar = "2"
retry = "2" retry = "2"
toml = "1"
[features] [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 = [] gpl-formats = []
# Enables python ffi using pyO3 python = ["dep:pyo3", "dep:numpy", "dep:color-eyre", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"]
python = ["dep:pyo3", "dep:numpy", "dep:serde_json", "dep:color-eyre"] czi = ["dep:libczirw-sys", "dep:xmltree", "dep:thread_local"]
# Enables writing as tiff bioformats_rust = ["dep:bioformats", "dep:thread_local"]
tiff = ["dep:tiffwrite", "dep:indicatif", "dep:rayon"] bioformats_java = ["dep:j4rs", "dep:thread_local", "dep:downloader"]
# Enables writing as mp4 using ffmpeg tiffwrite = ["dep:tiffwrite", "dep:indicatif", "dep:console", "dep:rayon"]
movie = ["dep:ffmpeg-sidecar"] 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] [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
-19
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
# ndbioimage # ndbioimage
[![Pytest](https://github.com/wimpomp/ndbioimage/actions/workflows/pytest.yml/badge.svg)](https://github.com/wimpomp/ndbioimage/actions/workflows/pytest.yml) [![Pytest](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml/badge.svg)](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml)
## Work in progress ## Work in progress
Rust rewrite of python version. Read bio image formats using the bio-formats java package. Rust rewrite of python version. Read bio image formats using the bio-formats java package.
+158 -112
View File
@@ -1,131 +1,177 @@
#[cfg(not(feature = "python"))] #[cfg(feature = "bioformats_java")]
use j4rs::{JvmBuilder, MavenArtifact, MavenArtifactRepo, MavenSettings, errors::J4RsError}; const BIOFORMATS_VERSION: &str = "8.5.0";
#[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 = "python")] #[cfg(feature = "bioformats_java")]
use j4rs::Jvm; mod bioformats {
use std::error::Error;
use std::fmt::{Display, Formatter};
#[cfg(feature = "movie")] #[derive(Clone, Debug)]
use ffmpeg_sidecar::download::auto_download; pub(crate) enum BuildError {
J4rsVersionNotFound,
}
#[cfg(not(feature = "python"))] impl Display for BuildError {
#[derive(Clone, Debug)] fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> {
enum BuildError { match self {
BioFormatsNotDownloaded, Self::J4rsVersionNotFound => write!(fmt, "J4rsVersion not found in Cargo.lock"),
} }
}
}
#[cfg(not(feature = "python"))] impl Error for BuildError {}
impl Display for BuildError {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> { fn get_j4rs_version() -> Result<String, Box<dyn Error>> {
write!(fmt, "Bioformats package not downloaded") 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"))] #[cfg(all(not(feature = "python"), feature = "bioformats_java"))]
impl Error for BuildError {} 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>> { #[derive(Clone, Debug)]
pub(crate) enum BuildError {
BioFormatsNotDownloaded,
}
impl Display for BuildError {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> {
write!(fmt, "Bioformats package not downloaded")
}
}
impl Error for BuildError {}
pub(crate) fn build() -> Result<(), Box<dyn Error>> {
retry(
Exponential::from_millis(1000).map(delay::jitter).take(4),
deploy_java_artifacts,
)?;
let path = default_jassets_path()?;
if !path.join("bioformats_package-8.5.0.jar").exists() {
Err(BuildError::BioFormatsNotDownloaded)?;
}
Ok(())
}
fn default_jassets_path() -> Result<std::path::PathBuf, J4RsError> {
let is_build_script = std::env::var("OUT_DIR").is_ok();
let mut start_path = if is_build_script {
std::path::PathBuf::from(std::env::var("OUT_DIR")?)
} else {
std::env::current_exe()?
};
start_path = std::fs::canonicalize(start_path)?;
while start_path.pop() {
for entry in std::fs::read_dir(&start_path)? {
let path = entry?.path();
if path.file_name().map(|x| x == "jassets").unwrap_or(false) {
return Ok(path);
}
}
}
Err(J4RsError::GeneralError(
"Can not find jassets directory".to_string(),
))
}
fn deploy_java_artifacts() -> Result<(), J4RsError> {
let jvm = JvmBuilder::new()
.skip_setting_native_lib()
.with_maven_settings(MavenSettings::new(vec![MavenArtifactRepo::from(
"openmicroscopy::https://artifacts.openmicroscopy.org/artifactory/ome.releases",
)]))
.build()?;
jvm.deploy_artifact(&MavenArtifact::from("ome:bioformats_package:8.5.0"))?;
#[cfg(feature = "gpl-formats")]
jvm.deploy_artifact(&MavenArtifact::from("ome:formats-gpl:8.5.0"))?;
Ok(())
}
}
#[cfg(all(feature = "python", feature = "bioformats_java"))]
mod python_bioformats {
pub(crate) fn build() -> Result<(), Box<dyn std::error::Error>> {
let py_src_path = std::env::current_dir()?.join("py").join("ndbioimage");
let py_jassets_path = py_src_path.join("jassets");
let py_deps_path = py_src_path.join("deps");
if py_jassets_path.exists() {
std::fs::remove_dir_all(&py_jassets_path)?;
}
if py_deps_path.exists() {
std::fs::remove_dir_all(&py_deps_path)?;
}
j4rs::Jvm::copy_j4rs_libs_under(py_src_path.to_str().unwrap())?;
Ok(())
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=build.rs");
if std::env::var("DOCS_RS").is_err() { if std::env::var("DOCS_RS").is_err() {
#[cfg(feature = "movie")] #[cfg(feature = "movie")]
auto_download()?; ffmpeg_sidecar::download::auto_download()?;
#[cfg(not(feature = "python"))] #[cfg(feature = "bioformats_java")]
{ bioformats::build()?;
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() {
Err(BuildError::BioFormatsNotDownloaded)?;
}
}
#[cfg(feature = "python")] #[cfg(all(not(feature = "python"), feature = "bioformats_java"))]
{ no_python_bioformats::build()?;
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())?; #[cfg(all(feature = "python", feature = "bioformats_java"))]
python_bioformats::build()?;
// 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(()) Ok(())
} }
#[cfg(not(feature = "python"))]
fn default_jassets_path() -> Result<PathBuf, J4RsError> {
let is_build_script = env::var("OUT_DIR").is_ok();
let mut start_path = if is_build_script {
PathBuf::from(env::var("OUT_DIR")?)
} else {
env::current_exe()?
};
start_path = fs::canonicalize(start_path)?;
while start_path.pop() {
for entry in std::fs::read_dir(&start_path)? {
let path = entry?.path();
if path.file_name().map(|x| x == "jassets").unwrap_or(false) {
return Ok(path);
}
}
}
Err(J4RsError::GeneralError(
"Can not find jassets directory".to_owned(),
))
}
#[cfg(not(feature = "python"))]
fn deploy_java_artifacts() -> Result<(), J4RsError> {
let jvm = JvmBuilder::new()
.skip_setting_native_lib()
.with_maven_settings(MavenSettings::new(vec![MavenArtifactRepo::from(
"openmicroscopy::https://artifacts.openmicroscopy.org/artifactory/ome.releases",
)]))
.build()?;
jvm.deploy_artifact(&MavenArtifact::from("ome:bioformats_package:8.3.0"))?;
#[cfg(feature = "gpl-formats")]
jvm.deploy_artifact(&MavenArtifact::from("ome:formats-gpl:8.3.0"))?;
Ok(())
}
+40 -582
View File
@@ -1,26 +1,28 @@
from __future__ import annotations from __future__ import annotations
import os import os
import sys
import warnings 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 importlib.metadata import version
from itertools import product
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Optional, Sequence, TypeVar from typing import TypeVar
import numpy as np import numpy as np
from numpy.typing import ArrayLike, DTypeLike from numpy.typing import ArrayLike
from tiffwrite import FrameInfo, IJTiffParallel
from tqdm.auto import tqdm 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 . 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 from .transforms import Transform, Transforms # noqa: F401
try:
import ome_metadata # noqa
except ModuleNotFoundError:
sys.modules["ome_metadata"] = rs.ome_metadata # noqa
try: try:
__version__ = version(Path(__file__).parent.name) __version__ = version(Path(__file__).parent.name)
except Exception: # noqa except Exception: # noqa
@@ -38,587 +40,43 @@ warnings.filterwarnings("ignore", "Reference to unknown ID")
Number = int | float | np.integer | np.floating Number = int | float | np.integer | np.floating
for dep_file in (Path(__file__).parent / "deps").glob("*_"): # backwards compatibility
dep_file.rename(str(dep_file)[:-1]) class JVMException(Exception):
pass
if not list((Path(__file__).parent / "jassets").glob("bioformats*.jar")):
rs.download_bioformats(True)
# backwards compatibility
class ReaderNotFoundError(Exception): class ReaderNotFoundError(Exception):
pass pass
class TransformTiff(IJTiffParallel): def ndbioimage_generate_stub():
"""transform frames in a parallel process to speed up saving""" if len(sys.argv) > 1:
path = Path(sys.argv[1]).resolve()
def __init__(self, image: Imread, *args: Any, **kwargs: Any) -> None: else:
self.image = image path = Path.cwd().resolve()
super().__init__(*args, **kwargs) if (path / "py" / "ndbioimage" / "__init__.py").exists():
rs.generate_stub(str(path)) # noqa
def parallel(self, frame: tuple[int, int, int]) -> tuple[FrameInfo]: else:
return ((np.asarray(self.image(*frame)), 0, 0, 0),) 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(
class DequeDict(OrderedDict): path / "py" / "ndbioimage" / "ndbioimage_rs.pyi"
def __init__(self, maxlen: int = None, *args: Any, **kwargs: Any) -> None: )
self.maxlen = maxlen (path / "py" / "ndbioimage" / "ndbioimage_rs").rmdir()
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
R = TypeVar("R") R = TypeVar("R")
def try_default(fun: Callable[..., R], default: Any, *args: Any, **kwargs: Any) -> R: def get_positions(path: str | Path) -> set[int]:
try: return Imread.get_available_series(path)
return fun(*args, **kwargs)
except Exception: # noqa
return default
class Shape(tuple): def is_noise(self, volume: ArrayLike = None) -> bool:
def __new__(cls, shape: Sequence[int] | Shape, axes: str = "yxczt") -> Shape: """True if volume only has noise"""
if isinstance(shape, Shape): if volume is None:
axes = shape.axes # type: ignore volume = self
new = super().__new__(cls, shape) fft = np.fft.fftn(volume)
new.axes = axes.lower() corr = np.fft.fftshift(np.fft.ifftn(fft * fft.conj()).real / np.sum(volume**2)) # type: ignore
return new # type: ignore return 1 - corr[tuple([0] * corr.ndim)] < 0.006 # 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:
"""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)
+419
View File
@@ -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: ...
-39
View File
@@ -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
View File
+17 -29
View File
@@ -1,55 +1,43 @@
[build-system] [build-system]
requires = ["maturin>=1.8,<2.0"] requires = ["maturin>=1.9.4,<2.0"]
build-backend = "maturin" build-backend = "maturin"
[project] [project]
name = "ndbioimage" name = "ndbioimage"
description = "Bio image reading, metadata and some affine registration." version = "2027.0.0"
authors = [
{ name = "W. Pomp", email = "w.pomp@nki.nl" }
]
license = "MIT"
readme = "README.md"
keywords = ["bioformats", "imread", "numpy", "metadata"]
requires-python = ">=3.10" requires-python = ">=3.10"
classifiers = [ classifiers = [
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Rust", "Programming Language :: Rust",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "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 = [ dependencies = [
"numpy", "numpy >= 1.16.0",
"tiffwrite",
"ome-metadata >= 0.2.1",
"tqdm",
] ]
[project.optional-dependencies] [project.optional-dependencies]
test = ["pytest"] test = ["pytest"]
write = ["matplotlib", "scikit-video"]
[project.urls]
Repository = "https://github.com/wimpomp/ndbioimage/tree/rs"
[project.scripts] [project.scripts]
ndbioimage = "ndbioimage:main" ndbioimage = "ndbioimage:ndbioimage_rs.main"
ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub"
[tool.pytest.ini_options]
filterwarnings = ["ignore:::(colorcet)"]
[tool.maturin] [tool.maturin]
python-source = "py" python-source = "py"
features = ["pyo3/extension-module", "python", "gpl-formats"] features = ["python", "bioformats_java", "tiffwrite", "czi"]
module-name = "ndbioimage.ndbioimage_rs" module-name = "ndbioimage.ndbioimage_rs"
exclude = ["py/ndbioimage/jassets/*", "py/ndbioimage/deps/*"] include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"]
strip = true strip = true
[tool.isort] [tool.isort]
+205 -34
View File
@@ -3,9 +3,11 @@ use crate::stats::MinMax;
use ndarray::{Array, Dimension, Ix2, SliceInfo, SliceInfoElem}; use ndarray::{Array, Dimension, Ix2, SliceInfo, SliceInfoElem};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{DeserializeAs, SerializeAs}; use serde_with::{DeserializeAs, SerializeAs};
use std::collections::HashMap;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher}; 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 /// a trait to find axis indices from any object
pub trait Ax { pub trait Ax {
@@ -25,13 +27,17 @@ pub trait Ax {
} }
/// Enum for CZTYX axes or a new axis /// 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 { pub enum Axis {
C, C,
Z, Z,
T, T,
Y, Y,
X, X,
#[strum(serialize = "N")]
New, 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 { impl Ax for Axis {
fn n(&self) -> usize { fn n(&self) -> usize {
*self as usize *self as usize
@@ -134,8 +110,11 @@ impl Ax for usize {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(
pub(crate) enum Operation { Clone, Debug, Serialize, Deserialize, EnumString, AsRefStr, Display, PartialEq, Eq, Hash,
)]
#[strum(ascii_case_insensitive)]
pub enum Operation {
Max, Max,
Min, Min,
Sum, Sum,
@@ -249,3 +228,195 @@ impl IntoIterator for &Slice {
self.clone() 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 => (),
}
}
}
-230
View File
@@ -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);
}
+94
View File
@@ -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
View File
@@ -1,171 +1,166 @@
use crate::error::Error; use crate::error::Error;
use lazy_static::lazy_static; use phf::phf_map;
use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
lazy_static! { pub static COLORS: phf::Map<&'static str, &'static str> = phf_map! {
static ref COLORS: HashMap<String, String> = { "b" => "#0000FF",
HashMap::from([ "g" => "#008000",
("b".to_string(), "#0000FF".to_string()), "r" => "#FF0000",
("g".to_string(), "#008000".to_string()), "c" => "#00BFBF",
("r".to_string(), "#FF0000".to_string()), "m" => "#BF00BF",
("c".to_string(), "#00BFBF".to_string()), "y" => "#BFBF00",
("m".to_string(), "#BF00BF".to_string()), "k" => "#000000",
("y".to_string(), "#BFBF00".to_string()), "w" => "#FFFFFF",
("k".to_string(), "#000000".to_string()), "aliceblue" => "#F0F8FF",
("w".to_string(), "#FFFFFF".to_string()), "antiquewhite" => "#FAEBD7",
("aliceblue".to_string(), "#F0F8FF".to_string()), "aqua" => "#00FFFF",
("antiquewhite".to_string(), "#FAEBD7".to_string()), "aquamarine" => "#7FFFD4",
("aqua".to_string(), "#00FFFF".to_string()), "azure" => "#F0FFFF",
("aquamarine".to_string(), "#7FFFD4".to_string()), "beige" => "#F5F5DC",
("azure".to_string(), "#F0FFFF".to_string()), "bisque" => "#FFE4C4",
("beige".to_string(), "#F5F5DC".to_string()), "black" => "#000000",
("bisque".to_string(), "#FFE4C4".to_string()), "blanchedalmond" => "#FFEBCD",
("black".to_string(), "#000000".to_string()), "blue" => "#0000FF",
("blanchedalmond".to_string(), "#FFEBCD".to_string()), "blueviolet" => "#8A2BE2",
("blue".to_string(), "#0000FF".to_string()), "brown" => "#A52A2A",
("blueviolet".to_string(), "#8A2BE2".to_string()), "burlywood" => "#DEB887",
("brown".to_string(), "#A52A2A".to_string()), "cadetblue" => "#5F9EA0",
("burlywood".to_string(), "#DEB887".to_string()), "chartreuse" => "#7FFF00",
("cadetblue".to_string(), "#5F9EA0".to_string()), "chocolate" => "#D2691E",
("chartreuse".to_string(), "#7FFF00".to_string()), "coral" => "#FF7F50",
("chocolate".to_string(), "#D2691E".to_string()), "cornflowerblue" => "#6495ED",
("coral".to_string(), "#FF7F50".to_string()), "cornsilk" => "#FFF8DC",
("cornflowerblue".to_string(), "#6495ED".to_string()), "crimson" => "#DC143C",
("cornsilk".to_string(), "#FFF8DC".to_string()), "cyan" => "#00FFFF",
("crimson".to_string(), "#DC143C".to_string()), "darkblue" => "#00008B",
("cyan".to_string(), "#00FFFF".to_string()), "darkcyan" => "#008B8B",
("darkblue".to_string(), "#00008B".to_string()), "darkgoldenrod" => "#B8860B",
("darkcyan".to_string(), "#008B8B".to_string()), "darkgray" => "#A9A9A9",
("darkgoldenrod".to_string(), "#B8860B".to_string()), "darkgreen" => "#006400",
("darkgray".to_string(), "#A9A9A9".to_string()), "darkgrey" => "#A9A9A9",
("darkgreen".to_string(), "#006400".to_string()), "darkkhaki" => "#BDB76B",
("darkgrey".to_string(), "#A9A9A9".to_string()), "darkmagenta" => "#8B008B",
("darkkhaki".to_string(), "#BDB76B".to_string()), "darkolivegreen" => "#556B2F",
("darkmagenta".to_string(), "#8B008B".to_string()), "darkorange" => "#FF8C00",
("darkolivegreen".to_string(), "#556B2F".to_string()), "darkorchid" => "#9932CC",
("darkorange".to_string(), "#FF8C00".to_string()), "darkred" => "#8B0000",
("darkorchid".to_string(), "#9932CC".to_string()), "darksalmon" => "#E9967A",
("darkred".to_string(), "#8B0000".to_string()), "darkseagreen" => "#8FBC8F",
("darksalmon".to_string(), "#E9967A".to_string()), "darkslateblue" => "#483D8B",
("darkseagreen".to_string(), "#8FBC8F".to_string()), "darkslategray" => "#2F4F4F",
("darkslateblue".to_string(), "#483D8B".to_string()), "darkslategrey" => "#2F4F4F",
("darkslategray".to_string(), "#2F4F4F".to_string()), "darkturquoise" => "#00CED1",
("darkslategrey".to_string(), "#2F4F4F".to_string()), "darkviolet" => "#9400D3",
("darkturquoise".to_string(), "#00CED1".to_string()), "deeppink" => "#FF1493",
("darkviolet".to_string(), "#9400D3".to_string()), "deepskyblue" => "#00BFFF",
("deeppink".to_string(), "#FF1493".to_string()), "dimgray" => "#696969",
("deepskyblue".to_string(), "#00BFFF".to_string()), "dimgrey" => "#696969",
("dimgray".to_string(), "#696969".to_string()), "dodgerblue" => "#1E90FF",
("dimgrey".to_string(), "#696969".to_string()), "firebrick" => "#B22222",
("dodgerblue".to_string(), "#1E90FF".to_string()), "floralwhite" => "#FFFAF0",
("firebrick".to_string(), "#B22222".to_string()), "forestgreen" => "#228B22",
("floralwhite".to_string(), "#FFFAF0".to_string()), "fuchsia" => "#FF00FF",
("forestgreen".to_string(), "#228B22".to_string()), "gainsboro" => "#DCDCDC",
("fuchsia".to_string(), "#FF00FF".to_string()), "ghostwhite" => "#F8F8FF",
("gainsboro".to_string(), "#DCDCDC".to_string()), "gold" => "#FFD700",
("ghostwhite".to_string(), "#F8F8FF".to_string()), "goldenrod" => "#DAA520",
("gold".to_string(), "#FFD700".to_string()), "gray" => "#808080",
("goldenrod".to_string(), "#DAA520".to_string()), "green" => "#008000",
("gray".to_string(), "#808080".to_string()), "greenyellow" => "#ADFF2F",
("green".to_string(), "#008000".to_string()), "grey" => "#808080",
("greenyellow".to_string(), "#ADFF2F".to_string()), "honeydew" => "#F0FFF0",
("grey".to_string(), "#808080".to_string()), "hotpink" => "#FF69B4",
("honeydew".to_string(), "#F0FFF0".to_string()), "indianred" => "#CD5C5C",
("hotpink".to_string(), "#FF69B4".to_string()), "indigo" => "#4B0082",
("indianred".to_string(), "#CD5C5C".to_string()), "ivory" => "#FFFFF0",
("indigo".to_string(), "#4B0082".to_string()), "khaki" => "#F0E68C",
("ivory".to_string(), "#FFFFF0".to_string()), "lavender" => "#E6E6FA",
("khaki".to_string(), "#F0E68C".to_string()), "lavenderblush" => "#FFF0F5",
("lavender".to_string(), "#E6E6FA".to_string()), "lawngreen" => "#7CFC00",
("lavenderblush".to_string(), "#FFF0F5".to_string()), "lemonchiffon" => "#FFFACD",
("lawngreen".to_string(), "#7CFC00".to_string()), "lightblue" => "#ADD8E6",
("lemonchiffon".to_string(), "#FFFACD".to_string()), "lightcoral" => "#F08080",
("lightblue".to_string(), "#ADD8E6".to_string()), "lightcyan" => "#E0FFFF",
("lightcoral".to_string(), "#F08080".to_string()), "lightgoldenrodyellow" => "#FAFAD2",
("lightcyan".to_string(), "#E0FFFF".to_string()), "lightgray" => "#D3D3D3",
("lightgoldenrodyellow".to_string(), "#FAFAD2".to_string()), "lightgreen" => "#90EE90",
("lightgray".to_string(), "#D3D3D3".to_string()), "lightgrey" => "#D3D3D3",
("lightgreen".to_string(), "#90EE90".to_string()), "lightpink" => "#FFB6C1",
("lightgrey".to_string(), "#D3D3D3".to_string()), "lightsalmon" => "#FFA07A",
("lightpink".to_string(), "#FFB6C1".to_string()), "lightseagreen" => "#20B2AA",
("lightsalmon".to_string(), "#FFA07A".to_string()), "lightskyblue" => "#87CEFA",
("lightseagreen".to_string(), "#20B2AA".to_string()), "lightslategray" => "#778899",
("lightskyblue".to_string(), "#87CEFA".to_string()), "lightslategrey" => "#778899",
("lightslategray".to_string(), "#778899".to_string()), "lightsteelblue" => "#B0C4DE",
("lightslategrey".to_string(), "#778899".to_string()), "lightyellow" => "#FFFFE0",
("lightsteelblue".to_string(), "#B0C4DE".to_string()), "lime" => "#00FF00",
("lightyellow".to_string(), "#FFFFE0".to_string()), "limegreen" => "#32CD32",
("lime".to_string(), "#00FF00".to_string()), "linen" => "#FAF0E6",
("limegreen".to_string(), "#32CD32".to_string()), "magenta" => "#FF00FF",
("linen".to_string(), "#FAF0E6".to_string()), "maroon" => "#800000",
("magenta".to_string(), "#FF00FF".to_string()), "mediumaquamarine" => "#66CDAA",
("maroon".to_string(), "#800000".to_string()), "mediumblue" => "#0000CD",
("mediumaquamarine".to_string(), "#66CDAA".to_string()), "mediumorchid" => "#BA55D3",
("mediumblue".to_string(), "#0000CD".to_string()), "mediumpurple" => "#9370DB",
("mediumorchid".to_string(), "#BA55D3".to_string()), "mediumseagreen" => "#3CB371",
("mediumpurple".to_string(), "#9370DB".to_string()), "mediumslateblue" => "#7B68EE",
("mediumseagreen".to_string(), "#3CB371".to_string()), "mediumspringgreen" => "#00FA9A",
("mediumslateblue".to_string(), "#7B68EE".to_string()), "mediumturquoise" => "#48D1CC",
("mediumspringgreen".to_string(), "#00FA9A".to_string()), "mediumvioletred" => "#C71585",
("mediumturquoise".to_string(), "#48D1CC".to_string()), "midnightblue" => "#191970",
("mediumvioletred".to_string(), "#C71585".to_string()), "mintcream" => "#F5FFFA",
("midnightblue".to_string(), "#191970".to_string()), "mistyrose" => "#FFE4E1",
("mintcream".to_string(), "#F5FFFA".to_string()), "moccasin" => "#FFE4B5",
("mistyrose".to_string(), "#FFE4E1".to_string()), "navajowhite" => "#FFDEAD",
("moccasin".to_string(), "#FFE4B5".to_string()), "navy" => "#000080",
("navajowhite".to_string(), "#FFDEAD".to_string()), "oldlace" => "#FDF5E6",
("navy".to_string(), "#000080".to_string()), "olive" => "#808000",
("oldlace".to_string(), "#FDF5E6".to_string()), "olivedrab" => "#6B8E23",
("olive".to_string(), "#808000".to_string()), "orange" => "#FFA500",
("olivedrab".to_string(), "#6B8E23".to_string()), "orangered" => "#FF4500",
("orange".to_string(), "#FFA500".to_string()), "orchid" => "#DA70D6",
("orangered".to_string(), "#FF4500".to_string()), "palegoldenrod" => "#EEE8AA",
("orchid".to_string(), "#DA70D6".to_string()), "palegreen" => "#98FB98",
("palegoldenrod".to_string(), "#EEE8AA".to_string()), "paleturquoise" => "#AFEEEE",
("palegreen".to_string(), "#98FB98".to_string()), "palevioletred" => "#DB7093",
("paleturquoise".to_string(), "#AFEEEE".to_string()), "papayawhip" => "#FFEFD5",
("palevioletred".to_string(), "#DB7093".to_string()), "peachpuff" => "#FFDAB9",
("papayawhip".to_string(), "#FFEFD5".to_string()), "peru" => "#CD853F",
("peachpuff".to_string(), "#FFDAB9".to_string()), "pink" => "#FFC0CB",
("peru".to_string(), "#CD853F".to_string()), "plum" => "#DDA0DD",
("pink".to_string(), "#FFC0CB".to_string()), "powderblue" => "#B0E0E6",
("plum".to_string(), "#DDA0DD".to_string()), "purple" => "#800080",
("powderblue".to_string(), "#B0E0E6".to_string()), "rebeccapurple" => "#663399",
("purple".to_string(), "#800080".to_string()), "red" => "#FF0000",
("rebeccapurple".to_string(), "#663399".to_string()), "rosybrown" => "#BC8F8F",
("red".to_string(), "#FF0000".to_string()), "royalblue" => "#4169E1",
("rosybrown".to_string(), "#BC8F8F".to_string()), "saddlebrown" => "#8B4513",
("royalblue".to_string(), "#4169E1".to_string()), "salmon" => "#FA8072",
("saddlebrown".to_string(), "#8B4513".to_string()), "sandybrown" => "#F4A460",
("salmon".to_string(), "#FA8072".to_string()), "seagreen" => "#2E8B57",
("sandybrown".to_string(), "#F4A460".to_string()), "seashell" => "#FFF5EE",
("seagreen".to_string(), "#2E8B57".to_string()), "sienna" => "#A0522D",
("seashell".to_string(), "#FFF5EE".to_string()), "silver" => "#C0C0C0",
("sienna".to_string(), "#A0522D".to_string()), "skyblue" => "#87CEEB",
("silver".to_string(), "#C0C0C0".to_string()), "slateblue" => "#6A5ACD",
("skyblue".to_string(), "#87CEEB".to_string()), "slategray" => "#708090",
("slateblue".to_string(), "#6A5ACD".to_string()), "slategrey" => "#708090",
("slategray".to_string(), "#708090".to_string()), "snow" => "#FFFAFA",
("slategrey".to_string(), "#708090".to_string()), "springgreen" => "#00FF7F",
("snow".to_string(), "#FFFAFA".to_string()), "steelblue" => "#4682B4",
("springgreen".to_string(), "#00FF7F".to_string()), "tan" => "#D2B48C",
("steelblue".to_string(), "#4682B4".to_string()), "teal" => "#008080",
("tan".to_string(), "#D2B48C".to_string()), "thistle" => "#D8BFD8",
("teal".to_string(), "#008080".to_string()), "tomato" => "#FF6347",
("thistle".to_string(), "#D8BFD8".to_string()), "turquoise" => "#40E0D0",
("tomato".to_string(), "#FF6347".to_string()), "violet" => "#EE82EE",
("turquoise".to_string(), "#40E0D0".to_string()), "wheat" => "#F5DEB3",
("violet".to_string(), "#EE82EE".to_string()), "white" => "#FFFFFF",
("wheat".to_string(), "#F5DEB3".to_string()), "whitesmoke" => "#F5F5F5",
("white".to_string(), "#FFFFFF".to_string()), "yellow" => "#FFFF00",
("whitesmoke".to_string(), "#F5F5F5".to_string()), "yellowgreen" => "#9ACD32",
("yellow".to_string(), "#FFFF00".to_string()), };
("yellowgreen".to_string(), "#9ACD32".to_string()),
])
};
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Color { pub struct Color {
+52 -2
View File
@@ -6,6 +6,7 @@ pub enum Error {
IO(#[from] std::io::Error), IO(#[from] std::io::Error),
#[error(transparent)] #[error(transparent)]
Shape(#[from] ndarray::ShapeError), Shape(#[from] ndarray::ShapeError),
#[cfg(feature = "bioformats_java")]
#[error(transparent)] #[error(transparent)]
J4rs(#[from] j4rs::errors::J4RsError), J4rs(#[from] j4rs::errors::J4RsError),
#[error(transparent)] #[error(transparent)]
@@ -14,12 +15,47 @@ pub enum Error {
ParseIntError(#[from] std::num::ParseIntError), ParseIntError(#[from] std::num::ParseIntError),
#[error(transparent)] #[error(transparent)]
Ome(#[from] ome_metadata::error::Error), 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)] #[error(transparent)]
TemplateError(#[from] indicatif::style::TemplateError), TemplateError(#[from] indicatif::style::TemplateError),
#[cfg(feature = "tiff")] #[cfg(feature = "tiffwrite")]
#[error(transparent)] #[error(transparent)]
TiffWrite(#[from] tiffwrite::error::Error), 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}")] #[error("invalid axis: {0}")]
InvalidAxis(String), InvalidAxis(String),
#[error("axis {0} not found in axes {1}")] #[error("axis {0} not found in axes {1}")]
@@ -29,6 +65,8 @@ pub enum Error {
#[error("file already exists {0}")] #[error("file already exists {0}")]
FileAlreadyExists(String), FileAlreadyExists(String),
#[error("could not download ffmpeg: {0}")] #[error("could not download ffmpeg: {0}")]
FfmpegDownload(String),
#[error("FFmpeg error: {0}")]
Ffmpeg(String), Ffmpeg(String),
#[error("index {0} out of bounds {1}")] #[error("index {0} out of bounds {1}")]
OutOfBounds(isize, isize), OutOfBounds(isize, isize),
@@ -52,6 +90,8 @@ pub enum Error {
InvalidAttenuation(String), InvalidAttenuation(String),
#[error("not a valid file name")] #[error("not a valid file name")]
InvalidFileName, InvalidFileName,
#[error("file has no parent")]
NoParent,
#[error("unknown pixel type {0}")] #[error("unknown pixel type {0}")]
UnknownPixelType(String), UnknownPixelType(String),
#[error("no mean")] #[error("no mean")]
@@ -62,4 +102,14 @@ pub enum Error {
NotImplemented(String), NotImplemented(String),
#[error("cannot parse: {0}")] #[error("cannot parse: {0}")]
Parse(String), 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
View File
@@ -1,89 +1,271 @@
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
mod bioformats;
pub mod axes; pub mod axes;
pub mod metadata;
#[cfg(feature = "python")] #[cfg(feature = "python")]
mod py; mod py;
pub mod reader;
pub mod stats; pub mod stats;
pub mod view; pub mod view;
pub mod colors; pub mod colors;
pub mod error; pub mod error;
pub mod metadata;
#[cfg(feature = "movie")] #[cfg(feature = "movie")]
pub mod movie; pub mod movie;
#[cfg(feature = "tiff")] pub mod readers;
pub mod tiff; #[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)] #[cfg(test)]
mod tests { mod tests {
use crate::axes::Axis;
use crate::error::Error; use crate::error::Error;
use crate::reader::{Frame, Reader}; use crate::readers::{DynReader, Frame, Reader};
use crate::stats::MinMax; use ndarray::Array2;
use crate::view::Item;
use downloader::{Download, Downloader};
use ndarray::{Array, Array4, Array5, NewAxis};
use ndarray::{Array2, s};
use rayon::prelude::*; use rayon::prelude::*;
fn open(file: &str) -> Result<Reader, Error> { fn open(file: &str) -> Result<DynReader, Error> {
let path = std::env::current_dir()? let path = std::env::current_dir()?
.join("tests") .join("tests")
.join("files") .join("files")
.join(file); .join(file);
Reader::new(&path, 0) DynReader::new(&path, 0, 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(())
} }
fn get_pixel_type(file: &str) -> Result<String, Error> { fn get_pixel_type(file: &str) -> Result<String, Error> {
let reader = open(file)?; let reader = open(file)?;
Ok(format!( Ok(format!(
"file: {}, pixel type: {:?}", "file: {}, pixel type: {:?}",
file, reader.pixel_type file,
reader.pixel_type()
)) ))
} }
@@ -94,9 +276,9 @@ mod tests {
#[test] #[test]
fn read_ser() -> Result<(), Error> { fn read_ser() -> Result<(), Error> {
let file = "Experiment-2029.czi"; let file = "czi/Experiment-2029.czi";
let reader = open(file)?; let reader = open(file)?;
println!("size: {}, {}", reader.size_y, reader.size_y); println!("shape: {}", reader.shape());
let frame = reader.get_frame(0, 0, 0)?; let frame = reader.get_frame(0, 0, 0)?;
if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) { if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
println!("{:?}", arr); println!("{:?}", arr);
@@ -108,7 +290,7 @@ mod tests {
#[test] #[test]
fn read_par() -> Result<(), Error> { 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 let pixel_type = files
.into_par_iter() .into_par_iter()
.map(|file| get_pixel_type(file).unwrap()) .map(|file| get_pixel_type(file).unwrap())
@@ -119,7 +301,7 @@ mod tests {
#[test] #[test]
fn read_frame_par() -> Result<(), Error> { 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 let frames = files
.into_par_iter() .into_par_iter()
.map(|file| get_frame(file).unwrap()) .map(|file| get_frame(file).unwrap())
@@ -127,288 +309,4 @@ mod tests {
println!("{:?}", frames); println!("{:?}", frames);
Ok(()) 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
View File
@@ -1,194 +1,3 @@
use clap::{Parser, Subcommand}; pub(crate) fn main() -> Result<(), ndbioimage::error::Error> {
#[cfg(feature = "movie")] ndbioimage::main::main(None)
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(())
} }
+59 -22
View File
@@ -1,17 +1,19 @@
use crate::axes::Axis; use crate::axes::Axis;
use crate::colors::Color; use crate::colors::Color;
use crate::error::Error; use crate::error::Error;
use crate::reader::PixelType; use crate::readers::{PixelType, Reader};
use crate::view::View; use crate::view::View;
use console::Term;
use ffmpeg_sidecar::command::FfmpegCommand; use ffmpeg_sidecar::command::FfmpegCommand;
use ffmpeg_sidecar::download::auto_download; use ffmpeg_sidecar::download::auto_download;
use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel}; use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel};
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use itertools::Itertools; use itertools::Itertools;
use ndarray::{Array2, Array3, Dimension, IxDyn, s, stack}; use ndarray::{Array2, Array3, Dimension, IxDyn, s, stack};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use std::thread; use std::time::Duration;
pub struct MovieOptions { pub struct MovieOptions {
velocity: f64, 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 let s = tyx
.as_array::<f64>()? .as_array::<f64>()?
.iter() .iter()
@@ -136,9 +138,25 @@ fn cframe(frame: Array2<f64>, color: &[u8], a: f64, b: f64) -> Array3<f64> {
stack(ndarray::Axis(2), &view).unwrap() 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 where
D: Dimension, D: Dimension,
R: Reader,
Self: 'static,
{ {
pub fn save_as_movie<P>(&self, path: P, options: &MovieOptions) -> Result<(), Error> pub fn save_as_movie<P>(&self, path: P, options: &MovieOptions) -> Result<(), Error>
where where
@@ -211,7 +229,7 @@ where
let ab = if options.no_scaling { let ab = if options.no_scaling {
vec![ vec![
match view.pixel_type { match view.pixel_type() {
PixelType::I8 => (i8::MIN as f64, i8::MAX as f64), PixelType::I8 => (i8::MIN as f64, i8::MAX as f64),
PixelType::U8 => (u8::MIN as f64, u8::MAX as f64), PixelType::U8 => (u8::MIN as f64, u8::MAX as f64),
PixelType::I16 => (i16::MIN as f64, i16::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), PixelType::U64 => (u64::MIN as f64, u64::MAX as f64),
_ => (0.0, 1.0), _ => (0.0, 1.0),
}; };
view.size_c view.shape().c
] ]
} else { } else {
(0..size_c) (0..size_c)
@@ -233,13 +251,16 @@ where
.collect::<Result<Vec<_>, Error>>()? .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 { for t in 0..size_t {
let mut frame = Array3::<f64>::zeros((size_y, size_x, 3)); let mut frame = Array3::<f64>::zeros((size_y, size_x, 3));
for c in 0..size_c { for c in 0..size_c {
frame = frame frame = frame
+ cframe( + cframe(
view.get_frame(c, 0, t).unwrap(), view.get_frame(c, 0, t)?,
&colors[c], &colors[c],
ab[c].0, ab[c].0,
ab[c].1 / brightness[c], 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 frame = (frame.clamp(0.0, 1.0) * 255.0).round().mapv(|i| i as u8);
let bytes: Vec<_> = frame.flatten().into_iter().collect(); 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 rt.block_on(progress_task)??;
.iter() rt.block_on(write_task)??;
.map_err(|e| Error::Ffmpeg(e.to_string()))? bar.finish();
.for_each(|e| match e {
FfmpegEvent::Log(LogLevel::Error, e) => println!("Error: {}", e),
FfmpegEvent::Progress(p) => println!("Progress: {} / 00:00:15", p.time),
_ => {}
});
Ok(()) Ok(())
} }
} }
@@ -266,20 +299,24 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::reader::Reader; use crate::readers::DynReader;
use crate::view::View;
#[cfg(any(feature = "czi", feature = "bioformats_java"))]
#[test] #[test]
fn movie() -> Result<(), Error> { fn movie() -> Result<(), Error> {
let file = "1xp53-01-AP1.czi"; let file = "czi/1xp53-01-AP1.czi";
let path = std::env::current_dir()? let path = std::env::current_dir()?
.join("tests") .join("tests")
.join("files") .join("files")
.join(file); .join(file);
let reader = Reader::new(&path, 0)?; let view: View<_, DynReader> = View::from_path(&path)?;
let view = reader.view();
let mut options = MovieOptions::default(); let mut options = MovieOptions::default();
options.set_overwrite(true); 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(()) Ok(())
} }
} }
+1266 -198
View File
File diff suppressed because it is too large Load Diff
-487
View File
@@ -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
View File
@@ -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)?
})
}
}
+612
View File
@@ -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(())
}
}
+325
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+483
View File
@@ -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",
}
}
+635
View File
@@ -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
View File
@@ -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(())
}
}
+325
View File
@@ -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(())
}
}
+418 -160
View File
@@ -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::error::Error;
use crate::metadata::Metadata; use crate::metadata::Metadata;
use crate::reader::Reader; use crate::readers::{Dimensions, DynReader, Reader};
use crate::stats::MinMax; use crate::stats::MinMax;
use indexmap::IndexMap; use indexmap::IndexMap;
use itertools::{Itertools, iproduct}; use itertools::{Itertools, iproduct};
@@ -14,13 +14,13 @@ use num::{Bounded, FromPrimitive, ToPrimitive, Zero};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
use std::any::type_name; use std::any::type_name;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
use std::iter::Sum; use std::iter::Sum;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{AddAssign, Deref, Div}; use std::ops::{AddAssign, Deref, Div};
use std::path::{Path, PathBuf}; use std::path::Path;
use std::sync::Arc;
fn idx_bnd(idx: isize, bnd: isize) -> Result<isize, Error> { fn idx_bnd(idx: isize, bnd: isize) -> Result<isize, Error> {
if idx < -bnd { if idx < -bnd {
@@ -64,9 +64,9 @@ impl<T> Number for T where
/// sliceable view on an image file /// sliceable view on an image file
#[serde_as] #[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct View<D: Dimension> { pub struct View<D: Dimension, R: Reader = DynReader> {
reader: Arc<Reader>, reader: R,
/// same order as axes /// same order as axes
#[serde_as(as = "Vec<SliceInfoElemDef>")] #[serde_as(as = "Vec<SliceInfoElemDef>")]
slice: Vec<SliceInfoElem>, slice: Vec<SliceInfoElem>,
@@ -76,8 +76,24 @@ pub struct View<D: Dimension> {
dimensionality: PhantomData<D>, dimensionality: PhantomData<D>,
} }
impl<D: Dimension> View<D> { impl<D, R> Hash for View<D, R>
pub(crate) fn new(reader: Arc<Reader>, slice: Vec<SliceInfoElem>, axes: Vec<Axis>) -> Self { 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 { Self {
reader, reader,
slice, slice,
@@ -88,33 +104,34 @@ impl<D: Dimension> View<D> {
} }
#[allow(dead_code)] #[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 mut slice = Vec::new();
let shape = reader.shape();
for axis in axes.iter() { for axis in axes.iter() {
match axis { match axis {
Axis::C => slice.push(SliceInfoElem::Slice { Axis::C => slice.push(SliceInfoElem::Slice {
start: 0, start: 0,
end: Some(reader.size_c as isize), end: Some(shape.c as isize),
step: 1, step: 1,
}), }),
Axis::Z => slice.push(SliceInfoElem::Slice { Axis::Z => slice.push(SliceInfoElem::Slice {
start: 0, start: 0,
end: Some(reader.size_z as isize), end: Some(shape.z as isize),
step: 1, step: 1,
}), }),
Axis::T => slice.push(SliceInfoElem::Slice { Axis::T => slice.push(SliceInfoElem::Slice {
start: 0, start: 0,
end: Some(reader.size_t as isize), end: Some(shape.t as isize),
step: 1, step: 1,
}), }),
Axis::Y => slice.push(SliceInfoElem::Slice { Axis::Y => slice.push(SliceInfoElem::Slice {
start: 0, start: 0,
end: Some(reader.size_y as isize), end: Some(shape.y as isize),
step: 1, step: 1,
}), }),
Axis::X => slice.push(SliceInfoElem::Slice { Axis::X => slice.push(SliceInfoElem::Slice {
start: 0, start: 0,
end: Some(reader.size_x as isize), end: Some(shape.x as isize),
step: 1, step: 1,
}), }),
Axis::New => { Axis::New => {
@@ -126,11 +143,11 @@ impl<D: Dimension> View<D> {
for axis in [Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X] { for axis in [Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X] {
if !axes.contains(&axis) { if !axes.contains(&axis) {
let size = match axis { let size = match axis {
Axis::C => reader.size_c, Axis::C => shape.c,
Axis::Z => reader.size_z, Axis::Z => shape.z,
Axis::T => reader.size_t, Axis::T => shape.t,
Axis::Y => reader.size_y, Axis::Y => shape.y,
Axis::X => reader.size_x, Axis::X => shape.x,
Axis::New => 1, Axis::New => 1,
}; };
if size > 1 { if size > 1 {
@@ -150,13 +167,13 @@ impl<D: Dimension> View<D> {
} }
/// the file path /// the file path
pub fn path(&self) -> &PathBuf { pub fn path(&self) -> &Path {
&self.reader.path self.reader.path()
} }
/// the series in the file /// the series in the file
pub fn series(&self) -> usize { pub fn series(&self) -> usize {
self.reader.series self.reader.series()
} }
fn with_operations(mut self, operations: IndexMap<Axis, Operation>) -> Self { 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 /// change the dimension into a dynamic dimension
pub fn into_dyn(self) -> View<IxDyn> { pub fn into_dyn(self) -> View<IxDyn, R> {
View { View {
reader: self.reader, reader: self.reader,
slice: self.slice, slice: self.slice,
@@ -176,7 +193,7 @@ impl<D: Dimension> View<D> {
} }
/// change the dimension into a concrete dimension /// 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 let Some(d) = D2::NDIM {
if d == self.ndim() { if d == self.ndim() {
Ok(View { Ok(View {
@@ -231,7 +248,7 @@ impl<D: Dimension> View<D> {
} }
/// remove axes of size 1 /// 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 view = self.clone().into_dyn();
let slice: Vec<_> = self let slice: Vec<_> = self
.shape() .shape()
@@ -286,14 +303,15 @@ impl<D: Dimension> View<D> {
} }
/// the shape of the view /// the shape of the view
pub fn shape(&self) -> Vec<usize> { pub fn shape(&self) -> Shape {
let mut shape = Vec::<usize>::new(); let mut shape = Shape::new();
for (ax, s) in self.axes.iter().zip(self.slice.iter()) { for (ax, s) in self.axes.iter().zip(self.slice.iter()) {
match s { match s {
SliceInfoElem::Slice { start, end, step } => { SliceInfoElem::Slice { start, end, step } => {
if !self.operations.contains_key(ax) { if !self.operations.contains_key(ax) {
if let Some(e) = end { 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 { } else {
panic!("slice has no end") panic!("slice has no end")
} }
@@ -302,7 +320,7 @@ impl<D: Dimension> View<D> {
SliceInfoElem::Index(_) => {} SliceInfoElem::Index(_) => {}
SliceInfoElem::NewAxis => { SliceInfoElem::NewAxis => {
if !self.operations.contains_key(ax) { if !self.operations.contains_key(ax) {
shape.push(1); shape.order.push(*ax);
} }
} }
} }
@@ -310,60 +328,6 @@ impl<D: Dimension> View<D> {
shape 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 /// swap two axes
pub fn swap_axes<A: Ax>(&self, axis0: A, axis1: A) -> Result<Self, Error> { 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())?; 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())) 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 /// reverse the order of the axes
pub fn transpose(&self) -> Result<Self, Error> { pub fn transpose(&self) -> Result<Self, Error> {
Ok(View::new( Ok(View::new(
@@ -402,7 +397,11 @@ impl<D: Dimension> View<D> {
.with_operations(self.operations.clone())) .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 pos = axis.pos_op(&self.axes, &self.slice, &self.op_axes())?;
let ax = self.axes[pos]; let ax = self.axes[pos];
let (axes, slice, operations) = if Axis::New == ax { let (axes, slice, operations) = if Axis::New == ax {
@@ -430,27 +429,27 @@ impl<D: Dimension> View<D> {
} }
/// maximum along axis /// 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) self.operate(axis, Operation::Max)
} }
/// minimum along axis /// 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) self.operate(axis, Operation::Min)
} }
/// sum along axis /// 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) self.operate(axis, Operation::Sum)
} }
/// mean along axis /// 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) self.operate(axis, Operation::Mean)
} }
/// created a new sliced view /// 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 where
I: SliceArg<D>, I: SliceArg<D>,
{ {
@@ -554,11 +553,7 @@ impl<D: Dimension> View<D> {
new_axes.push(*a.expect("axis should exist when slice exists")); new_axes.push(*a.expect("axis should exist when slice exists"));
r_idx += 1; r_idx += 1;
} }
_ => { _ => unreachable!(),
panic!("unreachable");
// n_idx += 1;
// r_idx += 1;
}
}, },
} }
} }
@@ -575,7 +570,7 @@ impl<D: Dimension> View<D> {
/// resets axes to cztyx order, with all 5 axes present, /// resets axes to cztyx order, with all 5 axes present,
/// inserts new axes in place of axes under operation (max_proj etc.) /// 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 axes = Vec::new();
let mut slice = 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, /// 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 /// 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 where
I: SliceArg<Ix5>, I: SliceArg<Ix5>,
{ {
@@ -644,14 +639,14 @@ impl<D: Dimension> View<D> {
Array2<T>: MinMax<Output = Array1<T>>, Array2<T>: MinMax<Output = Array1<T>>,
{ {
let mut op_xy = IndexMap::new(); let mut op_xy = IndexMap::new();
if let Some((&ax, op)) = self.operations.first() { if let Some((&ax, op)) = self.operations.first()
if (ax == Axis::X) || (ax == Axis::Y) { && ((ax == Axis::X) || (ax == Axis::Y))
op_xy.insert(ax, op.clone()); {
if let Some((&ax2, op2)) = self.operations.get_index(1) { op_xy.insert(ax, op.clone());
if (ax2 == Axis::X) || (ax2 == Axis::Y) { if let Some((&ax2, op2)) = self.operations.get_index(1)
op_xy.insert(ax2, op2.clone()); && ((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()) { let op_czt = if let Some((&ax, op)) = self.operations.get_index(op_xy.len()) {
@@ -686,33 +681,27 @@ impl<D: Dimension> View<D> {
} }
let mut slice_reader = vec![Slice::empty(); 5]; let mut slice_reader = vec![Slice::empty(); 5];
let mut xy_dim = 0usize; let mut xy_dim = 0usize;
let shape = [ let shape_reader = self.reader.shape();
self.size_c as isize,
self.size_z as isize,
self.size_t as isize,
self.size_y as isize,
self.size_x as isize,
];
for (s, &axis) in self.slice.iter().zip(&self.axes) { for (s, &axis) in self.slice.iter().zip(&self.axes) {
match axis { match axis {
Axis::New => {} Axis::New => {}
_ => match s { _ => match s {
SliceInfoElem::Slice { start, end, step } => { SliceInfoElem::Slice { start, end, step } => {
if let Axis::X | Axis::Y = axis { if let Axis::X | Axis::Y = axis
if !op_xy.contains_key(&axis) { && !op_xy.contains_key(&axis)
xy_dim += 1; {
} xy_dim += 1;
} }
slice_reader[axis as usize] = Slice::new( slice_reader[axis as usize] = Slice::new(
idx_bnd(*start, shape[axis as usize])?, idx_bnd(*start, shape_reader[axis] as isize)?,
slc_bnd(end.unwrap(), shape[axis as usize])?, slc_bnd(end.unwrap(), shape_reader[axis] as isize)?,
*step, *step,
); );
} }
SliceInfoElem::Index(j) => { SliceInfoElem::Index(j) => {
slice_reader[axis as usize] = Slice::new( slice_reader[axis as usize] = Slice::new(
idx_bnd(*j, shape[axis as usize])?, idx_bnd(*j, shape_reader[axis] as isize)?,
slc_bnd(*j + 1, shape[axis as usize])?, slc_bnd(*j + 1, shape_reader[axis] as isize)?,
1, 1,
); );
} }
@@ -737,10 +726,7 @@ impl<D: Dimension> View<D> {
} else { } else {
ArrayD::<T>::zeros(shape_out.into_dimension()) ArrayD::<T>::zeros(shape_out.into_dimension())
}; };
let size_c = self.reader.size_c as isize; let shape = self.reader.shape();
let size_z = self.reader.size_z as isize;
let size_t = self.reader.size_t as isize;
let mut axes_out_idx = [None; 5]; let mut axes_out_idx = [None; 5];
for (i, ax) in ax_out.iter().enumerate() { for (i, ax) in ax_out.iter().enumerate() {
if *ax < Axis::New { if *ax < Axis::New {
@@ -759,9 +745,9 @@ impl<D: Dimension> View<D> {
slice[i] = SliceInfoElem::Index(t) slice[i] = SliceInfoElem::Index(t)
}; };
let frame = self.reader.get_frame( let frame = self.reader.get_frame(
(c % size_c) as usize, (c % shape.c as isize) as usize,
(z % size_z) as usize, (z % shape.z as isize) as usize,
(t % size_t) as usize, (t % shape.t as isize) as usize,
)?; )?;
let arr_frame: Array2<T> = frame.try_into()?; let arr_frame: Array2<T> = frame.try_into()?;
@@ -860,11 +846,11 @@ impl<D: Dimension> View<D> {
} }
} }
let mut n = 1; let mut n = 1;
for (&ax, size) in self.axes.iter().zip(self.shape_all().iter()) { for (ax, size) in self.shape().to_hashmap().into_iter() {
if let Some(Operation::Mean) = self.operations.get(&ax) { if ((ax == Axis::C) || (ax == Axis::Z) || (ax == Axis::T))
if (ax == Axis::C) || (ax == Axis::Z) || (ax == Axis::T) { && let Some(Operation::Mean) = self.operations.get(&ax)
n *= size; {
} n *= size;
} }
} }
let array = if n == 1 { let array = if n == 1 {
@@ -999,9 +985,10 @@ impl<D: Dimension> View<D> {
/// gives a helpful summary of the recorded experiment /// gives a helpful summary of the recorded experiment
pub fn summary(&self) -> Result<String, Error> { pub fn summary(&self) -> Result<String, Error> {
let mut s = "".to_string(); let mut s = "".to_string();
s.push_str(&format!("path/filename: {}\n", self.path.display())); s.push_str(&format!("path/filename: {}\n", self.path().display()));
s.push_str(&format!("series/pos: {}\n", self.series)); s.push_str(&format!("series/pos: {}\n", self.series()));
s.push_str(&format!("dtype: {:?}\n", self.pixel_type)); s.push_str(&format!("reader: {}\n", self.reader_name()));
s.push_str(&format!("dtype: {:?}\n", self.pixel_type()));
let axes = self let axes = self
.axes() .axes()
.into_iter() .into_iter()
@@ -1015,45 +1002,47 @@ impl<D: Dimension> View<D> {
.join(" x "); .join(" x ");
let space = " ".repeat(6usize.saturating_sub(axes.len())); let space = " ".repeat(6usize.saturating_sub(axes.len()));
s.push_str(&format!("shape ({}):{}{}\n", axes, space, shape)); s.push_str(&format!("shape ({}):{}{}\n", axes, space, shape));
s.push_str(&self.get_ome()?.summary()?); s.push_str(&self.metadata()?.summary()?);
Ok(s) Ok(s)
} }
} }
impl<D: Dimension> Deref for View<D> { impl<D: Dimension, R: Reader> Deref for View<D, R> {
type Target = Reader; type Target = R;
fn deref(&self) -> &Self::Target { 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 where
T: Number, T: Number,
D: Dimension, D: Dimension,
R: Reader,
ArrayD<T>: MinMax<Output = ArrayD<T>>, ArrayD<T>: MinMax<Output = ArrayD<T>>,
Array1<T>: MinMax<Output = Array0<T>>, Array1<T>: MinMax<Output = Array0<T>>,
Array2<T>: MinMax<Output = Array1<T>>, Array2<T>: MinMax<Output = Array1<T>>,
{ {
type Error = Error; 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() 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 where
T: Number, T: Number,
D: Dimension, D: Dimension,
R: Reader,
ArrayD<T>: MinMax<Output = ArrayD<T>>, ArrayD<T>: MinMax<Output = ArrayD<T>>,
Array1<T>: MinMax<Output = Array0<T>>, Array1<T>: MinMax<Output = Array0<T>>,
Array2<T>: MinMax<Output = Array1<T>>, Array2<T>: MinMax<Output = Array1<T>>,
{ {
type Error = Error; 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() view.as_array()
} }
} }
@@ -1068,26 +1057,17 @@ pub trait Item {
Array2<T>: MinMax<Output = Array1<T>>; Array2<T>: MinMax<Output = Array1<T>>;
} }
impl View<Ix5> { impl<R: Reader> View<Ix5, R> {
pub fn from_path<P>(path: P, series: usize) -> Result<Self, Error> pub fn from_path<P>(path: P) -> Result<Self, Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let mut path = path.as_ref().to_path_buf(); let (path, dimensions) = Dimensions::parse_path(path)?;
if path.is_dir() { Ok(R::new(path, dimensions.s.unwrap_or(0), dimensions.p.unwrap_or(0))?.view())
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())
} }
} }
impl Item for View<Ix0> { impl<R: Reader> Item for View<Ix0, R> {
fn item<T>(&self) -> Result<T, Error> fn item<T>(&self) -> Result<T, Error>
where where
T: Number, 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 { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Ok(summary) = self.summary() { if let Ok(summary) = self.summary() {
write!(f, "{}", summary) write!(f, "{}", summary)
} else { } 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!( to_bytes_vec_impl!(
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64 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.