From 19c17a798d24b76a79dc52b835346066adf4a88a Mon Sep 17 00:00:00 2001 From: Wim Pomp Date: Thu, 17 Oct 2024 13:45:32 +0200 Subject: [PATCH] - python: remove deprecated shape argument - rust: make frame argument to save more generic --- .github/workflows/{wheels.yml => publish.yml} | 60 +++++++++++++-- Cargo.toml | 2 +- README.md | 73 +++++++++++-------- py/tiffwrite/__init__.py | 13 ++-- src/lib.rs | 44 +++++------ src/main.rs | 2 +- 6 files changed, 128 insertions(+), 66 deletions(-) rename .github/workflows/{wheels.yml => publish.yml} (63%) diff --git a/.github/workflows/wheels.yml b/.github/workflows/publish.yml similarity index 63% rename from .github/workflows/wheels.yml rename to .github/workflows/publish.yml index f8bc51b..a7e930d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: Wheels +name: Publish on: [push, pull_request, workflow_call] @@ -6,10 +6,10 @@ permissions: contents: read jobs: - wheels_pytest: + publish_pytest: uses: ./.github/workflows/pytest.yml linux: - needs: [ wheels_pytest ] + needs: [ publish_pytest ] runs-on: ${{ matrix.platform.runner }} strategy: matrix: @@ -45,7 +45,7 @@ jobs: path: dist windows: - needs: [ wheels_pytest ] + needs: [ publish_pytest ] runs-on: ${{ matrix.platform.runner }} strategy: matrix: @@ -73,7 +73,7 @@ jobs: path: dist macos: - needs: [ wheels_pytest ] + needs: [ publish_pytest ] runs-on: ${{ matrix.platform.runner }} strategy: matrix: @@ -100,7 +100,7 @@ jobs: path: dist sdist: - needs: [ wheels_pytest ] + needs: [ publish_pytest ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -128,3 +128,51 @@ jobs: with: command: upload args: --non-interactive --skip-existing wheels-*/* + + crates_io_publish: + name: Publish (crates.io) + needs: [ publish_pytest ] + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - name: cargo-release Cache + id: cargo_release_cache + uses: actions/cache@v3 + with: + path: ~/.cargo/bin/cargo-release + key: ${{ runner.os }}-cargo-release + + - run: cargo install cargo-release + if: steps.cargo_release_cache.outputs.cache-hit != 'true' + + - name: cargo login + run: cargo login ${{ secrets.CRATES_IO_API_TOKEN }} + + # allow-branch HEAD is because GitHub actions switches + # to the tag while building, which is a detached head + + # Publishing is currently messy, because: + # + # * `peace_rt_model_core` exports `NativeError` or `WebError` depending on the target. + # * `peace_rt_model_web` fails to build when publishing the workspace for a native target. + # * `peace_rt_model_web` still needs its dependencies to be published before it can be + # published. + # * `peace_rt_model_hack` needs `peace_rt_model_web` to be published before it can be + # published. + # + # We *could* pass through `--no-verify` so `cargo` doesn't build the crate before publishing, + # which is reasonable, since this job only runs after the Linux, Windows, and WASM builds + # have passed. + - name: "cargo release publish" + run: |- + cargo release \ + publish \ + --workspace \ + --all-features \ + --allow-branch HEAD \ + --no-confirm \ + --no-verify \ + --execute diff --git a/Cargo.toml b/Cargo.toml index 8f5ccc1..5be8341 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiffwrite" -version = "2024.10.7" +version = "2024.10.8" edition = "2021" authors = ["Wim Pomp "] license = "GPL-3.0-or-later" diff --git a/README.md b/README.md index 308162a..7db0fc7 100644 --- a/README.md +++ b/README.md @@ -14,36 +14,35 @@ makes that very hard anyway. - Colormaps - Extra tags, globally or frame dependent. +# Python ## Installation - pip install tiffwrite +```pip install tiffwrite``` + or -- install [rust](https://rustup.rs/) +- install [rust](https://rustup.rs/) +- ``` pip install tiffwrite@git+https://github.com/wimpomp/tiffwrite ``` - - pip install tiffwrite@git+https://github.com/wimpomp/tiffwrite - -# Usage -## Write an image stack +## Usage +### Write an image stack tiffwrite(file, data, axes='TZCXY', dtype=None, bar=False, *args, **kwargs) - file: string; filename of the new tiff file. - data: 2 to 5D numpy array in one of these datatypes: (u)int8, (u)int16, float32. - axes: string; order of dimensions in data, default: TZCXY for 5D, ZCXY for 4D, CXY for 3D, XY for 2D data. - dtype: string; cast data to dtype before saving, only (u)int8, (u)int16 and float32 are supported. -- bar: bool; whether or not to show a progress bar. +- bar: bool; whether to show a progress bar. - args, kwargs: arguments to be passed to IJTiffFile, see below. -## Write one frame at a time - with IJTiffFile(file, shape, dtype='uint16', colors=None, colormap=None, pxsize=None, deltaz=None, +### Write one frame at a time + with IJTiffFile(file, dtype='uint16', colors=None, colormap=None, pxsize=None, deltaz=None, timeinterval=None, **extratags) as tif: some loop: tif.save(frame, c, z, t) -- file: string; filename of the new tiff file. -- shape: iterable; shape (C, Z, T) of data to be written in file. -- dtype: string; cast data to dtype before saving, only (u)int8, (u)int16 and float32 are supported. +- path: string; path to the new tiff file. +- dtype: string; cast data to dtype before saving, only (u)int8, (u)int16 and float32 are supported by Fiji. - colors: iterable of strings; one color per channel, valid colors (also html) are defined in matplotlib.colors. Without colormap BioFormats will set the colors in this order: rgbwcmy. Note that the color green is dark, the usual green is named 'lime' here. @@ -51,22 +50,24 @@ or - pxsize: float; pixel size im um. - deltaz: float; z slice interval in um. - timeinterval: float; time between frames in seconds. -- extratags: other tags to be saved, example: Artist='John Doe', Tag4567=[400, 500] or - Copyright=Tag('ascii', 'Made by me'). See tiff_tag_registry.items(). +- compression: int; zstd compression level: -7 to 22. +- comment: str; comment to be saved in tif +- extratags: Sequence\[Tag\]; other tags to be saved, example: Tag.ascii(315, 'John Doe') or Tag.ascii(33432, 'Made by me'). + - frame: 2D numpy array with data. - c, z, t: int; channel, z, time coordinates of the frame. -# Examples -## Write an image stack +## Examples +### Write an image stack from tiffwrite import tiffwrite import numpy as np image = np.random.randint(0, 255, (5, 3, 64, 64), 'uint16') tiffwrite('file.tif', image, 'TCXY') -## Write one frame at a time +### Write one frame at a time from tiffwrite import IJTiffFile import numpy as np @@ -76,20 +77,34 @@ or for t in range(10): tif.save(np.random.randint(0, 10, (32, 32)), c, z, t) -## Saving multiple tiffs simultaneously +### Saving multiple tiffs simultaneously from tiffwrite import IJTiffFile import numpy as np - - shape = (3, 5, 10) # channels, z, time + with IJTiffFile('fileA.tif') as tif_a, IJTiffFile('fileB.tif') as tif_b: - for c in range(shape[0]): - for z in range(shape[1]): - for t in range(shape[2]): + for c in range(3): + for z in range(5): + for t in range(10): tif_a.save(np.random.randint(0, 10, (32, 32)), c, z, t) tif_b.save(np.random.randint(0, 10, (32, 32)), c, z, t) -## Tricks & tips -- The order of feeding frames to IJTiffFile is unimportant, IJTiffFile will order the ifd's such that the file will -be opened as a correctly ordered hyperstack. -- Using the colormap parameter you can make ImageJ open the file and apply the colormap. colormap='glasbey' is very -useful. + +# Rust + use ndarray::Array2; + use tiffwrite::IJTiffFile; + + { // f will be closed when f goes out of scope + let mut f = IJTiffFile::new("file.tif")?; + for c in 0..3 { + for z in 0..5 { + for t in 0..10 { + let arr = Array2::::zeros((100, 100)); + f.save(&arr, c, z, t)?; + } + } + } + } + +# Tricks & tips +- The order of feeding frames to IJTiffFile is unimportant, IJTiffFile will order the ifd's such that the file will be opened as a correctly ordered hyperstack. +- Using the colormap parameter you can make ImageJ open the file and apply the colormap. colormap='glasbey' is very useful. diff --git a/py/tiffwrite/__init__.py b/py/tiffwrite/__init__.py index 77215d0..38eafb9 100644 --- a/py/tiffwrite/__init__.py +++ b/py/tiffwrite/__init__.py @@ -36,26 +36,26 @@ class TiffWriteWarning(UserWarning): class IJTiffFile(rs.IJTiffFile): """ Writes a tiff file in a format that the BioFormats reader in Fiji understands. Zstd compression is done in parallel using Rust. - file: filename of the new tiff file - shape: not used anymore + path: path to the new tiff file dtype: datatype to use when saving to tiff colors: a tuple with a color per channel, chosen from matplotlib.colors, html colors are also possible colormap: name of a colormap from colorcet pxsize: pixel size in um deltaz: z slice interval in um timeinterval: time between frames in seconds + compression: zstd compression level: -7 to 22. + comment: comment to be saved in tif extratags: other tags to be saved, example: (Tag.ascii(315, 'John Doe'), Tag.bytes(4567, [400, 500]) or (Tag.ascii(33432, 'Made by me'),). """ def __new__(cls, path: str | Path, *args, **kwargs) -> IJTiffFile: return super().__new__(cls, str(path)) - def __init__(self, path: str | Path, shape: tuple[int, int, int] = None, dtype: DTypeLike = 'uint16', + def __init__(self, path: str | Path, *, dtype: DTypeLike = 'uint16', colors: Sequence[str] = None, colormap: str = None, pxsize: float = None, deltaz: float = None, timeinterval: float = None, compression: int = None, comment: str = None, extratags: Sequence[Tag] = None) -> None: self.path = Path(path) - self.shape = shape self.dtype = np.dtype(dtype) if compression is not None: if isinstance(compression, Sequence): @@ -79,9 +79,6 @@ class IJTiffFile(rs.IJTiffFile): if self.dtype.itemsize == 1 and colors is not None: warn('Fiji will not interpret colors saved in an (u)int8 tif, save as (u)int16 instead.', TiffWriteWarning, stacklevel=2) - if shape is not None: - warn('Providing shape is not needed anymore, the argument will be removed in the future.', - DeprecationWarning, stacklevel=2) if colors is not None and colormap is not None: warn('Cannot have colors and colormap simultaneously.', TiffWriteWarning, stacklevel=2) @@ -176,7 +173,7 @@ def tiffwrite(file: str | Path, data: np.ndarray, axes: str = 'TZCXY', dtype: DT try: from parfor import ParPool, Task - from abc import abstractmethod, ABCMeta + from abc import ABCMeta, abstractmethod from functools import wraps diff --git a/src/lib.rs b/src/lib.rs index d0d6f7a..5752ac8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ mod py; use anyhow::Result; use chrono::Utc; -use ndarray::{s, Array2, ArrayView2}; +use ndarray::{s, ArcArray2, Array2, ArrayView2, AsArray, Ix2}; use num::{traits::ToBytes, Complex, FromPrimitive, Rational32, Zero}; use rayon::prelude::*; use std::collections::HashSet; @@ -148,7 +148,8 @@ impl Tag { pub fn long(code: u16, value: &Vec) -> Self { Tag::new( code, - value.into_iter() + value + .into_iter() .map(|x| x.to_le_bytes()) .flatten() .collect(), @@ -253,7 +254,11 @@ impl Tag { pub fn ifd(code: u16, value: &Vec) -> Self { Tag::new( code, - value.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), + value + .into_iter() + .map(|x| x.to_le_bytes()) + .flatten() + .collect(), 13, ) } @@ -508,6 +513,8 @@ impl Drop for IJTiffFile { } impl IJTiffFile { + + /// create new tifffile from path string pub fn new(path: &str) -> Result { let mut file = OpenOptions::new() .create(true) @@ -535,10 +542,12 @@ impl IJTiffFile { }) } + /// set zstd compression level: -7 ..= 22 pub fn set_compression_level(&mut self, compression_level: i32) { self.compression_level = compression_level.max(-7).min(22); } + /// to be saved in description tag (270) pub fn description(&self, c_size: usize, z_size: usize, t_size: usize) -> String { let mut desc: String = String::from("ImageJ=1.11a"); if let Colors::None = self.colors { @@ -632,20 +641,12 @@ impl IJTiffFile { } } - pub fn save(&mut self, frame: ArrayView2, c: usize, z: usize, t: usize) -> Result<()> + pub fn save<'a, A, T>(&mut self, frame: A, c: usize, z: usize, t: usize) -> Result<()> where - T: Bytes + Clone + Send + Zero + 'static, + A: AsArray<'a, T, Ix2>, + T: Bytes + Clone + Send + Sync + Zero + 'static, { - self.compress_frame(frame.reversed_axes(), c, z, t)?; - Ok(()) - } - - fn compress_frame(&mut self, frame: ArrayView2, - c: usize, z: usize, t: usize) -> Result<()> - where - T: Bytes + Clone + Send + Zero + 'static, - { - fn compress(frame: Array2, compression_level: i32) -> CompressedFrame + fn compress(frame: ArcArray2, compression_level: i32) -> CompressedFrame where T: Bytes + Clone + Zero, { @@ -658,7 +659,7 @@ impl IJTiffFile { ) .max(16) .min(1024); - let tiles = IJTiffFile::tile(frame.view().reversed_axes(), tile_size); + let tiles = IJTiffFile::tile(frame.view(), tile_size); let byte_tiles: Vec> = tiles .into_iter() .map(|tile| tile.map(|x| x.bytes()).into_iter().flatten().collect()) @@ -691,7 +692,7 @@ impl IJTiffFile { sleep(Duration::from_millis(100)); } let compression_level = self.compression_level; - let frame = frame.to_owned(); + let frame = frame.into().to_shared(); self.threads.insert( (c, z, t), thread::spawn(move || compress(frame, compression_level)), @@ -906,12 +907,13 @@ impl IJTiffFile { let (c, z, t) = self.get_czt(*frame_number, *channel, c_size, z_size); println!("c: {c}, z: {z}, t: {t}") } - println!("Either you forgot them, \ - or an error occurred and the tif file was closed prematurely.") + println!( + "Either you forgot them, \ + or an error occurred and the tif file was closed prematurely." + ) } } - self.file - .seek(SeekFrom::Start(where_to_write_next_ifd_offset))?; + self.file.seek(SeekFrom::Start(where_to_write_next_ifd_offset))?; self.file.write(&0u64.to_le_bytes())?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 4078d62..87cd9c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,6 @@ fn main() -> Result<()> { arr.slice_mut(s![64.., ..64]).fill(1); arr.slice_mut(s![..64, 64..]).fill(2); arr.slice_mut(s![64.., 64..]).fill(3); - f.save(arr.view(), 1, 0, 0)?; + f.save(&arr, 1, 0, 0)?; Ok(()) }