Compare commits

...
46 Commits
Author SHA1 Message Date
w.pomp 645c75535c Merge remote-tracking branch 'origin/main'
pytest / Publish (pypi.org) (push) Failing after 32s
2026-11-04 17:10:29 +01:00
w.pomp 4a5a538bf3 - a lot of todo methods for Imread 2026-11-04 17:10:21 +01:00
wim 564e59cd9f Update .gitignore
pytest / Publish (pypi.org) (push) Successful in 4m9s
2026-10-30 22:09:23 +01:00
wim fcd9aa4e5d Update .github/workflows/publish_crates.yml
pytest / Publish (pypi.org) (push) Successful in 4m12s
2026-10-30 21:37:19 +01:00
wim 1809ec859f Update .gitignore
pytest / Publish (pypi.org) (push) Successful in 4m7s
2026-10-30 21:25:33 +01:00
wim 6da2cb5636 Update .github/workflows/publish_crates.yml
pytest / Publish (pypi.org) (push) Successful in 4m12s
2026-10-30 21:24:36 +01:00
Wim Pomp 6fc6b6dfc3 - fix workflow
pytest / Publish (pypi.org) (push) Successful in 4m56s
2026-10-30 21:04:15 +01:00
Wim Pomp 1574bad208 - fix workflow
pytest / Publish (pypi.org) (push) Successful in 4m26s
2026-10-30 20:02:03 +01:00
Wim Pomp 651e6dd25a - fix README
pytest / Publish (pypi.org) (push) Successful in 5m16s
2026-10-30 19:52:39 +01:00
Wim Pomp 9871a98946 - fix workflow
pytest / Publish (pypi.org) (push) Successful in 4m13s
2026-10-30 18:55:12 +01:00
Wim Pomp 4fbf5b23e2 - fix workflow
pytest / Publish (pypi.org) (push) Failing after 4m8s
2026-10-30 18:38:47 +01:00
Wim Pomp 9416e774ad - fix workflow
pytest / Publish (pypi.org) (push) Failing after 1m1s
2026-10-30 18:28:15 +01:00
Wim Pomp 60b4e0fe7d - fix slicing bug
pytest / Publish (pypi.org) (push) Failing after 15s
- add pytest workflow
2026-10-30 18:25:05 +01:00
w.pomp 825bc99523 - remove libczirw-sys submodule 2026-10-29 18:55:38 +01:00
w.pomp 05ed9112d8 build: keep debug info in the test profile only, not release
[profile.release] debug = true forced full DWARF into release builds.
On ELF targets (the manylinux/musllinux wheels) the debug sections are
embedded in the .so (~160 MB, over PyPI's 100 MB file limit); MSVC emits
a sidecar .pdb instead and the apple/zig link path drops it, so only the
Linux wheels were bloated. Move debug=true to [profile.test] so cargo
still gets readable backtraces without fattening the release wheels.
2026-10-29 17:20:13 +01:00
w.pomp 8338af454f build: bump libczirw-sys: Eigen is now a shallow git submodule (tag 3.4.0) 2026-10-29 16:06:09 +01:00
w.pomp 546e022cfa - bump libczirw-sys to fix Windowscodecs.lib case-symlink for aarch64-pc-windows-msvc 2026-10-29 14:19:05 +01:00
w.pomp d8696bdb04 - use submodules: recursive in checkout so libczirw-sys's nested libczi submodule (CMake source tree) is actually fetched in CI 2026-10-29 13:28:55 +01:00
w.pomp aadf235ca5 - fix clang detection loop: apt-cache search prints 'name - desc', so use awk field 1 (not 2) and strip the clang- prefix from the loop variable 2026-10-29 13:22:35 +01:00
w.pomp 120453582c - install the newest actually-installable versioned clang/llvm/lld/libclang toolchain from apt.llvm.org (the unversioned metas are currently broken: clang wants clang-23 which does not resolve); pass LLVM_BIN/LIBCLANG_PATH to the later steps 2026-10-29 13:02:00 +01:00
w.pomp 592e9a944b - pypi wheels against the libczirw-sys submodule (self-contained Eigen build, msvc runtime alignment, bindgen flags) - and slim down publish_pypi.yml: no pinned zig/clang versions, no cmake shim, no bindgen env vars; only the ring /imsvc clang shim remains 2026-10-29 12:00:24 +01:00
w.pomp aff6dfb51b - try fix publish_pypi 2026-10-28 17:24:47 +01:00
w.pomp 1020df649a - try fix publish_pypi 2026-10-28 16:34:38 +01:00
w.pomp d4a5bca8ba - try fix publish_pypi 2026-10-28 15:20:12 +01:00
w.pomp f20f82889d - try fix publish_pypi 2026-10-28 14:04:37 +01:00
w.pomp b049939214 - try fix publish_pypi 2026-10-28 13:22:28 +01:00
w.pomp d0c06256f1 - try fix publish_pypi 2026-10-28 12:51:59 +01:00
w.pomp 3c787e8d29 - install clang 2026-10-27 19:46:13 +01:00
w.pomp 3f8d1311aa - install cmake 2026-10-27 19:37:41 +01:00
w.pomp a400fbd800 - fix publish_pypi 2026-10-27 19:32:28 +01:00
w.pomp c5d77fe289 - remove doc_auto_cfg 2026-10-27 19:25:52 +01:00
w.pomp 2d13851a38 - remove redundant homepage 2026-10-27 19:23:37 +01:00
w.pomp 7db52bb63b - remove redundant homepage
- remove doc_auto_cfg
2026-10-27 19:22:30 +01:00
w.pomp d19a8d2d69 - repo link fix 2026-10-27 19:14:22 +01:00
w.pomp a055e08a20 - repo link fix 2026-10-27 19:12:05 +01:00
w.pomp 5f70c4152b - update workflows 2026-10-27 19:05:31 +01:00
w.pomp 02cfa39677 - pin zip dependency of bioformats 2026-10-27 19:01:15 +01:00
w.pomp 3c7af5da4d - return Error, raise Exception when trying to use f128 data
- clean parsing of python types into rust types
- finish PyView.reshape
2026-10-27 18:23:37 +01:00
Wim Pomp 8a4c965477 - default value argument for interpolation 2026-10-25 10:23:57 +01:00
Wim Pomp 22e8ab1dd2 - some more macro use
- implement reshape for python
- make Reader and View Sync
- register_translation_fft
- calculate_transforms_from and functions it needs
2026-10-24 11:58:06 +02:00
w.pomp 3e33fe18c1 - improve Imread slicing
- can_open function for Reader trait
2026-10-22 19:50:20 +02:00
w.pomp f0404d0419 - remove center and shape from Transform 2026-10-22 11:47:26 +02:00
w.pomp e1caa1c0b0 - fix Imread.transpose
- implement with_transform
- implement 2d transforms in View.as_array_dyn
2026-10-21 20:08:42 +02:00
Wim Pomp 453237cae9 - testing 2d transforms 2026-10-10 12:44:37 +02:00
Wim Pomp 4815690621 - read ome xml tiff 2026-10-03 23:08:23 +02:00
Wim Pomp 7f07f82fdb - readme 2026-10-03 19:40:24 +02:00
45 changed files with 7269 additions and 1242 deletions

No files matched your search

+11 -8
View File
@@ -1,4 +1,4 @@
name: Publish name: Publish_rs
on: [ workflow_dispatch ] on: [ workflow_dispatch ]
@@ -10,23 +10,27 @@ jobs:
name: Publish (crates.io) name: Publish (crates.io)
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 25 timeout-minutes: 25
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with:
submodules: recursive
- name: Restore cache - name: Restore cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
with: with:
path: | path: ${{ env.CARGO_HOME }}
~/.cargo
key: cache-ubuntu-cargo-publish key: cache-ubuntu-cargo-publish
- name: Install Rust - name: Install Rust
run: |- run: |-
export PATH="$HOME/.cargo/bin:$PATH" export PATH="$CARGO_HOME/bin:$PATH"
if ! command -v rustc >/dev/null 2>&1; then if ! command -v rustc >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
else else
rustup update rustup update
fi fi
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
cargo install cargo-release cargo install cargo-release
shell: bash shell: bash
@@ -46,9 +50,9 @@ jobs:
# which is reasonable, since this job only runs after the Linux, Windows, and WASM builds # which is reasonable, since this job only runs after the Linux, Windows, and WASM builds
# have passed. # have passed.
- name: "cargo release publish" - name: "cargo release publish"
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }}
run: |- run: |-
export PATH="$HOME/.osxcross/bin:$PATH"
cargo login ${{ secrets.CRATES_IO_API_TOKEN }}
cargo release \ cargo release \
publish \ publish \
--workspace \ --workspace \
@@ -61,6 +65,5 @@ jobs:
- name: Store cache - name: Store cache
uses: actions/cache/save@v4 uses: actions/cache/save@v4
with: with:
path: | path: ${{ env.CARGO_HOME }}
~/.cargo
key: cache-ubuntu-cargo-publish key: cache-ubuntu-cargo-publish
+120 -30
View File
@@ -1,4 +1,4 @@
name: CI name: Publish_py
on: [ workflow_dispatch ] on: [ workflow_dispatch ]
@@ -9,8 +9,14 @@ jobs:
pypi_publish: pypi_publish:
name: Publish (pypi.org) name: Publish (pypi.org)
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
RUSTUP_HOME: ${{ github.workspace }}/.rustup
RUSTUP_TOOLCHAIN: stable
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with:
submodules: recursive
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
@@ -23,33 +29,62 @@ jobs:
~/.cache/pip-wheel ~/.cache/pip-wheel
~/.cache/sccache ~/.cache/sccache
~/.cache/cargo-xwin ~/.cache/cargo-xwin
~/.cargo ${{ env.CARGO_HOME }}
~/.osxcross ~/.osxcross
key: cache-ubuntu-maturin-cross-compile key: cache-ubuntu-maturin-cross-compile
- name: Install llvm - name: Install build dependencies
run: | run: |
if ! command -v llvm-dlltool >/dev/null 2>&1; then # the stock clang on noble (18) is too old for the 2025+ MSVC STL that
# cargo-xwin downloads (yvals_core.h: "error STL1000: expected Clang
# 19.0.0 or newer"), so take a newer clang toolchain from apt.llvm.org.
# apt.llvm.org sometimes half-publishes a new version, in which case the
# unversioned meta packages become uninstallable ("clang: Depends:
# clang-NN ... not installable"); so pick the newest versioned set that
# actually resolves (a dry-run checks the whole dependency closure) -
# nothing is hardcoded, this tracks the newest working release.
sudo apt update sudo apt update
sudo apt install -y llvm echo "deb [trusted=yes] https://apt.llvm.org/noble/ llvm-toolchain-noble main" | sudo tee /etc/apt/sources.list.d/llvm-toolchain-noble.list >/dev/null
sudo apt update
sudo apt install -y build-essential cmake
NEWEST=""
for name in $(apt-cache search '^clang-[0-9]+$' | awk '{print $1}' | sort -rV); do
v=${name#clang-}
if sudo apt-get install --dry-run -y "clang-$v" "llvm-$v" "lld-$v" "libclang-$v-dev" >/dev/null 2>&1; then
NEWEST=$v
break
fi fi
done
[ -n "$NEWEST" ] || { echo "no installable clang toolchain found in apt.llvm.org" >&2; exit 1; }
echo "using clang/llvm toolchain $NEWEST"
sudo apt install -y "clang-$NEWEST" "llvm-$NEWEST" "lld-$NEWEST" "libclang-$NEWEST-dev"
# expose the toolchain's own bin/lib dirs (clang, clang-cl, lld-link,
# llvm-lib, libclang) to the later steps
echo "LLVM_BIN=/usr/lib/llvm-$NEWEST/bin" >> "$GITHUB_ENV"
echo "LIBCLANG_PATH=/usr/lib/llvm-$NEWEST/lib" >> "$GITHUB_ENV"
export PATH="/usr/lib/llvm-$NEWEST/bin:$PATH"
clang --version | head -1
command -v clang clang-cl lld-link llvm-lib
shell: bash shell: bash
- name: Install Rust - name: Install Rust
run: | run: |
export PATH="$HOME/.cargo/bin:$PATH" export PATH="$CARGO_HOME/bin:$PATH"
if ! command -v rustc >/dev/null 2>&1; then if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
else
rustup update
fi fi
rustup toolchain install stable --profile minimal
rustup default stable
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
cargo --version
shell: bash shell: bash
- name: Install sccache and maturin - name: Install sccache and maturin
run: | run: |
export PATH="$HOME/.cargo/bin:$PATH" python -m pip install --upgrade pip maturin ziglang cargo-zigbuild
python -m pip install --upgrade pip # in case an older copy was cargo-installed into the CARGO_HOME cache,
pip install maturin ziglang # prevent it from shadowing the pip-installed one
rm -f "${CARGO_HOME:?}/bin/cargo-zigbuild" || true
if ! command -v sccache >/dev/null 2>&1; then if ! command -v sccache >/dev/null 2>&1; then
cargo install sccache || pip install sccache cargo install sccache || pip install sccache
fi fi
@@ -57,7 +92,6 @@ jobs:
- name: Install xwin - name: Install xwin
run: | run: |
export PATH="$HOME/.cargo/bin:$PATH"
if ! command -v cargo-xwin >/dev/null 2>&1; then if ! command -v cargo-xwin >/dev/null 2>&1; then
cargo install cargo-xwin || pip install cargo-xwin cargo install cargo-xwin || pip install cargo-xwin
cargo xwin cache xwin cargo xwin cache xwin
@@ -67,32 +101,87 @@ jobs:
- name: Install osxcross - name: Install osxcross
run: | run: |
export PATH="$HOME/.osxcross/bin:$PATH" export PATH="$HOME/.osxcross/bin:$PATH"
if ! command -v osxcross >/dev/null 2>&1; then if [ ! -d "$HOME/.osxcross/bin" ]; then
wget ${{ secrets.OSXCROSS_LINK }} -O osxcross.tar.gz wget ${{ secrets.OSXCROSS_LINK }} -O osxcross.tar.gz
tar -xzf osxcross.tar.gz -C ~/ tar -xzf osxcross.tar.gz -C ~/
mv ~/osxcross ~/.osxcross mv ~/osxcross ~/.osxcross
fi fi
shell: bash
- name: Shim clang for ring's aarch64-windows build
run: |
# ring 0.17.14 forces a plain `clang` for its C build on
# aarch64-pc-windows-msvc (its "must use Clang on Win AArch64"
# workaround), while the CFLAGS injected by cargo-xwin carry
# clang-cl-style `/imsvc <dir>` pairs that plain clang rejects.
# This shim rewrites them to `-I <dir>` and execs the real clang
# found further down PATH; all other invocations pass through
# untouched, and tools with other names (clang-cl, lld-link) are not
# affected.
mkdir -p /root/ci-shims
cat > /root/ci-shims/clang <<'EOF'
#!/usr/bin/env bash
args=()
prev_imsvc=0
for a in "$@"; do
if [ "$prev_imsvc" = "1" ]; then
args+=("-I$a")
prev_imsvc=0
continue
fi
case "$a" in
/imsvc) prev_imsvc=1 ;;
/imsvc:*) args+=("-I${a#/imsvc:}") ;;
*) args+=("$a") ;;
esac
done
self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
real=""
IFS=":" read -r -a _dirs <<< "$PATH"
IFS=""
for d in "${_dirs[@]}"; do
[ "$d" = "$self_dir" ] && continue
if [ -x "$d/clang" ]; then real="$d/clang"; break; fi
done
[ -n "$real" ] || real=/usr/bin/clang
exec "$real" "${args[@]}"
EOF
chmod +x /root/ci-shims/clang
shell: bash
- name: Build wheels - name: Build wheels
shell: bash
run: | run: |
export PATH="$HOME/.cargo/bin:$HOME/.osxcross/bin:$PATH" export PATH="$HOME/.osxcross/bin:$PATH"
maturin sdist --out dist # LLVM_BIN and LIBCLANG_PATH were set by the "Install build
rustup default nightly # dependencies" step; put the shim (for ring's forced plain `clang`)
# in front of the toolchain so clang-cl / lld-link (distinct names)
# still resolve to the real toolchain
export PATH="/root/ci-shims:$LLVM_BIN:$PATH"
rm -rf dist
maturin sdist --out dist -m ndbioimage/Cargo.toml
rustup target add x86_64-unknown-linux-gnu --toolchain nightly rustup target add x86_64-apple-darwin
maturin build --release --out dist --target x86_64-unknown-linux-gnu maturin build --release --out dist --target x86_64-apple-darwin --zig --auditwheel repair -m ndbioimage/Cargo.toml
rustup target add aarch64-unknown-linux-gnu --toolchain nightly rustup target add aarch64-apple-darwin
maturin build --release --out dist --target aarch64-unknown-linux-gnu --zig maturin build --release --out dist --target aarch64-apple-darwin --zig --auditwheel repair -m ndbioimage/Cargo.toml
rustup target add x86_64-pc-windows-msvc --toolchain nightly rustup target add x86_64-unknown-linux-gnu
maturin build --release --out dist --target x86_64-pc-windows-msvc # the x86_64-gnu build links the runner's host liblzma (lzma-sys),
rustup target add aarch64-pc-windows-msvc --toolchain nightly # which needs glibc symbols up to 2.34 - higher than the
maturin build --release --out dist --target aarch64-pc-windows-msvc # manylinux_2_17 baseline
maturin build --release --out dist --target x86_64-unknown-linux-gnu --zig --auditwheel repair --compatibility manylinux_2_34 -m ndbioimage/Cargo.toml
rustup target add aarch64-unknown-linux-gnu
maturin build --release --out dist --target aarch64-unknown-linux-gnu --zig --auditwheel repair --compatibility manylinux_2_34 -m ndbioimage/Cargo.toml
rustup target add x86_64-unknown-linux-musl
maturin build --release --out dist --target x86_64-unknown-linux-musl --zig --auditwheel repair -m ndbioimage/Cargo.toml
rustup target add aarch64-unknown-linux-musl
maturin build --release --out dist --target aarch64-unknown-linux-musl --zig --auditwheel repair -m ndbioimage/Cargo.toml
rustup target add x86_64-apple-darwin --toolchain nightly rustup target add x86_64-pc-windows-msvc
maturin build --release --out dist --target x86_64-apple-darwin --zig maturin build --release --out dist --target x86_64-pc-windows-msvc --auditwheel repair -m ndbioimage/Cargo.toml
rustup target add aarch64-apple-darwin --toolchain nightly rustup target add aarch64-pc-windows-msvc
maturin build --release --out dist --target aarch64-apple-darwin --zig maturin build --release --out dist --target aarch64-pc-windows-msvc --auditwheel repair -m ndbioimage/Cargo.toml
- name: Store cache - name: Store cache
uses: actions/cache/save@v4 uses: actions/cache/save@v4
@@ -102,7 +191,7 @@ jobs:
~/.cache/pip-wheel ~/.cache/pip-wheel
~/.cache/sccache ~/.cache/sccache
~/.cache/cargo-xwin ~/.cache/cargo-xwin
~/.cargo ${{ env.CARGO_HOME }}
~/.osxcross ~/.osxcross
key: cache-ubuntu-maturin-cross-compile key: cache-ubuntu-maturin-cross-compile
@@ -110,5 +199,6 @@ jobs:
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
password: ${{ secrets.PYPI_API_TOKEN }} password: ${{ secrets.PYPI_API_TOKEN }}
skip-existing: true
env: env:
GITHUB_WORKFLOW_REF: 1.10.1 GITHUB_WORKFLOW_REF: 1.10.1
+79
View File
@@ -0,0 +1,79 @@
name: pytest
on: [ workflow_dispatch, workflow_call, push, pull_request ]
permissions:
contents: read
jobs:
pypi_publish:
name: Publish (pypi.org)
runs-on: ubuntu-latest
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
RUSTUP_HOME: ${{ github.workspace }}/.rustup
RUSTUP_TOOLCHAIN: stable
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- 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
${{ env.CARGO_HOME }}
key: cache-ubuntu-maturin-cross-compile
- name: Install build dependencies
run: |
sudo apt update
sudo apt install -y llvm cmake build-essential libclang-dev clang
shell: bash
- name: Install Rust
run: |
export PATH="$CARGO_HOME/bin:$PATH"
if ! command -v rustup >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
fi
rustup toolchain install stable --profile minimal
rustup default stable
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
cargo --version
shell: bash
- name: Install sccache and maturin
run: |
python -m pip install --upgrade pip maturin ziglang cargo-zigbuild
# in case an older copy was cargo-installed into the CARGO_HOME cache,
# prevent it from shadowing the pip-installed one
rm -f "${CARGO_HOME:?}/bin/cargo-zigbuild" || true
if ! command -v sccache >/dev/null 2>&1; then
cargo install sccache || pip install sccache
fi
shell: bash
- name: PyTest
run: |
cd ndbioimage
python -m pip install .[test]
python -m pytest
- name: Store cache
uses: actions/cache/save@v4
with:
path: |
~/.cache/pip
~/.cache/pip-wheel
~/.cache/sccache
~/.cache/cargo-xwin
${{ env.CARGO_HOME }}
~/.osxcross
key: cache-ubuntu-maturin-cross-compile
+2 -1
View File
@@ -29,6 +29,7 @@ venv/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
.cargo
# Installer logs # Installer logs
pip-log.txt pip-log.txt
@@ -72,11 +73,11 @@ docs/_build/
# Pyenv # Pyenv
.python-version .python-version
/ndbioimage/tests/files/*
AGENTS.md AGENTS.md
.agentbridge .agentbridge
.agent-work .agent-work
/ndbioimage/tests/files/*
/ndbioimage/py/ndbioimage/jassets /ndbioimage/py/ndbioimage/jassets
/ndbioimage/py/ndbioimage/deps /ndbioimage/py/ndbioimage/deps
/build_wheels.sh /build_wheels.sh
+6 -10
View File
@@ -3,28 +3,24 @@ resolver = "3"
members = ["ndbioimage", "image-tiff", "image-registration"] members = ["ndbioimage", "image-tiff", "image-registration"]
[workspace.package] [workspace.package]
version = "0.2.0" version = "0.3.2"
edition = "2024" edition = "2024"
# the workspace must satisfy every member, so this is the highest `rust-version` of the members rust-version = "1.94.1"
rust-version = "1.94.0"
authors = ["Wim Pomp <w.pomp@nki.nl>"] authors = ["Wim Pomp <w.pomp@nki.nl>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
description = "Read bio image formats using the bio-formats java package." description = "Read bio image formats using the bio-formats java package."
homepage = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs" repository = "https://git.pomppervova.nl/wim/ndbioimage"
repository = "https://git.pomppervova.nl/wim/ndbioimage/src/branch/rs"
documentation = "https://docs.rs/ndbioimage" documentation = "https://docs.rs/ndbioimage"
# inherited paths are resolved relative to the workspace root, not the member, so this has to point readme = "README.md"
# at the member's readme
readme = "ndbioimage/README.md"
keywords = ["bioformats", "imread", "ndarray", "metadata"] keywords = ["bioformats", "imread", "ndarray", "metadata"]
categories = ["multimedia::images", "science"] categories = ["multimedia::images", "science"]
# profiles are only honoured in the workspace root manifest
[profile.test] [profile.test]
inherits = "release" inherits = "release"
debug = true
[profile.release] [profile.release]
debug = true #debug = true
[profile.dev.package.backtrace] [profile.dev.package.backtrace]
opt-level = 3 opt-level = 3
+4 -19
View File
@@ -1,11 +1,10 @@
# ndbioimage [![pytest](https://git.pomppervova.nl/wim/ndbioimage/actions/workflows/pytest.yml/badge.svg)](https://git.pomppervova.nl/wim/ndbioimage/actions?workflow=pytest.yml)
[![Pytest](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml/badge.svg)](https://github.com/pomppervova/ndbioimage/actions/workflows/pytest.yml)
# NDBioImage
## Work in progress ## Work in progress
Rust rewrite of python version. Read bio image formats using the bio-formats java package. Rust rewrite of python version. Read bio image formats using some Rust code, the libczi api, and if needed the
[https://www.openmicroscopy.org/bio-formats/](https://www.openmicroscopy.org/bio-formats/) bio-formats java package.
Exposes (bio) images as a numpy ndarray-like object (Python) or a struct that can be sliced like an ndarray Array Exposes (bio) images as a numpy ndarray-like object (Python) or a struct that can be sliced like an ndarray Array
(Rust), but without loading the whole image into memory, reading from the file only when needed. Some metadata is read (Rust), but without loading the whole image into memory, reading from the file only when needed. Some metadata is read
@@ -18,20 +17,6 @@ Currently, it supports imagej tif files, czi files, micromanager tif sequences a
To transition to semver, versions before 0.1.0 were yanked from crates.io. To transition to semver, versions before 0.1.0 were yanked from crates.io.
## Installation
```sh
pip install ndbioimage
```
### Installation with option to write mp4 or mkv:
Work in progress! Make sure ffmpeg is installed.
```sh
pip install ndbioimage[write]
```
## Usage ## Usage
### Python ### Python
+8 -6
View File
@@ -6,12 +6,11 @@ rust-version.workspace = true
authors.workspace = true authors.workspace = true
license.workspace = true license.workspace = true
description.workspace = true description.workspace = true
homepage.workspace = true
repository.workspace = true repository.workspace = true
documentation.workspace = true documentation.workspace = true
readme.workspace = true
keywords.workspace = true keywords.workspace = true
categories.workspace = true categories.workspace = true
readme = "../README.md"
exclude = ["/tests"] exclude = ["/tests"]
[lib] [lib]
@@ -29,15 +28,15 @@ image-registration = { path = "../image-registration", version = "0.1", optional
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
indicatif = { version = "0.18", features = ["rayon"], optional = true } indicatif = { version = "0.18", features = ["rayon"], optional = true }
j4rs = { version = "0.25", optional = true } j4rs = { version = "0.25", optional = true }
libczirw-sys = { version = "0.5", optional = true } libczirw-sys = { version = "0.5.2", optional = true }
ndarray = { version = "0.17", features = ["serde"] } ndarray = { version = "0.17", features = ["serde"] }
num = "0.4" num = "0.4"
numpy = { version = "0.29", optional = true } numpy = { version = "0.29", optional = true }
ome-metadata = "0.5" ome-metadata = "0.6"
ordered-float = { version = "5", optional = true } ordered-float = { version = "5", optional = true }
phf = { version = "0.14", features = ["macros"] } phf = { version = "0.14", features = ["macros"] }
postcard = { version = "1", features = ["use-std"], optional = true } postcard = { version = "1", features = ["use-std"], optional = true }
pyo3 = { version = "0.29", features = ["abi3-py310", "anyhow", "generate-import-lib"], optional = true } pyo3 = { version = "0.29", features = ["abi3-py310", "anyhow", "generate-import-lib", "multiple-pymethods"], optional = true }
pyo3-stub-gen = { version = "0.23", optional = true } pyo3-stub-gen = { version = "0.23", optional = true }
rayon = { version = "1", optional = true } rayon = { version = "1", optional = true }
regex = "1" regex = "1"
@@ -51,6 +50,9 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread"], optional = true }
thread_local = { version = "1", optional = true } thread_local = { version = "1", optional = true }
xmltree = { version = "0.12", optional = true } xmltree = { version = "0.12", optional = true }
yaml_serde = { version = "0.10", optional = true } yaml_serde = { version = "0.10", optional = true }
# "bioformats 0.1.8" is written for "zip 2.4.x", where zip::read::ZipFile has a single lifetime parameter.
# "zip >= 2.5" added a second "R: Read" generic (2.6.1 breaks bioformats), so the build fails.
zip = { version = "=2.4.2", optional = true, default-features = false }
[dev-dependencies] [dev-dependencies]
rayon = "1" rayon = "1"
@@ -69,7 +71,7 @@ all = ["bioformats_java", "bioformats_rust", "czi", "gpl-formats", "movie", "tif
gpl-formats = [] gpl-formats = []
python = ["dep:pyo3", "dep:numpy", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"] python = ["dep:pyo3", "dep:numpy", "dep:pyo3-stub-gen", "dep:postcard", "ome-metadata/python"]
czi = ["dep:libczirw-sys", "dep:xmltree", "dep:thread_local"] czi = ["dep:libczirw-sys", "dep:xmltree", "dep:thread_local"]
bioformats_rust = ["dep:bioformats", "dep:thread_local"] bioformats_rust = ["dep:bioformats", "dep:thread_local", "dep:zip"]
bioformats_java = ["dep:j4rs", "dep:thread_local", "dep:downloader"] bioformats_java = ["dep:j4rs", "dep:thread_local", "dep:downloader"]
tiffwrite = ["dep:tiffwrite", "dep:indicatif", "dep:console", "dep:rayon"] tiffwrite = ["dep:tiffwrite", "dep:indicatif", "dep:console", "dep:rayon"]
tiffseq = ["dep:tiff", "dep:yaml_serde"] tiffseq = ["dep:tiff", "dep:yaml_serde"]
@@ -2,6 +2,8 @@
# ruff: noqa: E501, F401, F403, F405 # ruff: noqa: E501, F401, F403, F405
import builtins import builtins
import collections
import collections.abc
import os import os
import pathlib import pathlib
import typing import typing
@@ -52,6 +54,8 @@ class Flags:
def __new__(cls) -> Flags: ... def __new__(cls) -> Flags: ...
def __getnewargs__(self) -> None: ... def __getnewargs__(self) -> None: ...
def __getitem__(self, item: builtins.str) -> builtins.bool: ... def __getitem__(self, item: builtins.str) -> builtins.bool: ...
def __str__(self) -> builtins.str: ...
def __repr__(self) -> builtins.str: ...
class Imread: class Imread:
r""" r"""
@@ -96,16 +100,6 @@ class Imread:
the name of the reader used to open the file the name of the reader used to open the file
""" """
@property @property
def transforms(self) -> transforms.Transforms:
r"""
get the transformation
"""
@transforms.setter
def transforms(self, value: transforms.Transforms) -> None:
r"""
set the transformation
"""
@property
def path(self) -> pathlib.Path: def path(self) -> pathlib.Path:
r""" r"""
the file path the file path
@@ -220,6 +214,38 @@ class Imread:
r""" r"""
the name of the tube lens the name of the tube lens
""" """
@property
def channel_names(self) -> builtins.dict[builtins.int, builtins.str]:
r"""
names of the channels in order
"""
@property
def summary(self) -> builtins.str:
r"""
gives a helpful summary of the recorded experiment
"""
@property
def interpolation_order(self) -> typing.Optional[builtins.int]:
r"""
the bspline order used for interpolation when using transforms
None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
"""
@interpolation_order.setter
def interpolation_order(self, value: typing.Optional[builtins.int]) -> None:
r"""
the bspline order used for interpolation when using transforms
None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
"""
@property
def transforms(self) -> transforms.Transforms:
r"""
get the transformation
"""
@transforms.setter
def transforms(self, value: transforms.Transforms) -> None:
r"""
set the transformation
"""
def __new__( def __new__(
cls, cls,
path: str | pathlib.Path | Imread | bytes, path: str | pathlib.Path | Imread | bytes,
@@ -236,6 +262,10 @@ class Imread:
reader: use a specific reader to open the file, reader: use a specific reader to open the file,
view available choices with Imread.available_readers view available choices with Imread.available_readers
""" """
def debug(self) -> None:
r"""
some debug info
"""
@staticmethod @staticmethod
def warm_jvm() -> None: def warm_jvm() -> None:
r""" r"""
@@ -247,7 +277,10 @@ class Imread:
@staticmethod @staticmethod
def jvm_is_running() -> builtins.bool: def jvm_is_running() -> builtins.bool:
r""" r"""
returns whether the jvm was already created on this thread returns whether the jvm was already created in this process
this is false in a process that inherited a jvm across a fork, since such a jvm cannot be
used; reading a bio-formats file there raises instead
""" """
def set_flags( def set_flags(
self, self,
@@ -270,47 +303,19 @@ class Imread:
r""" r"""
only remains for backwards compatibility only remains for backwards compatibility
""" """
def reshape(self, order: builtins.str, copy: builtins.bool) -> typing.Any: def reshape(
r"""
reshape the view with a new axis order
"""
def with_transform(
self, self,
channels: builtins.bool = True, *shape: typing.Any,
drift: builtins.bool = False, order: builtins.str = "C",
file: typing.Optional[typing.Any] = None, copy: typing.Optional[builtins.bool] = None,
bead_files: typing.Optional[typing.Any] = None, ) -> Imread | numpy.NDArray:
main_channel: typing.Optional[builtins.int] = None,
default_transform: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Imread:
r""" r"""
return a new view with transformations applied (channel alignment, drift correction) reshape the view with a new axis order, arguments order and copy cannot be used and will
backward compatibility raise an exception if order != "C" or copy is False
""" """
def with_transforms( def squeeze(
self, self, axes: str | collections.abc.Sequence[int | str] = None
channels: builtins.bool = True, ) -> Imread | float | int: ...
drift: builtins.bool = False,
file: typing.Optional[typing.Any] = None,
bead_files: typing.Optional[typing.Any] = None,
main_channel: typing.Optional[builtins.int] = None,
default_transform: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Imread:
r"""
return a new view with transformations applied (channel alignment, drift correction)
"""
def load_transforms_from_yaml(
self, path: builtins.str | os.PathLike | pathlib.Path
) -> None: ...
def calculate_channel_transforms_2d(
self, main_channel: builtins.int
) -> builtins.dict[builtins.str, transforms.Transform]: ...
def calculate_channel_transforms_3d(
self, main_channel: builtins.int
) -> builtins.dict[builtins.str, transforms.Transform]: ...
def calculate_drift_transforms_2d(self) -> builtins.list[transforms.Transform]: ...
def calculate_drift_transforms_3d(self) -> builtins.list[transforms.Transform]: ...
def squeeze(self) -> numpy.ndarray | int | float: ...
def close(self) -> None: def close(self) -> None:
r""" r"""
close the file: does nothing as this is handled automatically close the file: does nothing as this is handled automatically
@@ -323,7 +328,19 @@ class Imread:
r""" r"""
change the data type of the view: (u)int(8/16/32) or float(32/64) change the data type of the view: (u)int(8/16/32) or float(32/64)
""" """
def __getitem__(self, n: typing.Any) -> typing.Any: def __call__(
self,
c: typing.Optional[int | slice | numpy.ArrayLike] = None,
z: typing.Optional[int | slice | numpy.ArrayLike] = None,
t: typing.Optional[int | slice | numpy.ArrayLike] = None,
y: typing.Optional[int | slice | numpy.ArrayLike] = None,
x: typing.Optional[int | slice | numpy.ArrayLike] = None,
) -> typing.Optional[int | Imread]:
r"""
same as im[] but allowing keyword axes, but slices need to made with slice()
also, no ellipsis or new axes
"""
def __getitem__(self, n: typing.Any) -> typing.Optional[int | Imread]:
r""" r"""
slice the view and return a new view or a single number slice the view and return a new view or a single number
""" """
@@ -331,7 +348,7 @@ class Imread:
self, self,
dtype: typing.Optional[numpy.typing.DTypeLike] = None, dtype: typing.Optional[numpy.typing.DTypeLike] = None,
copy: typing.Optional[builtins.bool] = None, copy: typing.Optional[builtins.bool] = None,
) -> typing.Any: ) -> numpy.typing.NDArray:
r""" r"""
convert to a numpy array, optionally with a different dtype convert to a numpy array, optionally with a different dtype
""" """
@@ -339,139 +356,199 @@ class Imread:
r""" r"""
check if an item is contained in the view (not implemented) check if an item is contained in the view (not implemented)
""" """
def __lt__(self, other: typing.Any) -> typing.Any: def __lt__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise less than comparison element-wise less than comparison
""" """
def __le__(self, other: typing.Any) -> typing.Any: def __le__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise less than or equal comparison element-wise less than or equal comparison
""" """
def __eq__(self, other: typing.Any) -> typing.Any: def __eq__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise equality comparison element-wise equality comparison
""" """
def __ne__(self, other: typing.Any) -> typing.Any: def __ne__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise not equal comparison element-wise not equal comparison
""" """
def __gt__(self, other: typing.Any) -> typing.Any: def __gt__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise greater than comparison element-wise greater than comparison
""" """
def __ge__(self, other: typing.Any) -> typing.Any: def __ge__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise greater than or equal comparison element-wise greater than or equal comparison
""" """
def __add__(self, other: typing.Any) -> typing.Any: def __add__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise addition element-wise addition
""" """
def __radd__(self, other: typing.Any) -> typing.Any: def __radd__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise addition (reflected) element-wise addition (reflected)
""" """
def __sub__(self, other: typing.Any) -> typing.Any: def __sub__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise subtraction element-wise subtraction
""" """
def __rsub__(self, other: typing.Any) -> typing.Any: def __rsub__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise subtraction (reflected) element-wise subtraction (reflected)
""" """
def __mul__(self, other: typing.Any) -> typing.Any: def __mul__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise multiplication element-wise multiplication
""" """
def __rmul__(self, other: typing.Any) -> typing.Any: def __rmul__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise multiplication (reflected) element-wise multiplication (reflected)
""" """
def __truediv__(self, other: typing.Any) -> typing.Any: def __truediv__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise true division element-wise true division
""" """
def __rtruediv__(self, other: typing.Any) -> typing.Any: def __rtruediv__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise true division (reflected) element-wise true division (reflected)
""" """
def __floordiv__(self, other: typing.Any) -> typing.Any: def __floordiv__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise floor division element-wise floor division
""" """
def __rfloordiv__(self, other: typing.Any) -> typing.Any: def __rfloordiv__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise floor division (reflected) element-wise floor division (reflected)
""" """
def __mod__(self, other: typing.Any) -> typing.Any: def __mod__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise modulo element-wise modulo
""" """
def __rmod__(self, other: typing.Any) -> typing.Any: def __rmod__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise modulo (reflected) element-wise modulo (reflected)
""" """
def __matmul__(self, other: typing.Any) -> typing.Any: def __matmul__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise matrix multiplication element-wise matrix multiplication
""" """
def __rmatmul__(self, other: typing.Any) -> typing.Any: def __rmatmul__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise matrix multiplication (reflected) element-wise matrix multiplication (reflected)
""" """
def __and__(self, other: typing.Any) -> typing.Any: def __and__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise bitwise AND element-wise bitwise AND
""" """
def __rand__(self, other: typing.Any) -> typing.Any: def __rand__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise bitwise AND (reflected) element-wise bitwise AND (reflected)
""" """
def __or__(self, other: typing.Any) -> typing.Any: def __or__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise bitwise OR element-wise bitwise OR
""" """
def __ror__(self, other: typing.Any) -> typing.Any: def __ror__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise bitwise OR (reflected) element-wise bitwise OR (reflected)
""" """
def __xor__(self, other: typing.Any) -> typing.Any: def __xor__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise bitwise XOR element-wise bitwise XOR
""" """
def __rxor__(self, other: typing.Any) -> typing.Any: def __rxor__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | bool:
r""" r"""
element-wise bitwise XOR (reflected) element-wise bitwise XOR (reflected)
""" """
def __lshift__(self, other: typing.Any) -> typing.Any: def __lshift__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise left shift element-wise left shift
""" """
def __rlshift__(self, other: typing.Any) -> typing.Any: def __rlshift__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise left shift (reflected) element-wise left shift (reflected)
""" """
def __rshift__(self, other: typing.Any) -> typing.Any: def __rshift__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise right shift element-wise right shift
""" """
def __rrshift__(self, other: typing.Any) -> typing.Any: def __rrshift__(
self, other: float | numpy.ArrayLike | Imread
) -> numpy.typing.NDArray | float:
r""" r"""
element-wise right shift (reflected) element-wise right shift (reflected)
""" """
def __neg__(self) -> typing.Any: def __neg__(self) -> numpy.typing.NDArray | float:
r""" r"""
element-wise negation element-wise negation
""" """
def __pos__(self) -> typing.Any: def __pos__(self) -> numpy.typing.NDArray | float:
r""" r"""
element-wise positive element-wise positive
""" """
def __abs__(self) -> typing.Any: def __abs__(self) -> numpy.typing.NDArray | float:
r""" r"""
element-wise absolute value element-wise absolute value
""" """
def __invert__(self) -> typing.Any: def __invert__(self) -> numpy.typing.NDArray | float:
r""" r"""
element-wise bitwise inversion element-wise bitwise inversion
""" """
@@ -524,13 +601,19 @@ class Imread:
r""" r"""
the file path as a string the file path as a string
""" """
def frame(
self, c: builtins.int, z: builtins.int, t: builtins.int
) -> numpy.typing.NDArray | float:
r"""
retrieve a single frame at czt, sliced accordingly, alias of get_frame
"""
def get_frame( def get_frame(
self, c: builtins.int, z: builtins.int, t: builtins.int self, c: builtins.int, z: builtins.int, t: builtins.int
) -> typing.Any: ) -> numpy.typing.NDArray | float:
r""" r"""
retrieve a single frame at czt, sliced accordingly retrieve a single frame at czt, sliced accordingly
""" """
def flatten(self) -> typing.Any: def flatten(self) -> numpy.typing.NDArray | float:
r""" r"""
flatten the view into a 1D numpy array flatten the view into a 1D numpy array
""" """
@@ -546,17 +629,21 @@ class Imread:
r""" r"""
find the position of an axis find the position of an axis
""" """
def swapaxes(self, ax0: int | str, ax1: int | str) -> Imread:
r"""
swap two axes
"""
def swap_axes(self, ax0: int | str, ax1: int | str) -> Imread: def swap_axes(self, ax0: int | str, ax1: int | str) -> Imread:
r""" r"""
swap two axes swap two axes
""" """
def transpose( def transpose(
self, axes: typing.Optional[typing.Sequence[int | str]] = None self, axes: typing.Optional[collections.abc.Sequence[int | str] | str] = None
) -> Imread: ) -> Imread:
r""" r"""
permute the order of the axes permute the order of the axes
""" """
def as_array(self) -> typing.Any: def as_array(self) -> numpy.typing.NDArray | float:
r""" r"""
collect data into a numpy array collect data into a numpy array
""" """
@@ -586,9 +673,9 @@ class Imread:
r""" r"""
the detector gain for a given channel the detector gain for a given channel
""" """
def summary(self) -> builtins.str: def channel_name(self, channel: builtins.int) -> builtins.str:
r""" r"""
gives a helpful summary of the recorded experiment the name of a particular channel
""" """
@staticmethod @staticmethod
def get_available_series( def get_available_series(
@@ -692,6 +779,73 @@ class Imread:
r""" r"""
Return the sum along a given axis. Arguments beyond axis are not implemented Return the sum along a given axis. Arguments beyond axis are not implemented
""" """
def with_interpolation_order(self, order: typing.Optional[builtins.int]) -> Imread:
r"""
the bspline order used for interpolation when using transforms
None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
"""
def get_default_interpolation_value(
self, channel: builtins.int
) -> transforms.DefaultValue:
r"""
the value of the interpolated image outside the data
"""
def set_default_interpolation_value(
self,
channel: builtins.int,
value: float | transforms.DefaultValue | collections.abc.Sequence[float],
) -> None:
r"""
the value of the interpolated image outside the data
"""
def with_default_interpolation_value(
self,
channel: builtins.int,
value: float | transforms.DefaultValue | collections.abc.Sequence[float],
) -> Imread:
r"""
the value of the interpolated image outside the data
"""
def with_transform(
self,
channels: builtins.bool = True,
drift: builtins.bool = False,
file: typing.Optional[builtins.str | os.PathLike | pathlib.Path] = None,
bead_files: typing.Optional[
typing.Sequence[builtins.str | os.PathLike | pathlib.Path]
] = None,
main_channel: typing.Optional[builtins.int] = None,
default_transform: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Imread:
r"""
return a new view with transformations applied (channel alignment, drift correction)
backward compatibility
"""
def with_transforms(
self,
channels: builtins.bool = True,
drift: builtins.bool = False,
file: typing.Optional[builtins.str | os.PathLike | pathlib.Path] = None,
bead_files: typing.Optional[
typing.Sequence[builtins.str | os.PathLike | pathlib.Path]
] = None,
main_channel: typing.Optional[builtins.int] = None,
default_transform: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Imread:
r"""
returns a view where channels and/or frames are registered with an affine transformation
"""
def load_transforms_from_yaml(
self, path: builtins.str | os.PathLike | pathlib.Path
) -> None: ...
def calculate_channel_transforms_2d(
self, main_channel: builtins.int
) -> builtins.dict[builtins.str, transforms.Transform]: ...
def calculate_channel_transforms_3d(
self, main_channel: builtins.int
) -> builtins.dict[builtins.str, transforms.Transform]: ...
def calculate_drift_transforms_2d(self) -> builtins.list[transforms.Transform]: ...
def calculate_drift_transforms_3d(self) -> builtins.list[transforms.Transform]: ...
class Shape: class Shape:
r""" r"""
@@ -761,7 +915,7 @@ class Shape:
arguments for pickling arguments for pickling
""" """
def __getitem__( def __getitem__(
self, idx: str | int | None | Ellipsis | slice | list[int] | tuple[int] self, idx: str | int | None | Ellipsis | slice | typing.Sequence[int]
) -> typing.Optional[int | tuple[int]]: ) -> typing.Optional[int | tuple[int]]:
r""" r"""
get dimension size by index or axis name get dimension size by index or axis name
@@ -2,6 +2,7 @@
# ruff: noqa: E501, F401, F403, F405 # ruff: noqa: E501, F401, F403, F405
import builtins import builtins
import collections.abc
import os import os
import pathlib import pathlib
import typing import typing
@@ -10,6 +11,7 @@ import numpy
import numpy.typing import numpy.typing
__all__ = [ __all__ = [
"DefaultValue",
"Optimizer", "Optimizer",
"RegistrationStep", "RegistrationStep",
"SamplingArg", "SamplingArg",
@@ -18,6 +20,21 @@ __all__ = [
"Transforms", "Transforms",
] ]
class DefaultValue:
@property
def values(self) -> builtins.list[builtins.float]: ...
@property
def weights(self) -> builtins.list[builtins.float]: ...
def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(
cls,
values: typing.Sequence[builtins.float],
weights: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> DefaultValue: ...
def __getnewargs__(
self,
) -> tuple[builtins.list[builtins.float], builtins.list[builtins.float]]: ...
class Optimizer: class Optimizer:
def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
def __new__(cls, optimizer: builtins.str) -> Optimizer: ... def __new__(cls, optimizer: builtins.str) -> Optimizer: ...
@@ -116,24 +133,6 @@ class Transform:
@dparameters.setter @dparameters.setter
def dparameters(self, value: typing.Sequence[builtins.float]) -> None: ... def dparameters(self, value: typing.Sequence[builtins.float]) -> None: ...
@property @property
def center(self) -> builtins.list[builtins.float]: ...
@center.setter
def center(self, value: typing.Sequence[builtins.float]) -> None: ...
@property
def origin(self) -> builtins.list[builtins.float]:
r"""
backwards compatibility
"""
@origin.setter
def origin(self, value: typing.Sequence[builtins.float]) -> None:
r"""
backwards compatibility
"""
@property
def shape(self) -> builtins.list[builtins.int]: ...
@shape.setter
def shape(self, value: typing.Sequence[builtins.int]) -> None: ...
@property
def ndim(self) -> builtins.int: ... def ndim(self) -> builtins.int: ...
@property @property
def matrix(self) -> numpy.typing.NDArray[numpy.float64]: ... def matrix(self) -> numpy.typing.NDArray[numpy.float64]: ...
@@ -149,18 +148,12 @@ class Transform:
def __new__( def __new__(
cls, cls,
parameters: typing.Optional[typing.Sequence[builtins.float]] = None, parameters: typing.Optional[typing.Sequence[builtins.float]] = None,
shape: typing.Optional[typing.Sequence[builtins.int]] = None, dparameters: typing.Optional[typing.Sequence[builtins.float]] = None,
center: typing.Optional[typing.Sequence[builtins.float]] = None, center: typing.Optional[typing.Sequence[builtins.float]] = None,
) -> Transform: ... ) -> Transform: ...
def __getnewargs__( def __getnewargs__(
self, self,
) -> tuple[ ) -> tuple[builtins.list[builtins.float], builtins.list[builtins.float]]: ...
builtins.list[builtins.float],
builtins.list[builtins.int],
typing.Optional[builtins.list[builtins.float]],
]: ...
def __getstate__(self) -> builtins.list[builtins.float]: ...
def __setstate__(self, state: typing.Sequence[builtins.float]) -> None: ...
def __add__(self, other: Transform) -> Transform: ... def __add__(self, other: Transform) -> Transform: ...
def __radd__(self, other: Transform) -> Transform: ... def __radd__(self, other: Transform) -> Transform: ...
def __sub__(self, other: Transform) -> Transform: ... def __sub__(self, other: Transform) -> Transform: ...
@@ -168,11 +161,6 @@ class Transform:
def __mul__(self, other: Transform | float) -> Transform: ... def __mul__(self, other: Transform | float) -> Transform: ...
def __rmul__(self, other: Transform | float) -> Transform: ... def __rmul__(self, other: Transform | float) -> Transform: ...
def __truediv__(self, other: builtins.float) -> Transform: ... def __truediv__(self, other: builtins.float) -> Transform: ...
def adapt(
self,
center: typing.Sequence[builtins.float],
shape: typing.Sequence[builtins.int],
) -> None: ...
@staticmethod @staticmethod
def from_scaling(scaling: typing.Sequence[builtins.float]) -> Transform: ... def from_scaling(scaling: typing.Sequence[builtins.float]) -> Transform: ...
@staticmethod @staticmethod
@@ -189,23 +177,32 @@ class Transform:
self, theta: builtins.float, center: typing.Sequence[builtins.float] self, theta: builtins.float, center: typing.Sequence[builtins.float]
) -> Transform: ... ) -> Transform: ...
def interpolate( def interpolate(
self, order: builtins.int, image: numpy.typing.ArrayLike self,
order: builtins.int,
image: numpy.typing.ArrayLike,
default_value: typing.Optional[DefaultValue] = None,
) -> numpy.typing.NDArray[numpy.float64]: ... ) -> numpy.typing.NDArray[numpy.float64]: ...
def interpolate_par( def interpolate_par(
self, order: builtins.int, image: numpy.typing.ArrayLike self,
order: builtins.int,
image: numpy.typing.ArrayLike,
default_value: typing.Optional[DefaultValue] = None,
) -> numpy.typing.NDArray[numpy.float64]: ... ) -> numpy.typing.NDArray[numpy.float64]: ...
def frame( def frame(
self, image: numpy.typing.ArrayLike, default: typing.Optional[typing.Any] = None self,
image: numpy.typing.ArrayLike,
default: typing.Optional[typing.Any] = None,
default_value: typing.Optional[DefaultValue] = None,
) -> numpy.ndarray: ) -> numpy.ndarray:
r""" r"""
backwards compatibility, default parameter is not implemented backwards compatibility, default parameter is not implemented
""" """
def is_unity(self) -> builtins.bool: ... def is_unity(self) -> builtins.bool: ...
def transform_point( def transform_point(
self, point: numpy.typing.ArrayLike self, point: numpy.typing.ArrayLike, center: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ... ) -> numpy.typing.NDArray[numpy.float64]: ...
def transform_points( def transform_points(
self, points: numpy.typing.ArrayLike self, points: numpy.typing.ArrayLike, center: numpy.typing.ArrayLike
) -> numpy.typing.NDArray[numpy.float64]: ... ) -> numpy.typing.NDArray[numpy.float64]: ...
@staticmethod @staticmethod
def register( def register(
@@ -222,9 +219,25 @@ class Transform:
def register_translation( def register_translation(
fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike
) -> Transform: ... ) -> Transform: ...
@staticmethod
def register_translation_fft(
fixed: numpy.typing.ArrayLike, moving: numpy.typing.ArrayLike
) -> Transform: ...
class Transforms: class Transforms:
@property @property
def interpolation_order(self) -> typing.Optional[builtins.int]:
r"""
the bspline order used for interpolation when using transforms,
None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
"""
@interpolation_order.setter
def interpolation_order(self, value: typing.Optional[builtins.int]) -> None:
r"""
the bspline order used for interpolation when using transforms
None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
"""
@property
def channel_transforms(self) -> builtins.dict[builtins.str, Transform]: ... def channel_transforms(self) -> builtins.dict[builtins.str, Transform]: ...
@channel_transforms.setter @channel_transforms.setter
def channel_transforms( def channel_transforms(
@@ -239,10 +252,48 @@ class Transforms:
cls, cls,
channel: typing.Mapping[builtins.str, Transform] = {}, channel: typing.Mapping[builtins.str, Transform] = {},
drift: typing.Sequence[Transform] = [], drift: typing.Sequence[Transform] = [],
interpolation_order: typing.Optional[builtins.int] = None,
default_value: typing.Optional[
typing.Mapping[builtins.str, DefaultValue]
] = None,
) -> Transforms: ... ) -> Transforms: ...
def __getnewargs__( def __getnewargs__(
self, self,
) -> tuple[builtins.dict[builtins.str, Transform], builtins.list[Transform]]: ... ) -> tuple[
builtins.dict[builtins.str, Transform],
builtins.list[Transform],
typing.Optional[builtins.int],
builtins.dict[builtins.str, DefaultValue],
]: ...
def with_interpolation_order(
self, order: typing.Optional[builtins.int]
) -> Transforms:
r"""
the bspline order used for interpolation when using transforms
None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
"""
def get_default_interpolation_value(
self, channel_name: builtins.str
) -> DefaultValue:
r"""
the value of the interpolated image outside the data
"""
def set_default_interpolation_value(
self,
channel_name: builtins.str,
value: float | DefaultValue | collections.abc.Sequence[float],
) -> None:
r"""
the value of the interpolated image outside the data
"""
def with_default_interpolation_value(
self,
channel_name: builtins.str,
value: float | DefaultValue | collections.abc.Sequence[float],
) -> Transforms:
r"""
the value of the interpolated image outside the data
"""
@staticmethod @staticmethod
def load(path: builtins.str | os.PathLike | pathlib.Path) -> Transforms: ... def load(path: builtins.str | os.PathLike | pathlib.Path) -> Transforms: ...
def save(self, path: builtins.str | os.PathLike | pathlib.Path) -> None: ... def save(self, path: builtins.str | os.PathLike | pathlib.Path) -> None: ...
+2
View File
@@ -1,4 +1,5 @@
from .ndbioimage_rs.transforms import ( from .ndbioimage_rs.transforms import (
DefaultValue,
Optimizer, Optimizer,
RegistrationStep, RegistrationStep,
SamplingArg, SamplingArg,
@@ -8,6 +9,7 @@ from .ndbioimage_rs.transforms import (
) )
__all__ = [ __all__ = [
"DefaultValue",
"Optimizer", "Optimizer",
"RegistrationStep", "RegistrationStep",
"SamplingArg", "SamplingArg",
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project] [project]
name = "ndbioimage" name = "ndbioimage"
version = "2027.0.5" version = "2027.0.8"
requires-python = ">=3.10" requires-python = ">=3.10"
classifiers = [ classifiers = [
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
@@ -35,7 +35,7 @@ ndbioimage_generate_stub = "ndbioimage:ndbioimage_generate_stub"
[tool.maturin] [tool.maturin]
python-source = "py" python-source = "py"
features = ["python", "bioformats_java", "gpl-formats", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"] features = ["python", "bioformats_java", "gpl-formats", "bioformats_rust", "czi", "tiff", "tiffseq", "tiffwrite", "movie", "transforms"]
no-default-features = true no-default-features = true
module-name = "ndbioimage.ndbioimage_rs" module-name = "ndbioimage.ndbioimage_rs"
include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"] include = ["py/ndbioimage/jassets/j4rs*", "py/ndbioimage/deps/libj4rs*"]
+18 -9
View File
@@ -196,11 +196,11 @@ impl<'de> DeserializeAs<'de, SliceInfoElem> for SliceInfoElemDef {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Copy)]
pub(crate) struct Slice { pub(crate) struct Slice {
start: isize, pub(crate) start: isize,
end: isize, pub(crate) end: isize,
step: isize, pub(crate) step: isize,
} }
impl Slice { impl Slice {
@@ -236,7 +236,7 @@ impl IntoIterator for &Slice {
type IntoIter = Slice; type IntoIter = Slice;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
self.clone() *self
} }
} }
@@ -314,16 +314,16 @@ impl Index<&Axis> for Shape {
impl Index<usize> for Shape { impl Index<usize> for Shape {
type Output = usize; type Output = usize;
fn index(&self, dim: usize) -> &Self::Output { fn index(&self, index: usize) -> &Self::Output {
&self[self.order[dim % self.order.len()]] &self[self.order[index]]
} }
} }
impl Index<&usize> for Shape { impl Index<&usize> for Shape {
type Output = usize; type Output = usize;
fn index(&self, dim: &usize) -> &Self::Output { fn index(&self, index: &usize) -> &Self::Output {
&self[self.order[dim % self.order.len()]] &self[self.order[*index]]
} }
} }
@@ -406,6 +406,10 @@ impl Shape {
} }
} }
pub fn get(&self, index: &usize) -> Option<&usize> {
self.order.get(*index).map(|axis| &self[axis])
}
/// the number of axes in the shape /// the number of axes in the shape
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.order.len() self.order.len()
@@ -421,6 +425,11 @@ impl Shape {
self.order.iter().map(|axis| self[axis]).collect() self.order.iter().map(|axis| self[axis]).collect()
} }
/// the sizes in cztyx order
pub fn cztyx(&self) -> [usize; 5] {
[self.c, self.z, self.t, self.y, self.x]
}
/// the sizes along the axes as a map from axis to size /// the sizes along the axes as a map from axis to size
pub fn to_hashmap(&self) -> HashMap<Axis, usize> { pub fn to_hashmap(&self) -> HashMap<Axis, usize> {
let mut map = HashMap::new(); let mut map = HashMap::new();
+108 -6
View File
@@ -1,15 +1,23 @@
use crate::axes::{Axis, Operation}; use crate::axes::{Axis, Operation};
use crate::readers::Frame;
#[cfg(feature = "transforms")]
use crate::transforms::{DefaultValues, Transforms};
#[cfg(feature = "transforms")]
use image_registration::transform::Transform;
use indexmap::{Equivalent, IndexMap}; use indexmap::{Equivalent, IndexMap};
#[cfg(feature = "transforms")]
use ndarray::IxDyn;
use ndarray::{ArrayD, SliceInfoElem}; use ndarray::{ArrayD, SliceInfoElem};
use std::any::Any; use std::any::Any;
#[cfg(feature = "transforms")]
use std::borrow::Cow;
use std::cmp::PartialEq;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock}; use std::sync::{Arc, Mutex, OnceLock};
use std::time::SystemTime; use std::time::SystemTime;
use crate::readers::Frame;
/// maximum number of frames held in the cache /// maximum number of frames held in the cache
const DEFAULT_FRAME_CACHE_SIZE: usize = 128; const DEFAULT_FRAME_CACHE_SIZE: usize = 128;
@@ -132,7 +140,33 @@ impl ReaderKeyRef<'_> {
} }
} }
pub(crate) type FrameKey = (ReaderKey, usize, usize, usize); #[derive(Debug, PartialEq, Eq)]
pub(crate) struct FrameKey {
pub(crate) reader: ReaderKey,
pub(crate) c: usize,
pub(crate) z: usize,
pub(crate) t: usize,
#[cfg(feature = "transforms")]
pub(crate) transform: Option<(Transform<IxDyn>, usize, DefaultValues)>,
}
impl Hash for FrameKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.reader.hash(state);
self.c.hash(state);
self.z.hash(state);
self.t.hash(state);
#[cfg(feature = "transforms")]
if let Some(transform) = self.transform.as_ref() {
transform.0.hash(state);
transform.1.hash(state);
transform.2.iter().for_each(|(k, v)| {
k.hash(state);
v.hash(state);
})
}
}
}
/// borrowed view of [`FrameKey`] for cache lookups without allocation. /// borrowed view of [`FrameKey`] for cache lookups without allocation.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@@ -141,6 +175,8 @@ pub(crate) struct FrameKeyRef<'a> {
pub(crate) c: usize, pub(crate) c: usize,
pub(crate) z: usize, pub(crate) z: usize,
pub(crate) t: usize, pub(crate) t: usize,
#[cfg(feature = "transforms")]
pub(crate) transform: Option<(Cow<'a, Transform<IxDyn>>, usize, Cow<'a, DefaultValues>)>,
} }
impl Hash for FrameKeyRef<'_> { impl Hash for FrameKeyRef<'_> {
@@ -149,19 +185,60 @@ impl Hash for FrameKeyRef<'_> {
self.c.hash(state); self.c.hash(state);
self.z.hash(state); self.z.hash(state);
self.t.hash(state); self.t.hash(state);
#[cfg(feature = "transforms")]
if let Some(transform) = self.transform.as_ref() {
transform.0.hash(state);
transform.1.hash(state);
transform.2.iter().for_each(|(k, v)| {
k.hash(state);
v.hash(state);
})
}
} }
} }
impl Equivalent<FrameKey> for FrameKeyRef<'_> { impl Equivalent<FrameKey> for FrameKeyRef<'_> {
fn equivalent(&self, key: &FrameKey) -> bool { fn equivalent(&self, key: &FrameKey) -> bool {
let (rk, c, z, t) = key; let FrameKey {
self.reader.equivalent(rk) && self.c == *c && self.z == *z && self.t == *t reader: reader_key,
c,
z,
t,
#[cfg(feature = "transforms")]
transform,
} = key;
#[cfg(feature = "transforms")]
{
self.reader.equivalent(reader_key)
&& self.c == *c
&& self.z == *z
&& self.t == *t
&& self
.transform
.as_ref()
.map(|(t, i, d)| (t.as_ref(), i, d.as_ref()))
== transform.as_ref().map(|(t, i, d)| (t, i, d))
}
#[cfg(not(feature = "transforms"))]
{
self.reader.equivalent(reader_key) && self.c == *c && self.z == *z && self.t == *t
}
} }
} }
impl FrameKeyRef<'_> { impl FrameKeyRef<'_> {
pub(crate) fn to_owned(&self) -> FrameKey { pub(crate) fn to_owned(&self) -> FrameKey {
(self.reader.to_owned(), self.c, self.z, self.t) FrameKey {
reader: self.reader.to_owned(),
c: self.c,
z: self.z,
t: self.t,
#[cfg(feature = "transforms")]
transform: self
.transform
.as_ref()
.map(|(t, i, d)| (t.clone().into_owned(), *i, d.clone().into_owned())),
}
} }
} }
@@ -173,6 +250,8 @@ pub(crate) struct ArrayKey {
pub(crate) slice: Vec<SliceInfoElem>, pub(crate) slice: Vec<SliceInfoElem>,
pub(crate) axes: Vec<Axis>, pub(crate) axes: Vec<Axis>,
pub(crate) operations: Vec<(Axis, Operation)>, pub(crate) operations: Vec<(Axis, Operation)>,
#[cfg(feature = "transforms")]
pub(crate) transforms: Transforms,
} }
/// borrowed view of [`ArrayKey`] for cache lookups without allocation. /// borrowed view of [`ArrayKey`] for cache lookups without allocation.
@@ -183,6 +262,8 @@ pub(crate) struct ArrayKeyRef<'a> {
pub(crate) slice: &'a [SliceInfoElem], pub(crate) slice: &'a [SliceInfoElem],
pub(crate) axes: &'a [Axis], pub(crate) axes: &'a [Axis],
pub(crate) operations: &'a IndexMap<Axis, Operation>, pub(crate) operations: &'a IndexMap<Axis, Operation>,
#[cfg(feature = "transforms")]
pub(crate) transforms: &'a Transforms,
} }
impl Hash for ArrayKeyRef<'_> { impl Hash for ArrayKeyRef<'_> {
@@ -196,11 +277,15 @@ impl Hash for ArrayKeyRef<'_> {
ax.hash(state); ax.hash(state);
op.hash(state); op.hash(state);
} }
#[cfg(feature = "transforms")]
self.transforms.hash(state);
} }
} }
impl Equivalent<ArrayKey> for ArrayKeyRef<'_> { impl Equivalent<ArrayKey> for ArrayKeyRef<'_> {
fn equivalent(&self, key: &ArrayKey) -> bool { fn equivalent(&self, key: &ArrayKey) -> bool {
#[cfg(feature = "transforms")]
{
self.reader.equivalent(&key.reader) self.reader.equivalent(&key.reader)
&& self.dtype == key.dtype && self.dtype == key.dtype
&& self.slice == key.slice && self.slice == key.slice
@@ -211,6 +296,21 @@ impl Equivalent<ArrayKey> for ArrayKeyRef<'_> {
.iter() .iter()
.zip(&key.operations) .zip(&key.operations)
.all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op) .all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op)
&& self.transforms == key.transforms
}
#[cfg(not(feature = "transforms"))]
{
self.reader.equivalent(&key.reader)
&& self.dtype == key.dtype
&& self.slice == key.slice
&& self.axes == key.axes
&& self.operations.len() == key.operations.len()
&& self
.operations
.iter()
.zip(&key.operations)
.all(|((ax, op), (key_ax, key_op))| ax == key_ax && op == key_op)
}
} }
} }
@@ -226,6 +326,8 @@ impl ArrayKeyRef<'_> {
.iter() .iter()
.map(|(ax, op)| (*ax, op.clone())) .map(|(ax, op)| (*ax, op.clone()))
.collect(), .collect(),
#[cfg(feature = "transforms")]
transforms: self.transforms.to_owned(),
} }
} }
} }
+15 -5
View File
@@ -65,10 +65,6 @@ pub enum Error {
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
#[error(transparent)] #[error(transparent)]
XmlTreeParse(#[from] xmltree::ParseError), XmlTreeParse(#[from] xmltree::ParseError),
/// a czi-specific error
#[cfg(feature = "czi")]
#[error(transparent)]
Czi(#[from] crate::readers::czi::CziError),
/// an error joining a tokio task /// an error joining a tokio task
#[cfg(feature = "movie")] #[cfg(feature = "movie")]
#[error(transparent)] #[error(transparent)]
@@ -121,6 +117,9 @@ pub enum Error {
/// the view is empty /// the view is empty
#[error("empty view")] #[error("empty view")]
EmptyView, EmptyView,
/// the view cannot be parsed as a scalar
#[error("not a scalar")]
NotScalar,
/// the color string could not be parsed /// the color string could not be parsed
#[error("invalid color: {0}")] #[error("invalid color: {0}")]
InvalidColor(String), InvalidColor(String),
@@ -173,12 +172,23 @@ pub enum Error {
#[error("{0} do not match in {1} and {2}")] #[error("{0} do not match in {1} and {2}")]
FileMismatch(String, String, String), FileMismatch(String, String, String),
/// a jvm created in another process was inherited across a fork /// a jvm created in another process was inherited across a fork
#[cfg(feature = "bioformats_java")]
#[error( #[error(
"the jvm was created in process {0} but this is process {1}: a forked child inherits a jvm \ "the jvm was created in process {0} but this is process {1}: a forked child inherits a jvm \
it cannot use. read the file before forking, or start the child with spawn/exec instead." it cannot use. read the file before forking, or start the child with spawn/exec instead."
)] )]
/// jvm was forked, so it cannot run in this process
ForkedJvm(u32, u32), ForkedJvm(u32, u32),
/// only 2d and 3d transformations are implemented
#[error("{0}D transformation is not implemented")]
TransformationNotImplemented(usize),
/// cannot find the name for some channel because it does not exist
#[error("no channel {0} present")]
NoChannel(usize),
/// no valid blocks in czi file
#[error("czi file has no valid blocks")]
CziNoValidBlocks,
#[error("f128 is not supported")]
F128NotSupported,
} }
impl Error { impl Error {
+5 -5
View File
@@ -6,8 +6,8 @@
//! structure. Additionally, it can automatically calculate an affine transform that corrects for //! structure. Additionally, it can automatically calculate an affine transform that corrects for
//! chromatic aberrations etc. and apply it on the fly to the image. //! chromatic aberrations etc. and apply it on the fly to the image.
//! //!
//! Currently, it supports imagej tif files, czi files, micromanager tif sequences and anything //! Currently, it supports imagej and ome tif files, czi files, micromanager tif sequences and
//! [bioformats](https://www.openmicroscopy.org/bio-formats/) can handle. //! anything [bioformats](https://www.openmicroscopy.org/bio-formats/) can handle.
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use ndarray::Array2; //! use ndarray::Array2;
@@ -16,7 +16,7 @@
//! # fn main() -> Result<(), ndbioimage::error::Error> { //! # fn main() -> Result<(), ndbioimage::error::Error> {
//! let path = "/path/to/file"; //! let path = "/path/to/file";
//! let reader = DynReader::new(&path, 0, 0)?; //! let reader = DynReader::new(&path, 0, 0)?;
//! println!("shape: {}", reader.shape()); //! println!("shape: {}", reader.raw_shape());
//! let frame = reader.get_frame(0, 0, 0)?; //! let frame = reader.get_frame(0, 0, 0)?;
//! if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) { //! if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
//! println!("{:?}", arr); //! println!("{:?}", arr);
@@ -70,7 +70,7 @@ pub mod readers;
pub mod tiffwrite; pub mod tiffwrite;
#[cfg(feature = "transforms")] #[cfg(feature = "transforms")]
/// image registration and warping images using affine transforms /// image registration and warping images using affine transforms
mod transforms; pub mod transforms;
mod utils; mod utils;
/// main entry point for the application /// main entry point for the application
@@ -338,7 +338,7 @@ mod tests {
fn read_ser() -> Result<(), Error> { fn read_ser() -> Result<(), Error> {
let file = "czi/Experiment-2029.czi"; let file = "czi/Experiment-2029.czi";
let reader = open(file)?; let reader = open(file)?;
println!("shape: {}", reader.shape()); println!("shape: {}", reader.raw_shape());
let frame = reader.get_frame(0, 0, 0)?; let frame = reader.get_frame(0, 0, 0)?;
if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) { if let Ok(arr) = <Frame as TryInto<Array2<i8>>>::try_into(frame) {
println!("{:?}", arr); println!("{:?}", arr);
+4
View File
@@ -67,6 +67,7 @@ fn main() -> PyResult<()> {
#[pymodule] #[pymodule]
#[pyo3(name = "ndbioimage_rs")] #[pyo3(name = "ndbioimage_rs")]
mod ndbioimage_rs { mod ndbioimage_rs {
#[cfg(feature = "transforms")]
use pyo3::prelude::*; use pyo3::prelude::*;
#[pymodule_export] #[pymodule_export]
@@ -100,6 +101,9 @@ mod ndbioimage_rs {
#[pymodule_export] #[pymodule_export]
use super::super::transforms::PyTransforms; use super::super::transforms::PyTransforms;
#[pymodule_export]
use super::super::transforms::PyDefaultValue;
#[pymodule_export] #[pymodule_export]
use super::super::transforms::PyRegistrationStep; use super::super::transforms::PyRegistrationStep;
+1409 -630
View File
File diff suppressed because it is too large. Load diff
+514 -110
View File
@@ -1,5 +1,7 @@
use crate::error::Error; use crate::error::Error;
use crate::py::imread::PyView;
use crate::transforms::Transforms; use crate::transforms::Transforms;
use image_registration::bspline::DefaultValue;
use image_registration::metric::{SamplingArg, Sigma}; use image_registration::metric::{SamplingArg, Sigma};
use image_registration::register::{Optimizer, RegistrationStep}; use image_registration::register::{Optimizer, RegistrationStep};
use image_registration::transform::Transform; use image_registration::transform::Transform;
@@ -17,6 +19,45 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
#[gen_stub_pyclass]
#[pyclass(
subclass,
from_py_object,
eq,
name = "DefaultValue",
module = "ndbioimage.transforms"
)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct PyDefaultValue {
inner: DefaultValue,
}
#[gen_stub_pymethods]
#[pymethods]
impl PyDefaultValue {
#[new]
#[pyo3(signature = (values, weights = None))]
fn new(values: Vec<f64>, weights: Option<Vec<f64>>) -> Self {
Self {
inner: DefaultValue::new(values, weights.unwrap_or_default()),
}
}
#[getter]
fn values(&self) -> Vec<f64> {
self.inner.values().to_vec()
}
#[getter]
fn weights(&self) -> Vec<f64> {
self.inner.weights().to_vec()
}
fn __getnewargs__(&self) -> (Vec<f64>, Vec<f64>) {
(self.inner.values().to_vec(), self.inner.weights().to_vec())
}
}
#[gen_stub_pyclass] #[gen_stub_pyclass]
#[pyclass( #[pyclass(
subclass, subclass,
@@ -34,41 +75,31 @@ pub(crate) struct PyTransform {
#[pymethods] #[pymethods]
impl PyTransform { impl PyTransform {
#[new] #[new]
#[pyo3(signature = (parameters = None, shape = None, center = None))] #[pyo3(signature = (parameters = None, dparameters = None, center = None))]
fn new( fn new(
parameters: Option<Vec<f64>>, parameters: Option<Vec<f64>>,
shape: Option<Vec<usize>>, dparameters: Option<Vec<f64>>,
center: Option<Vec<f64>>, center: Option<Vec<f64>>,
) -> Self { ) -> Self {
let parameters = parameters.unwrap_or_else(|| vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0]); let parameters = parameters.unwrap_or_else(|| vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
let shape = shape.unwrap_or_else(|| vec![512, 512]); let mut transform = if let Some(center) = center {
if let Some(center) = center { Transform::new_around_center(parameters, &center)
Self {
inner: Transform::new_with_center(parameters, center, shape),
}
} else { } else {
Self { Transform::new(parameters)
inner: Transform::new(parameters, shape), };
} if let Some(dparameters) = dparameters {
transform.dparameters = dparameters;
} }
Self { inner: transform }
} }
fn __getnewargs__(&self) -> (Vec<f64>, Vec<usize>, Option<Vec<f64>>) { fn __getnewargs__(&self) -> (Vec<f64>, Vec<f64>) {
( (
self.inner.parameters.clone(), self.inner.parameters.clone(),
self.inner.shape.clone(), self.inner.dparameters.clone(),
Some(self.inner.center.clone()),
) )
} }
fn __getstate__(&self) -> Vec<f64> {
self.inner.dparameters.clone()
}
fn __setstate__(&mut self, state: Vec<f64>) {
self.inner.dparameters = state;
}
fn __add__(&self, other: &PyTransform) -> PyTransform { fn __add__(&self, other: &PyTransform) -> PyTransform {
PyTransform { PyTransform {
inner: &self.inner + &other.inner, inner: &self.inner + &other.inner,
@@ -98,9 +129,9 @@ impl PyTransform {
py: Python, py: Python,
#[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound<PyAny>, #[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound<PyAny>,
) -> PyResult<PyTransform> { ) -> PyResult<PyTransform> {
if other.is_instance_of::<PyTransform>() { if let Ok(other) = other.extract::<PyTransform>() {
Ok(PyTransform { Ok(PyTransform {
inner: &self.inner * &other.extract::<PyTransform>()?.inner, inner: &self.inner * other.inner,
}) })
} else { } else {
let builtins = PyModule::import(py, "builtins")?; let builtins = PyModule::import(py, "builtins")?;
@@ -116,9 +147,9 @@ impl PyTransform {
py: Python, py: Python,
#[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound<PyAny>, #[gen_stub(override_type(type_repr = "Transform | float"))] other: &Bound<PyAny>,
) -> PyResult<PyTransform> { ) -> PyResult<PyTransform> {
if other.is_instance_of::<PyTransform>() { if let Ok(other) = other.extract::<PyTransform>() {
Ok(PyTransform { Ok(PyTransform {
inner: &other.extract::<PyTransform>()?.inner * &self.inner, inner: &other.inner * &self.inner,
}) })
} else { } else {
let builtins = PyModule::import(py, "builtins")?; let builtins = PyModule::import(py, "builtins")?;
@@ -157,38 +188,6 @@ impl PyTransform {
self.inner.dparameters = dparameters; self.inner.dparameters = dparameters;
} }
#[getter]
fn get_center(&self) -> Vec<f64> {
self.inner.center.clone()
}
#[setter]
fn set_center(&mut self, center: Vec<f64>) {
self.inner.center = center;
}
/// backwards compatibility
#[getter]
fn get_origin(&self) -> Vec<f64> {
self.inner.center.clone()
}
/// backwards compatibility
#[setter]
fn set_origin(&mut self, center: Vec<f64>) {
self.inner.center = center;
}
#[getter]
fn get_shape(&self) -> Vec<usize> {
self.inner.shape.clone()
}
#[setter]
fn set_shape(&mut self, shape: Vec<usize>) {
self.inner.shape = shape;
}
#[getter] #[getter]
fn get_ndim(&self) -> usize { fn get_ndim(&self) -> usize {
self.inner.ndim() self.inner.ndim()
@@ -231,10 +230,6 @@ impl PyTransform {
}) })
} }
fn adapt(&mut self, center: Vec<f64>, shape: Vec<usize>) {
self.inner.adapt(center.as_slice(), shape.as_slice());
}
#[staticmethod] #[staticmethod]
fn from_scaling(scaling: Vec<f64>) -> PyTransform { fn from_scaling(scaling: Vec<f64>) -> PyTransform {
PyTransform { PyTransform {
@@ -275,68 +270,86 @@ impl PyTransform {
} }
} }
#[pyo3(signature = (order, image, default_value = None))]
fn interpolate<'py>( fn interpolate<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
order: usize, order: usize,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
image: PyArrayLikeDyn<f64, AllowTypeChange>, image: PyArrayLikeDyn<f64, AllowTypeChange>,
default_value: Option<PyDefaultValue>,
) -> PyResult<Bound<'py, PyArrayDyn<f64>>> { ) -> PyResult<Bound<'py, PyArrayDyn<f64>>> {
let image = image.as_array(); let image = image.as_array();
let default_value = default_value.map(|i| i.inner).unwrap_or_default();
Ok(py Ok(py
.detach(|| match order { .detach(|| match order {
0 => Ok(self 0 => Ok(self.inner.interpolate::<0, _, _>(
.inner image.into_dimensionality().map_err(Error::from)?,
.interpolate::<0, _, _>(image.into_dimensionality().map_err(Error::from)?)), &default_value,
1 => Ok(self )),
.inner 1 => Ok(self.inner.interpolate::<1, _, _>(
.interpolate::<1, _, _>(image.into_dimensionality().map_err(Error::from)?)), image.into_dimensionality().map_err(Error::from)?,
2 => Ok(self &default_value,
.inner )),
.interpolate::<2, _, _>(image.into_dimensionality().map_err(Error::from)?)), 2 => Ok(self.inner.interpolate::<2, _, _>(
3 => Ok(self image.into_dimensionality().map_err(Error::from)?,
.inner &default_value,
.interpolate::<3, _, _>(image.into_dimensionality().map_err(Error::from)?)), )),
4 => Ok(self 3 => Ok(self.inner.interpolate::<3, _, _>(
.inner image.into_dimensionality().map_err(Error::from)?,
.interpolate::<4, _, _>(image.into_dimensionality().map_err(Error::from)?)), &default_value,
5 => Ok(self )),
.inner 4 => Ok(self.inner.interpolate::<4, _, _>(
.interpolate::<5, _, _>(image.into_dimensionality().map_err(Error::from)?)), image.into_dimensionality().map_err(Error::from)?,
&default_value,
)),
5 => Ok(self.inner.interpolate::<5, _, _>(
image.into_dimensionality().map_err(Error::from)?,
&default_value,
)),
_ => Err(PyValueError::new_err("order must be 0 <= order < 6")), _ => Err(PyValueError::new_err("order must be 0 <= order < 6")),
})? })?
.map_err(Error::from)? .map_err(Error::from)?
.into_pyarray(py)) .into_pyarray(py))
} }
#[pyo3(signature = (order, image, default_value = None))]
fn interpolate_par<'py>( fn interpolate_par<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
order: usize, order: usize,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
image: PyArrayLikeDyn<f64, AllowTypeChange>, image: PyArrayLikeDyn<f64, AllowTypeChange>,
default_value: Option<PyDefaultValue>,
) -> PyResult<Bound<'py, PyArrayDyn<f64>>> { ) -> PyResult<Bound<'py, PyArrayDyn<f64>>> {
let image = image.as_array(); let image = image.as_array();
let default_value = default_value.map(|i| i.inner).unwrap_or_default();
Ok(py Ok(py
.detach(|| match order { .detach(|| match order {
0 => Ok(self 0 => Ok(self.inner.interpolate_par::<0, _, _>(
.inner image.into_dimensionality().map_err(Error::from)?,
.interpolate_par::<0, _, _>(image.into_dimensionality().map_err(Error::from)?)), &default_value,
1 => Ok(self )),
.inner 1 => Ok(self.inner.interpolate_par::<1, _, _>(
.interpolate_par::<1, _, _>(image.into_dimensionality().map_err(Error::from)?)), image.into_dimensionality().map_err(Error::from)?,
2 => Ok(self &default_value,
.inner )),
.interpolate_par::<2, _, _>(image.into_dimensionality().map_err(Error::from)?)), 2 => Ok(self.inner.interpolate_par::<2, _, _>(
3 => Ok(self image.into_dimensionality().map_err(Error::from)?,
.inner &default_value,
.interpolate_par::<3, _, _>(image.into_dimensionality().map_err(Error::from)?)), )),
4 => Ok(self 3 => Ok(self.inner.interpolate_par::<3, _, _>(
.inner image.into_dimensionality().map_err(Error::from)?,
.interpolate_par::<4, _, _>(image.into_dimensionality().map_err(Error::from)?)), &default_value,
5 => Ok(self )),
.inner 4 => Ok(self.inner.interpolate_par::<4, _, _>(
.interpolate_par::<5, _, _>(image.into_dimensionality().map_err(Error::from)?)), image.into_dimensionality().map_err(Error::from)?,
&default_value,
)),
5 => Ok(self.inner.interpolate_par::<5, _, _>(
image.into_dimensionality().map_err(Error::from)?,
&default_value,
)),
_ => Err(PyValueError::new_err("order must be 0 <= order < 6")), _ => Err(PyValueError::new_err("order must be 0 <= order < 6")),
})? })?
.map_err(Error::from)? .map_err(Error::from)?
@@ -345,13 +358,14 @@ impl PyTransform {
/// backwards compatibility, default parameter is not implemented /// backwards compatibility, default parameter is not implemented
#[gen_stub(override_return_type(type_repr="numpy.ndarray", imports=("numpy")))] #[gen_stub(override_return_type(type_repr="numpy.ndarray", imports=("numpy")))]
#[pyo3(signature = (image, default = None))] #[pyo3(signature = (image, default = None, default_value = None))]
fn frame<'py>( fn frame<'py>(
&self, &self,
py: Python<'py>, py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
image: Bound<'py, PyUntypedArray>, image: Bound<'py, PyUntypedArray>,
default: Option<Bound<'py, PyAny>>, default: Option<Bound<'py, PyAny>>,
default_value: Option<PyDefaultValue>,
) -> PyResult<Bound<'py, PyAny>> { ) -> PyResult<Bound<'py, PyAny>> {
if default.is_some() { if default.is_some() {
return Err(PyNotImplementedError::new_err( return Err(PyNotImplementedError::new_err(
@@ -365,6 +379,7 @@ impl PyTransform {
self.inner.ndim() self.inner.ndim()
))); )));
} }
let default_value = default_value.map(|i| i.inner).unwrap_or_default();
macro_rules! interp { macro_rules! interp {
($order:expr, $t:tt) => { ($order:expr, $t:tt) => {
@@ -375,6 +390,7 @@ impl PyTransform {
.into_dimensionality() .into_dimensionality()
.map_err(Error::from)? .map_err(Error::from)?
.view(), .view(),
&default_value,
) )
.map_err(Error::from)? .map_err(Error::from)?
.mapv(|i| i as $t) .mapv(|i| i as $t)
@@ -392,6 +408,7 @@ impl PyTransform {
.into_dimensionality() .into_dimensionality()
.map_err(Error::from)? .map_err(Error::from)?
.view(), .view(),
&default_value,
) )
.map_err(Error::from)? .map_err(Error::from)?
.mapv(|i| i > 0.5) .mapv(|i| i > 0.5)
@@ -434,13 +451,19 @@ impl PyTransform {
py: Python<'py>, py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
point: PyArrayLike1<f64, AllowTypeChange>, point: PyArrayLike1<f64, AllowTypeChange>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
center: PyArrayLike1<f64, AllowTypeChange>,
) -> Bound<'py, PyArray1<f64>> { ) -> Bound<'py, PyArray1<f64>> {
let point = point.as_array(); let point = point.as_array();
if let Some(slice) = point.as_slice() { let center = center.as_array();
self.inner.transform_point(slice)
} else { match (point.as_slice(), center.as_slice()) {
let point = point.to_vec(); (Some(point), Some(center)) => self.inner.transform_point(point, center),
self.inner.transform_point(&point) (Some(point), None) => self.inner.transform_point(point, &center.to_vec()),
(None, Some(center)) => self.inner.transform_point(&point.to_vec(), center),
(None, None) => self
.inner
.transform_point(&point.to_vec(), &center.to_vec()),
} }
.into_pyarray(py) .into_pyarray(py)
} }
@@ -450,10 +473,19 @@ impl PyTransform {
py: Python<'py>, py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))] #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
points: PyArrayLike2<f64, AllowTypeChange>, points: PyArrayLike2<f64, AllowTypeChange>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
center: PyArrayLike1<f64, AllowTypeChange>,
) -> PyResult<Bound<'py, PyArray2<f64>>> { ) -> PyResult<Bound<'py, PyArray2<f64>>> {
let points = points.as_array(); let points = points.as_array();
let center = center.as_array();
Ok(py Ok(py
.detach(|| self.inner.transform_points(points)) .detach(|| {
if let Some(center) = center.as_slice() {
self.inner.transform_points(points, center)
} else {
self.inner.transform_points(points, &center.to_vec())
}
})
.map_err(Error::from)? .map_err(Error::from)?
.into_pyarray(py)) .into_pyarray(py))
} }
@@ -694,6 +726,29 @@ impl PyTransform {
))), ))),
}) })
} }
#[staticmethod]
fn register_translation_fft<'py>(
py: Python<'py>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
fixed: PyArrayLikeDyn<f64, AllowTypeChange>,
#[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy.typing")))]
moving: PyArrayLikeDyn<f64, AllowTypeChange>,
) -> PyResult<PyTransform> {
let fixed = fixed.as_array();
let moving = moving.as_array();
if fixed.shape() != moving.shape() {
return Err(PyErr::from(Error::ShapeMismatch(
fixed.shape().to_vec(),
moving.shape().to_vec(),
)));
}
py.detach(|| {
Ok(PyTransform {
inner: Transform::register_translation_fft(fixed, moving).map_err(Error::from)?,
})
})
}
} }
#[gen_stub_pyclass] #[gen_stub_pyclass]
@@ -709,22 +764,375 @@ pub(crate) struct PyTransforms {
pub(crate) inner: Transforms, pub(crate) inner: Transforms,
} }
#[gen_stub_pymethods]
#[pymethods]
impl PyView {
/// the bspline order used for interpolation when using transforms
/// None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
#[getter]
fn get_interpolation_order(&self) -> Option<usize> {
self.view.transforms.interpolation_order
}
/// the bspline order used for interpolation when using transforms
/// None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
#[setter]
fn set_interpolation_order(&mut self, order: Option<usize>) {
self.view.transforms.interpolation_order = order;
}
/// the bspline order used for interpolation when using transforms
/// None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
fn with_interpolation_order(&self, order: Option<usize>) -> Self {
let mut new = self.clone();
new.view.transforms.interpolation_order = order;
new
}
/// the value of the interpolated image outside the data
fn get_default_interpolation_value(&self, channel: usize) -> PyResult<PyDefaultValue> {
let channel_name = self.view.channel_name(channel)?.to_string();
Ok(PyDefaultValue {
inner: self
.view
.transforms
.default_value
.get(&channel_name)
.cloned()
.unwrap_or_default(),
})
}
/// the value of the interpolated image outside the data
fn set_default_interpolation_value(
&mut self,
channel: usize,
#[gen_stub(override_type(type_repr="float | transforms.DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))]
value: Bound<PyAny>,
) -> PyResult<()> {
let channel_name = self.view.channel_name(channel)?.to_string();
if let Ok(value) = value.extract::<PyDefaultValue>() {
self.view
.transforms
.default_value
.insert(channel_name, value.inner);
} else if let Ok(value) = value.extract::<f64>() {
self.view
.transforms
.default_value
.insert(channel_name, value.into());
} else if let Ok(value) = value.try_iter() {
self.view.transforms.default_value.insert(
channel_name,
value
.map(|i| i?.extract::<f64>())
.collect::<PyResult<Vec<_>>>()?
.into(),
);
} else {
return Err(PyValueError::new_err(format!(
"cannot parse value: {:?} into DefaultValue",
value
)));
}
Ok(())
}
/// the value of the interpolated image outside the data
fn with_default_interpolation_value(
&self,
channel: usize,
#[gen_stub(override_type(type_repr="float | transforms.DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))]
value: Bound<PyAny>,
) -> PyResult<Self> {
let mut new = self.clone();
new.set_default_interpolation_value(channel, value)?;
Ok(new)
}
/// return a new view with transformations applied (channel alignment, drift correction)
/// backward compatibility
#[cfg(feature = "transforms")]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))]
fn with_transform(
&self,
channels: bool,
drift: bool,
file: Option<PathBuf>,
bead_files: Option<Vec<PathBuf>>,
main_channel: Option<usize>,
default_transform: Option<Vec<f64>>,
) -> PyResult<PyView> {
self.with_transforms(
channels,
drift,
file,
bead_files,
main_channel,
default_transform,
)
}
/// 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'
// main channel: which channel to transform with default_transform
// default_transform: parameters of the default transform: [a, b, c, d, e, f] where the affine transform matrix is
// | a b e |
// | c d f |
// | 0 0 1 |
#[cfg(feature = "transforms")]
#[pyo3(signature = (channels = true, drift = false, file = None, bead_files = None, main_channel = None, default_transform = None))]
fn with_transforms(
&self,
channels: bool,
drift: bool,
file: Option<PathBuf>,
bead_files: Option<Vec<PathBuf>>,
main_channel: Option<usize>,
default_transform: Option<Vec<f64>>,
) -> PyResult<PyView> {
let mut new = self.clone();
let bead_files = bead_files
.as_ref()
.map(|i| i.iter().map(|j| j.as_path()).collect::<Vec<_>>());
new.view.transforms = Transforms::calculate_transforms_from(
file.as_deref(),
if channels {
bead_files.as_deref()
} else {
None
},
if drift { Some(self.view.path()) } else { None },
main_channel,
default_transform,
)?;
Ok(new)
}
/// get the transformation
#[getter]
fn get_transforms(&self) -> PyResult<PyTransforms> {
Ok(PyTransforms {
inner: self.view.get_transforms().clone(),
})
}
/// set the transformation
#[setter]
fn set_transforms(&mut self, transform: PyTransforms) {
self.view.set_transforms(transform.inner);
}
fn load_transforms_from_yaml(&mut self, path: PathBuf) -> PyResult<()> {
self.view.load_transforms_from_yaml(path.as_path())?;
Ok(())
}
fn calculate_channel_transforms_2d(
&self,
py: Python,
main_channel: usize,
) -> PyResult<HashMap<String, PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_channel_transforms_2d(main_channel)?
.into_iter()
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
})
}
fn calculate_channel_transforms_3d(
&self,
py: Python,
main_channel: usize,
) -> PyResult<HashMap<String, PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_channel_transforms_3d(main_channel)?
.into_iter()
.map(|(n, t)| {
(
n,
PyTransform {
inner: t.into_dyn(),
},
)
})
.collect())
})
}
fn calculate_drift_transforms_2d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_drift_transforms_2d()?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
})
.collect())
})
}
fn calculate_drift_transforms_3d(&self, py: Python) -> PyResult<Vec<PyTransform>> {
py.detach(|| {
Ok(self
.view
.calculate_drift_transforms_3d()?
.into_iter()
.map(|t| PyTransform {
inner: t.into_dyn(),
})
.collect())
})
}
}
#[gen_stub_pymethods] #[gen_stub_pymethods]
#[pymethods] #[pymethods]
impl PyTransforms { impl PyTransforms {
#[new] #[new]
#[pyo3(signature = (channel = HashMap::new(), drift = Vec::new()))] #[pyo3(signature = (channel = HashMap::new(), drift = Vec::new(), interpolation_order = None, default_value = None))]
fn new( fn new(
channel: HashMap<String, PyTransform>, channel: HashMap<String, PyTransform>,
drift: Vec<PyTransform>, drift: Vec<PyTransform>,
interpolation_order: Option<usize>,
default_value: Option<HashMap<String, PyDefaultValue>>,
) -> PyResult<PyTransforms> { ) -> PyResult<PyTransforms> {
let channel = channel.into_iter().map(|(n, t)| (n, t.inner)).collect(); let channel = channel.into_iter().map(|(n, t)| (n, t.inner)).collect();
let drift = drift.into_iter().map(|t| t.inner).collect(); let drift = drift.into_iter().map(|t| t.inner).collect();
Ok(PyTransforms { Ok(PyTransforms {
inner: Transforms { channel, drift }, inner: Transforms {
default_value: default_value
.into_iter()
.flatten()
.map(|(k, v)| (k, v.inner))
.collect::<HashMap<_, _>>(),
interpolation_order,
channel,
drift,
},
}) })
} }
#[allow(clippy::type_complexity)]
fn __getnewargs__(
&self,
) -> (
HashMap<String, PyTransform>,
Vec<PyTransform>,
Option<usize>,
HashMap<String, PyDefaultValue>,
) {
(
self.inner
.channel
.iter()
.map(|(k, v)| (k.to_string(), PyTransform { inner: v.clone() }))
.collect(),
self.inner
.drift
.iter()
.map(|i| PyTransform { inner: i.clone() })
.collect(),
self.inner.interpolation_order,
self.inner
.default_value
.iter()
.map(|(k, v)| (k.to_string(), PyDefaultValue { inner: v.clone() }))
.collect(),
)
}
/// the bspline order used for interpolation when using transforms,
/// None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
#[getter]
fn get_interpolation_order(&self) -> Option<usize> {
self.inner.interpolation_order
}
/// the bspline order used for interpolation when using transforms
/// None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
#[setter]
fn set_interpolation_order(&mut self, order: Option<usize>) {
self.inner.interpolation_order = order;
}
/// the bspline order used for interpolation when using transforms
/// None -> 0 (nearest neighbor) for int dtypes, 3 (cubic) for float dtypes
fn with_interpolation_order(&self, order: Option<usize>) -> Self {
let mut new = self.clone();
new.inner.interpolation_order = order;
new
}
/// the value of the interpolated image outside the data
fn get_default_interpolation_value(&self, channel_name: &str) -> PyResult<PyDefaultValue> {
Ok(PyDefaultValue {
inner: self
.inner
.default_value
.get(channel_name)
.cloned()
.unwrap_or_default(),
})
}
/// the value of the interpolated image outside the data
fn set_default_interpolation_value(
&mut self,
channel_name: String,
#[gen_stub(override_type(type_repr="float | DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))]
value: Bound<PyAny>,
) -> PyResult<()> {
if let Ok(value) = value.extract::<PyDefaultValue>() {
self.inner.default_value.insert(channel_name, value.inner);
} else if let Ok(value) = value.extract::<f64>() {
self.inner.default_value.insert(channel_name, value.into());
} else if let Ok(value) = value.try_iter() {
self.inner.default_value.insert(
channel_name,
value
.map(|i| i?.extract::<f64>())
.collect::<PyResult<Vec<_>>>()?
.into(),
);
} else {
return Err(PyValueError::new_err(format!(
"cannot parse value: {:?} into DefaultValue",
value
)));
}
Ok(())
}
/// the value of the interpolated image outside the data
fn with_default_interpolation_value(
&self,
channel_name: String,
#[gen_stub(override_type(type_repr="float | DefaultValue | collections.abc.Sequence[float]", imports=("collections.abc")))]
value: Bound<PyAny>,
) -> PyResult<Self> {
let mut new = self.clone();
new.set_default_interpolation_value(channel_name, value)?;
Ok(new)
}
#[getter] #[getter]
fn get_channel_transforms(&self) -> HashMap<String, PyTransform> { fn get_channel_transforms(&self) -> HashMap<String, PyTransform> {
self.inner self.inner
@@ -755,10 +1163,6 @@ impl PyTransforms {
Ok(()) Ok(())
} }
fn __getnewargs__(&self) -> (HashMap<String, PyTransform>, Vec<PyTransform>) {
(self.get_channel_transforms(), self.get_drift_transforms())
}
#[staticmethod] #[staticmethod]
pub(crate) fn load(path: PathBuf) -> PyResult<PyTransforms> { pub(crate) fn load(path: PathBuf) -> PyResult<PyTransforms> {
Ok(PyTransforms { Ok(PyTransforms {
+54 -19
View File
@@ -2,7 +2,7 @@ use crate::axes::{Axis, Shape};
use crate::error::Error; use crate::error::Error;
use crate::view::View; use crate::view::View;
use ndarray::{Array, Dimension, Ix2, Ix5, s}; use ndarray::{Array, Dimension, Ix2, Ix5, s};
use num::{FromPrimitive, Zero}; use num::{FromPrimitive, One, Zero};
use ome_metadata::Ome; use ome_metadata::Ome;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -128,7 +128,7 @@ pub enum PixelType {
I128, I128,
/// unsigned 128-bit integer /// unsigned 128-bit integer
U128, U128,
/// 128-bit float (emulated) /// 128-bit float (not supported)
F128, F128,
} }
@@ -148,6 +148,8 @@ impl PixelType {
/// array data with a specific pixel type /// array data with a specific pixel type
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ArrayT<D: Dimension> { pub enum ArrayT<D: Dimension> {
/// boolean array
Bool(Array<bool, D>),
/// signed 8-bit integer array /// signed 8-bit integer array
I8(Array<i8, D>), I8(Array<i8, D>),
/// unsigned 8-bit integer array /// unsigned 8-bit integer array
@@ -168,24 +170,28 @@ pub enum ArrayT<D: Dimension> {
I64(Array<i64, D>), I64(Array<i64, D>),
/// unsigned 64-bit integer array /// unsigned 64-bit integer array
U64(Array<u64, D>), U64(Array<u64, D>),
/// signed 128-bit integer array /// signed 128-bit integer array (not supported by numpy)
I128(Array<i128, D>), I128(Array<i128, D>),
/// unsigned 128-bit integer array /// unsigned 128-bit integer array (not supported by numpy)
U128(Array<u128, D>), U128(Array<u128, D>),
/// 128-bit float array (emulated as f64)
F128(Array<f64, D>),
} }
/// type alias for a single frame (2D image) /// type alias for a single frame (2D image)
pub type Frame = ArrayT<Ix2>; pub type Frame = ArrayT<Ix2>;
/// trait for reading image files /// trait for reading image files
pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> { pub trait Reader: Clone + Sized + Debug + Sync + Send + Hash + Into<DynReader> {
/// create a new reader for the given path, series, and position /// create a new reader for the given path, series, and position
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error> fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
where where
P: AsRef<Path>; P: AsRef<Path>;
/// a simple fast test to see if the reader can open a path without parsing the whole file
/// not a guarantee of success, but rather an early failure
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>;
/// the name of the reader type /// the name of the reader type
fn reader_name(&self) -> &'static str { fn reader_name(&self) -> &'static str {
type_name::<Self>() type_name::<Self>()
@@ -197,9 +203,9 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
/// get a sliceable view on the image file /// get a sliceable view on the image file
fn view(&self) -> View<Ix5, Self> { fn view(&self) -> View<Ix5, Self> {
let shape = self.shape(); let shape = self.raw_shape();
let slice = s![0..shape.c, 0..shape.z, 0..shape.t, 0..shape.y, 0..shape.x,]; let slice = s![0..shape.c, 0..shape.z, 0..shape.t, 0..shape.y, 0..shape.x,];
View::new( View::new_with_slice_and_axes(
self.clone(), self.clone(),
slice.as_ref().to_vec(), slice.as_ref().to_vec(),
vec![Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X], vec![Axis::C, Axis::Z, Axis::T, Axis::Y, Axis::X],
@@ -216,7 +222,7 @@ pub trait Reader: Clone + Sized + Debug + Send + Hash + Into<DynReader> {
/// the position index /// the position index
fn position(&self) -> usize; fn position(&self) -> usize;
/// the shape of the image data /// the shape of the image data
fn shape(&self) -> &Shape; fn raw_shape(&self) -> &Shape;
/// the pixel type of the image data /// the pixel type of the image data
fn pixel_type(&self) -> &PixelType; fn pixel_type(&self) -> &PixelType;
/// get all available positions for a given series /// get all available positions for a given series
@@ -314,6 +320,7 @@ macro_rules! try_into_array_body {
($self:expr, $map:ident) => {{ ($self:expr, $map:ident) => {{
let mut err = Ok(()); let mut err = Ok(());
let arr = match $self { let arr = match $self {
ArrayT::Bool(v) => v.$map(|x| if x { T::one() } else { T::zero() }),
ArrayT::I8(v) => v.$map(|x| { ArrayT::I8(v) => v.$map(|x| {
T::from_i8(x).unwrap_or_else(|| { T::from_i8(x).unwrap_or_else(|| {
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string())); err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
@@ -356,7 +363,7 @@ macro_rules! try_into_array_body {
T::zero() T::zero()
}) })
}), }),
ArrayT::F64(v) | ArrayT::F128(v) => v.$map(|x| { ArrayT::F64(v) => v.$map(|x| {
T::from_f64(x).unwrap_or_else(|| { T::from_f64(x).unwrap_or_else(|| {
err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string())); err = Err(Error::Cast(x.to_string(), type_name::<T>().to_string()));
T::zero() T::zero()
@@ -397,7 +404,7 @@ macro_rules! try_into_array_body {
impl<D, T> TryInto<Array<T, D>> for ArrayT<D> impl<D, T> TryInto<Array<T, D>> for ArrayT<D>
where where
D: Dimension, D: Dimension,
T: FromPrimitive + Zero + 'static, T: FromPrimitive + Zero + One + 'static,
{ {
type Error = Error; type Error = Error;
@@ -409,7 +416,7 @@ where
impl<D, T> TryInto<Array<T, D>> for &ArrayT<D> impl<D, T> TryInto<Array<T, D>> for &ArrayT<D>
where where
D: Dimension, D: Dimension,
T: Clone + FromPrimitive + Zero + 'static, T: Clone + FromPrimitive + Zero + One + 'static,
{ {
type Error = Error; type Error = Error;
@@ -475,6 +482,34 @@ impl Reader for DynReader {
)) ))
} }
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
let mut can_open = false;
#[cfg(feature = "tiff")]
{
can_open |= tiff::TiffReader::can_open(&path)?;
}
#[cfg(feature = "tiffseq")]
{
can_open |= tiffseq::TiffSeqReader::can_open(&path)?;
}
#[cfg(feature = "czi")]
{
can_open |= czi::CziReader::can_open(&path)?;
}
#[cfg(feature = "bioformats_rust")]
{
can_open |= bioformats_rust::BioFormatsRustReader::can_open(&path)?;
}
#[cfg(feature = "bioformats_java")]
{
can_open |= bioformats_java::BioFormatsJavaReader::can_open(&path)?;
}
Ok(can_open)
}
fn reader_name(&self) -> &'static str { fn reader_name(&self) -> &'static str {
match self { match self {
#[cfg(feature = "tiff")] #[cfg(feature = "tiff")]
@@ -577,18 +612,18 @@ impl Reader for DynReader {
} }
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
match self { match self {
#[cfg(feature = "tiff")] #[cfg(feature = "tiff")]
DynReader::Tiff(r) => r.shape(), DynReader::Tiff(r) => r.raw_shape(),
#[cfg(feature = "tiffseq")] #[cfg(feature = "tiffseq")]
DynReader::TiffSeq(r) => r.shape(), DynReader::TiffSeq(r) => r.raw_shape(),
#[cfg(feature = "czi")] #[cfg(feature = "czi")]
DynReader::Czi(r) => r.shape(), DynReader::Czi(r) => r.raw_shape(),
#[cfg(feature = "bioformats_rust")] #[cfg(feature = "bioformats_rust")]
DynReader::BioFormatsRust(r) => r.shape(), DynReader::BioFormatsRust(r) => r.raw_shape(),
#[cfg(feature = "bioformats_java")] #[cfg(feature = "bioformats_java")]
DynReader::BioFormatsJava(r) => r.shape(), DynReader::BioFormatsJava(r) => r.raw_shape(),
#[expect(unreachable_patterns)] #[expect(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
} }
+12 -5
View File
@@ -38,7 +38,7 @@ static JVM_PID: AtomicU32 = AtomicU32::new(0);
/// else -- verified by `JNI_GetCreatedJavaVMs`, which still reports 1 in the child, so a second /// else -- verified by `JNI_GetCreatedJavaVMs`, which still reports 1 in the child, so a second
/// `JNI_CreateJavaVM` (which is what j4rs' `Jvm::attach_thread` calls) takes the already-exists /// `JNI_CreateJavaVM` (which is what j4rs' `Jvm::attach_thread` calls) takes the already-exists
/// path. An `exec` replaces the address space, and that does reset the count to 0. /// path. An `exec` replaces the address space, and that does reset the count to 0.
fn check_not_forked() -> Result<(), Error> { pub fn check_not_forked() -> Result<(), Error> {
let built_in = JVM_PID.load(Ordering::Relaxed); let built_in = JVM_PID.load(Ordering::Relaxed);
if built_in != 0 && built_in != std::process::id() { if built_in != 0 && built_in != std::process::id() {
return Err(Error::ForkedJvm(built_in, std::process::id())); return Err(Error::ForkedJvm(built_in, std::process::id()));
@@ -521,7 +521,7 @@ impl BioFormatsJavaReader {
PixelType::U64 => 8, PixelType::U64 => 8,
PixelType::I128 => 16, PixelType::I128 => 16,
PixelType::U128 => 16, PixelType::U128 => 16,
PixelType::F128 => 8, PixelType::F128 => 16,
}; };
Ok(bytes Ok(bytes
.chunks(chunk_size) .chunks(chunk_size)
@@ -587,7 +587,7 @@ impl BioFormatsJavaReader {
(PixelType::U64, true) => get_frame!(u64, <8), (PixelType::U64, true) => get_frame!(u64, <8),
(PixelType::I128, true) => get_frame!(i128, <16), (PixelType::I128, true) => get_frame!(i128, <16),
(PixelType::U128, true) => get_frame!(u128, <16), (PixelType::U128, true) => get_frame!(u128, <16),
(PixelType::F128, true) => get_frame!(f64, <8), (PixelType::F128, true) => Err(Error::F128NotSupported),
(PixelType::Bool, false) => get_frame!(bool, >1), (PixelType::Bool, false) => get_frame!(bool, >1),
(PixelType::I8, false) => get_frame!(i8, >1), (PixelType::I8, false) => get_frame!(i8, >1),
(PixelType::U8, false) => get_frame!(u8, >1), (PixelType::U8, false) => get_frame!(u8, >1),
@@ -601,7 +601,7 @@ impl BioFormatsJavaReader {
(PixelType::U64, false) => get_frame!(u64, >8), (PixelType::U64, false) => get_frame!(u64, >8),
(PixelType::I128, false) => get_frame!(i128, >16), (PixelType::I128, false) => get_frame!(i128, >16),
(PixelType::U128, false) => get_frame!(u128, >16), (PixelType::U128, false) => get_frame!(u128, >16),
(PixelType::F128, false) => get_frame!(f64, >8), (PixelType::F128, false) => Err(Error::F128NotSupported),
} }
} }
} }
@@ -666,6 +666,13 @@ impl Reader for BioFormatsJavaReader {
Ok(new) Ok(new)
} }
fn can_open<P>(_path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
Ok(true)
}
fn metadata(&self) -> Result<Ome, Error> { fn metadata(&self) -> Result<Ome, Error> {
self.get_ome() self.get_ome()
} }
@@ -698,7 +705,7 @@ impl Reader for BioFormatsJavaReader {
0 0
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
+11 -4
View File
@@ -140,7 +140,7 @@ impl BioFormatsRustReader {
PixelType::U64 => 8, PixelType::U64 => 8,
PixelType::I128 => 16, PixelType::I128 => 16,
PixelType::U128 => 16, PixelType::U128 => 16,
PixelType::F128 => 8, PixelType::F128 => 16,
}; };
Ok(bytes Ok(bytes
.chunks(chunk_size) .chunks(chunk_size)
@@ -206,7 +206,7 @@ impl BioFormatsRustReader {
(PixelType::U64, true) => get_frame!(u64, <8), (PixelType::U64, true) => get_frame!(u64, <8),
(PixelType::I128, true) => get_frame!(i128, <16), (PixelType::I128, true) => get_frame!(i128, <16),
(PixelType::U128, true) => get_frame!(u128, <16), (PixelType::U128, true) => get_frame!(u128, <16),
(PixelType::F128, true) => get_frame!(f64, <8), (PixelType::F128, true) => Err(Error::F128NotSupported),
(PixelType::Bool, false) => get_frame!(bool, >1), (PixelType::Bool, false) => get_frame!(bool, >1),
(PixelType::I8, false) => get_frame!(i8, >1), (PixelType::I8, false) => get_frame!(i8, >1),
(PixelType::U8, false) => get_frame!(u8, >1), (PixelType::U8, false) => get_frame!(u8, >1),
@@ -220,7 +220,7 @@ impl BioFormatsRustReader {
(PixelType::U64, false) => get_frame!(u64, >8), (PixelType::U64, false) => get_frame!(u64, >8),
(PixelType::I128, false) => get_frame!(i128, >16), (PixelType::I128, false) => get_frame!(i128, >16),
(PixelType::U128, false) => get_frame!(u128, >16), (PixelType::U128, false) => get_frame!(u128, >16),
(PixelType::F128, false) => get_frame!(f64, >8), (PixelType::F128, false) => Err(Error::F128NotSupported),
} }
} }
} }
@@ -274,6 +274,13 @@ impl Reader for BioFormatsRustReader {
Ok(new) Ok(new)
} }
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
Ok(path.as_ref().extension() != Some(std::ffi::OsStr::new("czi")))
}
fn metadata(&self) -> Result<Ome, Error> { fn metadata(&self) -> Result<Ome, Error> {
let reader = self.get_reader()?; let reader = self.get_reader()?;
let metadata = reader.metadata(); let metadata = reader.metadata();
@@ -309,7 +316,7 @@ impl Reader for BioFormatsRustReader {
0 0
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
+11 -12
View File
@@ -122,14 +122,6 @@ impl From<Option<String>> for Version {
} }
} }
/// errors specific to czi file reading
#[derive(Debug, thiserror::Error)]
pub enum CziError {
/// czi file has no valid blocks
#[error("czi file has no valid blocks")]
NoValidBlocks,
}
impl Reader for CziReader { impl Reader for CziReader {
fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error> fn new<P>(path: P, series: usize, position: usize) -> Result<Self, Error>
where where
@@ -195,7 +187,7 @@ impl Reader for CziReader {
let mut max_y = i32::MIN; let mut max_y = i32::MIN;
for &i in Ok(block_map.get(&(0, 0, 0))) for &i in Ok(block_map.get(&(0, 0, 0)))
.transpose() .transpose()
.unwrap_or_else(|| block_map.values().next().ok_or(CziError::NoValidBlocks))? .unwrap_or_else(|| block_map.values().next().ok_or(Error::CziNoValidBlocks))?
{ {
if let Ok(info) = reader.try_get_sub_block_info_for_index(i) { if let Ok(info) = reader.try_get_sub_block_info_for_index(i) {
let rect = info.get_logical_rect(); let rect = info.get_logical_rect();
@@ -219,10 +211,17 @@ impl Reader for CziReader {
new.shape.x = (max_y - min_y) as usize; new.shape.x = (max_y - min_y) as usize;
new.shape.y = (max_x - min_x) as usize; new.shape.y = (max_x - min_x) as usize;
new.block_map = block_map; new.block_map = block_map;
new.pixel_type = pixel_type.ok_or(CziError::NoValidBlocks)?; new.pixel_type = pixel_type.ok_or(Error::CziNoValidBlocks)?;
Ok(new) Ok(new)
} }
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
Ok(path.as_ref().extension() == Some(std::ffi::OsStr::new("czi")))
}
fn metadata(&self) -> Result<Ome, Error> { fn metadata(&self) -> Result<Ome, Error> {
let mut ome = Ome::default(); let mut ome = Ome::default();
let xml = xmltree::Element::parse(self.metadata_xml()?.as_bytes())?; let xml = xmltree::Element::parse(self.metadata_xml()?.as_bytes())?;
@@ -1133,7 +1132,7 @@ impl Reader for CziReader {
PixelType::U64 => get_frame!(u64, 8), PixelType::U64 => get_frame!(u64, 8),
PixelType::I128 => get_frame!(i128, 16), PixelType::I128 => get_frame!(i128, 16),
PixelType::U128 => get_frame!(u128, 16), PixelType::U128 => get_frame!(u128, 16),
PixelType::F128 => get_frame!(f64, 8), PixelType::F128 => Err(Error::F128NotSupported),
} }
} }
@@ -1149,7 +1148,7 @@ impl Reader for CziReader {
self.position self.position
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
+64 -5
View File
@@ -156,6 +156,18 @@ impl Reader for TiffReader {
.unwrap_or(1) .unwrap_or(1)
== 2; == 2;
let image_description = reader.current_ifd().get_tag(Tag::ImageDescription);
let p_ndim = if n_samples > 1 { 3u8 } else { 2u8 };
let (size_c, size_t, size_z) = if let Ok(Value::Ascii(xml)) = image_description
&& xml.starts_with("<?xml")
{
let ome = Ome::from_xml(xml)?;
(
ome.image[0].pixels.size_c as usize,
ome.image[0].pixels.size_t as usize,
ome.image[0].pixels.size_z as usize,
)
} else {
let imagej_bytes = reader let imagej_bytes = reader
.current_ifd() .current_ifd()
.get_tag(Tag::Unknown(50839)) .get_tag(Tag::Unknown(50839))
@@ -166,7 +178,7 @@ impl Reader for TiffReader {
}; };
let metadata_str = String::from_utf8_lossy(&imagej_bytes); let metadata_str = String::from_utf8_lossy(&imagej_bytes);
let metadata_map = parse_imagej_metadata(&metadata_str); 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 { let size_c = if p_ndim == 3 {
n_samples n_samples
} else { } else {
@@ -198,6 +210,8 @@ impl Reader for TiffReader {
} else { } else {
size_z size_z
}; };
(size_c, size_t, size_z)
};
drop(reader); drop(reader);
new.shape = Shape { new.shape = Shape {
@@ -218,8 +232,32 @@ impl Reader for TiffReader {
Ok(new) Ok(new)
} }
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let mut file = File::open(path)?;
let mut sig = [0; 2];
file.read_exact(&mut sig)?;
if sig == [73, 73] {
file.read_exact(&mut sig)?;
Ok((sig[0] == 42) || (sig[0] == 43))
} else if sig == [77, 77] {
Ok((sig[1] == 42) || (sig[1] == 43))
} else {
Ok(false)
}
}
fn metadata(&self) -> Result<Ome, Error> { fn metadata(&self) -> Result<Ome, Error> {
let mut reader = self.get_reader()?; let mut reader = self.get_reader()?;
let image_description = reader.current_ifd().get_tag(Tag::ImageDescription);
if let Ok(Value::Ascii(xml)) = image_description
&& xml.starts_with("<?xml")
{
Ok(Ome::from_xml(xml)?)
} else {
let imagej_bytes = reader let imagej_bytes = reader
.current_ifd() .current_ifd()
.get_tag(Tag::Unknown(50839)) .get_tag(Tag::Unknown(50839))
@@ -354,6 +392,7 @@ impl Reader for TiffReader {
}); });
Ok(ome) Ok(ome)
} }
}
fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> { fn get_frame(&self, c: usize, z: usize, t: usize) -> Result<Frame, Error> {
let (page_idx, offset, stride) = if self.p_ndim == 3 { let (page_idx, offset, stride) = if self.p_ndim == 3 {
@@ -438,25 +477,43 @@ impl Reader for TiffReader {
fn position(&self) -> usize { fn position(&self) -> usize {
self.position self.position
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
fn pixel_type(&self) -> &PixelType { fn pixel_type(&self) -> &PixelType {
&self.pixel_type &self.pixel_type
} }
fn get_available_positions<P>(_path: P, _series: usize) -> Result<HashSet<usize>, Error> fn get_available_positions<P>(path: P, _series: usize) -> Result<HashSet<usize>, Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = path.as_ref();
if Self::can_open(path)? {
Ok(HashSet::from([0])) Ok(HashSet::from([0]))
} else {
Err(Error::InvalidReader(
"TiffReader".to_string(),
path.to_string_lossy().to_string(),
"it is not a tiff".to_string(),
))
}
} }
fn get_available_series<P>(_path: P) -> Result<HashSet<usize>, Error> fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = path.as_ref();
if Self::can_open(path)? {
Ok(HashSet::from([0])) Ok(HashSet::from([0]))
} else {
Err(Error::InvalidReader(
"TiffReader".to_string(),
path.to_string_lossy().to_string(),
"it is not a tiff".to_string(),
))
}
} }
} }
@@ -521,6 +578,8 @@ mod tests {
metadata_e: "tiff/1xp53-01-AP1.tiff", metadata_e: "tiff/1xp53-01-AP1.tiff",
metadata_f: "tiff/test.tif", metadata_f: "tiff/test.tif",
metadata_g: "tiff/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif", metadata_g: "tiff/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif",
metadata_h: "tiff/Wapl-AID_C46_13_tagRFPt_MCP_well1_video1_1_max.tif" metadata_h: "tiff/Wapl-AID_C46_13_tagRFPt_MCP_well1_video1_1_max.tif",
metadata_i: "tiff/R1_NRF1_Gtf3a500nM_REAL_20260731_25pct_500ms_001_MMStack_PosR1_NRF1_Gtf3a500nM_REAL_20260731_25pct_500ms_001.ome.tif"
metadata_j: "tiff/20260922_Pos0_max.tif"
} }
} }
+18 -2
View File
@@ -239,6 +239,13 @@ impl Reader for TiffSeqReader {
}) })
} }
fn can_open<P>(path: P) -> Result<bool, Error>
where
P: AsRef<Path>,
{
Ok(path.as_ref().is_dir())
}
fn metadata(&self) -> Result<Ome, Error> { fn metadata(&self) -> Result<Ome, Error> {
let mut ome = Ome::default(); let mut ome = Ome::default();
let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping()); let info = self.metadata_map.get("Info").and_then(|v| v.as_mapping());
@@ -572,7 +579,7 @@ impl Reader for TiffSeqReader {
self.position self.position
} }
fn shape(&self) -> &Shape { fn raw_shape(&self) -> &Shape {
&self.shape &self.shape
} }
@@ -580,11 +587,20 @@ impl Reader for TiffSeqReader {
&self.pixel_type &self.pixel_type
} }
fn get_available_positions<P>(_path: P, _series: usize) -> Result<HashSet<usize>, Error> fn get_available_positions<P>(path: P, _series: usize) -> Result<HashSet<usize>, Error>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = path.as_ref();
if Self::can_open(path)? {
Ok(HashSet::from([0])) Ok(HashSet::from([0]))
} else {
Err(Error::InvalidReader(
"TiffSeqReader".to_string(),
path.to_string_lossy().to_string(),
"not a tiff sequence".to_string(),
))
}
} }
fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error> fn get_available_series<P>(path: P) -> Result<HashSet<usize>, Error>
+1 -1
View File
@@ -178,7 +178,7 @@ where
PixelType::U64 => self.save_as_tiff_with_type::<u64, P>(path, options)?, PixelType::U64 => self.save_as_tiff_with_type::<u64, P>(path, options)?,
PixelType::I128 => self.save_as_tiff_with_type::<i64, P>(path, options)?, PixelType::I128 => self.save_as_tiff_with_type::<i64, P>(path, options)?,
PixelType::U128 => self.save_as_tiff_with_type::<u64, P>(path, options)?, PixelType::U128 => self.save_as_tiff_with_type::<u64, P>(path, options)?,
PixelType::F128 => self.save_as_tiff_with_type::<f64, P>(path, options)?, PixelType::F128 => Err(Error::F128NotSupported)?,
} }
Ok(()) Ok(())
+600 -74
View File
@@ -1,13 +1,19 @@
use crate::axes::Axis; use crate::axes::Axis;
use crate::error::Error; use crate::error::Error;
use crate::readers::{DynReader, Reader}; use crate::readers::{DynReader, Frame, PixelType, Reader};
use crate::view::View; use crate::view::View;
pub use image_registration::transform::Transform; use image_registration::{bspline::DefaultValue, filter, transform::Transform};
use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, s}; use ndarray::{Array2, Dimension, Ix2, Ix3, IxDyn, array, concatenate, s};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::hash::{Hash, Hasher};
use std::io::Seek; use std::io::Seek;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use tiffwrite::IJTiffFile;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
struct TransformOld { struct TransformOld {
@@ -26,16 +32,20 @@ impl TryFrom<HashMap<String, TransformOld>> for Transforms {
fn try_from(t: HashMap<String, TransformOld>) -> Result<Self, Error> { fn try_from(t: HashMap<String, TransformOld>) -> Result<Self, Error> {
Ok(Self { Ok(Self {
default_value: Default::default(),
interpolation_order: None,
channel: t channel: t
.into_iter() .into_iter()
.map(|(n, i)| { .map(|(n, i)| {
let center = i
.size
.iter()
.zip(i.center_of_rotation_point.iter())
.map(|(s, c)| c - s / 2.0 + 0.5)
.collect::<Vec<_>>();
Ok(( Ok((
n, n,
Transform::new_with_center( Transform::new_around_center(i.transform_parameters, &center)
i.transform_parameters,
i.center_of_rotation_point,
i.size.into_iter().map(|j| j as usize).collect(),
)
.with_dparameters(i.dtransform_parameters) .with_dparameters(i.dtransform_parameters)
.inverse()?, .inverse()?,
)) ))
@@ -46,14 +56,40 @@ impl TryFrom<HashMap<String, TransformOld>> for Transforms {
} }
} }
pub type DefaultValues = HashMap<String, DefaultValue>;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
pub struct Transforms { pub struct Transforms {
#[serde(default)]
pub(crate) interpolation_order: Option<usize>,
#[serde(default)]
pub(crate) default_value: DefaultValues,
/// a transform for each channel by channel name
#[serde(default)] #[serde(default)]
pub channel: HashMap<String, Transform<IxDyn>>, pub channel: HashMap<String, Transform<IxDyn>>,
/// a transform for each time point
#[serde(default)] #[serde(default)]
pub drift: Vec<Transform<IxDyn>>, pub drift: Vec<Transform<IxDyn>>,
} }
impl Hash for Transforms {
fn hash<H: Hasher>(&self, state: &mut H) {
let mut keys: Vec<_> = self.channel.keys().collect();
keys.sort();
for key in keys {
key.hash(state);
self.channel[key].hash(state);
}
self.drift.hash(state);
}
}
impl PartialEq<Transforms> for &Transforms {
fn eq(&self, other: &Transforms) -> bool {
**self == *other
}
}
impl Serialize for Transforms { impl Serialize for Transforms {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@@ -69,6 +105,8 @@ impl Serialize for Transforms {
#[derive(Serialize)] #[derive(Serialize)]
struct Binary<'a> { struct Binary<'a> {
interpolation_order: Option<usize>,
default_value: &'a DefaultValues,
channel: &'a HashMap<String, Transform<IxDyn>>, channel: &'a HashMap<String, Transform<IxDyn>>,
drift: &'a Vec<Transform<IxDyn>>, drift: &'a Vec<Transform<IxDyn>>,
} }
@@ -81,6 +119,8 @@ impl Serialize for Transforms {
.serialize(serializer) .serialize(serializer)
} else { } else {
Binary { Binary {
interpolation_order: self.interpolation_order,
default_value: &self.default_value,
channel: &self.channel, channel: &self.channel,
drift: &self.drift, drift: &self.drift,
} }
@@ -89,7 +129,31 @@ impl Serialize for Transforms {
} }
} }
/// the kind of corrections the transforms are meant to correct for
#[derive(Clone, Debug, PartialEq, Copy)]
pub enum TransformKind {
/// no tranforms
None,
/// correct warp between channels, ex: camera/filter misalignment, chromatic aberrations
Channel,
/// drift in the miscrocope between time points
Drift,
/// both warp between channels and drift
Both,
}
impl Transforms { impl Transforms {
/// the kind of corrections the transforms are meant to correct for
pub fn kind(&self) -> TransformKind {
match (self.channel.is_empty(), self.drift.is_empty()) {
(true, true) => TransformKind::None,
(false, true) => TransformKind::Channel,
(true, false) => TransformKind::Drift,
(false, false) => TransformKind::Both,
}
}
/// load the transforms from a yml file
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> { pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let mut file = std::fs::File::open(&path)?; let mut file = std::fs::File::open(&path)?;
Ok( Ok(
@@ -104,6 +168,7 @@ impl Transforms {
) )
} }
/// save the transforms to a yml file
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> { pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
let file = std::fs::OpenOptions::new() let file = std::fs::OpenOptions::new()
.write(true) .write(true)
@@ -113,57 +178,164 @@ impl Transforms {
Ok(yaml_serde::to_writer(file, self)?) Ok(yaml_serde::to_writer(file, self)?)
} }
pub fn calculate_channel_transforms_2d( /// helper function to eiter load transforms from file or calculate and save them
bead_files: &[&Path], pub fn calculate_transforms_from<P, Q, R>(
transform_file: Option<P>,
bead_files: Option<&[Q]>,
drift_file: Option<R>,
main_channel: Option<usize>,
default_transform: Option<Vec<f64>>,
) -> Result<Self, Error>
where
P: AsRef<Path>,
Q: AsRef<Path>,
R: AsRef<Path>,
{
if let Some(transform_file) = transform_file.as_ref()
&& let Ok(transforms) = Self::load(transform_file.as_ref().with_extension("yml"))
{
Ok(transforms)
} else {
let default_transform = default_transform.map(Transform::new);
let mut transforms = Self::default();
if let Some(bead_files) = bead_files {
let main_channel = main_channel.unwrap_or(0);
transforms.channel = Self::calculate_channel_transforms_2d(
bead_files,
main_channel,
default_transform,
)?
.into_iter()
.map(|(k, t)| (k, t.into_dyn()))
.collect();
};
if let Some(drift_file) = drift_file {
transforms.drift = Self::calculate_drift_transforms_2d(drift_file)?
.into_iter()
.map(|t| t.into_dyn())
.collect();
};
if let Some(transform_file) = transform_file {
let transform_file = transform_file.as_ref();
transforms.save(transform_file.with_extension("yml"))?;
if let Some(bead_files) = bead_files {
transforms.save_channel_transform_tiff(
transform_file.with_extension("tif"),
bead_files,
)?;
}
}
Ok(transforms)
}
}
/// save a tiff file with the uncorrected and corrected frames of the bead files side by side
pub fn save_channel_transform_tiff<P, Q>(&self, file: P, bead_files: &[Q]) -> Result<(), Error>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let mut transforms = self.clone();
transforms.drift = Vec::new();
let mut tif = IJTiffFile::new(file)?;
for (t, bead_file) in bead_files.iter().enumerate() {
let mut view = View::<_, DynReader>::from_path(bead_file)?.into_dyn();
let axes = view.axes();
if axes.contains(&Axis::Z) {
view = view.max_proj(Axis::Z)?;
}
if axes.contains(&Axis::T) {
view = view.max_proj(Axis::T)?;
}
let view_u = view.into_dimensionality::<Ix3>()?;
let view_t = view_u.with_transforms(transforms.clone());
for c in 0..view_u.shape().c {
let a = view_u.slice_cztyx(s![c, 0, 0, .., ..])?.as_array::<u16>()?;
let b = view_t.slice_cztyx(s![c, 0, 0, .., ..])?.as_array::<u16>()?;
let frame = concatenate(ndarray::Axis(1), &[a.view(), b.view()])?;
tif.save(frame, c, 0, t)?;
}
}
Ok(())
}
/// calculate drift transforms in yx using image file
pub fn calculate_drift_transforms_2d<P: AsRef<Path>>(
file: P,
) -> Result<Vec<Transform<Ix2>>, Error> {
View::<_, DynReader>::from_path(file)?.calculate_drift_transforms_2d()
}
/// calculate drift transforms in zyx using image file
pub fn calculate_drift_transforms_3d<P: AsRef<Path>>(
file: P,
) -> Result<Vec<Transform<Ix3>>, Error> {
View::<_, DynReader>::from_path(file)?.calculate_drift_transforms_3d()
}
/// calculate channel transforms in yx using image file
pub fn calculate_channel_transforms_2d<P: AsRef<Path>>(
bead_files: &[P],
main_channel: usize, main_channel: usize,
default_transform: Option<Transform<Ix2>>, default_transform: Option<Transform<Ix2>>,
) -> Result<HashMap<String, Transform<Ix2>>, Error> { ) -> Result<HashMap<String, Transform<Ix2>>, Error> {
// TODO: autodetect main channel
let mut transforms = Vec::new(); let mut transforms = Vec::new();
let default_transform = default_transform.unwrap_or_default(); let default_transform = default_transform.unwrap_or_default();
for file in bead_files { for file in bead_files {
let view = View::<_, DynReader>::from_path(file)?; let view = View::<_, DynReader>::from_path(file)?;
transforms.push(view.calculate_channel_transforms_2d(main_channel)?) transforms.push(view.calculate_channel_transforms_2d(main_channel)?)
} }
let channels = transforms let transforms = transforms.into_iter().flat_map(|map| map.into_iter()).fold(
.iter() HashMap::new(),
.flat_map(|t| t.keys()) |mut acc, (channel, transform)| {
.collect::<HashSet<_>>(); acc.entry(channel)
let mut average_transforms = HashMap::new(); .or_insert_with(Vec::new)
for channel in channels { .push(transform * &default_transform);
let (s, n) = transforms acc
.iter() },
.filter_map(|t| {
if t.contains_key(channel) {
Some((&t[channel] * &default_transform).matrix())
} else {
None
}
})
.fold((Array2::zeros((3, 3)), 0), |(s, n), t| (s + t, n + 1));
let matrix = s / (n as f64);
let (s, n) = transforms
.iter()
.filter_map(|t| {
if t.contains_key(channel) {
Some(((&t[channel] * &default_transform).matrix() - &matrix).powi(2))
} else {
None
}
})
.fold((Array2::zeros((3, 3)), 0), |(s, n), t| (s + t, n + 1));
let dmatrix = (s / n as f64).sqrt();
average_transforms.insert(
channel.clone(),
Transform::default()
.with_matrix(matrix.view())
.with_dmatrix(dmatrix.view()),
); );
Ok(transforms
.iter()
.map(|(channel, ts)| {
let n = ts.len();
let parameters = ts
.iter()
.fold(vec![0.0; 6], |mut acc, t| {
for (a, p) in acc.iter_mut().zip(&t.parameters) {
*a += p;
} }
Ok(average_transforms) acc
})
.iter()
.map(|i| i / n as f64)
.collect::<Vec<_>>();
let dparameters = if n == 1 {
ts[0].dparameters.clone()
} else {
ts.iter()
.fold(vec![0.0; 6], |mut acc, t| {
for ((a, p), dp) in acc.iter_mut().zip(&parameters).zip(&t.dparameters)
{
*a += (dp - p).powi(2);
}
acc
})
.iter()
.map(|i| (i / n as f64).sqrt())
.collect::<Vec<_>>()
};
(
channel.to_string(),
Transform::new(parameters).with_dparameters(dparameters),
)
})
.collect())
} }
pub fn calculate_channel_transforms_3d( /// calculate channel transforms in zyx using image file
bead_files: &[&Path], pub fn calculate_channel_transforms_3d<P: AsRef<Path>>(
bead_files: &[P],
main_channel: usize, main_channel: usize,
default_transform: Option<Transform<Ix3>>, default_transform: Option<Transform<Ix3>>,
) -> Result<HashMap<String, Transform<Ix3>>, Error> { ) -> Result<HashMap<String, Transform<Ix3>>, Error> {
@@ -210,29 +382,56 @@ impl Transforms {
} }
Ok(average_transforms) Ok(average_transforms)
} }
/// get the combination of channel and drift transforms for a certain channel and time point
pub fn get_transform(
&self,
channel: &str,
t: usize,
) -> Result<Option<Cow<'_, Transform<IxDyn>>>, Error> {
match (self.channel.get(channel), self.drift.get(t)) {
(Some(channel), Some(drift)) => match (channel.ndim(), drift.ndim()) {
(2, 2) | (3, 3) => Ok(Some(Cow::Owned(drift * channel))),
(2, 3) => Ok(Some(Cow::Owned(drift.add_dimension(0) * channel))),
(3, 2) => Ok(Some(Cow::Owned(drift * channel.add_dimension(0)))),
(2 | 3, n) | (n, _) => Err(Error::TransformationNotImplemented(n)),
},
(Some(channel), None) => Ok(Some(Cow::Borrowed(channel))),
(None, Some(drift)) => Ok(Some(Cow::Borrowed(drift))),
(None, None) => Ok(None),
}
}
} }
impl<D: Dimension, R: Reader> View<D, R> { impl<D: Dimension, R: Reader> View<D, R> {
#[cfg(feature = "transforms")] pub fn get_transforms(&self) -> &Transforms {
pub(crate) fn get_transforms(&self) -> &Transforms {
&self.transforms &self.transforms
} }
#[cfg(feature = "transforms")] pub fn set_transforms(&mut self, transforms: Transforms) {
pub(crate) fn set_transforms(&mut self, transforms: Transforms) {
self.transforms = transforms; self.transforms = transforms;
} }
/// apply transforms to a new view
pub fn with_transforms(&self, transforms: Transforms) -> Self {
let mut new = self.clone();
new.transforms = transforms;
new
}
/// load the transforms from a yml file and apply to a new view
pub fn with_transforms_from_yaml(mut self, path: &Path) -> Result<Self, Error> { pub fn with_transforms_from_yaml(mut self, path: &Path) -> Result<Self, Error> {
self.set_transforms(Transforms::load(path)?); self.set_transforms(Transforms::load(path)?);
Ok(self) Ok(self)
} }
/// load the transforms from a yml file and apply to the view
pub fn load_transforms_from_yaml(&mut self, path: &Path) -> Result<(), Error> { pub fn load_transforms_from_yaml(&mut self, path: &Path) -> Result<(), Error> {
self.set_transforms(Transforms::load(path)?); self.set_transforms(Transforms::load(path)?);
Ok(()) Ok(())
} }
/// calculate channel transforms in yx using this image view
pub fn calculate_channel_transforms_2d( pub fn calculate_channel_transforms_2d(
&self, &self,
main_channel: usize, main_channel: usize,
@@ -242,23 +441,13 @@ impl<D: Dimension, R: Reader> View<D, R> {
.max_proj(Axis::Z)? .max_proj(Axis::Z)?
.as_array::<f64>()?; .as_array::<f64>()?;
let mut transforms = HashMap::new(); let mut transforms = HashMap::new();
let shape = vec![self.shape().y, self.shape().x];
let center = shape
.iter()
.map(|&i| (i as f64 - 1.0) / 2.0)
.collect::<Vec<_>>();
for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() { for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() {
let name = metadata let name = metadata
.name .name
.clone() .clone()
.unwrap_or_else(|| format!("channel{}", channel)); .unwrap_or_else(|| format!("channel{}", channel));
if channel == main_channel { if channel == main_channel {
transforms.insert( transforms.insert(name, Transform::default());
name,
Transform::default()
.with_shape(shape.clone())
.with_center(center.clone()),
);
} else { } else {
let max = self let max = self
.slice_cztyx(s![channel, .., 0, .., ..])? .slice_cztyx(s![channel, .., 0, .., ..])?
@@ -273,6 +462,7 @@ impl<D: Dimension, R: Reader> View<D, R> {
Ok(transforms) Ok(transforms)
} }
/// calculate channel transforms in zyx using this image view
pub fn calculate_channel_transforms_3d( pub fn calculate_channel_transforms_3d(
&self, &self,
main_channel: usize, main_channel: usize,
@@ -281,23 +471,13 @@ impl<D: Dimension, R: Reader> View<D, R> {
.slice_cztyx(s![main_channel, .., 0, .., ..])? .slice_cztyx(s![main_channel, .., 0, .., ..])?
.as_array::<f64>()?; .as_array::<f64>()?;
let mut transforms = HashMap::new(); let mut transforms = HashMap::new();
let shape = vec![self.shape().z, self.shape().y, self.shape().x];
let center = shape
.iter()
.map(|&i| (i as f64 - 1.0) / 2.0)
.collect::<Vec<_>>();
for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() { for (channel, metadata) in self.metadata()?.image[0].pixels.channel.iter().enumerate() {
let name = metadata let name = metadata
.name .name
.clone() .clone()
.unwrap_or_else(|| format!("channel{}", channel)); .unwrap_or_else(|| format!("channel{}", channel));
if channel == main_channel { if channel == main_channel {
transforms.insert( transforms.insert(name, Transform::default());
name,
Transform::default()
.with_shape(shape.clone())
.with_center(center.clone()),
);
} else { } else {
let max = self let max = self
.slice_cztyx(s![channel, .., 0, .., ..])? .slice_cztyx(s![channel, .., 0, .., ..])?
@@ -311,18 +491,295 @@ impl<D: Dimension, R: Reader> View<D, R> {
Ok(transforms) Ok(transforms)
} }
/// calculate drift transforms in yx using this image view
pub fn calculate_drift_transforms_2d(&self) -> Result<Vec<Transform<Ix2>>, Error> { pub fn calculate_drift_transforms_2d(&self) -> Result<Vec<Transform<Ix2>>, Error> {
let mut view = self.clone().into_dyn();
let axes = view.axes();
if axes.contains(&Axis::Z) {
view = view.max_proj(Axis::Z)?;
}
if axes.contains(&Axis::C) {
view = view.max_proj(Axis::C)?;
}
view = view.permute_axes(&[Axis::T, Axis::Y, Axis::X])?;
let n = view.shape().t;
let m = (n as f64).sqrt().round() as usize;
let t_groups = (0..=m)
.map(|i| (((i * n) as f64) / (m as f64)).round() as usize)
.collect::<Vec<_>>();
let t_keys = t_groups
.array_windows()
.map(|[start, end]| (start + end) / 2)
.collect::<Vec<_>>();
let t_pairs = t_keys
.iter()
.zip(t_groups.array_windows())
.flat_map(|(t_key, [start, end])| (*start..*end).map(|i| (*t_key, i)))
.chain(t_keys.array_windows().map(|[start, end]| (*start, *end)))
.collect::<Vec<_>>();
let fmaxz_keys = t_keys
.iter()
.map(|t| {
Ok((
t,
filter::gaussian_smooth(
&view.slice(s![*t, .., ..])?.as_array::<f64>()?,
&[5.0; 2],
)?,
))
})
.collect::<Result<HashMap<_, _>, Error>>()?;
let shifts = t_pairs
.into_par_iter()
.map(|(t_key, t)| {
if t_key == t {
Ok(Transform::<Ix2>::from_translation(&vec![0.0; view.ndim()]))
} else {
if let Some(fmaxz) = fmaxz_keys.get(&t_key) {
Ok(Transform::<Ix2>::register_translation(
fmaxz_keys[&t_key].view(),
fmaxz,
)?)
} else {
let fmaxz = filter::gaussian_smooth(
&view.slice(s![t, .., ..])?.as_array::<f64>()?,
&[5.0; 2],
)?;
Ok(Transform::<Ix2>::register_translation(
fmaxz_keys[&t_key].view(),
fmaxz.view(),
)?)
}
}
})
.collect::<Result<Vec<_>, Error>>()?;
let mut t = Transform::<Ix2>::from_translation(&[0.0; 2]);
let mut key_shifts = vec![t.clone()];
key_shifts.extend(shifts.iter().skip(n).map(|i| {
t = i * &t;
t.clone()
}));
Ok(key_shifts
.iter()
.zip(t_groups.array_windows())
.flat_map(|(s, [start, end])| {
let mut t = s.clone();
(*start..*end)
.map(|i| {
t = &shifts[i] * &t;
t.clone()
})
.collect::<Vec<_>>()
})
.collect())
}
/// calculate drift transforms in zyx using this image view
pub fn calculate_drift_transforms_3d(&self) -> Result<Vec<Transform<Ix3>>, Error> {
todo!() todo!()
} }
pub fn calculate_drift_transforms_3d(&self) -> Result<Vec<Transform<Ix3>>, Error> { /// retrieve a frame and warp it according to the transform
todo!() pub(crate) fn get_transformed_frame(
&self,
c: usize,
z: usize,
t: usize,
transform: &Transform<IxDyn>,
order: usize,
default_value: &DefaultValue,
) -> Result<Arc<Frame>, Error> {
match transform.ndim() {
2 => {
let tr = transform.clone().into_dimensionality::<Ix2>()?;
macro_rules! interpolate {
($array:expr ,$var:ident, bool) => {
Ok(Arc::from(Frame::$var(
tr.interpolate_par_order(
&$array.mapv(|i| if i { 1.0 } else { 0.0 }),
order,
default_value,
)?
.mapv(|i| i > 0.5),
)))
};
($array:expr ,$var:ident, f64) => {
Ok(Arc::from(Frame::$var(tr.interpolate_par_order(
$array,
order,
default_value,
)?)))
};
($array:expr ,$var:ident, $t:ty) => {
Ok(Arc::from(Frame::$var(
tr.interpolate_par_order($array, order, default_value)?
.mapv(|i| i as $t),
)))
};
}
match self.get_cached_frame(c, z, t, None)?.as_ref() {
Frame::Bool(frame) => interpolate!(frame, Bool, bool),
Frame::I8(frame) => interpolate!(frame, I8, i8),
Frame::U8(frame) => interpolate!(frame, U8, u8),
Frame::I16(frame) => interpolate!(frame, I16, i16),
Frame::U16(frame) => interpolate!(frame, U16, u16),
Frame::I32(frame) => interpolate!(frame, I32, i32),
Frame::U32(frame) => interpolate!(frame, U32, u32),
Frame::U64(frame) => interpolate!(frame, U64, u64),
Frame::I64(frame) => interpolate!(frame, I64, i64),
Frame::I128(frame) => interpolate!(frame, I128, i128),
Frame::U128(frame) => interpolate!(frame, U128, u128),
Frame::F32(frame) => interpolate!(frame, F32, f32),
Frame::F64(frame) => interpolate!(frame, F64, f64),
}
}
3 => {
let tr = transform.clone().into_dimensionality::<Ix3>()?;
let shape = self.shape();
let center = shape.iter().map(|&i| (i as f64) / 2.0).collect::<Vec<_>>();
let zf = z as f64;
let yf = shape.y as f64 - 1.0;
let xf = shape.x as f64 - 1.0;
let corners = array![[zf, 0.0, 0.0], [zf, 0.0, xf], [zf, yf, 0.0], [zf, yf, xf]];
let extent = tr.transform_points(&corners, &center)?;
let z_extent = extent.slice(s![.., 2]);
let z_min = (z_extent
.iter()
.min_by(|a, b| a.total_cmp(b))
.unwrap()
.floor() as isize
- 2)
.max(0) as usize;
let z_max = (z_extent
.iter()
.max_by(|a, b| a.total_cmp(b))
.unwrap()
.ceil() as isize
+ 2)
.min(shape.z as isize - 1)
.max(0) as usize;
let frames = (z_min..=z_max)
.map(|zi| self.get_cached_frame(c, zi, t, None))
.collect::<Result<Vec<_>, Error>>()?;
// TODO; check these translations
let u0 = tr.as_translation(&[z_min as f64, 0.0, 0.0]);
let u1 = tr.as_translation(&[-(z_min as f64), 0.0, 0.0]);
let tr = u0 * tr * u1;
match frames.first().map(|f| f.as_ref()) {
Some(first) => {
macro_rules! interpolate {
($var:ident, bool) => {{
let views = frames
.iter()
.map(|f| match f.as_ref() {
Frame::$var(a) => Ok(a.view()),
_ => unreachable!(),
})
.collect::<Result<Vec<_>, Error>>()?;
let stack = ndarray::stack(ndarray::Axis(0), &views)?;
let transformed = tr.interpolate_par_order(
&stack.mapv(|i| if i { 1.0 } else { 0.0 }),
order,
default_value,
)?;
Ok(Arc::from(Frame::$var(
transformed.slice(s![0, .., ..]).mapv(|i| i > 0.5),
)))
}};
($var:ident, f64) => {{
let views = frames
.iter()
.map(|f| match f.as_ref() {
Frame::$var(a) => Ok(a.view()),
_ => unreachable!(),
})
.collect::<Result<Vec<_>, Error>>()?;
let stack = ndarray::stack(ndarray::Axis(0), &views)?;
let transformed =
tr.interpolate_par_order(&stack, order, default_value)?;
Ok(Arc::from(Frame::$var(
transformed.slice(s![0, .., ..]).to_owned(),
)))
}};
($var:ident, $t:ty) => {{
let views = frames
.iter()
.map(|f| match f.as_ref() {
Frame::$var(a) => Ok(a.view()),
_ => unreachable!(),
})
.collect::<Result<Vec<_>, Error>>()?;
let stack = ndarray::stack(ndarray::Axis(0), &views)?;
let transformed =
tr.interpolate_par_order(&stack, order, default_value)?;
Ok(Arc::from(Frame::$var(
transformed.slice(s![0, .., ..]).mapv(|i| i as $t),
)))
}};
}
match first {
Frame::Bool(_) => interpolate!(Bool, bool),
Frame::I8(_) => interpolate!(I8, i8),
Frame::U8(_) => interpolate!(U8, u8),
Frame::I16(_) => interpolate!(I16, i16),
Frame::U16(_) => interpolate!(U16, u16),
Frame::I32(_) => interpolate!(I32, i32),
Frame::U32(_) => interpolate!(U32, u32),
Frame::F32(_) => interpolate!(F32, f32),
Frame::F64(_) => interpolate!(F64, f64),
Frame::I64(_) => interpolate!(I64, i64),
Frame::U64(_) => interpolate!(U64, u64),
Frame::I128(_) => interpolate!(I128, i128),
Frame::U128(_) => interpolate!(U128, u128),
}
}
None => {
let shape = self.shape();
Ok(Arc::new(match self.pixel_type() {
PixelType::Bool => Frame::Bool(Array2::default((shape.y, shape.x))),
PixelType::I8 => Frame::I8(Array2::zeros((shape.y, shape.x))),
PixelType::U8 => Frame::U8(Array2::zeros((shape.y, shape.x))),
PixelType::I16 => Frame::I16(Array2::zeros((shape.y, shape.x))),
PixelType::U16 => Frame::U16(Array2::zeros((shape.y, shape.x))),
PixelType::I32 => Frame::I32(Array2::zeros((shape.y, shape.x))),
PixelType::U32 => Frame::U32(Array2::zeros((shape.y, shape.x))),
PixelType::F32 => Frame::F32(Array2::zeros((shape.y, shape.x))),
PixelType::F64 => Frame::F64(Array2::zeros((shape.y, shape.x))),
PixelType::I64 => Frame::I64(Array2::zeros((shape.y, shape.x))),
PixelType::U64 => Frame::U64(Array2::zeros((shape.y, shape.x))),
PixelType::I128 => Frame::I128(Array2::zeros((shape.y, shape.x))),
PixelType::U128 => Frame::U128(Array2::zeros((shape.y, shape.x))),
PixelType::F128 => Err(Error::F128NotSupported)?,
}))
}
}
}
d => Err(Error::TransformationNotImplemented(d)),
}
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::axes::Axis;
use crate::error::Error;
use crate::readers::{DynReader, Reader};
use crate::transforms::Transforms; use crate::transforms::Transforms;
use image_registration::transform::Transform;
use ndarray::s;
use std::collections::HashMap;
fn open(file: &str) -> Result<DynReader, Error> {
let path = std::env::current_dir()?
.join("tests")
.join("files")
.join(file);
DynReader::new(&path, 0, 0)
}
#[test] #[test]
fn test_transforms() -> Result<(), Box<dyn std::error::Error>> { fn test_transforms() -> Result<(), Box<dyn std::error::Error>> {
@@ -348,4 +805,73 @@ mod tests {
assert_eq!(t.channel["TV2"].ndim(), 2); assert_eq!(t.channel["TV2"].ndim(), 2);
Ok(()) Ok(())
} }
#[test]
fn transforms_as_array() -> Result<(), Box<dyn std::error::Error>> {
let file = "czi/beads_2020_08_05__11_43_07.czi";
let reader = open(file)?;
let view = reader.view();
let mut channel = HashMap::new();
channel.insert(
"TV1".to_string(),
Transform::new(vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
);
channel.insert(
"TV2".to_string(),
Transform::new(vec![
0.9995868322142533,
0.012140955373934502,
0.012592501668218145,
0.8571690967587099,
-5.270859902118885,
-5.81118547158953,
]),
);
let view_t = view.with_transforms(Transforms {
default_value: Default::default(),
interpolation_order: None,
channel,
drift: Vec::new(),
});
let s_a = view.max_proj(Axis::Z)?.slice_cztyx(s![1, 0, .., .., ..])?;
println!(
"s_a: {:?}, {:?}, {:?}",
s_a.get_slice(),
s_a.get_axes(),
s_a.get_operations()
);
let s_b = view_t
.max_proj(Axis::Z)?
.slice_cztyx(s![1, 0, .., .., ..])?;
println!(
"s_b: {:?}, {:?}, {:?}",
s_b.get_slice(),
s_b.get_axes(),
s_b.get_operations()
);
let a = s_a.as_array::<u16>()?;
let b = s_b.as_array::<u16>()?;
assert_ne!(a, b);
// let a = view.slice_cztyx(s![1, 0, 0, .., ..])?.as_array::<u16>()?;
// let b = view_t.slice_cztyx(s![1, 0, 0, .., ..])?.as_array::<u16>()?;
// assert_ne!(a, b);
//
// let a = view
// .max_proj(Axis::Z)?
// .slice(s![1, 0, .., ..])?
// .as_array::<u16>()?;
// let b = view_t
// .max_proj(Axis::Z)?
// .slice(s![1, 0, .., ..])?
// .as_array::<u16>()?;
// assert_ne!(a, b);
Ok(())
}
} }
+604 -147
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,12 @@
{
"Channels": [{
"Name": "mScarlet_GFP-mSc-filter",
"DisplayMode": 1,
"HistogramMax": -1,
"Max": 523,
"Gamma": 1,
"Min": 352,
"Color": -1
}],
"Comments": {"Summary": "5-GFP-mScarlet/GFP/OD2.0/100x/1x/2x2binning/15s_interval/{-2,2,0/5zstack}/autofocus/mScarlet_GFP-mSc-filter/500ms/100% OR: GFP 20%/exp150ms"}
}
+3 -1
View File
@@ -26,8 +26,10 @@ def image(array):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"s", "s",
list(
combinations_with_replacement( combinations_with_replacement(
(0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1)), 5 (0, -1, 1, slice(None), slice(0, 1), slice(-1, 0), slice(1, 1), slice(1, 2)), 5
)
), ),
) )
def test_slicing(s, image, array): def test_slicing(s, image, array):
+2
View File
@@ -26,6 +26,7 @@ def image(array):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fun_and_axis", "fun_and_axis",
list(
product( product(
( (
np.sum, np.sum,
@@ -44,6 +45,7 @@ def image(array):
np.nanstd, np.nanstd,
), ),
(None, 0, 1, 2, 3, 4), (None, 0, 1, 2, 3, 4),
)
), ),
) )
def test_ufuncs(fun_and_axis, image, array): def test_ufuncs(fun_and_axis, image, array):