diff --git a/.github/workflows/publish_crates.yml b/.github/workflows/publish_crates.yml new file mode 100644 index 0000000..358acf4 --- /dev/null +++ b/.github/workflows/publish_crates.yml @@ -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 diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml new file mode 100644 index 0000000..6b15650 --- /dev/null +++ b/.github/workflows/publish_pypi.yml @@ -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 diff --git a/.gitignore b/.gitignore index 3bc6b6f..8989cbf 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,5 @@ docs/_build/ .python-version /tests/files/* +AGENTS.md +.agentbridge \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 90b75dc..483e969 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,19 @@ [package] name = "ndbioimage" -version = "0.1.0" +version = "0.2.0" edition = "2024" -rust-version = "1.87.0" -authors = ["Wim Pomp "] -license = "MIT" +rust-version = "1.94.0" +authors = ["Wim Pomp ", "opencode"] +license = "MIT OR Apache-2.0" description = "Read bio image formats using the bio-formats java package." -homepage = "https://git.wimpomp.nl/wim/ndbioimage/src/branch/rs" -repository = "https://git.wimpomp.nl/wim/ndbioimage/src/branch/rs" +homepage = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs" +repository = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs" documentation = "https://docs.rs/ndbioimage" readme = "README.md" keywords = ["bioformats", "imread", "ndarray", "metadata"] categories = ["multimedia::images", "science"] exclude = ["/tests"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "ndbioimage" crate-type = ["cdylib", "rlib"] @@ -22,50 +21,65 @@ crate-type = ["cdylib", "rlib"] [dependencies] clap = { version = "4", features = ["derive"] } color-eyre = { version = "0.6", optional = true } +console = { version = "0.16", optional = true } +downloader = { version = "0.2", optional = true } ffmpeg-sidecar = { version = "2", optional = true } -itertools = "0.14" +itertools = "0.15" indexmap = { version = "2", features = ["serde"] } indicatif = { version = "0.18", features = ["rayon"], optional = true } -j4rs = "0.24" +j4rs = { version = "0.25", optional = true } +libczirw-sys = { path = "../libczirw-sys", optional = true } ndarray = { version = "0.17", features = ["serde"] } num = "0.4" -numpy = { version = "0.28", optional = true } -ordered-float = "5" +numpy = { version = "0.29", optional = true } +ome-metadata = { path = "../ome-metadata/ome-metadata" } +ordered-float = { version = "5", optional = true } +phf = { version = "0.14", features = ["macros"] } +postcard = { version = "1", features = ["use-std"], optional = true } +pyo3 = { version = "0.29", features = ["abi3-py310", "eyre", "anyhow", "generate-import-lib"], optional = true } +pyo3-stub-gen = { version = "0.23", optional = true } rayon = { version = "1", optional = true } -serde = { version = "1", features = ["rc"] } -serde_json = { version = "1", optional = true } +regex = "1" +serde = { version = "1", features = ["rc", "derive"] } +serde_yaml = { version = "0.9", optional = true } serde_with = "3" -tiffwrite = { version = ">=2026.1.1, <2026.2.0", optional = true} -thread_local = "1" -ome-metadata = "0.4" -lazy_static = "1" +strum = { version = "0.28", features = ["derive"] } thiserror = "2" - -[dependencies.pyo3] -version = "0.28" -features = ["extension-module", "abi3-py310", "eyre", "generate-import-lib"] -optional = true +tiff = { version = "0.11", features = ["zstd"], optional = true } +tiffwrite = { version = "2026.6.0", optional = true } +tokio = { version = "1", features = ["rt", "rt-multi-thread"], optional = true } +thread_local = { version = "1", optional = true } +xmltree = { version = "0.12", optional = true } +bioformats = { version = "0.1", optional = true } [dev-dependencies] -downloader = "0.2" rayon = "1" regex = "1" -reqwest = { version = "0.13", features = ["blocking"] } [build-dependencies] -j4rs = "0.24" +j4rs = "0.25" ffmpeg-sidecar = "2" retry = "2" +toml = "1" [features] -# Enables formats for which code in bioformats with a GPL license is needed +default = [] +all = ["bioformats_java", "bioformats_rust", "czi", "gpl-formats", "movie", "tiffseq", "tiffwrite", "tiff"] gpl-formats = [] -# Enables python ffi using pyO3 -python = ["dep:pyo3", "dep:numpy", "dep:serde_json", "dep:color-eyre"] -# Enables writing as tiff -tiff = ["dep:tiffwrite", "dep:indicatif", "dep:rayon"] -# Enables writing as mp4 using ffmpeg -movie = ["dep:ffmpeg-sidecar"] +python = ["dep:pyo3", "dep:numpy", "dep:color-eyre", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"] +czi = ["dep:libczirw-sys", "dep:xmltree", "dep:thread_local"] +bioformats_rust = ["dep:bioformats", "dep:thread_local"] +bioformats_java = ["dep:j4rs", "dep:thread_local", "dep:downloader"] +tiffwrite = ["dep:tiffwrite", "dep:indicatif", "dep:console", "dep:rayon"] +tiffseq = ["dep:tiff", "dep:serde_yaml"] +tiff = ["dep:tiff", "dep:thread_local"] +movie = ["dep:ffmpeg-sidecar", "dep:tokio", "dep:ordered-float", "dep:indicatif", "dep:console"] [package.metadata.docs.rs] -features = ["gpl-formats", "tiff", "movie"] \ No newline at end of file +features = ["bioformats", "gpl-formats", "czi", "tiff", "movie"] + +[profile.test] +inherits = "release" + +[profile.dev.package.backtrace] +opt-level = 3 \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f9b7904..0000000 --- a/LICENSE +++ /dev/null @@ -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. \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..f8e5e5e --- /dev/null +++ b/LICENSE-APACHE @@ -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. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..5a3aa7f --- /dev/null +++ b/LICENSE-MIT @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index 88a38f1..90a63a7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 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 Rust rewrite of python version. Read bio image formats using the bio-formats java package. diff --git a/build.rs b/build.rs index 1a8617f..d4e53ef 100644 --- a/build.rs +++ b/build.rs @@ -1,131 +1,177 @@ -#[cfg(not(feature = "python"))] -use j4rs::{JvmBuilder, MavenArtifact, MavenArtifactRepo, MavenSettings, errors::J4RsError}; -#[cfg(not(feature = "python"))] -use retry::{delay, delay::Exponential, retry}; -use std::error::Error; -#[cfg(not(feature = "python"))] -use std::fmt::Display; -#[cfg(not(feature = "python"))] -use std::fmt::Formatter; -#[cfg(not(feature = "python"))] -use std::path::PathBuf; -#[cfg(not(feature = "python"))] -use std::{env, fs}; +#[cfg(feature = "bioformats_java")] +const BIOFORMATS_VERSION: &str = "8.5.0"; -#[cfg(feature = "python")] -use j4rs::Jvm; +#[cfg(feature = "bioformats_java")] +mod bioformats { + use std::error::Error; + use std::fmt::{Display, Formatter}; -#[cfg(feature = "movie")] -use ffmpeg_sidecar::download::auto_download; + #[derive(Clone, Debug)] + pub(crate) enum BuildError { + J4rsVersionNotFound, + } -#[cfg(not(feature = "python"))] -#[derive(Clone, Debug)] -enum BuildError { - BioFormatsNotDownloaded, -} + impl Display for BuildError { + fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> { + match self { + Self::J4rsVersionNotFound => write!(fmt, "J4rsVersion not found in Cargo.lock"), + } + } + } -#[cfg(not(feature = "python"))] -impl Display for BuildError { - fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> { - write!(fmt, "Bioformats package not downloaded") + impl Error for BuildError {} + + fn get_j4rs_version() -> Result> { + 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> { + let j4rs_version = get_j4rs_version()?; + let out_dir = std::env::var("OUT_DIR")?; + let dest_path = std::path::Path::new(&out_dir).join("constants.rs"); + let contents = format!( + r#" +/// Generated by build.rs +pub(super) const J4RS_VERSION: &str = "{}"; +pub(super) const BIOFORMATS_VERSION: &str = "{}"; + "#, + j4rs_version, + crate::BIOFORMATS_VERSION, + ); + std::fs::write(&dest_path, contents)?; + Ok(()) } } -#[cfg(not(feature = "python"))] -impl Error for BuildError {} +#[cfg(all(not(feature = "python"), feature = "bioformats_java"))] +mod no_python_bioformats { + use j4rs::errors::J4RsError; + use j4rs::{JvmBuilder, MavenArtifact, MavenArtifactRepo, MavenSettings}; + use retry::delay::Exponential; + use retry::{delay, retry}; + use std::error::Error; + use std::fmt::{Display, Formatter}; -fn main() -> Result<(), Box> { + #[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> { + 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 { + 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> { + 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> { println!("cargo::rerun-if-changed=build.rs"); if std::env::var("DOCS_RS").is_err() { #[cfg(feature = "movie")] - auto_download()?; + ffmpeg_sidecar::download::auto_download()?; - #[cfg(not(feature = "python"))] - { - 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 = "bioformats_java")] + bioformats::build()?; - #[cfg(feature = "python")] - { - let py_src_path = std::env::current_dir()?.join("py").join("ndbioimage"); - let py_jassets_path = py_src_path.join("jassets"); - let py_deps_path = py_src_path.join("deps"); - if py_jassets_path.exists() { - std::fs::remove_dir_all(&py_jassets_path)?; - } - if py_deps_path.exists() { - std::fs::remove_dir_all(&py_deps_path)?; - } + #[cfg(all(not(feature = "python"), feature = "bioformats_java"))] + no_python_bioformats::build()?; - Jvm::copy_j4rs_libs_under(py_src_path.to_str().unwrap())?; - - // rename else maturin will ignore them - for file in std::fs::read_dir(&py_deps_path)? { - let f = file?.path().to_str().unwrap().to_string(); - if !f.ends_with("_") { - std::fs::rename(&f, std::format!("{f}_"))?; - } - } - - // remove so we don't include too much accidentally - for file in std::fs::read_dir(&py_jassets_path)? { - let f = file?.path(); - if !f.file_name().unwrap().to_str().unwrap().starts_with("j4rs") { - std::fs::remove_file(&f)?; - } - } - } + #[cfg(all(feature = "python", feature = "bioformats_java"))] + python_bioformats::build()?; } Ok(()) } - -#[cfg(not(feature = "python"))] -fn default_jassets_path() -> Result { - 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(()) -} diff --git a/py/ndbioimage/__init__.py b/py/ndbioimage/__init__.py index a110188..e08fcc4 100755 --- a/py/ndbioimage/__init__.py +++ b/py/ndbioimage/__init__.py @@ -1,26 +1,28 @@ from __future__ import annotations import os +import sys import warnings -from abc import ABC -from argparse import ArgumentParser -from collections import OrderedDict -from functools import cached_property, wraps from importlib.metadata import version -from itertools import product from pathlib import Path -from typing import Any, Callable, Optional, Sequence, TypeVar +from typing import TypeVar import numpy as np -from numpy.typing import ArrayLike, DTypeLike -from tiffwrite import FrameInfo, IJTiffParallel -from tqdm.auto import tqdm +from numpy.typing import ArrayLike + +os.environ["RUST_BACKTRACE"] = "full" +os.environ["COLORBT_SHOW_HIDDEN"] = "1" -from ome_metadata import Ome -from ome_metadata.ome_metadata_rs import Length # noqa from . import ndbioimage_rs as rs # noqa +from .ndbioimage_rs import View as Imread +from .ndbioimage_rs import batch_to_tiff from .transforms import Transform, Transforms # noqa: F401 +try: + import ome_metadata # noqa +except ModuleNotFoundError: + sys.modules["ome_metadata"] = rs.ome_metadata # noqa + try: __version__ = version(Path(__file__).parent.name) except Exception: # noqa @@ -38,587 +40,43 @@ warnings.filterwarnings("ignore", "Reference to unknown ID") Number = int | float | np.integer | np.floating -for dep_file in (Path(__file__).parent / "deps").glob("*_"): - dep_file.rename(str(dep_file)[:-1]) - -if not list((Path(__file__).parent / "jassets").glob("bioformats*.jar")): - rs.download_bioformats(True) +# backwards compatibility +class JVMException(Exception): + pass +# backwards compatibility class ReaderNotFoundError(Exception): pass -class TransformTiff(IJTiffParallel): - """transform frames in a parallel process to speed up saving""" - - def __init__(self, image: Imread, *args: Any, **kwargs: Any) -> None: - self.image = image - super().__init__(*args, **kwargs) - - def parallel(self, frame: tuple[int, int, int]) -> tuple[FrameInfo]: - return ((np.asarray(self.image(*frame)), 0, 0, 0),) - - -class DequeDict(OrderedDict): - def __init__(self, maxlen: int = None, *args: Any, **kwargs: Any) -> None: - self.maxlen = maxlen - super().__init__(*args, **kwargs) - - def __setitem__(self, *args: Any, **kwargs: Any) -> None: - super().__setitem__(*args, **kwargs) - self.truncate() - - def truncate(self) -> None: - if self.maxlen is not None: - while len(self) > self.maxlen: - self.popitem(False) - - def update(self, *args: Any, **kwargs: Any) -> None: - super().update(*args, **kwargs) # type: ignore - self.truncate() - - -def find(obj: Sequence[Any], **kwargs: Any) -> Any: - for item in obj: - try: - if all([getattr(item, key) == value for key, value in kwargs.items()]): - return item - except AttributeError: - pass - return None +def ndbioimage_generate_stub(): + if len(sys.argv) > 1: + path = Path(sys.argv[1]).resolve() + else: + path = Path.cwd().resolve() + if (path / "py" / "ndbioimage" / "__init__.py").exists(): + rs.generate_stub(str(path)) # noqa + else: + raise ModuleNotFoundError(str(path / "py" / "ndbioimage" / "__init__.py")) + (path / "py" / "ndbioimage" / "__init__.pyi").unlink(missing_ok=True) + (path / "py" / "ndbioimage" / "ndbioimage_rs" / "__init__.pyi").rename( + path / "py" / "ndbioimage" / "ndbioimage_rs.pyi" + ) + (path / "py" / "ndbioimage" / "ndbioimage_rs").rmdir() R = TypeVar("R") -def try_default(fun: Callable[..., R], default: Any, *args: Any, **kwargs: Any) -> R: - try: - return fun(*args, **kwargs) - except Exception: # noqa - return default +def get_positions(path: str | Path) -> set[int]: + return Imread.get_available_series(path) -class Shape(tuple): - def __new__(cls, shape: Sequence[int] | Shape, axes: str = "yxczt") -> Shape: - if isinstance(shape, Shape): - axes = shape.axes # type: ignore - new = super().__new__(cls, shape) - new.axes = axes.lower() - return new # type: ignore - - def __getitem__(self, n: int | str) -> int | tuple[int]: - if isinstance(n, str): - if len(n) == 1: - return self[self.axes.find(n.lower())] if n.lower() in self.axes else 1 - else: - return tuple(self[i] for i in n) # type: ignore - return super().__getitem__(n) - - @cached_property - def yxczt(self) -> tuple[int, int, int, int, int]: - return tuple(self[i] for i in "yxczt") # type: ignore - - -class OmeCache(DequeDict): - """prevent (potentially expensive) rereading of ome data by caching""" - - instance = None - - def __new__(cls) -> OmeCache: - if cls.instance is None: - cls.instance = super().__new__(cls) - return cls.instance - - def __init__(self) -> None: - super().__init__(64) - - def __reduce__(self) -> tuple[type, tuple]: - return self.__class__, () - - def __getitem__(self, path: Path | str | tuple) -> Ome: - if isinstance(path, tuple): - return super().__getitem__(path) - else: - return super().__getitem__(self.path_and_lstat(path)) - - def __setitem__(self, path: Path | str | tuple, value: Ome) -> None: - if isinstance(path, tuple): - super().__setitem__(path, value) - else: - super().__setitem__(self.path_and_lstat(path), value) - - def __contains__(self, path: Path | str | tuple) -> bool: - if isinstance(path, tuple): - return super().__contains__(path) - else: - return super().__contains__(self.path_and_lstat(path)) - - @staticmethod - def path_and_lstat( - path: str | Path, - ) -> tuple[Path, Optional[os.stat_result], Optional[os.stat_result]]: - path = Path(path) - return ( - path, - (path.lstat() if path.exists() else None), - ( - path.with_suffix(".ome.xml").lstat() - if path.with_suffix(".ome.xml").exists() - else None - ), - ) - - -def get_positions(path: str | Path) -> Optional[list[int]]: # noqa - # TODO - return None - - -class Imread(rs.View, np.lib.mixins.NDArrayOperatorsMixin, ABC): - """class to read image files, while taking good care of important metadata, - currently optimized for .czi files, but can open anything that bioformats can handle - path: path to the image file - optional: - axes: order of axes, default: cztyx, but omitting any axes with lenght 1 - dtype: datatype to be used when returning frames - - Examples: - >> im = Imread('/path/to/file.image', axes='czt) - >> im - << shows summary - >> im.shape - << (15, 26, 1000, 1000) - >> im.axes - << 'ztyx' - >> plt.imshow(im[1, 0]) - << plots frame at position z=1, t=0 (python type indexing) - >> plt.imshow(im[:, 0].max('z')) - << plots max-z projection at t=0 - >> im.pxsize - << 0.09708737864077668 image-plane pixel size in um - >> im.laserwavelengths - << [642, 488] - >> im.laserpowers - << [0.02, 0.0005] in % - - See __init__ and other functions for more ideas. - - # TODO: argmax, argmin, nanmax, nanmin, nanmean, nansum, nanstd, nanvar, std, var, squeeze - """ - - def __getitem__(self, item): - new = super().__getitem__(item) - return Imread(new) if isinstance(new, rs.View) else new - - def __copy__(self): - Imread(super().__copy__()) - - def copy(self): - Imread(super().copy()) - - def astype(self): - Imread(super().astype()) - - def squeeze(self): - new = super().squeeze() - return Imread(new) if isinstance(new, rs.View) else new - - def min(self, *args, **kwargs) -> Imread | float: - new = super().min(*args, **kwargs) - return Imread(new) if isinstance(new, rs.View) else new - - def max(self, *args, **kwargs) -> Imread | float: - new = super().max(*args, **kwargs) - return Imread(new) if isinstance(new, rs.View) else new - - def mean(self, *args, **kwargs) -> Imread | float: - new = super().mean(*args, **kwargs) - return Imread(new) if isinstance(new, rs.View) else new - - def sum(self, *args, **kwargs) -> Imread | float: - new = super().sum(*args, **kwargs) - return Imread(new) if isinstance(new, rs.View) else new - - def transpose(self, *args, **kwargs) -> Imread | float: - new = super().transpose(*args, **kwargs) - return Imread(new) if isinstance(new, rs.View) else new - - def swap_axes(self, *args, **kwargs) -> Imread | float: - new = super().swap_axes(*args, **kwargs) - return Imread(new) if isinstance(new, rs.View) else new - - @property - def T(self) -> Imread | float: - return Imread(super().T) - - @staticmethod - def get_positions(path: str | Path) -> Optional[list[int]]: # noqa - # TODO - return None - - @staticmethod - def as_axis(axis): - if axis is None: - return None - elif isinstance(axis, int): - return axis - else: - return str(axis) - - @wraps(np.moveaxis) - def moveaxis(self, source, destination): - raise NotImplementedError("moveaxis is not implemented") - - @wraps(np.ndarray.flatten) - def flatten(self, *args, **kwargs) -> np.ndarray: - return np.asarray(self).flatten(*args, **kwargs) - - @wraps(np.ndarray.reshape) - def reshape(self, *args, **kwargs) -> np.ndarray: - return np.asarray(self).reshape(*args, **kwargs) # noqa - - def as_array(self) -> np.ndarray: - return self.__array__() - - @wraps(np.ndarray.astype) - def astype(self, dtype: DTypeLike, *_, **__) -> Imread: - return Imread(super().astype(str(np.dtype(dtype)))) - - @staticmethod - def fix_ome(ome: Ome) -> Ome: - # fix ome if necessary - for image in ome.image: - try: - if ( - image.pixels.physical_size_z is None - and len(set([plane.the_z for plane in image.pixels.planes])) > 1 - ): - z = np.array( - [ - ( - plane.position_z_unit.convert("um", plane.position_z), - plane.the_z, - ) - for plane in image.pixels.planes - if plane.the_c == 0 and plane.the_t == 0 - ] - ) - i = np.argsort(z[:, 1]) - image.pixels.physical_size_z = ( - np.nanmean(np.true_divide(*np.diff(z[i], axis=0).T)) * 1e6 - ) - image.pixels.physical_size_z_unit = Length("um") # type: ignore - except Exception: # noqa - pass - return ome - - @staticmethod - def read_ome(path: str | Path) -> Optional[Ome]: - path = Path(path) # type: ignore - if path.with_suffix(".ome.xml").exists(): - return Ome.from_xml(path.with_suffix(".ome.xml").read_text()) - return None - - def get_ome(self) -> Ome: - """OME metadata structure""" - return Ome.from_xml(self.get_ome_xml()) - - @cached_property - def ome(self) -> Ome: - cache = OmeCache() - if self.path not in cache: - ome = self.read_ome(self.path) - if ome is None: - ome = self.get_ome() - cache[self.path] = self.fix_ome(ome) - return cache[self.path] - - def is_noise(self, volume: ArrayLike = None) -> bool: - """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) +def is_noise(self, volume: ArrayLike = None) -> bool: + """True if volume only has noise""" + if volume is None: + volume = self + fft = np.fft.fftn(volume) + corr = np.fft.fftshift(np.fft.ifftn(fft * fft.conj()).real / np.sum(volume**2)) # type: ignore + return 1 - corr[tuple([0] * corr.ndim)] < 0.006 # type: ignore diff --git a/py/ndbioimage/ndbioimage_rs.pyi b/py/ndbioimage/ndbioimage_rs.pyi new file mode 100644 index 0000000..6451be9 --- /dev/null +++ b/py/ndbioimage/ndbioimage_rs.pyi @@ -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: ... diff --git a/py/ndbioimage/ome.py b/py/ndbioimage/ome.py deleted file mode 100644 index f21b7d4..0000000 --- a/py/ndbioimage/ome.py +++ /dev/null @@ -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 diff --git a/py/ndbioimage/py.typed b/py/ndbioimage/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 7c4ee75..1739cc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,55 +1,43 @@ [build-system] -requires = ["maturin>=1.8,<2.0"] +requires = ["maturin>=1.9.4,<2.0"] build-backend = "maturin" [project] name = "ndbioimage" -description = "Bio image reading, metadata and some affine registration." -authors = [ - { name = "W. Pomp", email = "w.pomp@nki.nl" } -] -license = "MIT" -readme = "README.md" -keywords = ["bioformats", "imread", "numpy", "metadata"] +version = "2027.0.0" requires-python = ">=3.10" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Rust", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", ] -dynamic = ["version"] +dynamic = [ + "description", + "readme", + "license", + "license-files", + "authors", + "maintainers", + "keywords", + "urls", +] dependencies = [ - "numpy", - "tiffwrite", - "ome-metadata >= 0.2.1", - "tqdm", + "numpy >= 1.16.0", ] [project.optional-dependencies] test = ["pytest"] -write = ["matplotlib", "scikit-video"] - -[project.urls] -Repository = "https://github.com/wimpomp/ndbioimage/tree/rs" [project.scripts] -ndbioimage = "ndbioimage:main" - -[tool.pytest.ini_options] -filterwarnings = ["ignore:::(colorcet)"] +ndbioimage = "ndbioimage:ndbioimage_rs.main" +ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub" [tool.maturin] python-source = "py" -features = ["pyo3/extension-module", "python", "gpl-formats"] +features = ["python", "bioformats_java", "tiffwrite", "czi"] module-name = "ndbioimage.ndbioimage_rs" -exclude = ["py/ndbioimage/jassets/*", "py/ndbioimage/deps/*"] +include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"] strip = true [tool.isort] diff --git a/src/axes.rs b/src/axes.rs index 18db6fe..17f0c3c 100644 --- a/src/axes.rs +++ b/src/axes.rs @@ -3,9 +3,11 @@ use crate::stats::MinMax; use ndarray::{Array, Dimension, Ix2, SliceInfo, SliceInfoElem}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::{DeserializeAs, SerializeAs}; +use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; -use std::str::FromStr; +use std::ops::Index; +use strum::{AsRefStr, Display, EnumString}; /// a trait to find axis indices from any object pub trait Ax { @@ -25,13 +27,17 @@ pub trait Ax { } /// Enum for CZTYX axes or a new axis -#[derive(Clone, Copy, Debug, Eq, Ord, PartialOrd, Serialize, Deserialize)] +#[derive( + Clone, Copy, Debug, Eq, Ord, PartialOrd, Serialize, Deserialize, EnumString, AsRefStr, Display, +)] +#[strum(ascii_case_insensitive)] pub enum Axis { C, Z, T, Y, X, + #[strum(serialize = "N")] New, } @@ -41,36 +47,6 @@ impl Hash for Axis { } } -impl FromStr for Axis { - type Err = Error; - - fn from_str(s: &str) -> Result { - match s.to_uppercase().as_str() { - "C" => Ok(Axis::C), - "Z" => Ok(Axis::Z), - "T" => Ok(Axis::T), - "Y" => Ok(Axis::Y), - "X" => Ok(Axis::X), - "NEW" => Ok(Axis::New), - _ => Err(Error::InvalidAxis(s.to_string())), - } - } -} - -impl Display for Axis { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let s = match self { - Axis::C => "C", - Axis::Z => "Z", - Axis::T => "T", - Axis::Y => "Y", - Axis::X => "X", - Axis::New => "N", - }; - write!(f, "{}", s) - } -} - impl Ax for Axis { fn n(&self) -> usize { *self as usize @@ -134,8 +110,11 @@ impl Ax for usize { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) enum Operation { +#[derive( + Clone, Debug, Serialize, Deserialize, EnumString, AsRefStr, Display, PartialEq, Eq, Hash, +)] +#[strum(ascii_case_insensitive)] +pub enum Operation { Max, Min, Sum, @@ -249,3 +228,195 @@ impl IntoIterator for &Slice { self.clone() } } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Shape { + /// size c (# channels) + pub c: usize, + /// size z (# slices) + pub z: usize, + /// size t (# time/frames) + pub t: usize, + /// size y (vertical) + pub y: usize, + /// size x (horizontal) + pub x: usize, + pub order: Vec, +} + +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::>() + .join(""); + let shape = self + .order + .iter() + .map(|i| format!("{}", self[i])) + .collect::>() + .join(", "); + write!(f, "{}: {}", order, shape) + } +} + +impl Index 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 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 for Vec { + fn from(shape: Shape) -> Self { + shape.order.iter().map(|axis| shape[axis]).collect() + } +} + +impl From for HashMap { + 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 { + 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 { + 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 { + self.order.iter().map(|axis| self[axis]).collect() + } + + pub fn to_hashmap(&self) -> HashMap { + 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 => (), + } + } +} diff --git a/src/bioformats.rs b/src/bioformats.rs deleted file mode 100644 index 9ae2121..0000000 --- a/src/bioformats.rs +++ /dev/null @@ -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> = const { OnceCell::new() } -} - -/// Ensure 1 jvm per thread -fn jvm() -> Rc { - 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 = 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(vec: Vec) -> Vec { - 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 { - 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, Error> { - Ok(transmute_vec(self.open_bi8(index)?)) - } - - method!(open_bi8, "openBytes", [index: i32|p] => Vec|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 { - let reader = jvm().create_instance("loci.formats.ImageReader", InvocationArg::empty())?; - Ok(ImageReader(reader)) - } - - pub(crate) fn open_bytes(&self, index: i32) -> Result, Error> { - Ok(transmute_vec(self.open_bi8(index)?)) - } - - pub(crate) fn ome_xml(&self) -> Result { - 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|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 { - let meta_data_tools = - jvm().create_instance("loci.formats.MetadataTools", InvocationArg::empty())?; - Ok(MetadataTools(meta_data_tools)) - } - - method!(create_ome_xml_metadata, "createOMEXMLMetadata" => Instance); -} diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..c725c1f --- /dev/null +++ b/src/cache.rs @@ -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>>> = + 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(&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(&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> 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 for ViewEquiv<'_> { + fn equivalent(&self, key: &ViewHash) -> bool { + key.equivalent(self) + } +} + +pub fn cache_get_or_insert(f: &dyn Fn() -> ArrayT, reader: &str, path: &Path, series: usize, position: usize, c: isize, z: isize, t: isize) -> Result, 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) + } +} \ No newline at end of file diff --git a/src/colors.rs b/src/colors.rs index d133982..e271f79 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -1,171 +1,166 @@ use crate::error::Error; -use lazy_static::lazy_static; -use std::collections::HashMap; +use phf::phf_map; use std::fmt::Display; use std::str::FromStr; -lazy_static! { - static ref COLORS: HashMap = { - HashMap::from([ - ("b".to_string(), "#0000FF".to_string()), - ("g".to_string(), "#008000".to_string()), - ("r".to_string(), "#FF0000".to_string()), - ("c".to_string(), "#00BFBF".to_string()), - ("m".to_string(), "#BF00BF".to_string()), - ("y".to_string(), "#BFBF00".to_string()), - ("k".to_string(), "#000000".to_string()), - ("w".to_string(), "#FFFFFF".to_string()), - ("aliceblue".to_string(), "#F0F8FF".to_string()), - ("antiquewhite".to_string(), "#FAEBD7".to_string()), - ("aqua".to_string(), "#00FFFF".to_string()), - ("aquamarine".to_string(), "#7FFFD4".to_string()), - ("azure".to_string(), "#F0FFFF".to_string()), - ("beige".to_string(), "#F5F5DC".to_string()), - ("bisque".to_string(), "#FFE4C4".to_string()), - ("black".to_string(), "#000000".to_string()), - ("blanchedalmond".to_string(), "#FFEBCD".to_string()), - ("blue".to_string(), "#0000FF".to_string()), - ("blueviolet".to_string(), "#8A2BE2".to_string()), - ("brown".to_string(), "#A52A2A".to_string()), - ("burlywood".to_string(), "#DEB887".to_string()), - ("cadetblue".to_string(), "#5F9EA0".to_string()), - ("chartreuse".to_string(), "#7FFF00".to_string()), - ("chocolate".to_string(), "#D2691E".to_string()), - ("coral".to_string(), "#FF7F50".to_string()), - ("cornflowerblue".to_string(), "#6495ED".to_string()), - ("cornsilk".to_string(), "#FFF8DC".to_string()), - ("crimson".to_string(), "#DC143C".to_string()), - ("cyan".to_string(), "#00FFFF".to_string()), - ("darkblue".to_string(), "#00008B".to_string()), - ("darkcyan".to_string(), "#008B8B".to_string()), - ("darkgoldenrod".to_string(), "#B8860B".to_string()), - ("darkgray".to_string(), "#A9A9A9".to_string()), - ("darkgreen".to_string(), "#006400".to_string()), - ("darkgrey".to_string(), "#A9A9A9".to_string()), - ("darkkhaki".to_string(), "#BDB76B".to_string()), - ("darkmagenta".to_string(), "#8B008B".to_string()), - ("darkolivegreen".to_string(), "#556B2F".to_string()), - ("darkorange".to_string(), "#FF8C00".to_string()), - ("darkorchid".to_string(), "#9932CC".to_string()), - ("darkred".to_string(), "#8B0000".to_string()), - ("darksalmon".to_string(), "#E9967A".to_string()), - ("darkseagreen".to_string(), "#8FBC8F".to_string()), - ("darkslateblue".to_string(), "#483D8B".to_string()), - ("darkslategray".to_string(), "#2F4F4F".to_string()), - ("darkslategrey".to_string(), "#2F4F4F".to_string()), - ("darkturquoise".to_string(), "#00CED1".to_string()), - ("darkviolet".to_string(), "#9400D3".to_string()), - ("deeppink".to_string(), "#FF1493".to_string()), - ("deepskyblue".to_string(), "#00BFFF".to_string()), - ("dimgray".to_string(), "#696969".to_string()), - ("dimgrey".to_string(), "#696969".to_string()), - ("dodgerblue".to_string(), "#1E90FF".to_string()), - ("firebrick".to_string(), "#B22222".to_string()), - ("floralwhite".to_string(), "#FFFAF0".to_string()), - ("forestgreen".to_string(), "#228B22".to_string()), - ("fuchsia".to_string(), "#FF00FF".to_string()), - ("gainsboro".to_string(), "#DCDCDC".to_string()), - ("ghostwhite".to_string(), "#F8F8FF".to_string()), - ("gold".to_string(), "#FFD700".to_string()), - ("goldenrod".to_string(), "#DAA520".to_string()), - ("gray".to_string(), "#808080".to_string()), - ("green".to_string(), "#008000".to_string()), - ("greenyellow".to_string(), "#ADFF2F".to_string()), - ("grey".to_string(), "#808080".to_string()), - ("honeydew".to_string(), "#F0FFF0".to_string()), - ("hotpink".to_string(), "#FF69B4".to_string()), - ("indianred".to_string(), "#CD5C5C".to_string()), - ("indigo".to_string(), "#4B0082".to_string()), - ("ivory".to_string(), "#FFFFF0".to_string()), - ("khaki".to_string(), "#F0E68C".to_string()), - ("lavender".to_string(), "#E6E6FA".to_string()), - ("lavenderblush".to_string(), "#FFF0F5".to_string()), - ("lawngreen".to_string(), "#7CFC00".to_string()), - ("lemonchiffon".to_string(), "#FFFACD".to_string()), - ("lightblue".to_string(), "#ADD8E6".to_string()), - ("lightcoral".to_string(), "#F08080".to_string()), - ("lightcyan".to_string(), "#E0FFFF".to_string()), - ("lightgoldenrodyellow".to_string(), "#FAFAD2".to_string()), - ("lightgray".to_string(), "#D3D3D3".to_string()), - ("lightgreen".to_string(), "#90EE90".to_string()), - ("lightgrey".to_string(), "#D3D3D3".to_string()), - ("lightpink".to_string(), "#FFB6C1".to_string()), - ("lightsalmon".to_string(), "#FFA07A".to_string()), - ("lightseagreen".to_string(), "#20B2AA".to_string()), - ("lightskyblue".to_string(), "#87CEFA".to_string()), - ("lightslategray".to_string(), "#778899".to_string()), - ("lightslategrey".to_string(), "#778899".to_string()), - ("lightsteelblue".to_string(), "#B0C4DE".to_string()), - ("lightyellow".to_string(), "#FFFFE0".to_string()), - ("lime".to_string(), "#00FF00".to_string()), - ("limegreen".to_string(), "#32CD32".to_string()), - ("linen".to_string(), "#FAF0E6".to_string()), - ("magenta".to_string(), "#FF00FF".to_string()), - ("maroon".to_string(), "#800000".to_string()), - ("mediumaquamarine".to_string(), "#66CDAA".to_string()), - ("mediumblue".to_string(), "#0000CD".to_string()), - ("mediumorchid".to_string(), "#BA55D3".to_string()), - ("mediumpurple".to_string(), "#9370DB".to_string()), - ("mediumseagreen".to_string(), "#3CB371".to_string()), - ("mediumslateblue".to_string(), "#7B68EE".to_string()), - ("mediumspringgreen".to_string(), "#00FA9A".to_string()), - ("mediumturquoise".to_string(), "#48D1CC".to_string()), - ("mediumvioletred".to_string(), "#C71585".to_string()), - ("midnightblue".to_string(), "#191970".to_string()), - ("mintcream".to_string(), "#F5FFFA".to_string()), - ("mistyrose".to_string(), "#FFE4E1".to_string()), - ("moccasin".to_string(), "#FFE4B5".to_string()), - ("navajowhite".to_string(), "#FFDEAD".to_string()), - ("navy".to_string(), "#000080".to_string()), - ("oldlace".to_string(), "#FDF5E6".to_string()), - ("olive".to_string(), "#808000".to_string()), - ("olivedrab".to_string(), "#6B8E23".to_string()), - ("orange".to_string(), "#FFA500".to_string()), - ("orangered".to_string(), "#FF4500".to_string()), - ("orchid".to_string(), "#DA70D6".to_string()), - ("palegoldenrod".to_string(), "#EEE8AA".to_string()), - ("palegreen".to_string(), "#98FB98".to_string()), - ("paleturquoise".to_string(), "#AFEEEE".to_string()), - ("palevioletred".to_string(), "#DB7093".to_string()), - ("papayawhip".to_string(), "#FFEFD5".to_string()), - ("peachpuff".to_string(), "#FFDAB9".to_string()), - ("peru".to_string(), "#CD853F".to_string()), - ("pink".to_string(), "#FFC0CB".to_string()), - ("plum".to_string(), "#DDA0DD".to_string()), - ("powderblue".to_string(), "#B0E0E6".to_string()), - ("purple".to_string(), "#800080".to_string()), - ("rebeccapurple".to_string(), "#663399".to_string()), - ("red".to_string(), "#FF0000".to_string()), - ("rosybrown".to_string(), "#BC8F8F".to_string()), - ("royalblue".to_string(), "#4169E1".to_string()), - ("saddlebrown".to_string(), "#8B4513".to_string()), - ("salmon".to_string(), "#FA8072".to_string()), - ("sandybrown".to_string(), "#F4A460".to_string()), - ("seagreen".to_string(), "#2E8B57".to_string()), - ("seashell".to_string(), "#FFF5EE".to_string()), - ("sienna".to_string(), "#A0522D".to_string()), - ("silver".to_string(), "#C0C0C0".to_string()), - ("skyblue".to_string(), "#87CEEB".to_string()), - ("slateblue".to_string(), "#6A5ACD".to_string()), - ("slategray".to_string(), "#708090".to_string()), - ("slategrey".to_string(), "#708090".to_string()), - ("snow".to_string(), "#FFFAFA".to_string()), - ("springgreen".to_string(), "#00FF7F".to_string()), - ("steelblue".to_string(), "#4682B4".to_string()), - ("tan".to_string(), "#D2B48C".to_string()), - ("teal".to_string(), "#008080".to_string()), - ("thistle".to_string(), "#D8BFD8".to_string()), - ("tomato".to_string(), "#FF6347".to_string()), - ("turquoise".to_string(), "#40E0D0".to_string()), - ("violet".to_string(), "#EE82EE".to_string()), - ("wheat".to_string(), "#F5DEB3".to_string()), - ("white".to_string(), "#FFFFFF".to_string()), - ("whitesmoke".to_string(), "#F5F5F5".to_string()), - ("yellow".to_string(), "#FFFF00".to_string()), - ("yellowgreen".to_string(), "#9ACD32".to_string()), - ]) - }; -} +pub static COLORS: phf::Map<&'static str, &'static str> = phf_map! { + "b" => "#0000FF", + "g" => "#008000", + "r" => "#FF0000", + "c" => "#00BFBF", + "m" => "#BF00BF", + "y" => "#BFBF00", + "k" => "#000000", + "w" => "#FFFFFF", + "aliceblue" => "#F0F8FF", + "antiquewhite" => "#FAEBD7", + "aqua" => "#00FFFF", + "aquamarine" => "#7FFFD4", + "azure" => "#F0FFFF", + "beige" => "#F5F5DC", + "bisque" => "#FFE4C4", + "black" => "#000000", + "blanchedalmond" => "#FFEBCD", + "blue" => "#0000FF", + "blueviolet" => "#8A2BE2", + "brown" => "#A52A2A", + "burlywood" => "#DEB887", + "cadetblue" => "#5F9EA0", + "chartreuse" => "#7FFF00", + "chocolate" => "#D2691E", + "coral" => "#FF7F50", + "cornflowerblue" => "#6495ED", + "cornsilk" => "#FFF8DC", + "crimson" => "#DC143C", + "cyan" => "#00FFFF", + "darkblue" => "#00008B", + "darkcyan" => "#008B8B", + "darkgoldenrod" => "#B8860B", + "darkgray" => "#A9A9A9", + "darkgreen" => "#006400", + "darkgrey" => "#A9A9A9", + "darkkhaki" => "#BDB76B", + "darkmagenta" => "#8B008B", + "darkolivegreen" => "#556B2F", + "darkorange" => "#FF8C00", + "darkorchid" => "#9932CC", + "darkred" => "#8B0000", + "darksalmon" => "#E9967A", + "darkseagreen" => "#8FBC8F", + "darkslateblue" => "#483D8B", + "darkslategray" => "#2F4F4F", + "darkslategrey" => "#2F4F4F", + "darkturquoise" => "#00CED1", + "darkviolet" => "#9400D3", + "deeppink" => "#FF1493", + "deepskyblue" => "#00BFFF", + "dimgray" => "#696969", + "dimgrey" => "#696969", + "dodgerblue" => "#1E90FF", + "firebrick" => "#B22222", + "floralwhite" => "#FFFAF0", + "forestgreen" => "#228B22", + "fuchsia" => "#FF00FF", + "gainsboro" => "#DCDCDC", + "ghostwhite" => "#F8F8FF", + "gold" => "#FFD700", + "goldenrod" => "#DAA520", + "gray" => "#808080", + "green" => "#008000", + "greenyellow" => "#ADFF2F", + "grey" => "#808080", + "honeydew" => "#F0FFF0", + "hotpink" => "#FF69B4", + "indianred" => "#CD5C5C", + "indigo" => "#4B0082", + "ivory" => "#FFFFF0", + "khaki" => "#F0E68C", + "lavender" => "#E6E6FA", + "lavenderblush" => "#FFF0F5", + "lawngreen" => "#7CFC00", + "lemonchiffon" => "#FFFACD", + "lightblue" => "#ADD8E6", + "lightcoral" => "#F08080", + "lightcyan" => "#E0FFFF", + "lightgoldenrodyellow" => "#FAFAD2", + "lightgray" => "#D3D3D3", + "lightgreen" => "#90EE90", + "lightgrey" => "#D3D3D3", + "lightpink" => "#FFB6C1", + "lightsalmon" => "#FFA07A", + "lightseagreen" => "#20B2AA", + "lightskyblue" => "#87CEFA", + "lightslategray" => "#778899", + "lightslategrey" => "#778899", + "lightsteelblue" => "#B0C4DE", + "lightyellow" => "#FFFFE0", + "lime" => "#00FF00", + "limegreen" => "#32CD32", + "linen" => "#FAF0E6", + "magenta" => "#FF00FF", + "maroon" => "#800000", + "mediumaquamarine" => "#66CDAA", + "mediumblue" => "#0000CD", + "mediumorchid" => "#BA55D3", + "mediumpurple" => "#9370DB", + "mediumseagreen" => "#3CB371", + "mediumslateblue" => "#7B68EE", + "mediumspringgreen" => "#00FA9A", + "mediumturquoise" => "#48D1CC", + "mediumvioletred" => "#C71585", + "midnightblue" => "#191970", + "mintcream" => "#F5FFFA", + "mistyrose" => "#FFE4E1", + "moccasin" => "#FFE4B5", + "navajowhite" => "#FFDEAD", + "navy" => "#000080", + "oldlace" => "#FDF5E6", + "olive" => "#808000", + "olivedrab" => "#6B8E23", + "orange" => "#FFA500", + "orangered" => "#FF4500", + "orchid" => "#DA70D6", + "palegoldenrod" => "#EEE8AA", + "palegreen" => "#98FB98", + "paleturquoise" => "#AFEEEE", + "palevioletred" => "#DB7093", + "papayawhip" => "#FFEFD5", + "peachpuff" => "#FFDAB9", + "peru" => "#CD853F", + "pink" => "#FFC0CB", + "plum" => "#DDA0DD", + "powderblue" => "#B0E0E6", + "purple" => "#800080", + "rebeccapurple" => "#663399", + "red" => "#FF0000", + "rosybrown" => "#BC8F8F", + "royalblue" => "#4169E1", + "saddlebrown" => "#8B4513", + "salmon" => "#FA8072", + "sandybrown" => "#F4A460", + "seagreen" => "#2E8B57", + "seashell" => "#FFF5EE", + "sienna" => "#A0522D", + "silver" => "#C0C0C0", + "skyblue" => "#87CEEB", + "slateblue" => "#6A5ACD", + "slategray" => "#708090", + "slategrey" => "#708090", + "snow" => "#FFFAFA", + "springgreen" => "#00FF7F", + "steelblue" => "#4682B4", + "tan" => "#D2B48C", + "teal" => "#008080", + "thistle" => "#D8BFD8", + "tomato" => "#FF6347", + "turquoise" => "#40E0D0", + "violet" => "#EE82EE", + "wheat" => "#F5DEB3", + "white" => "#FFFFFF", + "whitesmoke" => "#F5F5F5", + "yellow" => "#FFFF00", + "yellowgreen" => "#9ACD32", +}; #[derive(Clone, Debug)] pub struct Color { diff --git a/src/error.rs b/src/error.rs index dcd5945..35bc3d9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,6 +6,7 @@ pub enum Error { IO(#[from] std::io::Error), #[error(transparent)] Shape(#[from] ndarray::ShapeError), + #[cfg(feature = "bioformats_java")] #[error(transparent)] J4rs(#[from] j4rs::errors::J4RsError), #[error(transparent)] @@ -14,12 +15,47 @@ pub enum Error { ParseIntError(#[from] std::num::ParseIntError), #[error(transparent)] Ome(#[from] ome_metadata::error::Error), - #[cfg(feature = "tiff")] + #[cfg(feature = "bioformats_java")] + #[error(transparent)] + Downloader(#[from] downloader::Error), + #[error(transparent)] + Strum(#[from] strum::ParseError), + #[cfg(feature = "tiffwrite")] #[error(transparent)] TemplateError(#[from] indicatif::style::TemplateError), - #[cfg(feature = "tiff")] + #[cfg(feature = "tiffwrite")] #[error(transparent)] TiffWrite(#[from] tiffwrite::error::Error), + #[cfg(feature = "tiffseq")] + #[error(transparent)] + SerdeYaml(#[from] serde_yaml::Error), + #[cfg(any(feature = "tiffseq", feature = "tiff"))] + #[error(transparent)] + Tiff(#[from] tiff::TiffError), + #[cfg(feature = "python")] + #[error(transparent)] + PostCard(#[from] postcard::Error), + #[cfg(feature = "czi")] + #[error(transparent)] + LibCzi(#[from] libczirw_sys::error::Error), + #[error(transparent)] + RegexError(#[from] regex::Error), + #[cfg(feature = "czi")] + #[error(transparent)] + XmlTree(#[from] xmltree::Error), + #[cfg(feature = "czi")] + #[error(transparent)] + XmlTreeParse(#[from] xmltree::ParseError), + #[cfg(feature = "czi")] + #[error(transparent)] + Czi(#[from] crate::readers::czi::CziError), + #[cfg(feature = "movie")] + #[error(transparent)] + TokioJoin(#[from] tokio::task::JoinError), + #[cfg(feature = "bioformats_rust")] + #[error(transparent)] + BioFormats(#[from] bioformats::error::BioFormatsError), + #[error("invalid axis: {0}")] InvalidAxis(String), #[error("axis {0} not found in axes {1}")] @@ -29,6 +65,8 @@ pub enum Error { #[error("file already exists {0}")] FileAlreadyExists(String), #[error("could not download ffmpeg: {0}")] + FfmpegDownload(String), + #[error("FFmpeg error: {0}")] Ffmpeg(String), #[error("index {0} out of bounds {1}")] OutOfBounds(isize, isize), @@ -52,6 +90,8 @@ pub enum Error { InvalidAttenuation(String), #[error("not a valid file name")] InvalidFileName, + #[error("file has no parent")] + NoParent, #[error("unknown pixel type {0}")] UnknownPixelType(String), #[error("no mean")] @@ -62,4 +102,14 @@ pub enum Error { NotImplemented(String), #[error("cannot parse: {0}")] Parse(String), + #[error("cannot convert libczi pixel type: {0}")] + Conversion(String), + #[error("no reader found for {0}, tried: {1}")] + NoReader(String, String), + #[error("reader {0} cannot open file {1} because {2}")] + InvalidReader(String, String, String), + #[error("file does not exist: {0}")] + FileDoesNotExist(String), + #[error("cannot remove axes {0}, size {1} != 1")] + SizeMismatch(String, usize), } diff --git a/src/lib.rs b/src/lib.rs index 9c56367..139810a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,89 +1,271 @@ #![cfg_attr(docsrs, feature(doc_cfg))] -mod bioformats; - pub mod axes; -pub mod metadata; #[cfg(feature = "python")] mod py; -pub mod reader; pub mod stats; pub mod view; pub mod colors; pub mod error; +pub mod metadata; #[cfg(feature = "movie")] pub mod movie; -#[cfg(feature = "tiff")] -pub mod tiff; +pub mod readers; +#[cfg(feature = "tiffwrite")] +pub mod tiffwrite; +// mod cache; -pub use bioformats::download_bioformats; +pub mod main { + #[cfg(feature = "tiffwrite")] + use crate::axes::{Axis, Operation}; + use crate::error::Error; + #[cfg(feature = "movie")] + use crate::movie::MovieOptions; + use crate::readers::{Dimensions, DynReader, Reader}; + use crate::view::View; + use clap::{Parser, Subcommand}; + #[cfg(feature = "movie")] + use ndarray::SliceInfoElem; + use std::path::PathBuf; + + #[derive(Parser)] + #[command(arg_required_else_help = true, version, about, long_about = None, propagate_version = true)] + struct Cli { + #[command(subcommand)] + command: Commands, + } + + #[derive(Subcommand)] + enum Commands { + /// Print some metadata + Info { + #[arg(value_name = "FILE", num_args(1..))] + file: Vec, + }, + /// save ome metadata as xml + ExtractOME { + #[arg(value_name = "FILE", num_args(1..))] + file: Vec, + }, + /// Save the image as tiff file + #[cfg(feature = "tiffwrite")] + Tiff { + #[arg(value_name = "FILE", num_args(1..))] + file: Vec, + #[arg(short = 'C', long, value_name = "COLOR", num_args(1..))] + colors: Vec, + #[arg(short = 'f', long, value_name = "OVERWRITE")] + overwrite: bool, + #[arg(short = 'q', long, value_name = "OPERATIONS", num_args(1..))] + operations: Vec, + #[arg(short = 'o', long, value_name = "OUTPUT")] + output: Option, + }, + /// Save the image as mp4 file + #[cfg(feature = "movie")] + Movie { + #[arg(value_name = "FILE", num_args(1..))] + file: Vec, + #[arg(short, long, value_name = "VELOCITY", default_value = "3.6")] + velocity: f64, + #[arg(short, long, value_name = "BRIGHTNESS", num_args(1..))] + brightness: Vec, + #[arg(short, long, value_name = "SCALE", default_value = "1.0")] + scale: f64, + #[arg(short = 'C', long, value_name = "COLOR", num_args(1..))] + colors: Vec, + #[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, + #[arg(short, long, value_name = "ZSLICE")] + zslice: Option, + #[arg(short, long, value_name = "TIME")] + time: Option, + #[arg(short, long, value_name = "NO-SCALE-BRIGHTNESS")] + no_scaling: bool, + #[arg(short = 'o', long, value_name = "OUTPUT")] + output: Option, + }, + } + + #[cfg(feature = "movie")] + fn parse_slice(s: &str) -> Result { + let mut t = s + .trim() + .replace("..", ":") + .split(":") + .map(|i| i.parse().ok()) + .collect::>>(); + 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>) -> 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::>(); + if let Some(ax) = a.first() + && let Some(op) = a.get(1) + { + view = view.operate(ax.parse::()?, op.parse::()?)?; + } + } + let out = output + .as_ref() + .filter(|_| file.len() == 1) + .cloned() + .unwrap_or_else(|| f.with_extension("tiff")); + view.save_as_tiff(&out, &options)?; + } + } + #[cfg(feature = "movie")] + Commands::Movie { + file, + velocity: speed, + brightness, + scale, + colors, + overwrite, + register, + channel, + zslice, + time, + no_scaling, + output, + } => { + let options = MovieOptions::new( + *speed, + brightness.to_vec(), + *scale, + colors.to_vec(), + *overwrite, + *register, + *no_scaling, + )?; + for f in file { + let view: View<_, DynReader> = View::from_path(f)?; + let mut s = [SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }; 5]; + if let Some(channel) = channel { + s[0] = SliceInfoElem::Index(*channel); + }; + if let Some(zslice) = zslice { + s[1] = parse_slice(zslice)?; + } + if let Some(time) = time { + s[2] = parse_slice(time)?; + } + let out = output + .as_ref() + .filter(|_| file.len() == 1) + .cloned() + .unwrap_or_else(|| f.with_extension("mp4")); + view.into_dyn() + .slice(s.as_slice())? + .save_as_movie(&out, &options)?; + } + } + } + + Ok(()) + } +} #[cfg(test)] mod tests { - use crate::axes::Axis; use crate::error::Error; - use crate::reader::{Frame, Reader}; - use crate::stats::MinMax; - use crate::view::Item; - use downloader::{Download, Downloader}; - use ndarray::{Array, Array4, Array5, NewAxis}; - use ndarray::{Array2, s}; + use crate::readers::{DynReader, Frame, Reader}; + use ndarray::Array2; use rayon::prelude::*; - fn open(file: &str) -> Result { + fn open(file: &str) -> Result { let path = std::env::current_dir()? .join("tests") .join("files") .join(file); - Reader::new(&path, 0) - } - - #[test] - fn read_ome() -> Result<(), Box> { - 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#"]+)">[^<>]+\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::()?; - if size < 10 * 1024usize.pow(2) { - if !path.join(&link).exists() { - downloads.push(Download::new(&format!("{}{}", url, link))); - } - files.push(path.join(link)); - } - } - } - if !downloads.is_empty() { - let mut downloader = Downloader::builder().download_folder(&path).build()?; - downloader.download(&downloads)?; - } - let mut count = 0; - for file in files { - if let Ok(reader) = Reader::new(&file, 0) { - let _ome = reader.get_ome()?; - count += 1; - } - } - println!("count: {}", count); - assert!(count > 30); - Ok(()) + DynReader::new(&path, 0, 0) } fn get_pixel_type(file: &str) -> Result { let reader = open(file)?; Ok(format!( "file: {}, pixel type: {:?}", - file, reader.pixel_type + file, + reader.pixel_type() )) } @@ -94,9 +276,9 @@ mod tests { #[test] fn read_ser() -> Result<(), Error> { - let file = "Experiment-2029.czi"; + let file = "czi/Experiment-2029.czi"; let reader = open(file)?; - println!("size: {}, {}", reader.size_y, reader.size_y); + println!("shape: {}", reader.shape()); let frame = reader.get_frame(0, 0, 0)?; if let Ok(arr) = >>::try_into(frame) { println!("{:?}", arr); @@ -108,7 +290,7 @@ mod tests { #[test] fn read_par() -> Result<(), Error> { - let files = vec!["Experiment-2029.czi", "test.tif"]; + let files = vec!["czi/Experiment-2029.czi", "tiff/test.tif"]; let pixel_type = files .into_par_iter() .map(|file| get_pixel_type(file).unwrap()) @@ -119,7 +301,7 @@ mod tests { #[test] fn read_frame_par() -> Result<(), Error> { - let files = vec!["Experiment-2029.czi", "test.tif"]; + let files = vec!["czi/Experiment-2029.czi", "tiff/test.tif"]; let frames = files .into_par_iter() .map(|file| get_frame(file).unwrap()) @@ -127,288 +309,4 @@ mod tests { println!("{:?}", frames); Ok(()) } - - #[test] - fn read_sequence() -> Result<(), Error> { - let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; - let reader = open(file)?; - println!("reader: {:?}", reader); - let frame = reader.get_frame(0, 4, 0)?; - println!("frame: {:?}", frame); - let frame = reader.get_frame(0, 2, 0)?; - println!("frame: {:?}", frame); - Ok(()) - } - - #[test] - fn read_sequence1() -> Result<(), Error> { - let file = "4-Pos_001_002/img_000000000_Cy3-Cy3_filter_000.tif"; - let reader = open(file)?; - println!("reader: {:?}", reader); - Ok(()) - } - - #[test] - fn ome_xml() -> Result<(), Error> { - let file = "Experiment-2029.czi"; - let reader = open(file)?; - let xml = reader.get_ome_xml()?; - println!("{}", xml); - Ok(()) - } - - #[test] - fn view() -> Result<(), Error> { - let file = "YTL1841B2-2-1_1hr_DMSO_galinduction_1/Pos0/img_000000000_mScarlet_GFP-mSc-filter_004.tif"; - let reader = open(file)?; - let view = reader.view(); - let a = view.slice(s![0, 5, 0, .., ..])?; - let b = reader.get_frame(0, 5, 0)?; - let c: Array2 = a.try_into()?; - let d: Array2 = 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::::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::::zeros((s[0], s[1], s[2], s[3], s[4])); - assert_eq!(view.shape(), a.shape()); - let b: Array5 = 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 = 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 = 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 = view.clone().try_into()?; - let view = view.max_proj($b)?; - let a: Array4 = 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 = view.slice($b)?.try_into()?; - let a5: Array5 = 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - assert_eq!(q.shape(), [1, 1280, 1280]); - assert_eq!(c.shape(), [1, 1280, 1280]); - let p = q.reset_axes()?; - let d = p.as_array::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - assert_eq!(mt.shape(), [1280, 1280]); - assert_eq!(a.shape(), [1280, 1280]); - Ok(()) - } } diff --git a/src/main.rs b/src/main.rs index b4c7e03..1923fd6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,194 +1,3 @@ -use clap::{Parser, Subcommand}; -#[cfg(feature = "movie")] -use ndarray::SliceInfoElem; -use ndbioimage::error::Error; -#[cfg(feature = "movie")] -use ndbioimage::movie::MovieOptions; -use ndbioimage::reader::{split_path_and_series, Reader}; -#[cfg(feature = "tiff")] -use ndbioimage::tiff::TiffOptions; -use ndbioimage::view::View; -use std::path::PathBuf; - -#[derive(Parser)] -#[command(arg_required_else_help = true, version, about, long_about = None, propagate_version = true)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Print some metadata - Info { - #[arg(value_name = "FILE", num_args(1..))] - file: Vec, - }, - /// save ome metadata as xml - ExtractOME { - #[arg(value_name = "FILE", num_args(1..))] - file: Vec, - }, - /// Save the image as tiff file - #[cfg(feature = "tiff")] - Tiff { - #[arg(value_name = "FILE", num_args(1..))] - file: Vec, - #[arg(short, long, value_name = "COLOR", num_args(1..))] - colors: Vec, - #[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, - #[arg(short, long, value_name = "VELOCITY", default_value = "3.6")] - velocity: f64, - #[arg(short, long, value_name = "BRIGHTNESS", num_args(1..))] - brightness: Vec, - #[arg(short, long, value_name = "SCALE", default_value = "1.0")] - scale: f64, - #[arg(short = 'C', long, value_name = "COLOR", num_args(1..))] - colors: Vec, - #[arg(short, long, value_name = "OVERWRITE")] - overwrite: bool, - #[arg(short, long, value_name = "REGISTER")] - register: bool, - #[arg(short, long, value_name = "CHANNEL")] - channel: Option, - #[arg(short, long, value_name = "ZSLICE")] - zslice: Option, - #[arg(short, long, value_name = "TIME")] - time: Option, - #[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 { - let mut t = s - .trim() - .replace("..", ":") - .split(":") - .map(|i| i.parse().ok()) - .collect::>>(); - if t.len() > 3 { - return Err(Error::Parse(s.to_string())); - } - while t.len() < 3 { - t.push(None); - } - match t[..] { - [Some(start), None, None] => Ok(SliceInfoElem::Index(start)), - [Some(start), end, None] => Ok(SliceInfoElem::Slice { - start, - end, - step: 1, - }), - [Some(start), end, Some(step)] => Ok(SliceInfoElem::Slice { start, end, step }), - [None, end, None] => Ok(SliceInfoElem::Slice { - start: 0, - end, - step: 1, - }), - [None, end, Some(step)] => Ok(SliceInfoElem::Slice { - start: 0, - end, - step, - }), - _ => Err(Error::Parse(s.to_string())), - } -} - -pub(crate) fn main() -> Result<(), Error> { - let cli = Cli::parse(); - match &cli.command { - Commands::Info { file } => { - for f in file { - let (path, series) = split_path_and_series(f)?; - let view = View::from_path(path, series.unwrap_or(0))?.squeeze()?; - println!("{}", view.summary()?); - } - } - Commands::ExtractOME { file } => { - for f in file { - let (path, series) = split_path_and_series(f)?; - let reader = Reader::new(&path, series.unwrap_or(0))?; - let xml = reader.get_ome_xml()?; - std::fs::write(path.with_extension("xml"), xml)?; - } - } - #[cfg(feature = "tiff")] - Commands::Tiff { - file, - colors, - overwrite, - } => { - let mut options = TiffOptions::new(true, None, colors.clone(), *overwrite)?; - options.enable_bar()?; - for f in file { - let (path, series) = split_path_and_series(f)?; - let view = View::from_path(path, series.unwrap_or(0))?; - view.save_as_tiff(f.with_extension("tiff"), &options)?; - } - } - #[cfg(feature = "movie")] - Commands::Movie { - file, - velocity: speed, - brightness, - scale, - colors, - overwrite, - register, - channel, - zslice, - time, - no_scaling, - } => { - let options = MovieOptions::new( - *speed, - brightness.to_vec(), - *scale, - colors.to_vec(), - *overwrite, - *register, - *no_scaling, - )?; - for f in file { - let (path, series) = split_path_and_series(f)?; - let view = View::from_path(path, series.unwrap_or(0))?; - let mut s = [SliceInfoElem::Slice { - start: 0, - end: None, - step: 1, - }; 5]; - if let Some(channel) = channel { - s[0] = SliceInfoElem::Index(*channel); - }; - if let Some(zslice) = zslice { - s[1] = parse_slice(zslice)?; - } - if let Some(time) = time { - s[2] = parse_slice(time)?; - } - view.into_dyn() - .slice(s.as_slice())? - .save_as_movie(f.with_extension("mp4"), &options)?; - } - } - Commands::DownloadBioFormats { gpl_formats } => { - ndbioimage::download_bioformats(*gpl_formats)? - } - } - - Ok(()) +pub(crate) fn main() -> Result<(), ndbioimage::error::Error> { + ndbioimage::main::main(None) } diff --git a/src/movie.rs b/src/movie.rs index 753009c..d00b740 100644 --- a/src/movie.rs +++ b/src/movie.rs @@ -1,17 +1,19 @@ use crate::axes::Axis; use crate::colors::Color; use crate::error::Error; -use crate::reader::PixelType; +use crate::readers::{PixelType, Reader}; use crate::view::View; +use console::Term; use ffmpeg_sidecar::command::FfmpegCommand; use ffmpeg_sidecar::download::auto_download; use ffmpeg_sidecar::event::{FfmpegEvent, LogLevel}; +use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; use itertools::Itertools; use ndarray::{Array2, Array3, Dimension, IxDyn, s, stack}; use ordered_float::OrderedFloat; use std::io::Write; use std::path::Path; -use std::thread; +use std::time::Duration; pub struct MovieOptions { velocity: f64, @@ -93,7 +95,7 @@ impl MovieOptions { } } -fn get_ab(tyx: View) -> Result<(f64, f64), Error> { +fn get_ab(tyx: View) -> Result<(f64, f64), Error> { let s = tyx .as_array::()? .iter() @@ -136,9 +138,25 @@ fn cframe(frame: Array2, color: &[u8], a: f64, b: f64) -> Array3 { stack(ndarray::Axis(2), &view).unwrap() } -impl View +/// a progress bar with an ok style that when py::detach is used also works in jupyter +pub fn get_bar(count: Option) -> 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 View where D: Dimension, + R: Reader, + Self: 'static, { pub fn save_as_movie

(&self, path: P, options: &MovieOptions) -> Result<(), Error> where @@ -211,7 +229,7 @@ where let ab = if options.no_scaling { vec![ - match view.pixel_type { + match view.pixel_type() { PixelType::I8 => (i8::MIN as f64, i8::MAX as f64), PixelType::U8 => (u8::MIN as f64, u8::MAX as f64), PixelType::I16 => (i16::MIN as f64, i16::MAX as f64), @@ -222,7 +240,7 @@ where PixelType::U64 => (u64::MIN as f64, u64::MAX as f64), _ => (0.0, 1.0), }; - view.size_c + view.shape().c ] } else { (0..size_c) @@ -233,13 +251,16 @@ where .collect::, Error>>()? }; - thread::spawn(move || { + let rt = tokio::runtime::Runtime::new()?; + let bar = get_bar(Some(size_t)); + let rt_bar = bar.clone(); + let write_task = rt.spawn(async move { for t in 0..size_t { let mut frame = Array3::::zeros((size_y, size_x, 3)); for c in 0..size_c { frame = frame + cframe( - view.get_frame(c, 0, t).unwrap(), + view.get_frame(c, 0, t)?, &colors[c], ab[c].0, ab[c].1 / brightness[c], @@ -247,18 +268,30 @@ where } let frame = (frame.clamp(0.0, 1.0) * 255.0).round().mapv(|i| i as u8); let bytes: Vec<_> = frame.flatten().into_iter().collect(); - stdin.write_all(&bytes).unwrap(); + stdin.write_all(&bytes)?; + rt_bar.inc(1); } + Ok::<(), Error>(()) + }); + bar.finish(); + + let bar = get_bar(Some(size_t)); + let rt_bar = bar.clone(); + let progress_task = rt.spawn(async move { + for event in movie.iter().map_err(|e| Error::Ffmpeg(e.to_string()))? { + match event { + FfmpegEvent::Log(LogLevel::Error, e) => Err(Error::Ffmpeg(e))?, + FfmpegEvent::Progress(p) => rt_bar.set_position(p.frame as u64), + _ => {} + } + } + Ok::<(), Error>(()) }); - movie - .iter() - .map_err(|e| Error::Ffmpeg(e.to_string()))? - .for_each(|e| match e { - FfmpegEvent::Log(LogLevel::Error, e) => println!("Error: {}", e), - FfmpegEvent::Progress(p) => println!("Progress: {} / 00:00:15", p.time), - _ => {} - }); + rt.block_on(progress_task)??; + rt.block_on(write_task)??; + bar.finish(); + Ok(()) } } @@ -266,20 +299,24 @@ where #[cfg(test)] mod tests { use super::*; - use crate::reader::Reader; + use crate::readers::DynReader; + use crate::view::View; + #[cfg(any(feature = "czi", feature = "bioformats_java"))] #[test] fn movie() -> Result<(), Error> { - let file = "1xp53-01-AP1.czi"; + let file = "czi/1xp53-01-AP1.czi"; let path = std::env::current_dir()? .join("tests") .join("files") .join(file); - let reader = Reader::new(&path, 0)?; - let view = reader.view(); + let view: View<_, DynReader> = View::from_path(&path)?; let mut options = MovieOptions::default(); options.set_overwrite(true); - view.save_as_movie("/home/wim/tmp/movie.mp4", &options)?; + view.save_as_movie( + std::env::home_dir().unwrap().join("tmp/movie.mp4"), + &options, + )?; Ok(()) } } diff --git a/src/py.rs b/src/py.rs index 34c3753..4728bb6 100644 --- a/src/py.rs +++ b/src/py.rs @@ -1,21 +1,30 @@ -use crate::axes::Axis; -use crate::bioformats::download_bioformats; +use crate::axes::{Axis, Shape}; use crate::error::Error; use crate::metadata::Metadata; -use crate::reader::{PixelType, Reader}; +#[cfg(feature = "movie")] +use crate::movie::MovieOptions; +use crate::readers::{DynReader, PixelType, Reader}; use crate::view::{Item, View}; use itertools::Itertools; -use ndarray::{Ix0, IxDyn, SliceInfoElem}; -use numpy::IntoPyArray; +use ndarray::{Ix0, Ix1, IxDyn, SliceInfoElem}; +use numpy::{ + AllowTypeChange, IntoPyArray, PyArray, PyArrayDescr, PyArrayLike0, PyArrayLike1, + PyArrayMethods, dtype, +}; use ome_metadata::Ome; +use postcard::{from_bytes, to_stdvec}; use pyo3::IntoPyObjectExt; -use pyo3::exceptions::{PyNotImplementedError, PyValueError}; +use pyo3::exceptions::{PyIndexError, PyNotImplementedError, PyTypeError, PyValueError}; use pyo3::prelude::*; -use pyo3::types::{PyEllipsis, PyInt, PyList, PySlice, PySliceMethods, PyString, PyTuple}; +use pyo3::types::{ + PyBytes, PyEllipsis, PyInt, PyList, PyNone, PySlice, PySliceMethods, PyString, PyTuple, +}; +use pyo3_stub_gen::derive::*; +use pyo3_stub_gen::inventory::submit; +use pyo3_stub_gen::{StubGenConfig, StubInfo}; use serde::{Deserialize, Serialize}; -use serde_json::{from_str, to_string}; +use std::collections::HashSet; use std::path::PathBuf; -use std::sync::Arc; impl From for PyErr { fn from(err: crate::error::Error) -> PyErr { @@ -23,85 +32,221 @@ impl From for PyErr { } } -#[pyclass(module = "ndbioimage.ndbioimage_rs")] -struct ViewConstructor; - -#[pymethods] -impl ViewConstructor { - #[new] - fn new() -> Self { - Self - } - - fn __getnewargs__<'py>(&self, py: Python<'py>) -> Bound<'py, PyTuple> { - PyTuple::empty(py) - } - - #[staticmethod] - fn __call__(state: String) -> PyResult { - if let Ok(new) = from_str(&state) { - Ok(new) - } else { - Err(PyErr::new::( - "cannot parse state".to_string(), - )) - } - } -} - -#[pyclass(subclass, from_py_object, module = "ndbioimage.ndbioimage_rs")] -#[pyo3(name = "View")] +/// 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 +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + name = "View", + module = "ndbioimage.ndbioimage_rs" +)] #[derive(Clone, Debug, Serialize, Deserialize)] struct PyView { - view: View, + view: View, dtype: PixelType, - ome: Arc, + ome: Ome, + index: usize, } +unsafe impl Send for PyView {} +unsafe impl Sync for PyView {} + +impl PyView { + fn item(py: Python, view: View, dtype: PixelType) -> PyResult> { + Ok(match dtype { + PixelType::I8 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U8 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I16 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U16 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F32 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U64 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::I128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::U128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + PixelType::F128 => view + .into_dimensionality::()? + .item::()? + .into_pyobject(py)? + .into_any(), + }) + } +} + +#[gen_stub_pymethods] #[pymethods] impl PyView { /// new view on a file at path, open series #, open as dtype: (u)int(8/16/32) or float(32/64) #[new] - #[pyo3(signature = (path, series = 0, dtype = "uint16", axes = "cztyx"))] + #[pyo3(signature = (path, dtype = None, axes = "cztyx", reader = None))] fn new<'py>( py: Python<'py>, + #[gen_stub(override_type(type_repr="str | pathlib.Path | View | bytes", imports=("pathlib")))] path: Bound<'py, PyAny>, - series: usize, - dtype: &str, + #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")))] + dtype: Option>, axes: &str, + reader: Option<&str>, ) -> PyResult { if path.is_instance_of::() { Ok(path.cast_into::()?.extract::()?) + } else if path.is_instance_of::() { + Ok(from_bytes(&path.extract::>()?).map_err(Error::from)?) } else { let builtins = PyModule::import(py, "builtins")?; - let mut path = PathBuf::from( + let path = PathBuf::from( builtins .getattr("str")? .call1((path,))? .cast_into::()? .extract::()?, ); - 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 axes = axes .chars() - .map(|a| a.to_string().parse()) + .map(|a| a.to_string().parse().map_err(Error::from)) .collect::, Error>>()?; - let reader = Reader::new(&path, series)?; - let view = View::new_with_axes(Arc::new(reader), axes)?; - let dtype = dtype.parse()?; - let ome = Arc::new(view.get_ome()?); - Ok(Self { view, dtype, ome }) + let view = if let Some(reader) = reader { + DynReader::from_path_select_reader(&path, reader)?.view() + } else { + View::<_, DynReader>::from_path(&path)? + } + .permute_axes_dyn(&axes)?; + let dtype = if let Some(dtype) = dtype { + let np = PyModule::import(py, "numpy")?; + let dt = np.getattr("dtype")?.call1((&dtype,))?; + let name = dt.getattr("name")?; + let dtype_str = name.extract::()?; + dtype_str.parse()? + } else { + PixelType::U16 + }; + let ome = view.metadata()?; + Ok(Self { + view, + dtype, + ome, + index: 0, + }) } } + #[staticmethod] + fn get_positions<'py>( + py: Python, + #[gen_stub(override_type(type_repr="str | pathlib.Path | View | bytes", imports=("pathlib")))] + path: Bound<'py, PyAny>, + ) -> PyResult> { + Self::get_available_series(py, path, None) + } + + /// only remains for backwards compatibility + #[staticmethod] + fn kill_vm() {} + + #[getter] + fn reader_name(&self) -> String { + self.view.reader_name() + } + + #[allow(unused_variables)] + fn reshape<'py>(&self, order: &str, copy: bool) -> PyResult> { + todo!() + } + + #[allow(unused_variables)] + #[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None))] + fn with_transform<'py>( + &self, + channels: bool, + drift: bool, + file: Option>, + bead_files: Option>, + ) -> PyResult { + todo!() + } + + #[getter] + fn get_transform(&self) -> PyResult<()> { + todo!() + } + + #[gen_stub(override_return_type(type_repr="numpy.ndarray | int | float", imports=("numpy")))] fn squeeze<'py>(&self, py: Python<'py>) -> PyResult> { let view = self.view.squeeze()?; if view.ndim() == 0 { @@ -175,8 +320,9 @@ impl PyView { } else { PyView { view, - dtype: self.dtype.clone(), + dtype: self.dtype, ome: self.ome.clone(), + index: 0, } .into_bound_py_any(py) } @@ -188,17 +334,32 @@ impl PyView { } /// change the data type of the view: (u)int(8/16/32) or float(32/64) - fn as_type(&self, dtype: &str) -> PyResult { + fn as_type( + &self, + py: Python<'_>, + #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")))] + dtype: Bound<'_, PyAny>, + ) -> PyResult { + let np = PyModule::import(py, "numpy")?; + let dt = np.getattr("dtype")?.call1((&dtype,))?; + let name = dt.getattr("name")?; + let dtype_str = name.extract::()?; Ok(PyView { view: self.view.clone(), - dtype: dtype.parse()?, + dtype: dtype_str.parse()?, ome: self.ome.clone(), + index: 0, }) } /// change the data type of the view: (u)int(8/16/32) or float(32/64) - fn astype(&self, dtype: &str) -> PyResult { - self.as_type(dtype) + fn astype( + &self, + py: Python<'_>, + #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")))] + dtype: Bound<'_, PyAny>, + ) -> PyResult { + self.as_type(py, dtype) } /// slice the view and return a new view or a single number @@ -218,7 +379,13 @@ impl PyView { let mut ellipsis = None; let shape = self.view.shape(); for (i, (s, t)) in slice.iter().zip(shape.iter()).enumerate() { - if s.is_instance_of::() { + if s.is_none() { + new_slice.push(SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }); + } else if s.is_instance_of::() { new_slice.push(SliceInfoElem::Index(s.cast::()?.extract::()?)); } else if s.is_instance_of::() { let u = s.cast::()?.indices(*t as isize)?; @@ -234,15 +401,73 @@ impl PyView { )); } let _ = ellipsis.insert(i); + } else if let Ok(arr_like) = s.extract::>() { + let pyarr: &Bound> = &arr_like; + let mut index = *pyarr.readonly().as_array().into_scalar(); + let index0 = index; + if index < 0 { + index += *t as isize; + } + if (index < 0) || (index >= *t as isize) { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for axis {} with size {}", + index0, i, t + ))); + } + new_slice.push(SliceInfoElem::Index(index)); + } else if let Ok(arr_like) = s.extract::>() { + let pyarr: &Bound> = &arr_like; + let read = pyarr.readonly(); + let mut indices = read.as_array().to_vec(); + for index in indices.iter_mut() { + let index0 = *index; + if *index < 0 { + *index += *t as isize; + } + if (*index < 0) || (*index >= *t as isize) { + return Err(PyIndexError::new_err(format!( + "index {} is out of bounds for axis {} with size {}", + index0, i, t + ))); + } + } + if indices.is_empty() { + new_slice.push(SliceInfoElem::Slice { + start: 0, + end: Some(0), + step: 1, + }) + } else { + let d = indices + .windows(2) + .map(|i| i[1] - i[0]) + .collect::>(); + if d.is_empty() { + let index = indices[0]; + new_slice.push(SliceInfoElem::Slice { + start: index, + end: Some(index + 1), + step: 1, + }); + } else if d.len() == 1 { + new_slice.push(SliceInfoElem::Slice { + start: indices[0], + end: indices.last().map(|j| j + 1), + step: d.into_iter().collect::>()[0], + }); + } else { + return Err(PyValueError::new_err("indices array must regularly spaced")); + } + }; } else { - return Err(PyErr::new::(format!( + return Err(PyValueError::new_err(format!( "cannot convert {:?} to slice", s ))); } } if new_slice.len() > shape.len() { - return Err(PyErr::new::(format!( + return Err(PyValueError::new_err(format!( "got more indices ({}) than dimensions ({})", new_slice.len(), shape.len() @@ -268,87 +493,29 @@ impl PyView { } let view = self.view.slice(new_slice.as_slice())?; if view.ndim() == 0 { - Ok(match self.dtype { - PixelType::I8 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U8 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I16 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U16 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F32 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U64 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::I128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::U128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - PixelType::F128 => view - .into_dimensionality::()? - .item::()? - .into_pyobject(py)? - .into_any(), - }) + Self::item(py, view, self.dtype) } else { PyView { view, - dtype: self.dtype.clone(), + dtype: self.dtype, ome: self.ome.clone(), + index: 0, } .into_bound_py_any(py) } } - #[pyo3(signature = (dtype = None))] - fn __array__<'py>(&self, py: Python<'py>, dtype: Option<&str>) -> PyResult> { + #[allow(unused_variables)] + #[pyo3(signature = (dtype = None, copy = None))] + fn __array__<'py>( + &self, + py: Python<'py>, + #[gen_stub(override_type(type_repr = "numpy.typing.DTypeLike", imports=("numpy", "numpy.typing")))] + dtype: Option>, + copy: Option, + ) -> PyResult> { if let Some(dtype) = dtype { - self.as_type(dtype)?.as_array(py) + self.as_type(py, dtype)?.as_array(py) } else { self.as_array(py) } @@ -358,6 +525,396 @@ impl PyView { Err(PyNotImplementedError::new_err("contains not implemented")) } + fn __lt__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("less")?.call1((&a, &b)) + } + + fn __le__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("less_equal")?.call1((&a, &b)) + } + + fn __eq__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("equal")?.call1((&a, &b)) + } + + fn __ne__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("not_equal")?.call1((&a, &b)) + } + + fn __gt__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("greater")?.call1((&a, &b)) + } + + fn __ge__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("greater_equal")?.call1((&a, &b)) + } + + fn __add__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("add")?.call1((&a, &b)) + } + + fn __radd__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("add")?.call1((&a, &b)) + } + + fn __sub__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("subtract")?.call1((&a, &b)) + } + + fn __rsub__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("subtract")?.call1((&a, &b)) + } + + fn __mul__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("multiply")?.call1((&a, &b)) + } + + fn __rmul__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("multiply")?.call1((&a, &b)) + } + + fn __truediv__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("true_divide")?.call1((&a, &b)) + } + + fn __rtruediv__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("true_divide")?.call1((&a, &b)) + } + + fn __floordiv__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("floor_divide")?.call1((&a, &b)) + } + + fn __rfloordiv__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("floor_divide")?.call1((&a, &b)) + } + + fn __mod__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("remainder")?.call1((&a, &b)) + } + + fn __rmod__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("remainder")?.call1((&a, &b)) + } + + #[gen_stub(skip)] + fn __pow__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + r#mod: Option>, + ) -> PyResult> { + if r#mod.is_some() { + return Err(PyNotImplementedError::new_err( + "cannot use pow or ** on View with mod != None", + )); + } + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("power")?.call1((&a, &b)) + } + + #[gen_stub(skip)] + fn __rpow__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + r#mod: Option>, + ) -> PyResult> { + if r#mod.is_some() { + return Err(PyNotImplementedError::new_err( + "cannot use pow or ** on View with mod != None", + )); + } + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("power")?.call1((&a, &b)) + } + + fn __matmul__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("matmul")?.call1((&a, &b)) + } + + fn __rmatmul__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("matmul")?.call1((&a, &b)) + } + + fn __and__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_and")?.call1((&a, &b)) + } + + fn __rand__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_and")?.call1((&a, &b)) + } + + fn __or__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_or")?.call1((&a, &b)) + } + + fn __ror__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_or")?.call1((&a, &b)) + } + + fn __xor__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_xor")?.call1((&a, &b)) + } + + fn __rxor__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("bitwise_xor")?.call1((&a, &b)) + } + + fn __lshift__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("left_shift")?.call1((&a, &b)) + } + + fn __rlshift__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("left_shift")?.call1((&a, &b)) + } + + fn __rshift__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + let b = np.getattr("asarray")?.call1((&other,))?; + np.getattr("right_shift")?.call1((&a, &b)) + } + + fn __rrshift__<'py>( + &self, + py: Python<'py>, + other: Bound<'py, PyAny>, + ) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let b = self.as_array(py)?; + let a = np.getattr("asarray")?.call1((&other,))?; + np.getattr("right_shift")?.call1((&a, &b)) + } + + fn __neg__<'py>(&self, py: Python<'py>) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + np.getattr("negative")?.call1((&a,)) + } + + fn __pos__<'py>(&self, py: Python<'py>) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + np.getattr("positive")?.call1((&a,)) + } + + fn __abs__<'py>(&self, py: Python<'py>) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + np.getattr("absolute")?.call1((&a,)) + } + + fn __invert__<'py>(&self, py: Python<'py>) -> PyResult> { + let np = PyModule::import(py, "numpy")?; + let a = self.as_array(py)?; + np.getattr("invert")?.call1((&a,)) + } + fn __enter__<'py>(slf: PyRef<'py, Self>) -> PyResult> { Ok(slf) } @@ -373,29 +930,74 @@ impl PyView { self.close() } - fn __reduce__(&self) -> PyResult<(ViewConstructor, (String,))> { - if let Ok(s) = to_string(self) { - Ok((ViewConstructor, (s,))) - } else { - Err(PyErr::new::( - "cannot get state".to_string(), - )) - } + pub(crate) fn __getnewargs__(&self) -> PyResult<(Vec,)> { + Ok((to_stdvec(self).map_err(Error::from)?,)) } fn __copy__(&self) -> Self { Self { view: self.view.clone(), - dtype: self.dtype.clone(), + dtype: self.dtype, ome: self.ome.clone(), + index: 0, + } + } + + fn __deepcopy__(&self) -> Self { + Self { + view: self.view.clone(), + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, } } fn copy(&self) -> Self { Self { view: self.view.clone(), - dtype: self.dtype.clone(), + dtype: self.dtype, ome: self.ome.clone(), + index: 0, + } + } + + fn __iter__(&self) -> Self { + Self { + view: self.view.clone(), + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + } + + fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult>> { + let shape = self.view.shape(); + if shape.is_empty() || (self.index == shape[0]) { + self.index = 0; + Ok(None) + } else { + let mut new_slice = vec![SliceInfoElem::Index(self.index as isize)]; + self.index += 1; + for _ in 1..shape.len() { + new_slice.push(SliceInfoElem::Slice { + start: 0, + end: None, + step: 1, + }) + } + let view = self.view.slice(new_slice.as_slice())?; + Some(if view.ndim() == 0 { + Self::item(py, view, self.dtype) + } else { + PyView { + view, + dtype: self.dtype, + ome: self.ome.clone(), + index: 0, + } + .into_bound_py_any(py) + }) + .transpose() } } @@ -408,7 +1010,7 @@ impl PyView { } fn __str__(&self) -> PyResult { - Ok(self.view.path.display().to_string()) + Ok(self.view.path().display().to_string()) } /// retrieve a single frame at czt, sliced accordingly @@ -529,20 +1131,22 @@ impl PyView { } /// retrieve the ome metadata as an XML string - fn get_ome_xml(&self) -> PyResult { - Ok(self.view.get_ome_xml()?) + #[gen_stub(skip)] + #[getter] + fn get_ome(&self) -> Ome { + self.ome.clone() } /// the file path #[getter] - fn path(&self) -> PyResult { - Ok(self.view.path.display().to_string()) + fn path(&self) -> PyResult { + Ok(self.view.path().to_owned()) } /// the series in the file #[getter] fn series(&self) -> PyResult { - Ok(self.view.series) + Ok(self.view.series()) } /// the axes in the view @@ -553,8 +1157,10 @@ impl PyView { /// the shape of the view #[getter] - fn shape(&self) -> Vec { - self.view.shape() + fn shape(&self) -> PyShape { + PyShape { + inner: self.view.shape(), + } } #[getter] @@ -580,13 +1186,16 @@ impl PyView { } /// find the position of an axis - #[pyo3(text_signature = "axis: str | int")] - fn get_ax(&self, axis: Bound) -> PyResult { + fn get_ax( + &self, + #[gen_stub(override_type(type_repr = "int | str"))] axis: Bound, + ) -> PyResult { if axis.is_instance_of::() { let axis = axis .cast_into::()? .extract::()? - .parse::()?; + .parse::() + .map_err(Error::from)?; self.view .axes() .iter() @@ -604,21 +1213,29 @@ impl PyView { } /// swap two axes - #[pyo3(text_signature = "ax0: str | int, ax1: str | int")] - fn swap_axes(&self, ax0: Bound, ax1: Bound) -> PyResult { + fn swap_axes( + &self, + #[gen_stub(override_type(type_repr = "int | str"))] ax0: Bound, + #[gen_stub(override_type(type_repr = "int | str"))] ax1: Bound, + ) -> PyResult { let ax0 = self.get_ax(ax0)?; let ax1 = self.get_ax(ax1)?; let view = self.view.swap_axes(ax0, ax1)?; Ok(PyView { view, - dtype: self.dtype.clone(), + dtype: self.dtype, ome: self.ome.clone(), + index: 0, }) } /// permute the order of the axes - #[pyo3(signature = (axes = None), text_signature = "axes: list[str | int] = None")] - fn transpose(&self, axes: Option>>) -> PyResult { + #[pyo3(signature = (axes = None))] + fn transpose( + &self, + #[gen_stub(override_type(type_repr="typing.Optional[typing.Sequence[int | str]]", imports=("typing")))] + axes: Option>>, + ) -> PyResult { let view = if let Some(axes) = axes { let ax = axes .into_iter() @@ -630,14 +1247,15 @@ impl PyView { }; Ok(PyView { view, - dtype: self.dtype.clone(), + dtype: self.dtype, ome: self.ome.clone(), + index: 0, }) } #[allow(non_snake_case)] #[getter] - fn T(&self) -> PyResult { + fn T(&self) -> PyResult { self.transpose(None) } @@ -660,34 +1278,45 @@ impl PyView { }) } + #[gen_stub(override_return_type(type_repr = "numpy.dtype", imports=("numpy")))] #[getter] - fn get_dtype(&self) -> PyResult<&str> { - Ok(match self.dtype { - PixelType::I8 => "int8", - PixelType::U8 => "uint8", - PixelType::I16 => "int16", - PixelType::U16 => "uint16", - PixelType::I32 => "int32", - PixelType::U32 => "uint32", - PixelType::F32 => "float32", - PixelType::F64 => "float64", - PixelType::I64 => "int64", - PixelType::U64 => "uint64", - PixelType::I128 => "int128", - PixelType::U128 => "uint128", - PixelType::F128 => "float128", - }) + fn get_dtype<'py>(&self, py: Python<'py>) -> PyResult> { + match self.dtype { + PixelType::I8 => Ok(dtype::(py)), + PixelType::U8 => Ok(dtype::(py)), + PixelType::I16 => Ok(dtype::(py)), + PixelType::U16 => Ok(dtype::(py)), + PixelType::I32 => Ok(dtype::(py)), + PixelType::U32 => Ok(dtype::(py)), + PixelType::F32 => Ok(dtype::(py)), + PixelType::F64 => Ok(dtype::(py)), + PixelType::I64 => Ok(dtype::(py)), + PixelType::U64 => Ok(dtype::(py)), + PixelType::I128 => Err(PyTypeError::new_err( + "type is i128, but this cannot be represented by numpy.dtype", + )), + PixelType::U128 => Err(PyTypeError::new_err( + "type is u128, but this cannot be represented by numpy.dtype", + )), + PixelType::F128 => Ok(dtype::(py)), + } } + #[gen_stub(skip)] #[setter] - fn set_dtype(&mut self, dtype: String) -> PyResult<()> { - self.dtype = dtype.parse()?; + fn set_dtype(&mut self, py: Python, dtype: Bound<'_, PyAny>) -> PyResult<()> { + let np = PyModule::import(py, "numpy")?; + let dt = np.getattr("dtype")?.call1((&dtype,))?; + let name = dt.getattr("name")?; + let dtype_str = name.extract::()?; + self.dtype = dtype_str.parse()?; Ok(()) } /// get the maximum overall or along a given axis #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=0, r#where=true), text_signature = "axis: str | int")] + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=None, r#where=true), text_signature = "axis: str | int")] fn max<'py>( &self, py: Python<'py>, @@ -712,9 +1341,10 @@ impl PyView { } if let Some(axis) = axis { PyView { - dtype: self.dtype.clone(), + dtype: self.dtype, view: self.view.max_proj(self.get_ax(axis)?)?, ome: self.ome.clone(), + index: 0, } .into_bound_py_any(py) } else { @@ -738,7 +1368,8 @@ impl PyView { /// get the minimum overall or along a given axis #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=0, r#where=true), text_signature = "axis: str | int")] + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int")] fn min<'py>( &self, py: Python<'py>, @@ -763,9 +1394,10 @@ impl PyView { } if let Some(axis) = axis { PyView { - dtype: self.dtype.clone(), + dtype: self.dtype, view: self.view.min_proj(self.get_ax(axis)?)?, ome: self.ome.clone(), + index: 0, } .into_bound_py_any(py) } else { @@ -787,6 +1419,7 @@ impl PyView { } } + #[gen_stub(skip)] #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, *, r#where=true), text_signature = "axis: str | int")] fn mean<'py>( &self, @@ -812,6 +1445,7 @@ impl PyView { dtype, view: self.view.mean_proj(self.get_ax(axis)?)?, ome: self.ome.clone(), + index: 0, } .into_bound_py_any(py) } else { @@ -824,7 +1458,8 @@ impl PyView { /// get the sum overall or along a given axis #[allow(clippy::too_many_arguments)] - #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=0, r#where=true), text_signature = "axis: str | int")] + #[gen_stub(skip)] + #[pyo3(signature = (axis=None, dtype=None, out=None, keepdims=false, initial=Some(0), r#where=true), text_signature = "axis: str | int")] fn sum<'py>( &self, py: Python<'py>, @@ -867,6 +1502,7 @@ impl PyView { dtype, view: self.view.sum_proj(self.get_ax(axis)?)?, ome: self.ome.clone(), + index: 0, } .into_bound_py_any(py) } else { @@ -892,6 +1528,16 @@ impl PyView { } } + /// backwards compatibility + #[getter] + fn zstack(&self) -> PyResult { + if let Some(s) = self.view.size_ax(Axis::Z) { + Ok(s > 1) + } else { + Ok(false) + } + } + #[getter] fn time_series(&self) -> PyResult { if let Some(s) = self.view.size_ax(Axis::T) { @@ -901,11 +1547,33 @@ impl PyView { } } + /// backwards compatibility + #[getter] + fn timeseries(&self) -> PyResult { + if let Some(s) = self.view.size_ax(Axis::T) { + Ok(s > 1) + } else { + Ok(false) + } + } + #[getter] fn pixel_size(&self) -> PyResult> { Ok(self.ome.pixel_size()?) } + /// backwards compatibility + #[getter] + fn pxsize_um(&self) -> PyResult> { + Ok(self.ome.pixel_size()?.map(|p| p / 1000.)) + } + + /// backwards compatibility + #[getter] + fn deltaz_um(&self) -> PyResult> { + Ok(self.ome.delta_z()?.map(|p| p / 1000.)) + } + #[getter] fn delta_z(&self) -> PyResult> { Ok(self.ome.delta_z()?) @@ -916,10 +1584,24 @@ impl PyView { Ok(self.ome.time_interval()?) } + /// backwards compatibility + #[getter] + fn timeinterval(&self) -> PyResult> { + Ok(self.ome.time_interval()?) + } + fn exposure_time(&self, channel: usize) -> PyResult> { Ok(self.ome.exposure_time(channel)?) } + /// backwards compatibility + #[getter] + fn exposuretime_s(&self) -> PyResult>> { + Ok((0..self.view.shape().c) + .map(|c| self.ome.exposure_time(c)) + .collect::, Error>>()?) + } + fn binning(&self, channel: usize) -> Option { self.ome.binning(channel) } @@ -959,6 +1641,356 @@ impl PyView { fn summary(&self) -> PyResult { Ok(self.view.summary()?) } + + /// get all series contained in the file + #[staticmethod] + #[pyo3(signature = (path, reader = None))] + fn get_available_series<'py>( + py: Python<'py>, + #[gen_stub(override_type(type_repr="str | pathlib.Path | View", imports=("pathlib")))] + path: Bound<'py, PyAny>, + reader: Option<&str>, + ) -> PyResult> { + let path = if path.is_instance_of::() { + let py_view: Self = path.cast_into::()?.extract::()?; + py_view.view.path().to_owned() + } else { + let builtins = PyModule::import(py, "builtins")?; + let mut path = PathBuf::from( + builtins + .getattr("str")? + .call1((path,))? + .cast_into::()? + .extract::()?, + ); + if path.is_dir() { + for file in path.read_dir()?.flatten().sorted_by_key(|i| i.file_name()) { + let p = file.path(); + if file.path().is_file() && (p.extension() == Some("tif".as_ref())) { + path = p; + break; + } + } + } + path + }; + + Ok(DynReader::get_available_series_select_reader(path, reader)?) + } + + /// get all series contained in the file + #[staticmethod] + #[pyo3(signature = (path, series, reader = None))] + fn get_available_positions<'py>( + py: Python<'py>, + #[gen_stub(override_type(type_repr="str | pathlib.Path | View", imports=("pathlib")))] + path: Bound<'py, PyAny>, + series: usize, + reader: Option<&str>, + ) -> PyResult> { + let path = if path.is_instance_of::() { + let py_view: Self = path.cast_into::()?.extract::()?; + py_view.view.path().to_owned() + } else { + let builtins = PyModule::import(py, "builtins")?; + let mut path = PathBuf::from( + builtins + .getattr("str")? + .call1((path,))? + .cast_into::()? + .extract::()?, + ); + if path.is_dir() { + for file in path.read_dir()?.flatten().sorted_by_key(|i| i.file_name()) { + let p = file.path(); + if file.path().is_file() && (p.extension() == Some("tif".as_ref())) { + path = p; + break; + } + } + } + path + }; + Ok(DynReader::get_available_positions_select_reader( + path, series, reader, + )?) + } + + #[cfg(feature = "tiffwrite")] + #[pyo3(signature = (file, colors = None, overwrite = false, bar = true))] + fn save_as_tiff( + &self, + py: Python, + file: PathBuf, + colors: Option>, + overwrite: bool, + bar: bool, + ) -> PyResult<()> { + let bar = if bar { + Some(crate::tiffwrite::get_bar( + Some(0), + Some("writing tiff file".to_string()), + )) + } else { + None + }; + let options = + crate::tiffwrite::TiffOptions::new(bar, None, colors.unwrap_or_default(), overwrite)?; + py.detach(|| match self.dtype { + PixelType::I8 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U8 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::I16 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U16 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::I32 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U32 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::I64 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U64 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::I128 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::U128 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::F32 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::F64 => self.view.save_as_tiff_with_type::(file, &options), + PixelType::F128 => Err(Error::NotImplemented( + "saving as f128 is not implemented".to_string(), + )), + })?; + Ok(()) + } + + #[cfg(feature = "movie")] + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (file, speed = 1.0, brightness = None, scale = 1.0, colors = None, overwrite = false, register = false, no_scaling = false))] + fn save_as_movie( + &self, + file: PathBuf, + speed: f64, + brightness: Option>, + scale: f64, + colors: Option>, + overwrite: bool, + register: bool, + no_scaling: bool, + ) -> PyResult<()> { + let options = MovieOptions::new( + speed, + brightness.unwrap_or_default(), + scale, + colors.unwrap_or_default(), + overwrite, + register, + no_scaling, + )?; + self.view.save_as_movie(file, &options)?; + Ok(()) + } +} + +submit! { + gen_methods_from_python! { + r#" + import typing + import numpy.typing + + class PyView: + def __pow__(other: View | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ... + def __rpow__(other: View | numpy.typing.NDArray | int | float, mod: typing.Any = None, /) -> numpy.typing.NDArray: ... + + def max(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: + """ Return the maximum along a given axis. Arguments beyond axis are not implemented """ + + def min(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: + """ Return the minimum along a given axis. Arguments beyond axis are not implemented """ + + def mean(axis: int | str = None, dtype: numpy.typing.DTypeLike = None, out: typing.Any = None, keepdims: bool = False) -> View | numpy.typing.NDArray | int | float: + """ Return the mean along a given axis. Arguments beyond axis are not implemented """ + + def sum(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: + """ Return the sum along a given axis. Arguments beyond axis are not implemented """ + "# + } +} + +#[cfg(feature = "tiffwrite")] +#[allow(clippy::too_many_arguments)] +#[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")] +#[pyfunction] +#[pyo3(signature = (files_in, files_out, operations = None, colors = None, overwrite = false, bar = true, message = None))] +fn batch_to_tiff( + py: Python, + files_in: Vec, + files_out: Vec, + operations: Option>, + colors: Option>, + overwrite: bool, + bar: bool, + message: Option, +) -> PyResult<()> { + py.detach(|| { + crate::tiffwrite::batch_to_tiff( + &files_in, &files_out, operations, colors, overwrite, bar, message, + ) + })?; + Ok(()) +} + +#[gen_stub_pyclass] +#[pyclass( + subclass, + from_py_object, + frozen, + name = "Shape", + module = "ndbioimage.ndbioimage_rs" +)] +#[derive(Clone, Debug, Serialize, Deserialize)] +struct PyShape { + inner: Shape, +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyShape { + #[new] + #[pyo3(signature = (order, c = 1, z = 1, t = 1, y = 1, x = 1))] + fn new(order: String, c: usize, z: usize, t: usize, y: usize, x: usize) -> PyResult { + Ok(Self { + inner: Shape { + c, + z, + t, + y, + x, + order: order + .chars() + .map(|c| c.to_uppercase().to_string().parse::()) + .collect::, _>>() + .map_err(Error::from)?, + }, + }) + } + + #[getter] + fn get_c(&self) -> usize { + self.inner.c + } + + #[getter] + fn get_z(&self) -> usize { + self.inner.z + } + + #[getter] + fn get_t(&self) -> usize { + self.inner.t + } + + #[getter] + fn get_y(&self) -> usize { + self.inner.y + } + + #[getter] + fn get_x(&self) -> usize { + self.inner.x + } + + fn __str__(&self) -> String { + format!("{}", self.inner) + } + + fn __repr__(&self) -> String { + format!( + "PyShape({}, {}, {}, {}, {})", + self.inner.c, self.inner.z, self.inner.t, self.inner.x, self.inner.y + ) + } + + fn __getnewargs__(&self) -> (String, usize, usize, usize, usize, usize) { + ( + self.inner + .order + .iter() + .map(|axis| format!("{}", axis)) + .collect::>() + .join(""), + self.inner.c, + self.inner.z, + self.inner.t, + self.inner.y, + self.inner.x, + ) + } + + #[gen_stub(override_return_type(type_repr="typing.Optional[int | list[int]]", imports=("typing")))] + fn __getitem__<'py>( + &self, + py: Python<'py>, + #[gen_stub(override_type( + type_repr = "str | int | None | Ellipsis | slice | list[int] | tuple[int]" + ))] + idx: Bound<'py, PyAny>, + ) -> PyResult> { + let idx = if idx.is_instance_of::() || idx.is_instance_of::() { + vec![0, 1, 2, 3, 4] + } else if idx.is_instance_of::() { + let indices = idx.cast::()?.indices(5)?; + if indices.step > 0 { + (indices.start..indices.stop) + .step_by(indices.step as usize) + .map(|i| i as usize) + .collect::>() + } else { + (indices.stop..indices.start) + .step_by(-indices.step as usize) + .map(|i| i as usize) + .collect::>() + } + } else if idx.is_instance_of::() { + idx.cast::()?.extract::>()? + } else if idx.is_instance_of::() { + idx.cast::()?.extract::>()? + } else if idx.is_instance_of::() { + let s = idx.cast::()?.extract::()?; + s.to_uppercase() + .chars() + .map(|i| match i { + 'C' => Ok(0), + 'Z' => Ok(1), + 'T' => Ok(2), + 'Y' => Ok(3), + 'X' => Ok(4), + _ => Err(Error::Parse(s.to_string())), + }) + .collect::, _>>()? + } else if idx.is_instance_of::() { + vec![idx.cast::()?.extract::()?] + } else { + return Err(PyErr::new::("Unknown type")); + }; + let shape = [ + self.inner.c, + self.inner.z, + self.inner.t, + self.inner.y, + self.inner.x, + ]; + let shape = idx.into_iter().map(|i| shape[i % 5]).collect::>(); + if shape.is_empty() { + Ok(PyNone::get(py).into_bound_py_any(py)?) + } else if shape.len() == 1 { + Ok(shape[0].into_bound_py_any(py)?) + } else { + Ok(shape.into_bound_py_any(py)?) + } + } + + fn to_list(&self) -> Vec { + vec![ + self.inner.c, + self.inner.z, + self.inner.t, + self.inner.y, + self.inner.x, + ] + } } pub(crate) fn ndbioimage_file() -> PathBuf { @@ -972,19 +2004,55 @@ pub(crate) fn ndbioimage_file() -> PathBuf { PathBuf::from(file) } +/// generates ndbioimage/__init__.pyi #[pyfunction] -#[pyo3(name = "download_bioformats")] -fn py_download_bioformats(gpl_formats: bool) -> PyResult<()> { - download_bioformats(gpl_formats)?; - Ok(()) +pub fn generate_stub(dest_path: String) -> PyResult<()> { + Ok(StubInfo::from_project_root( + "ndbioimage".to_string(), + PathBuf::from(dest_path).join("py"), + true, + StubGenConfig::default(), + )? + .generate()?) +} + +#[gen_stub_pyfunction(module = "ndbioimage.ndbioimage_rs")] +#[pyfunction] +fn main() -> PyResult<()> { + Ok(crate::main::main(Some( + std::env::args() + .skip_while(|arg| !arg.ends_with("ndbioimage")) + .collect(), + ))?) } #[pymodule] #[pyo3(name = "ndbioimage_rs")] -fn ndbioimage_rs(m: &Bound) -> PyResult<()> { - color_eyre::install()?; - m.add_class::()?; - m.add_class::()?; - m.add_function(wrap_pyfunction!(py_download_bioformats, m)?)?; - Ok(()) +mod ndbioimage_rs { + use pyo3::prelude::*; + + #[pymodule_export] + use super::main; + + #[cfg(feature = "tiffwrite")] + #[pymodule_export] + use super::batch_to_tiff; + + #[pymodule_export] + use super::generate_stub; + + #[pymodule_export] + use super::PyView; + + #[pymodule_export] + use super::PyShape; + + #[pymodule_export] + use ome_metadata::py::ome_metadata; + + #[pymodule_init] + fn init(_: &Bound<'_, PyModule>) -> PyResult<()> { + let _ = color_eyre::install(); + Ok(()) + } } diff --git a/src/reader.rs b/src/reader.rs deleted file mode 100644 index d64bfe5..0000000 --- a/src/reader.rs +++ /dev/null @@ -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

(path: P) -> Result<(PathBuf, Option), Error> -where - P: Into, -{ - 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::() { - 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 for PixelType { - type Error = Error; - - fn try_from(value: i32) -> Result { - 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 { - 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` using try_into. -#[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Debug)] -pub enum Frame { - I8(Array2), - U8(Array2), - I16(Array2), - U16(Array2), - I32(Array2), - U32(Array2), - F32(Array2), - F64(Array2), - I64(Array2), - U64(Array2), - I128(Array2), - U128(Array2), - F128(Array2), // f128 is nightly -} - -macro_rules! impl_frame_cast { - ($($t:tt: $s:ident $(,)?)*) => { - $( - impl From> 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 TryInto> for Frame -where - T: FromPrimitive + Zero + 'static, -{ - type Error = Error; - - fn try_into(self) -> Result, 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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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, - /// 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

(path: P, series: usize) -> Result - where - P: AsRef, - { - 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 { - let mut ome = self.ome_xml()?.parse::()?; - 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 { - self.ome_xml() - } - - fn deinterleave(&self, bytes: Vec, channel: usize) -> Result, 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 { - 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) -> Result { - 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 { - 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(); - } - } -} diff --git a/src/readers.rs b/src/readers.rs new file mode 100644 index 0000000..2b594ab --- /dev/null +++ b/src/readers.rs @@ -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 = LazyLock::new(|| Regex::new(r"^([CZTSP])\D+(\d+)$").unwrap()); + +#[derive(Debug, Clone, Copy, Default)] +pub struct Dimensions { + pub c: Option, + pub z: Option, + pub t: Option, + pub s: Option, + pub p: Option, +} + +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

(path: P) -> Result<(PathBuf, Self), Error> + where + P: AsRef, + { + 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` using try_into. +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Debug)] +pub enum ArrayT { + I8(Array), + U8(Array), + I16(Array), + U16(Array), + I32(Array), + U32(Array), + F32(Array), + F64(Array), + I64(Array), + U64(Array), + I128(Array), + U128(Array), + F128(Array), // f128 is nightly +} + +pub(crate) type Frame = ArrayT; + +pub trait Reader: Clone + Sized + Debug + Send + Hash + Into { + fn new

(path: P, series: usize, position: usize) -> Result + where + P: AsRef; + + fn reader_name(&self) -> String { + type_name::().to_string() + } + + // TODO: read from file if present + fn metadata(&self) -> Result; + + /// get a sliceable view on the image file + fn view(&self) -> View { + 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; + + 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

(path: P, series: usize) -> Result, Error> + where + P: AsRef; + fn get_available_series

(path: P) -> Result, Error> + where + P: AsRef; +} + +impl TryFrom for PixelType { + type Error = Error; + + fn try_from(value: i32) -> Result { + 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 { + 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 From> for ArrayT { + 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 TryInto> for ArrayT +where + D: Dimension, + T: FromPrimitive + Zero + 'static, +{ + type Error = Error; + + fn try_into(self) -> Result, 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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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::().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

(path: P, series: usize, position: usize) -> Result + where + P: AsRef, + { + let mut errors = Vec::::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 { + 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 { + 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

(path: P, series: usize) -> Result, Error> + where + P: AsRef, + { + let mut errors = Vec::::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

(path: P) -> Result, Error> + where + P: AsRef, + { + let mut errors = Vec::::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(path: P, reader: R) -> Result + where + P: AsRef, + R: AsRef, + { + 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( + path: P, + series: usize, + reader: Option, + ) -> Result, Error> + where + P: AsRef, + R: AsRef, + { + 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( + path: P, + reader: Option, + ) -> Result, Error> + where + P: AsRef, + R: AsRef, + { + 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)? + }) + } +} diff --git a/src/readers/bioformats_java.rs b/src/readers/bioformats_java.rs new file mode 100644 index 0000000..1cab8cd --- /dev/null +++ b/src/readers/bioformats_java.rs @@ -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> = const { OnceCell::new() } +} + +static DOWNLOAD_LOCK: Mutex<()> = Mutex::new(()); +static JVM_BUILT: Mutex = Mutex::new(false); + +/// Ensure 1 jvm per thread +fn jvm() -> Rc { + 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::, _>>().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::, _>>().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::, _>>().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 = 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(vec: Vec) -> Vec { + 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 { + 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, Error> { + Ok(transmute_vec(self.open_bi8(index)?)) + } + + method!(open_bi8, "openBytes", [index: i32|p] => Vec|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 { + let reader = jvm().create_instance("loci.formats.ImageReader", InvocationArg::empty())?; + Ok(ImageReader(reader)) + } + + pub(crate) fn open_bytes(&self, index: i32) -> Result, Error> { + Ok(transmute_vec(self.open_bi8(index)?)) + } + + pub(crate) fn ome_xml(&self) -> Result { + 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|c); +} + +/// Wrapper around bioformats java class loci.formats.MetadataTools +pub(crate) struct MetadataTools(Instance); + +impl MetadataTools { + pub(crate) fn new() -> Result { + 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, + /// 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 for DynReader { + fn from(value: BioFormatsJavaReader) -> Self { + DynReader::BioFormatsJava(value) + } +} + +impl Hash for BioFormatsJavaReader { + fn hash(&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 { + 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 { + self.ome_xml() + } + + fn deinterleave(&self, bytes: Vec, channel: usize) -> Result, 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) -> Result { + 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

(path: P, series: usize, _position: usize) -> Result + where + P: AsRef, + { + 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 { + self.get_ome() + } + + /// Retrieve fame at channel c, slize z and time t. + fn get_frame(&self, c: usize, z: usize, t: usize) -> Result { + 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

(_path: P, _series: usize) -> Result, Error> + where + P: AsRef, + { + Ok(HashSet::from([0])) + } + + fn get_available_series

(path: P) -> Result, Error> + where + P: AsRef, + { + 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 { + 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(()) + } +} diff --git a/src/readers/bioformats_rust.rs b/src/readers/bioformats_rust.rs new file mode 100644 index 0000000..fd572ed --- /dev/null +++ b/src/readers/bioformats_rust.rs @@ -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>, + 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 for DynReader { + fn from(value: BioFormatsRustReader) -> Self { + DynReader::BioFormatsRust(value) + } +} + +impl Hash for BioFormatsRustReader { + fn hash(&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>; + + fn deref(&self) -> &Self::Target { + &self.reader + } +} + +fn map_pixel_type(bf: bioformats::PixelType) -> Result { + 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, 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, channel: usize) -> Result, 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) -> Result { + 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

(path: P, series: usize, _position: usize) -> Result + where + P: AsRef, + { + 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 { + 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 { + 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

(_path: P, _series: usize) -> Result, Error> + where + P: AsRef, + { + Ok(HashSet::from([0])) + } + + fn get_available_series

(path: P) -> Result, Error> + where + P: AsRef, + { + 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 { + 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" + } +} diff --git a/src/readers/czi.rs b/src/readers/czi.rs new file mode 100644 index 0000000..e7fdf2f --- /dev/null +++ b/src/readers/czi.rs @@ -0,0 +1,1250 @@ +use crate::colors::Color; +use crate::error::Error; +use crate::readers::{ArrayT, DynReader, Frame, PixelType, Reader, Shape}; +use indexmap::IndexMap; +use itertools::Itertools; +use libczirw_sys::{AttachmentData, Dimension, InputStream, ReaderOpenInfo}; +use ndarray::{Array2, s}; +use ome_metadata::{Ome, ome}; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; +use thread_local::ThreadLocal; + +#[derive(Debug, Deserialize, Serialize)] +pub struct CziReader { + #[serde(skip)] + reader: ThreadLocal, + block_map: HashMap<(usize, usize, usize), Vec>, + path: PathBuf, + series: usize, + position: usize, + shape: Shape, + pixel_type: PixelType, +} + +impl From for DynReader { + fn from(value: CziReader) -> Self { + DynReader::Czi(value) + } +} + +impl Hash for CziReader { + fn hash(&self, state: &mut H) { + self.path.hash(state); + self.series.hash(state); + self.position.hash(state); + } +} + +impl PartialEq for CziReader { + 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 CziReader {} + +impl Clone for CziReader { + fn clone(&self) -> Self { + Self { + reader: ThreadLocal::default(), + block_map: self.block_map.clone(), + path: self.path.clone(), + series: self.series, + position: self.position, + shape: self.shape.clone(), + pixel_type: self.pixel_type, + } + } +} + +impl CziReader { + fn get_reader(&self) -> Result<&libczirw_sys::CziReader, Error> { + self.reader.get_or_try(|| { + let reader = libczirw_sys::CziReader::create()?; + let stream = InputStream::create_from_file_utf8(self.path.to_string_lossy().as_ref())?; + let open_info = ReaderOpenInfo::new(&stream); + reader.open(open_info)?; + Ok(reader) + }) + } + + // pub fn set_reader(&self) -> Result<(), Error> { + // self.get_reader().map(|_| ()) + // } + + fn metadata_xml(&self) -> Result { + let reader = self.get_reader()?; + let metadata_segment = reader.get_metadata_segment()?; + let xml = metadata_segment.get_metadata_as_xml()?; + Ok(String::try_from(&xml)?) + } + + fn attachments(&self) -> Result, Error> { + let reader = self.get_reader()?; + let n = reader.get_attachment_count()?; + let mut attachments = HashMap::new(); + for i in 0..n { + let attachment = reader.read_attachment(i)?; + let info = attachment.get_info()?; + let name = info.get_name()?; + let data = attachment.get_data()?; + attachments.insert(name, data); + } + Ok(attachments) + } +} + +#[derive(Debug, Clone)] +enum Version { + /// unspecified + None, + /// 1.0 + One, + /// 1.1, 1.2, etc + Other, +} + +impl From> for Version { + fn from(s: Option) -> Self { + if let Some(v) = s { + if v == "1.0" { Self::One } else { Self::Other } + } else { + Self::None + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum CziError { + #[error("czi file has no valid blocks")] + NoValidBlocks, +} + +impl Reader for CziReader { + fn new

(path: P, series: usize, position: usize) -> Result + where + P: AsRef, + { + let mut new = Self { + reader: ThreadLocal::default(), + block_map: HashMap::new(), + path: path.as_ref().to_path_buf(), + series, + position, + shape: Shape::default(), + pixel_type: PixelType::U8, + }; + + let reader = new.get_reader()?; + let statistics_simple = reader.get_statistics_simple()?; + let c = statistics_simple.get_sub_block_count(); + let mut block_map: HashMap<(usize, usize, usize), Vec> = HashMap::new(); + let mut pixel_type = None; + let m = statistics_simple.get_max_m_index() != i32::MIN; + + for i in 0..c { + if let Ok(info) = reader.try_get_sub_block_info_for_index(i) { + if m && info.get_m_index() != position as i32 { + continue; + } + let coordinate = info.get_coordinate(); + let d = Dimension::vec_from_bitflags(coordinate.get_dimensions_valid()); + let v = coordinate.get_value(); + if d.contains(&Dimension::S) && v[Dimension::S as usize - 1] as usize != series { + continue; + } + let c = if d.contains(&Dimension::C) { + v[Dimension::C as usize - 1] as usize + } else { + 0 + }; + let z = if d.contains(&Dimension::Z) { + v[Dimension::Z as usize - 1] as usize + } else { + 0 + }; + let t = if d.contains(&Dimension::T) { + v[Dimension::T as usize - 1] as usize + } else { + 0 + }; + if let Some(blocks) = block_map.get_mut(&(c, z, t)) { + blocks.push(i); + } else { + block_map.insert((c, z, t), vec![i]); + } + if pixel_type.is_none() { + pixel_type = Some(info.get_pixel_type()?.try_into()?) + } + } + } + + let mut min_x = i32::MAX; + let mut max_x = i32::MIN; + let mut min_y = i32::MAX; + let mut max_y = i32::MIN; + for &i in Ok(block_map.get(&(0, 0, 0))) + .transpose() + .unwrap_or_else(|| block_map.values().next().ok_or(CziError::NoValidBlocks))? + { + if let Ok(info) = reader.try_get_sub_block_info_for_index(i) { + let rect = info.get_logical_rect(); + min_x = min_x.min(rect.get_x()); + max_x = max_x.max(rect.get_x() + rect.get_w()); + min_y = min_y.min(rect.get_y()); + max_y = max_y.max(rect.get_y() + rect.get_h()); + } + } + + let dim_bounds = statistics_simple.get_dim_bounds(); + let dimensions = Dimension::vec_from_bitflags(dim_bounds.get_dimensions_valid()); + for (i, d) in dim_bounds.get_size().into_iter().zip(dimensions) { + match d { + Dimension::C => new.shape.c = i as usize, + Dimension::Z => new.shape.z = i as usize, + Dimension::T => new.shape.t = i as usize, + _ => {} + } + } + new.shape.x = (max_y - min_y) as usize; + new.shape.y = (max_x - min_x) as usize; + new.block_map = block_map; + new.pixel_type = pixel_type.ok_or(CziError::NoValidBlocks)?; + Ok(new) + } + + fn metadata(&self) -> Result { + let mut ome = Ome::default(); + let xml = xmltree::Element::parse(self.metadata_xml()?.as_bytes())?; + + if let Some(metadata) = xml.get_child("Metadata") { + let experiment = metadata.get_child("Experiment"); + let version: Version = if let Some(version) = metadata.get_child("Version") { + version.get_text().map(|s| s.to_string()) + } else if let Some(experiment) = experiment + && let Some(version) = experiment.attributes.get("Version") + { + Some(version.to_string()) + } else { + None + } + .into(); + + let information = metadata.get_child("Information"); + let display_setting = metadata.get_child("DisplaySetting"); + let acquisition_block = experiment + .and_then(|e| e.get_child("ExperimentBlocks")) + .and_then(|e| e.get_child("AcquisitionBlock")); + let instrument = information.and_then(|i| i.get_child("Instrument")); + let image = information.and_then(|i| i.get_child("Image")); + let multi_track_setup = acquisition_block.and_then(|e| e.get_child("MultiTrackSetup")); + + // set experimenters + if let Version::One = version { + ome.experimenter.push(ome::Experimenter { + id: "Experimenter:0".to_string(), + user_name: information + .and_then(|i| i.get_child("User")) + .and_then(|u| u.get_child("DisplayName")) + .and_then(|d| d.get_text()) + .map(|t| t.to_string()), + ..Default::default() + }); + } else if let Version::Other = version { + ome.experimenter.push(ome::Experimenter { + id: "Experimenter:0".to_string(), + user_name: information + .and_then(|i| i.get_child("Document")) + .and_then(|d| d.get_child("UserName")) + .and_then(|u| u.get_text()) + .map(|t| t.to_string()), + ..Default::default() + }); + } + + // set instruments + if let Version::One = version { + ome.instrument.push(ome::Instrument { + id: instrument + .and_then(|i| i.attributes.get("Id")) + .map(|i| i.to_string()) + .unwrap_or("Instrument:0".to_string()), + ..Default::default() + }) + } else if let Version::Other = version + && let Some(microscopes) = instrument.and_then(|i| i.get_child("Microscopes")) + { + for i in 0..microscopes.children.len() { + ome.instrument.push(ome::Instrument { + id: format!("Instrument:{}", i), + ..Default::default() + }) + } + } + + // set detectors + if let Some(detectors) = instrument.and_then(|i| i.get_child("Detectors")) { + if let Version::One = version { + for detector in &detectors.children { + if let Some(detector) = detector.as_element() { + let n = ome.instrument[0].detector.len(); + ome.instrument[0].detector.push(ome::Detector { + id: detector + .attributes + .get("Id") + .map(|i| i.to_string()) + .unwrap_or(format!("Detector:{}", n)), + model: detector + .get_child("Manufacturer") + .and_then(|m| m.get_child("Model")) + .and_then(|m| m.get_text()) + .map(|t| t.to_string()), + amplification_gain: detector + .get_child("AmplificationGain") + .and_then(|a| a.get_text()) + .and_then(|t| t.parse().ok()), + gain: detector + .get_child("Gain") + .and_then(|g| g.get_text()) + .and_then(|t| t.parse().ok()), + zoom: detector + .get_child("Zoom") + .and_then(|z| z.get_text()) + .and_then(|t| t.parse().ok()), + r#type: Some( + detector + .get_child("Type") + .and_then(|t| t.get_text()) + .and_then(|t| t.parse().ok()) + .unwrap_or(ome::DetectorType::Other), + ), + ..Default::default() + }) + } + } + } else if let Version::Other = version { + for detector in &detectors.children { + if let Some(detector) = detector.as_element() { + let n = ome.instrument[0].detector.len(); + ome.instrument[0].detector.push(ome::Detector { + id: detector + .attributes + .get("Id") + .map(|i| i.replace(" ", "")) + .unwrap_or(format!("Detector:{}", n)), + model: detector + .get_child("Manufacturer") + .and_then(|m| m.get_child("Model")) + .and_then(|m| m.get_text()) + .map(|t| t.to_string()), + r#type: Some( + detector + .get_child("Type") + .and_then(|t| t.get_text()) + .and_then(|t| t.parse().ok()) + .unwrap_or(ome::DetectorType::Other), + ), + ..Default::default() + }) + } + } + } + } + + // set objectives + if let Some(objectives) = instrument.and_then(|i| i.get_child("Objectives")) { + for objective in &objectives.children { + if let Some(objective) = objective.as_element() { + let n = ome.instrument[0].objective.len(); + ome.instrument[0].objective.push(ome::Objective { + id: objective + .attributes + .get("Id") + .map(|t| t.to_string()) + .unwrap_or(format!("Objective:{}", n)), + model: objective + .get_child("Manufacturer") + .and_then(|m| m.get_child("Model")) + .and_then(|m| m.get_text()) + .map(|t| t.to_string()), + lens_na: objective + .get_child("LensNA") + .and_then(|l| l.get_text()) + .and_then(|l| l.parse().ok()), + nominal_magnification: objective + .get_child("NominalMagnification") + .and_then(|n| n.get_text()) + .and_then(|l| l.parse().ok()), + ..Default::default() + }) + } + } + } + + // set tube lenses + let pat = regex::Regex::new(r"\d+[,.]\d*")?; + if let Version::One = version { + if let Some(multi_track_setup) = multi_track_setup { + for (idx, track_setup) in multi_track_setup.children.iter().enumerate() { + if let Some(tube_lens) = track_setup + .as_element() + .and_then(|i| i.get_child("TubeLens")) + { + let text = tube_lens.get_text().map(|t| t.to_string()); + ome.instrument[0].objective.push(ome::Objective { + id: format!("Objective:Tubelens:{idx}"), + nominal_magnification: Some( + text.as_ref() + .map(|t| t.replace(",", ".")) + .and_then(|t| { + pat.captures(t.as_str()) + .and_then(|c| c.get(0)) + .and_then(|c| c.as_str().parse().ok()) + }) + .unwrap_or(1.0), + ), + model: text, + ..Default::default() + }); + } + } + } + } else if let Version::Other = version + && let Some(tube_lenses) = instrument.and_then(|i| i.get_child("TubeLenses")) + { + for (idx, tube_lens) in tube_lenses.children.iter().enumerate() { + if let Some(tube_lens) = tube_lens.as_element() { + let text = tube_lens.attributes.get("Name").map(|t| t.to_string()); + ome.instrument[0].objective.push(ome::Objective { + id: format!( + "Objective:{}", + tube_lenses + .attributes + .get("Id") + .unwrap_or(&format!("Tubelens:{}", idx)) + ), + nominal_magnification: Some( + text.as_ref() + .map(|t| t.replace(",", ".")) + .and_then(|t| { + pat.captures(t.as_str()) + .and_then(|c| c.get(0)) + .and_then(|c| c.as_str().parse().ok()) + }) + .unwrap_or(1.0), + ), + model: text, + ..Default::default() + }) + } + } + } + + // set light sources + if let Some(light_sources) = instrument.and_then(|i| i.get_child("LightSources")) { + if let Version::One = version { + for (idx, light_source) in light_sources.children.iter().enumerate() { + if let Some(light_source) = light_source.as_element() + && let Some(laser) = light_source + .get_child("LightSourceType") + .and_then(|l| l.get_child("Laser")) + { + ome.instrument[0] + .light_source_group + .push(ome::LightSourceGroup::Laser(ome::Laser { + id: light_source + .attributes + .get("Id") + .map(|i| i.to_string()) + .unwrap_or(format!("Laser:{}", idx)), + model: light_source + .get_child("Manufacturer") + .and_then(|m| m.get_child("Model")) + .and_then(|m| m.get_text()) + .map(|t| t.to_string()), + power: light_source + .get_child("Power") + .and_then(|p| p.get_text()) + .and_then(|t| t.parse().ok()), + wavelength: laser + .get_child("Wavelength") + .and_then(|w| w.get_text()) + .and_then(|t| t.parse().ok()), + ..Default::default() + })) + } + } + } else if let Version::Other = version { + let pat = regex::Regex::new(r"^.*?(\d*)$")?; + for (idx, light_source) in light_sources.children.iter().enumerate() { + if let Some(light_source) = light_source.as_element() + && light_source + .get_child("LightSourceType") + .and_then(|l| l.get_child("Laser")) + .is_some() + { + let id = light_source.attributes.get("Id"); + ome.instrument[0] + .light_source_group + .push(ome::LightSourceGroup::Laser(ome::Laser { + id: format!( + "LightSource:{}", + id.unwrap_or(&format!("{}", idx)) + ), + power: light_source + .get_child("Power") + .and_then(|p| p.get_text()) + .and_then(|t| t.parse().ok()), + wavelength: id.and_then(|i| { + pat.captures(i) + .and_then(|c| c.get(1)) + .and_then(|c| c.as_str().parse().ok()) + }), + ..Default::default() + })) + } + } + } + } + + // set filters + if let Version::One = version + && let Some(multi_track_setup) = multi_track_setup + { + for (idx, track_setup) in multi_track_setup.children.iter().enumerate() { + if let Some(track_setup) = track_setup.as_element() + && let Some(beam_splitters) = track_setup.get_child("BeamSplitters") + { + for beam_splitter in &beam_splitters.children { + if let Some(beam_splitter) = beam_splitter.as_element() { + ome.instrument[0].filter_set.push(ome::FilterSet { + id: format!("FilterSet:{}", idx), + model: beam_splitter + .get_child("Filter") + .and_then(|f| f.get_text()) + .map(|t| t.to_string()), + ..Default::default() + }) + } + } + } + } + } + + // get positions + let (pos_x, pos_y, pos_z) = if let Version::One = version { + image + .and_then(|i| i.get_child("S")) + .and_then(|s| s.get_child("Scenes")) + .and_then(|s| s.children.first()) + .and_then(|c| c.as_element()) + .and_then(|e| e.get_child("Positions")) + .and_then(|p| p.children.first()) + .and_then(|p| p.as_element()) + .map(|p| { + ( + p.attributes.get("X").and_then(|x| x.parse::().ok()), + p.attributes.get("Y").and_then(|y| y.parse::().ok()), + p.attributes.get("Z").and_then(|z| z.parse::().ok()), + ) + }) + .unwrap_or((None, None, None)) + } else if let Version::Other = version { + image + .and_then(|i| i.get_child("Dimensions")) + .and_then(|d| d.get_child("S")) + .and_then(|s| s.get_child("Scenes")) + .and_then(|s| s.get_child("CenterPosition")) + .and_then(|c| c.get_text()) + .map(|s| { + let c = s + .split(',') + .take(2) + .map(|i| i.parse::().ok()) + .collect::>(); + ( + c.first().cloned().flatten(), + c.get(1).cloned().flatten(), + None, + ) + }) + .unwrap_or((None, None, None)) + } else { + (None, None, None) + }; + + // set pixels + if let Some(image) = information.and_then(|i| i.get_child("Image")) { + let mut pxsize_x: Option = None; + let mut pxsize_y: Option = None; + let mut pxsize_z: Option = None; + if let Some(items) = metadata + .get_child("Scaling") + .and_then(|m| m.get_child("Items")) + { + for distance in &items.children { + if let Some(distance) = distance.as_element() { + match distance.attributes.get("Id").map(|i| i.as_str()) { + Some("X") => { + pxsize_x = distance + .get_child("Value") + .and_then(|v| v.get_text()) + .and_then(|v| v.parse().ok()) + } + Some("Y") => { + pxsize_y = distance + .get_child("Value") + .and_then(|v| v.get_text()) + .and_then(|v| v.parse().ok()) + } + Some("Z") => { + pxsize_z = distance + .get_child("Value") + .and_then(|v| v.get_text()) + .and_then(|v| v.parse().ok()) + } + _ => {} + } + } + } + } + + let objective_settings = image.get_child("ObjectiveSettings"); + ome.image.push(ome::Image { + id: "Image:0".to_string(), + name: information + .and_then(|i| i.get_child("Document")) + .and_then(|d| d.get_child("Name")) + .and_then(|n| n.get_text()) + .map(|t| format!("{} #1", t)), + 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: image + .get_child("PixelType") + .and_then(|p| p.get_text()) + .and_then(|t| t.to_lowercase().replace("gray", "uint").parse().ok()) + .unwrap_or(ome::PixelType::Uint16), + significant_bits: image + .get_child("ComponentBitCount") + .and_then(|c| c.get_text()) + .and_then(|t| t.parse().ok()), + big_endian: None, + interleaved: None, + metadata_only: None, + physical_size_x: pxsize_x.map(|p| p * 1e9), + physical_size_x_unit: ome::UnitsLength::nm, + physical_size_y: pxsize_y.map(|p| p * 1e9), + physical_size_y_unit: ome::UnitsLength::nm, + physical_size_z: pxsize_z.map(|p| p * 1e9), + physical_size_z_unit: ome::UnitsLength::nm, + time_increment: None, + time_increment_unit: ome::UnitsTime::s, + channel: Vec::new(), + bin_data: Vec::new(), + tiff_data: Vec::new(), + plane: Vec::new(), + }, + experimenter_ref: Some(ome::AnnotationRef { + id: "Experimenter:0".to_string(), + }), + instrument_ref: Some(ome::AnnotationRef { + id: "Instrument:0".to_string(), + }), + objective_settings: objective_settings.map(|o| ome::ObjectiveSettings { + id: o + .get_child("ObjectiveRef") + .and_then(|o| o.attributes.get("Id")) + .map(|t| t.to_string()) + .unwrap_or("Objective:0".to_string()), + medium: o + .get_child("Medium") + .and_then(|m| m.get_text()) + .and_then(|t| t.parse().ok()), + refractive_index: o + .get_child("RefractiveIndex") + .and_then(|r| r.get_text()) + .and_then(|t| t.parse().ok()), + ..Default::default() + }), + stage_label: Some(ome::StageLabel { + name: "Scene position #0".to_string(), + x: pos_x, + x_unit: ome::UnitsLength::um, + y: pos_y, + y_unit: ome::UnitsLength::um, + z: pos_z, + z_unit: ome::UnitsLength::um, + }), + acquisition_date: None, + description: None, + experiment_ref: None, + experimenter_group_ref: None, + imaging_environment: None, + roi_ref: Vec::new(), + microbeam_manipulation_ref: Vec::new(), + annotation_ref: Vec::new(), + }); + } + + // channels + let channels_im = image + .and_then(|i| i.get_child("Dimensions")) + .and_then(|d| d.get_child("Channels")) + .map(|c| { + c.children + .iter() + .filter_map(|c| { + c.as_element() + .and_then(|e| e.attributes.get("Id").map(|id| (id, e))) + }) + .collect::>() + }) + .unwrap_or_else(IndexMap::new); + + let channels_ds = display_setting + .and_then(|d| d.get_child("Channels")) + .map(|c| { + c.children + .iter() + .filter_map(|c| { + c.as_element() + .and_then(|e| e.attributes.get("Id").map(|id| (id, e))) + }) + .collect::>() + }) + .unwrap_or_else(HashMap::new); + + let channels_ts = experiment + .and_then(|e| e.get_child("ExperimentBlocks")) + .and_then(|e| e.get_child("AcquisitionBlock")) + .and_then(|a| a.get_child("MultiTrackSetup")) + .map(|m| { + m.children + .iter() + .filter_map(|t| { + t.as_element().and_then(|ts| { + ts.get_child("Detectors").map(move |d| { + d.children.iter().filter_map(move |d| { + d.as_element() + .and_then(|d| d.attributes.get("Id").map(|id| (id, ts))) + }) + }) + }) + }) + .flatten() + .collect::>() + }) + .unwrap_or_else(HashMap::new); + + // set channels + for (idx, (&key, &channel)) in channels_im.iter().enumerate() { + let detector_settings = channel.get_child("DetectorSettings"); + let laser_scan_info = channel.get_child("LaserScanInfo"); + let detector = detector_settings.and_then(|ds| ds.get_child("Detector")); + let filter_set = channels_ts + .get(key) + .and_then(|c| c.get_child("BeamSplitters")) + .and_then(|b| b.children.first()) + .and_then(|b| b.as_element()) + .and_then(|b| b.get_child("Filter")) + .and_then(|f| f.get_text()) + .map(|t| t.to_string()) + .and_then(|fs| { + ome.instrument[0] + .filter_set + .iter() + .find(|&f| Some(&fs) == f.model.as_ref()) + }); + + let light_source_settings = if let Version::One = version { + // no space in ome for multiple lightsources simultaneously + channel + .get_child("LightSourcesSettings") + .and_then(|ls| { + if ls.children.is_empty() { + None + } else if ls.children.len() > idx { + Some(&ls.children[idx]) + } else { + Some(&ls.children[0]) + } + }) + .and_then(|lss| lss.as_element()) + .and_then(|lss| lss.get_child("LightSource").map(|ls| (lss, ls))) + .and_then(|(lss, ls)| ls.attributes.get("Id").map(|id| (lss, id))) + .map(|(lss, id)| ome::LightSourceSettings { + id: id.to_string(), + attenuation: lss + .get_child("Attenuation") + .and_then(|a| a.get_text()) + .and_then(|t| t.parse().ok()), + wavelength: lss + .get_child("Wavelength") + .and_then(|w| w.get_text()) + .and_then(|t| t.parse().ok()) + .and_then(|w| if w > 0.0 { Some(w) } else { None }), + wavelength_unit: ome::UnitsLength::nm, + }) + } else if let Version::Other = version { + channel.get_child("LightSourcesSettings").and_then(|ls| { + ls.children.first().and_then(|l| l.as_element()).map(|f| { + ome::LightSourceSettings { + id: format!( + "LightSource:{}", + ls.children + .iter() + .filter_map(|l| l + .as_element() + .and_then(|i| i.attributes.get("Id"))) + .join("_") + ), + attenuation: f + .get_child("Attenuation") + .and_then(|a| a.get_text()) + .and_then(|t| t.parse().ok()), + wavelength: f + .get_child("Wavelength") + .and_then(|w| w.get_text()) + .and_then(|t| t.parse().ok()), + wavelength_unit: ome::UnitsLength::nm, + } + }) + }) + } else { + None + }; + + ome.image[0].pixels.channel.push(ome::Channel { + id: format!("Channel:{}", idx), + name: channel.attributes.get("Name").map(|n| n.to_string()), + acquisition_mode: channel + .get_child("AcquisitionMode") + .and_then(|a| a.get_text()) + .and_then(|t| { + t.replace("SingleMoleculeLocalisation", "SingleMoleculeImaging") + .parse() + .ok() + }), + color: channel + .attributes + .get("Id") + .and_then(|id| channels_ds.get(id)) + .and_then(|c| c.get_child("Color")) + .and_then(|c| c.get_text()) + .and_then(|c| c.parse::().ok()) + .map(|i| { + let rgb = i.to_rgb(); + 65536 * (rgb[0] as i32) + 256 * (rgb[1] as i32) + (rgb[2] as i32) + }) + .unwrap_or(0), + detector_settings: detector.and_then(|d| d.attributes.get("Id")).map(|id| { + ome::DetectorSettings { + id: id.to_string().replace(" ", ""), + binning: Some( + detector_settings + .and_then(|d| d.get_child("Binning")) + .and_then(|b| { + if b.children.is_empty() { + None + } else if b.children.len() < self.shape.c { + b.children.first() + } else { + b.children.get(idx) + } + }) + .and_then(|b| b.as_text()) + .and_then(|t| t.parse().ok()) + .unwrap_or(ome::BinningType::Other), + ), + ..Default::default() + } + }), + emission_wavelength: channel + .get_child("EmissionWaveLength") + .and_then(|e| e.get_text()) + .and_then(|t| t.parse().ok()) + .and_then(|w| if w > 0.0 { Some(w) } else { None }), + emission_wavelength_unit: ome::UnitsLength::nm, + excitation_wavelength: light_source_settings + .as_ref() + .and_then(|ls| ls.wavelength) + .or_else(|| { + channel + .get_child("ExcitationWavelength") + .and_then(|e| e.get_text()) + .and_then(|t| t.parse().ok()) + }), + excitation_wavelength_unit: light_source_settings + .as_ref() + .map(|ls| ls.wavelength_unit) + .unwrap_or(ome::UnitsLength::nm), + filter_set_ref: filter_set.map(|fs| ome::AnnotationRef { id: fs.id.clone() }), + illumination_type: channel + .get_child("IlluminationType") + .and_then(|i| i.get_text()) + .and_then(|t| t.parse().ok()), + light_source_settings, + samples_per_pixel: laser_scan_info + .and_then(|ls| ls.get_child("Averaging")) + .and_then(|a| a.get_text()) + .and_then(|t| t.parse().ok()), + contrast_method: channel + .get_child("ContrastMethod") + .and_then(|cm| cm.get_text()) + .and_then(|t| t.parse().ok()), + ..Default::default() + }); + } + + // set planes + let time_stamps = self + .attachments()? + .get("TimeStamps") + .and_then(|a| a.try_into_float().ok()) + .map(|mut time_stamps| { + time_stamps.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let mut dt = time_stamps + .array_windows() + .filter_map(|[a, b]| if b > a { Some(b - a) } else { None }) + .collect::>(); + if !dt.is_empty() { + let mean = dt.iter().sum::() / dt.len() as f64; + let var = + dt.iter().map(|i| (i - mean).powi(2)).sum::() / dt.len() as f64; + if dt.len() > 2 && var.sqrt() / mean > 0.02 { + dt.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let n = dt.len(); + let median = if n.is_multiple_of(2) { + (dt[n / 2 - 1] + dt[n / 2]) / 2.0 + } else { + dt[n / 2] + }; + println!( + "warning: delta_t is inconsistent, using median value {}", + median + ); + (0..time_stamps.len()) + .map(|i| (i as f64) * median) + .collect::>() + } else { + time_stamps + } + } else { + time_stamps + } + }); + + let exposure_times = channels_im + .iter() + .filter_map(|(_, &c)| { + c.get_child("LaserScanInfo") + .and_then(|ls| ls.get_child("FrameTime")) + .and_then(|ft| ft.get_text()) + .and_then(|t| t.parse::().ok()) + }) + .collect::>(); + + for c in 0..self.shape.c { + let exposure_time = if exposure_times.is_empty() { + None + } else if c < exposure_times.len() { + Some(exposure_times[0]) + } else { + Some(exposure_times[c]) + }; + + for z in 0..self.shape.z { + for t in 0..self.shape.t { + ome.image[0].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: time_stamps + .as_ref() + .and_then(|ts| ts.get(t).map(|&t| t as f32)), + position_x: pos_x, + position_x_unit: ome::UnitsLength::nm, + position_y: pos_y, + position_y_unit: ome::UnitsLength::nm, + position_z: pos_z, + position_z_unit: ome::UnitsLength::nm, + exposure_time, + exposure_time_unit: ome::UnitsTime::s, + ..Default::default() + }) + } + } + } + + // annotations + if let Some(layers) = metadata.get_child("Layers") { + for layer in &layers.children { + if let Some(geometry) = layer + .as_element() + .and_then(|layer| layer.get_child("Elements")) + .and_then(|elements| elements.get_child("Rectangle")) + .and_then(|rectangle| rectangle.get_child("Geometry")) + { + ome.roi.push(ome::Roi { + id: format!("ROI:{}", ome.roi.len()), + union: Some(ome::RoiUnion { + shape_group: vec![ome::ShapeGroup::Rectangle(ome::Rectangle { + id: "Shape:0:0".to_string(), + height: geometry + .get_child("Height") + .and_then(|height| height.get_text()) + .and_then(|t| t.parse().ok()) + .unwrap_or(f32::NAN), + width: geometry + .get_child("Width") + .and_then(|width| width.get_text()) + .and_then(|t| t.parse().ok()) + .unwrap_or(f32::NAN), + x: geometry + .get_child("Left") + .and_then(|left| left.get_text()) + .and_then(|t| t.parse().ok()) + .unwrap_or(f32::NAN), + y: geometry + .get_child("Right") + .and_then(|right| right.get_text()) + .and_then(|t| t.parse().ok()) + .unwrap_or(f32::NAN), + ..Default::default() + })], + }), + ..Default::default() + }) + } + } + } + } + Ok(ome) + } + + fn get_frame(&self, c: usize, z: usize, t: usize) -> Result { + let reader = self.get_reader()?; + let mut min_x = i32::MAX; + let mut min_y = i32::MAX; + if let Some(indices) = self.block_map.get(&(c, z, t)) { + for &i in indices { + if let Ok(info) = reader.try_get_sub_block_info_for_index(i) { + let rect = info.get_logical_rect(); + min_x = min_x.min(rect.get_x()); + min_y = min_y.min(rect.get_y()); + } + } + } + + macro_rules! get_frame { + ($t:tt, $n:expr) => {{ + let mut array = Array2::zeros((self.shape.y, self.shape.x)); + if let Some(indices) = self.block_map.get(&(c, z, t)) { + for &i in indices { + let sub_block = reader.read_sub_block(i)?; + let bitmap = sub_block.create_bitmap()?.lock()?; + let bytes = bitmap.lock_info.get_data_roi(); + let info = sub_block.get_info()?; + let rect = info.get_logical_rect(); + let x = (rect.get_x() - min_x) as usize; + let y = (rect.get_y() - min_y) as usize; + let w = rect.get_w() as usize; + let h = rect.get_h() as usize; + array + .slice_mut(s![x..x + w, y..y + h]) + .assign(&Array2::from_shape_vec( + (w, h), + bytes + .chunks($n) + .map(|x| $t::from_le_bytes(x.try_into().unwrap())) + .collect(), + )?); + } + } + Ok(ArrayT::from(array)) + }}; + } + + match self.pixel_type { + PixelType::I8 => get_frame!(i8, 1), + PixelType::U8 => get_frame!(u8, 1), + PixelType::I16 => get_frame!(i16, 2), + PixelType::U16 => get_frame!(u16, 2), + PixelType::I32 => get_frame!(i32, 4), + PixelType::U32 => get_frame!(u32, 4), + PixelType::F32 => get_frame!(f32, 4), + PixelType::F64 => get_frame!(f64, 8), + PixelType::I64 => get_frame!(i64, 8), + PixelType::U64 => get_frame!(u64, 8), + PixelType::I128 => get_frame!(i128, 16), + PixelType::U128 => get_frame!(u128, 16), + PixelType::F128 => get_frame!(f64, 8), + } + } + + 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

(path: P, series: usize) -> Result, Error> + where + P: AsRef, + { + let new = Self { + reader: ThreadLocal::default(), + block_map: HashMap::new(), + path: path.as_ref().to_path_buf(), + series, + position: 0, + shape: Shape::default(), + pixel_type: PixelType::U8, + }; + let reader = new.get_reader()?; + let statistics_simple = reader.get_statistics_simple()?; + let c = statistics_simple.get_sub_block_count(); + let mut positions = HashSet::new(); + + for i in 0..c { + if let Ok(info) = reader.try_get_sub_block_info_for_index(i) { + let coordinate = info.get_coordinate(); + let d = Dimension::vec_from_bitflags(coordinate.get_dimensions_valid()); + let v = coordinate.get_value(); + if d.contains(&Dimension::S) && v[Dimension::S as usize - 1] as usize != series { + continue; + } + positions.insert(info.get_m_index() as usize); + } + } + if positions.is_empty() { + positions.insert(0); + } + Ok(positions) + } + + fn get_available_series

(path: P) -> Result, Error> + where + P: AsRef, + { + let new = Self { + reader: ThreadLocal::default(), + block_map: HashMap::new(), + path: path.as_ref().to_path_buf(), + series: 0, + position: 0, + shape: Shape::default(), + pixel_type: PixelType::U8, + }; + let reader = new.get_reader()?; + let statistics_simple = reader.get_statistics_simple()?; + let c = statistics_simple.get_sub_block_count(); + let mut series = HashSet::new(); + for i in 0..c { + if let Ok(info) = reader.try_get_sub_block_info_for_index(i) { + let coordinate = info.get_coordinate(); + let d = Dimension::vec_from_bitflags(coordinate.get_dimensions_valid()); + let v = coordinate.get_value(); + if d.contains(&Dimension::S) { + series.insert(v[Dimension::S as usize - 1] as usize); + } + } + } + if series.is_empty() { + series.insert(0); + } + Ok(series) + } +} + +impl TryFrom for PixelType { + type Error = Error; + + fn try_from(value: libczirw_sys::PixelType) -> Result { + match value { + libczirw_sys::PixelType::Gray8 => Ok(PixelType::U8), + libczirw_sys::PixelType::Gray16 => Ok(PixelType::U16), + libczirw_sys::PixelType::Gray32 => Ok(PixelType::U32), + libczirw_sys::PixelType::Gray64Float => Ok(PixelType::F64), + _ => Err(Error::Conversion(format!("{:?}", value))), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn open(file: &str) -> Result { + let path = std::env::current_dir()? + .join("tests") + .join("files") + .join(file); + CziReader::new(&path, 0, 0) + } + + // fn write_xml(file: &str, xml: &str) -> Result<(), Error> { + // let path = std::env::current_dir()? + // .join("tests") + // .join("files") + // .join("czi_xml") + // .join(file) + // .with_extension("xml"); + // std::fs::write(path, xml)?; + // Ok(()) + // } + + macro_rules! test_metadata { + ($($name:ident: $file:expr $(,)?)*) => { + $( + #[test] + fn $name() -> Result<(), Error> { + let czi = open($file)?; + // let ome = czi.metadata()?; + // write_xml($file, &czi.metadata_xml()?)?; + // let ome = czi.metadata()?; + // println!("{:?}", ome); + println!("{}", czi.view().squeeze()?.summary()?); + // println!("block map: {:#?}", czi.block_map); + 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", + } +} diff --git a/src/readers/tiff.rs b/src/readers/tiff.rs new file mode 100644 index 0000000..113376e --- /dev/null +++ b/src/readers/tiff.rs @@ -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>>, + 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, +} + +impl From for DynReader { + fn from(value: TiffReader) -> Self { + DynReader::Tiff(value) + } +} + +impl Hash for TiffReader { + fn hash(&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>, 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 { + 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 { + 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

(path: P, series: usize, position: usize) -> Result + where + P: AsRef, + { + 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::().ok()) + .unwrap_or(1) + }; + let size_z = metadata_map + .get("slices") + .and_then(|v| v.parse::().ok()) + .unwrap_or(1); + let size_t = metadata_map + .get("frames") + .and_then(|v| v.parse::().ok()) + .unwrap_or(1); + let interval_t = metadata_map + .get("interval") + .and_then(|v| v.parse::().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 { + 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 { + 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

(_path: P, _series: usize) -> Result, Error> + where + P: AsRef, + { + Ok(HashSet::from([0])) + } + + fn get_available_series

(_path: P) -> Result, Error> + where + P: AsRef, + { + Ok(HashSet::from([0])) + } +} + +fn tag_first_u16(reader: &mut RefMut>, tag: Tag) -> Option { + 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 { + 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", + } +} diff --git a/src/readers/tiffseq.rs b/src/readers/tiffseq.rs new file mode 100644 index 0000000..5b00c33 --- /dev/null +++ b/src/readers/tiffseq.rs @@ -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, + #[serde(skip)] + metadata_map: HashMap, +} + +impl From for DynReader { + fn from(value: TiffSeqReader) -> Self { + DynReader::TiffSeq(value) + } +} + +impl Hash for TiffSeqReader { + fn hash(&self, state: &mut H) { + self.path.hash(state); + self.series.hash(state); + self.position.hash(state); + } +} + +impl TiffSeqReader { + fn find_pos_dir>(path: P, series: usize) -> Result { + 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>(path: P) -> Result, 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, 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

(path: P, series: usize, position: usize) -> Result + where + P: AsRef, + { + 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 = 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 = if cnamelist.is_empty() { + let mut names: Vec = 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 { + 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::().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::().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::().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::().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::().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::().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 { + 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

(_path: P, _series: usize) -> Result, Error> + where + P: AsRef, + { + Ok(HashSet::from([0])) + } + + fn get_available_series

(path: P) -> Result, Error> + where + P: AsRef, + { + 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::() + { + series.insert(s); + } + } + } + Ok(series) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn open(file: &str) -> Result { + 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", + } +} diff --git a/src/tiff.rs b/src/tiff.rs deleted file mode 100644 index fa50e65..0000000 --- a/src/tiff.rs +++ /dev/null @@ -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, - compression: Compression, - colors: Option>>, - 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, - colors: Vec, - overwrite: bool, - ) -> Result { - 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::()) - .collect::, 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 View -where - D: Dimension, -{ - /// save as tiff with a certain type - pub fn save_as_tiff_with_type(&self, path: P, options: &TiffOptions) -> Result<(), Error> - where - P: AsRef, - T: Bytes + Number + Send + Sync, - ArrayD: MinMax>, - Array1: MinMax>, - Array2: MinMax>, - { - 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::>() - .into_par_iter() - .map(|(c, z, t)| { - if let Ok(mut tiff) = tiff.lock() { - tiff.save(&self.get_frame::(c, z, t)?, c, z, t)?; - bar.inc(1); - Ok(()) - } else { - Err(Error::TiffLock) - } - }) - .collect::>()?; - bar.finish(); - } else { - iproduct!(0..size_c, 0..size_z, 0..size_t) - .collect::>() - .into_par_iter() - .map(|(c, z, t)| { - if let Ok(mut tiff) = tiff.lock() { - tiff.save(&self.get_frame::(c, z, t)?, c, z, t)?; - Ok(()) - } else { - Err(Error::TiffLock) - } - }) - .collect::>()?; - }; - Ok(()) - } - - /// save as tiff with whatever pixel type the view has - pub fn save_as_tiff

(&self, path: P, options: &TiffOptions) -> Result<(), Error> - where - P: AsRef, - { - match self.pixel_type { - PixelType::I8 => self.save_as_tiff_with_type::(path, options)?, - PixelType::U8 => self.save_as_tiff_with_type::(path, options)?, - PixelType::I16 => self.save_as_tiff_with_type::(path, options)?, - PixelType::U16 => self.save_as_tiff_with_type::(path, options)?, - PixelType::I32 => self.save_as_tiff_with_type::(path, options)?, - PixelType::U32 => self.save_as_tiff_with_type::(path, options)?, - PixelType::F32 => self.save_as_tiff_with_type::(path, options)?, - PixelType::F64 => self.save_as_tiff_with_type::(path, options)?, - PixelType::I64 => self.save_as_tiff_with_type::(path, options)?, - PixelType::U64 => self.save_as_tiff_with_type::(path, options)?, - PixelType::I128 => self.save_as_tiff_with_type::(path, options)?, - PixelType::U128 => self.save_as_tiff_with_type::(path, options)?, - PixelType::F128 => self.save_as_tiff_with_type::(path, options)?, - } - - Ok(()) - } -} diff --git a/src/tiffwrite.rs b/src/tiffwrite.rs new file mode 100644 index 0000000..620636b --- /dev/null +++ b/src/tiffwrite.rs @@ -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, + compression: Compression, + colors: Option>>, + 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, message: Option) -> 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, + compression: Option, + colors: Vec, + overwrite: bool, + ) -> Result { + 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) { + 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::()) + .collect::, 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 View +where + D: Dimension, +{ + /// save as tiff with a certain type + pub fn save_as_tiff_with_type(&self, path: P, options: &TiffOptions) -> Result<(), Error> + where + P: AsRef, + T: Bytes + Number + Send + Sync, + ArrayD: MinMax>, + Array1: MinMax>, + Array2: MinMax>, + { + 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::>() + .into_iter() + .try_for_each(|(c, z, t)| { + if let Ok(mut tiff) = tiff.lock() { + tiff.save(&self.get_frame::(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

(&self, path: P, options: &TiffOptions) -> Result<(), Error> + where + P: AsRef, + { + match self.pixel_type() { + PixelType::I8 => self.save_as_tiff_with_type::(path, options)?, + PixelType::U8 => self.save_as_tiff_with_type::(path, options)?, + PixelType::I16 => self.save_as_tiff_with_type::(path, options)?, + PixelType::U16 => self.save_as_tiff_with_type::(path, options)?, + PixelType::I32 => self.save_as_tiff_with_type::(path, options)?, + PixelType::U32 => self.save_as_tiff_with_type::(path, options)?, + PixelType::F32 => self.save_as_tiff_with_type::(path, options)?, + PixelType::F64 => self.save_as_tiff_with_type::(path, options)?, + PixelType::I64 => self.save_as_tiff_with_type::(path, options)?, + PixelType::U64 => self.save_as_tiff_with_type::(path, options)?, + PixelType::I128 => self.save_as_tiff_with_type::(path, options)?, + PixelType::U128 => self.save_as_tiff_with_type::(path, options)?, + PixelType::F128 => self.save_as_tiff_with_type::(path, options)?, + } + + Ok(()) + } +} + +pub fn batch_to_tiff( + files_in: &[PathBuf], + files_out: &[PathBuf], + operations: Option>, + colors: Option>, + overwrite: bool, + bar: bool, + message: Option, +) -> 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::>() + .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::()?, op.parse()?)?; + } + } + view.save_as_tiff(file_out, &options)?; + { + let mut count = lock.lock().unwrap(); + *count -= 1; + cvar.notify_one(); + } + Ok(()) + }) + .collect::, 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::, Error>>()?; + let files_out = files + .iter() + .map(|file| { + std::env::home_dir() + .unwrap() + .join("tmp") + .join(PathBuf::from(file).with_extension("tif")) + }) + .collect::>(); + for file in &files_out { + create_dir_all(file.parent().unwrap())?; + } + batch_to_tiff(&files_in, &files_out, None, None, true, true, None)?; + Ok(()) + } +} diff --git a/src/view.rs b/src/view.rs index 7ecb8e8..a484705 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,7 +1,7 @@ -use crate::axes::{Ax, Axis, Operation, Slice, SliceInfoElemDef, slice_info}; +use crate::axes::{Ax, Axis, Operation, Shape, Slice, SliceInfoElemDef, slice_info}; use crate::error::Error; use crate::metadata::Metadata; -use crate::reader::Reader; +use crate::readers::{Dimensions, DynReader, Reader}; use crate::stats::MinMax; use indexmap::IndexMap; use itertools::{Itertools, iproduct}; @@ -14,13 +14,13 @@ use num::{Bounded, FromPrimitive, ToPrimitive, Zero}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use std::any::type_name; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; +use std::hash::{Hash, Hasher}; use std::iter::Sum; use std::marker::PhantomData; use std::ops::{AddAssign, Deref, Div}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::path::Path; fn idx_bnd(idx: isize, bnd: isize) -> Result { if idx < -bnd { @@ -64,9 +64,9 @@ impl Number for T where /// sliceable view on an image file #[serde_as] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct View { - reader: Arc, +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct View { + reader: R, /// same order as axes #[serde_as(as = "Vec")] slice: Vec, @@ -76,8 +76,24 @@ pub struct View { dimensionality: PhantomData, } -impl View { - pub(crate) fn new(reader: Arc, slice: Vec, axes: Vec) -> Self { +impl Hash for View +where + D: Dimension, + R: Reader, +{ + fn hash(&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 View { + pub(crate) fn new(reader: R, slice: Vec, axes: Vec) -> Self { Self { reader, slice, @@ -88,33 +104,34 @@ impl View { } #[allow(dead_code)] - pub(crate) fn new_with_axes(reader: Arc, axes: Vec) -> Result { + pub(crate) fn new_with_axes(reader: R, axes: Vec) -> Result { let mut slice = Vec::new(); + let shape = reader.shape(); for axis in axes.iter() { match axis { Axis::C => slice.push(SliceInfoElem::Slice { start: 0, - end: Some(reader.size_c as isize), + end: Some(shape.c as isize), step: 1, }), Axis::Z => slice.push(SliceInfoElem::Slice { start: 0, - end: Some(reader.size_z as isize), + end: Some(shape.z as isize), step: 1, }), Axis::T => slice.push(SliceInfoElem::Slice { start: 0, - end: Some(reader.size_t as isize), + end: Some(shape.t as isize), step: 1, }), Axis::Y => slice.push(SliceInfoElem::Slice { start: 0, - end: Some(reader.size_y as isize), + end: Some(shape.y as isize), step: 1, }), Axis::X => slice.push(SliceInfoElem::Slice { start: 0, - end: Some(reader.size_x as isize), + end: Some(shape.x as isize), step: 1, }), Axis::New => { @@ -126,11 +143,11 @@ impl View { for axis in [Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X] { if !axes.contains(&axis) { let size = match axis { - Axis::C => reader.size_c, - Axis::Z => reader.size_z, - Axis::T => reader.size_t, - Axis::Y => reader.size_y, - Axis::X => reader.size_x, + Axis::C => shape.c, + Axis::Z => shape.z, + Axis::T => shape.t, + Axis::Y => shape.y, + Axis::X => shape.x, Axis::New => 1, }; if size > 1 { @@ -150,13 +167,13 @@ impl View { } /// the file path - pub fn path(&self) -> &PathBuf { - &self.reader.path + pub fn path(&self) -> &Path { + self.reader.path() } /// the series in the file pub fn series(&self) -> usize { - self.reader.series + self.reader.series() } fn with_operations(mut self, operations: IndexMap) -> Self { @@ -165,7 +182,7 @@ impl View { } /// change the dimension into a dynamic dimension - pub fn into_dyn(self) -> View { + pub fn into_dyn(self) -> View { View { reader: self.reader, slice: self.slice, @@ -176,7 +193,7 @@ impl View { } /// change the dimension into a concrete dimension - pub fn into_dimensionality(self) -> Result, Error> { + pub fn into_dimensionality(self) -> Result, Error> { if let Some(d) = D2::NDIM { if d == self.ndim() { Ok(View { @@ -231,7 +248,7 @@ impl View { } /// remove axes of size 1 - pub fn squeeze(&self) -> Result, Error> { + pub fn squeeze(&self) -> Result, Error> { let view = self.clone().into_dyn(); let slice: Vec<_> = self .shape() @@ -286,14 +303,15 @@ impl View { } /// the shape of the view - pub fn shape(&self) -> Vec { - let mut shape = Vec::::new(); + pub fn shape(&self) -> Shape { + let mut shape = Shape::new(); for (ax, s) in self.axes.iter().zip(self.slice.iter()) { match s { SliceInfoElem::Slice { start, end, step } => { if !self.operations.contains_key(ax) { if let Some(e) = end { - shape.push(((e - start).max(0) / step) as usize); + shape.order.push(*ax); + shape.set_axis(ax, ((e - start).max(0) / step) as usize); } else { panic!("slice has no end") } @@ -302,7 +320,7 @@ impl View { SliceInfoElem::Index(_) => {} SliceInfoElem::NewAxis => { if !self.operations.contains_key(ax) { - shape.push(1); + shape.order.push(*ax); } } } @@ -310,60 +328,6 @@ impl View { 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 { - let mut shape = Vec::::new(); - for s in self.slice.iter() { - match s { - SliceInfoElem::Slice { start, end, step } => { - if let Some(e) = end { - shape.push(((e - start).max(0) / step) as usize); - } else { - panic!("slice has no end") - } - } - _ => shape.push(1), - } - } - shape - } - /// swap two axes pub fn swap_axes(&self, axis0: A, axis1: A) -> Result { let idx0 = axis0.pos_op(&self.axes, &self.slice, &self.op_axes())?; @@ -392,6 +356,37 @@ impl View { 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(&self, axes: &[A]) -> Result, Error> { + let idx: Vec = 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::>(); + for (&i, j) in idx.iter().zip(jdx) { + new_slice[j] = self.slice[i]; + new_axes[j] = self.axes[i]; + } + for (ax, s) in new_axes.iter().zip(new_slice.iter_mut()) { + if let SliceInfoElem::Slice { start, end, step } = s + && !axes.contains(&ax.n()) + { + let size = ((end.expect("slice has no end") - *start).max(0) / *step) as usize; + if size != 1 { + return Err(Error::SizeMismatch(ax.to_string(), size)); + } + *s = SliceInfoElem::Index(*start) + } + } + + Ok(View::new(self.reader.clone(), new_slice, new_axes) + .with_operations(self.operations.clone())) + } + /// reverse the order of the axes pub fn transpose(&self) -> Result { Ok(View::new( @@ -402,7 +397,11 @@ impl View { .with_operations(self.operations.clone())) } - fn operate(&self, axis: A, operation: Operation) -> Result, Error> { + pub fn operate( + &self, + axis: A, + operation: Operation, + ) -> Result, Error> { let pos = axis.pos_op(&self.axes, &self.slice, &self.op_axes())?; let ax = self.axes[pos]; let (axes, slice, operations) = if Axis::New == ax { @@ -430,27 +429,27 @@ impl View { } /// maximum along axis - pub fn max_proj(&self, axis: A) -> Result, Error> { + pub fn max_proj(&self, axis: A) -> Result, Error> { self.operate(axis, Operation::Max) } /// minimum along axis - pub fn min_proj(&self, axis: A) -> Result, Error> { + pub fn min_proj(&self, axis: A) -> Result, Error> { self.operate(axis, Operation::Min) } /// sum along axis - pub fn sum_proj(&self, axis: A) -> Result, Error> { + pub fn sum_proj(&self, axis: A) -> Result, Error> { self.operate(axis, Operation::Sum) } /// mean along axis - pub fn mean_proj(&self, axis: A) -> Result, Error> { + pub fn mean_proj(&self, axis: A) -> Result, Error> { self.operate(axis, Operation::Mean) } /// created a new sliced view - pub fn slice(&self, info: I) -> Result, Error> + pub fn slice(&self, info: I) -> Result, Error> where I: SliceArg, { @@ -554,11 +553,7 @@ impl View { new_axes.push(*a.expect("axis should exist when slice exists")); r_idx += 1; } - _ => { - panic!("unreachable"); - // n_idx += 1; - // r_idx += 1; - } + _ => unreachable!(), }, } } @@ -575,7 +570,7 @@ impl View { /// resets axes to cztyx order, with all 5 axes present, /// inserts new axes in place of axes under operation (max_proj etc.) - pub fn reset_axes(&self) -> Result, Error> { + pub fn reset_axes(&self) -> Result, Error> { let mut axes = Vec::new(); let mut slice = Vec::new(); @@ -603,7 +598,7 @@ impl View { /// slice, but slice elements are in cztyx order, all cztyx must be given, /// but axes not present in view will be ignored, view axes are reordered in cztyx order - pub fn slice_cztyx(&self, info: I) -> Result, Error> + pub fn slice_cztyx(&self, info: I) -> Result, Error> where I: SliceArg, { @@ -644,14 +639,14 @@ impl View { Array2: MinMax>, { let mut op_xy = IndexMap::new(); - if let Some((&ax, op)) = self.operations.first() { - if (ax == Axis::X) || (ax == Axis::Y) { - op_xy.insert(ax, op.clone()); - if let Some((&ax2, op2)) = self.operations.get_index(1) { - if (ax2 == Axis::X) || (ax2 == Axis::Y) { - op_xy.insert(ax2, op2.clone()); - } - } + if let Some((&ax, op)) = self.operations.first() + && ((ax == Axis::X) || (ax == Axis::Y)) + { + op_xy.insert(ax, op.clone()); + if let Some((&ax2, op2)) = self.operations.get_index(1) + && ((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()) { @@ -686,33 +681,27 @@ impl View { } let mut slice_reader = vec![Slice::empty(); 5]; let mut xy_dim = 0usize; - let shape = [ - self.size_c as isize, - self.size_z as isize, - self.size_t as isize, - self.size_y as isize, - self.size_x as isize, - ]; + let shape_reader = self.reader.shape(); for (s, &axis) in self.slice.iter().zip(&self.axes) { match axis { Axis::New => {} _ => match s { SliceInfoElem::Slice { start, end, step } => { - if let Axis::X | Axis::Y = axis { - if !op_xy.contains_key(&axis) { - xy_dim += 1; - } + if let Axis::X | Axis::Y = axis + && !op_xy.contains_key(&axis) + { + xy_dim += 1; } slice_reader[axis as usize] = Slice::new( - idx_bnd(*start, shape[axis as usize])?, - slc_bnd(end.unwrap(), shape[axis as usize])?, + idx_bnd(*start, shape_reader[axis] as isize)?, + slc_bnd(end.unwrap(), shape_reader[axis] as isize)?, *step, ); } SliceInfoElem::Index(j) => { slice_reader[axis as usize] = Slice::new( - idx_bnd(*j, shape[axis as usize])?, - slc_bnd(*j + 1, shape[axis as usize])?, + idx_bnd(*j, shape_reader[axis] as isize)?, + slc_bnd(*j + 1, shape_reader[axis] as isize)?, 1, ); } @@ -737,10 +726,7 @@ impl View { } else { ArrayD::::zeros(shape_out.into_dimension()) }; - let size_c = self.reader.size_c as isize; - let size_z = self.reader.size_z as isize; - let size_t = self.reader.size_t as isize; - + let shape = self.reader.shape(); let mut axes_out_idx = [None; 5]; for (i, ax) in ax_out.iter().enumerate() { if *ax < Axis::New { @@ -759,9 +745,9 @@ impl View { slice[i] = SliceInfoElem::Index(t) }; let frame = self.reader.get_frame( - (c % size_c) as usize, - (z % size_z) as usize, - (t % size_t) as usize, + (c % shape.c as isize) as usize, + (z % shape.z as isize) as usize, + (t % shape.t as isize) as usize, )?; let arr_frame: Array2 = frame.try_into()?; @@ -860,11 +846,11 @@ impl View { } } let mut n = 1; - for (&ax, size) in self.axes.iter().zip(self.shape_all().iter()) { - if let Some(Operation::Mean) = self.operations.get(&ax) { - if (ax == Axis::C) || (ax == Axis::Z) || (ax == Axis::T) { - n *= size; - } + for (ax, size) in self.shape().to_hashmap().into_iter() { + if ((ax == Axis::C) || (ax == Axis::Z) || (ax == Axis::T)) + && let Some(Operation::Mean) = self.operations.get(&ax) + { + n *= size; } } let array = if n == 1 { @@ -999,9 +985,10 @@ impl View { /// gives a helpful summary of the recorded experiment pub fn summary(&self) -> Result { let mut s = "".to_string(); - s.push_str(&format!("path/filename: {}\n", self.path.display())); - s.push_str(&format!("series/pos: {}\n", self.series)); - s.push_str(&format!("dtype: {:?}\n", self.pixel_type)); + s.push_str(&format!("path/filename: {}\n", self.path().display())); + s.push_str(&format!("series/pos: {}\n", self.series())); + s.push_str(&format!("reader: {}\n", self.reader_name())); + s.push_str(&format!("dtype: {:?}\n", self.pixel_type())); let axes = self .axes() .into_iter() @@ -1015,45 +1002,47 @@ impl View { .join(" x "); let space = " ".repeat(6usize.saturating_sub(axes.len())); s.push_str(&format!("shape ({}):{}{}\n", axes, space, shape)); - s.push_str(&self.get_ome()?.summary()?); + s.push_str(&self.metadata()?.summary()?); Ok(s) } } -impl Deref for View { - type Target = Reader; +impl Deref for View { + type Target = R; fn deref(&self) -> &Self::Target { - self.reader.as_ref() + &self.reader } } -impl TryFrom> for Array +impl TryFrom> for Array where T: Number, D: Dimension, + R: Reader, ArrayD: MinMax>, Array1: MinMax>, Array2: MinMax>, { type Error = Error; - fn try_from(view: View) -> Result { + fn try_from(view: View) -> Result { view.as_array() } } -impl TryFrom<&View> for Array +impl TryFrom<&View> for Array where T: Number, D: Dimension, + R: Reader, ArrayD: MinMax>, Array1: MinMax>, Array2: MinMax>, { type Error = Error; - fn try_from(view: &View) -> Result { + fn try_from(view: &View) -> Result { view.as_array() } } @@ -1068,26 +1057,17 @@ pub trait Item { Array2: MinMax>; } -impl View { - pub fn from_path

(path: P, series: usize) -> Result +impl View { + pub fn from_path

(path: P) -> Result where P: AsRef, { - let mut path = path.as_ref().to_path_buf(); - if path.is_dir() { - for file in path.read_dir()?.flatten() { - let p = file.path(); - if file.path().is_file() && (p.extension() == Some("tif".as_ref())) { - path = p; - break; - } - } - } - Ok(Reader::new(path, series)?.view()) + let (path, dimensions) = Dimensions::parse_path(path)?; + Ok(R::new(path, dimensions.s.unwrap_or(0), dimensions.p.unwrap_or(0))?.view()) } } -impl Item for View { +impl Item for View { fn item(&self) -> Result where T: Number, @@ -1099,12 +1079,12 @@ impl Item for View { } } -impl Display for View { +impl Display for View { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if let Ok(summary) = self.summary() { write!(f, "{}", summary) } else { - write!(f, "{}", self.path.display()) + write!(f, "{}", self.path().display()) } } } @@ -1131,3 +1111,281 @@ macro_rules! to_bytes_vec_impl { to_bytes_vec_impl!( u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64 ); + +#[cfg(test)] +mod tests { + use crate::axes::Axis; + use crate::error::Error; + use crate::readers::{DynReader, Reader}; + use crate::stats::MinMax; + use crate::view::Item; + use ndarray::{Array, Array4, Array5, NewAxis}; + use ndarray::{Array2, s}; + + fn open(file: &str) -> Result { + 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 = a.try_into()?; + println!("c {:?}", c); + let d: Array2 = 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::::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::::zeros((s[0], s[1], s[2], s[3], s[4])); + assert_eq!(view.shape().to_vec(), a.shape()); + let b: Array5 = 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 = 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 = 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 = view.clone().try_into()?; + let view = view.max_proj($b)?; + let a: Array4 = 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 = view.slice($b)?.try_into()?; + let a5: Array5 = 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + assert_eq!(mt.shape().to_vec(), [1280, 1280]); + assert_eq!(a.shape(), [1280, 1280]); + Ok(()) + } +} diff --git a/tests/files/Experiment-2029.czi b/tests/files/Experiment-2029.czi deleted file mode 100644 index 400f985..0000000 Binary files a/tests/files/Experiment-2029.czi and /dev/null differ diff --git a/tests/files/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif b/tests/files/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif deleted file mode 100644 index 7027055..0000000 Binary files a/tests/files/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif and /dev/null differ diff --git a/tests/files/test.tif b/tests/files/test.tif deleted file mode 100644 index 63054d7..0000000 Binary files a/tests/files/test.tif and /dev/null differ