- make zstd block include content size so fiji can actually read it

- add compression level argument
- remove shape argument
- some more pytest tests
This commit is contained in:
Wim Pomp
2024-10-11 18:52:49 +02:00
parent 1197806a6f
commit 4d31933a38
8 changed files with 171 additions and 89 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "tiffwrite" name = "tiffwrite"
version = "2024.10.1" version = "2024.10.2"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -33,11 +33,15 @@ FrameInfo = tuple[np.ndarray, None, CZT]
class IJTiffFile(rs.IJTiffFile): class IJTiffFile(rs.IJTiffFile):
def __new__(cls, path: str | Path, shape: tuple[int, int, int], dtype: DTypeLike = 'uint16', def __new__(cls, path: str | Path, shape: tuple[int, int, int] = None, dtype: DTypeLike = 'uint16',
colors: Sequence[str] = None, colormap: str = None, pxsize: float = None, colors: Sequence[str] = None, colormap: str = None, pxsize: float = None,
deltaz: float = None, timeinterval: float = None, comment: str = None, deltaz: float = None, timeinterval: float = None, compression: int = None, comment: str = None,
**extratags: Tag) -> IJTiffFile: **extratags: Tag) -> IJTiffFile:
new = super().__new__(cls, str(path), shape) new = super().__new__(cls, str(path))
if compression is not None:
if isinstance(compression, Sequence):
compression = compression[-1]
new.set_compression_level(compression)
if colors is not None: if colors is not None:
new.colors = np.array([get_color(color) for color in colors]) new.colors = np.array([get_color(color) for color in colors])
if colormap is not None: if colormap is not None:
@@ -54,7 +58,7 @@ class IJTiffFile(rs.IJTiffFile):
new.append_extra_tag(extra_tag, None) new.append_extra_tag(extra_tag, None)
return new return new
def __init__(self, path: str | Path, shape: tuple[int, int, int], dtype: DTypeLike = 'uint16', # noqa def __init__(self, path: str | Path, shape: tuple[int, int, int] = None, dtype: DTypeLike = 'uint16', # noqa
colors: Sequence[str] = None, colormap: str = None, pxsize: float = None, # noqa colors: Sequence[str] = None, colormap: str = None, pxsize: float = None, # noqa
deltaz: float = None, timeinterval: float = None, comment: str = None, # noqa deltaz: float = None, timeinterval: float = None, comment: str = None, # noqa
**extratags: Tag.Value | Tag) -> None: # noqa **extratags: Tag.Value | Tag) -> None: # noqa

View File

@@ -15,7 +15,7 @@ classifiers = [
dependencies = ["colorcet", "matplotlib", "numpy", "tqdm"] dependencies = ["colorcet", "matplotlib", "numpy", "tqdm"]
[project.optional-dependencies] [project.optional-dependencies]
test = ["pytest"] test = ["pytest", "tifffile"]
[tool.maturin] [tool.maturin]
python-source = "py" python-source = "py"

View File

@@ -1,26 +1,43 @@
// #[cfg(not(feature = "nopython"))] #[cfg(not(feature = "nopython"))]
mod py; mod py;
use anyhow::Result; use anyhow::Result;
use chrono::Utc; use chrono::Utc;
use ndarray::{s, Array2}; use ndarray::{s, Array2};
use num::traits::ToBytes; use num::{traits::ToBytes, Complex, FromPrimitive, Rational32, Zero};
use num::{Complex, FromPrimitive, Rational32, Zero};
use rayon::prelude::*; use rayon::prelude::*;
use std::cmp::Ordering; use std::{cmp::Ordering, collections::HashMap};
use std::collections::HashMap;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{copy, Read, Seek, SeekFrom, Write};
use std::thread; use std::{thread, thread::JoinHandle};
use std::thread::JoinHandle; use zstd::{DEFAULT_COMPRESSION_LEVEL, stream::Encoder};
use zstd::stream::encode_all;
const TAG_SIZE: usize = 20; const TAG_SIZE: usize = 20;
const OFFSET_SIZE: usize = 8; const OFFSET_SIZE: usize = 8;
const OFFSET: u64 = 16; const OFFSET: u64 = 16;
const COMPRESSION: u16 = 50000; const COMPRESSION: u16 = 50000;
pub fn encode_all(source: Vec<u8>, level: i32) -> Result<Vec<u8>> {
let mut result = Vec::<u8>::new();
copy_encode(&*source, &mut result, level, source.len() as u64)?;
Ok(result)
}
/// copy_encode from zstd crate, but let it include the content size in the zstd block header
pub fn copy_encode<R, W>(mut source: R, destination: W, level: i32, length: u64) -> Result<()>
where
R: Read,
W: Write,
{
let mut encoder = Encoder::new(destination, level)?;
encoder.include_contentsize(true)?;
encoder.set_pledged_src_size(Some(length))?;
copy(&mut source, &mut encoder)?;
encoder.finish()?;
Ok(())
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct IFD { struct IFD {
tags: Vec<Tag>, tags: Vec<Tag>,
@@ -366,8 +383,8 @@ struct CompressedFrame {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct Frame { struct Frame {
tileoffsets: Vec<u64>, offsets: Vec<u64>,
tilebytecounts: Vec<u64>, bytecounts: Vec<u64>,
image_width: u32, image_width: u32,
image_length: u32, image_length: u32,
bits_per_sample: u16, bits_per_sample: u16,
@@ -378,8 +395,8 @@ struct Frame {
impl Frame { impl Frame {
fn new( fn new(
tileoffsets: Vec<u64>, offsets: Vec<u64>,
tilebytecounts: Vec<u64>, bytecounts: Vec<u64>,
image_width: u32, image_width: u32,
image_length: u32, image_length: u32,
bits_per_sample: u16, bits_per_sample: u16,
@@ -388,8 +405,8 @@ impl Frame {
tile_length: u16, tile_length: u16,
) -> Self { ) -> Self {
Frame { Frame {
tileoffsets, offsets,
tilebytecounts, bytecounts,
image_width, image_width,
image_length, image_length,
bits_per_sample, bits_per_sample,
@@ -455,7 +472,7 @@ pub struct IJTiffFile {
frames: HashMap<(usize, usize, usize), Frame>, frames: HashMap<(usize, usize, usize), Frame>,
hashes: HashMap<u64, u64>, hashes: HashMap<u64, u64>,
threads: HashMap<(usize, usize, usize), JoinHandle<CompressedFrame>>, threads: HashMap<(usize, usize, usize), JoinHandle<CompressedFrame>>,
pub shape: (usize, usize, usize), pub compression_level: i32,
pub colors: Colors, pub colors: Colors,
pub comment: Option<String>, pub comment: Option<String>,
pub px_size: Option<f64>, pub px_size: Option<f64>,
@@ -473,7 +490,7 @@ impl Drop for IJTiffFile {
} }
impl IJTiffFile { impl IJTiffFile {
pub fn new(path: &str, shape: (usize, usize, usize)) -> Result<Self> { pub fn new(path: &str) -> Result<Self> {
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.create(true) .create(true)
.truncate(true) .truncate(true)
@@ -490,7 +507,7 @@ impl IJTiffFile {
frames: HashMap::new(), frames: HashMap::new(),
hashes: HashMap::new(), hashes: HashMap::new(),
threads: HashMap::new(), threads: HashMap::new(),
shape, compression_level: DEFAULT_COMPRESSION_LEVEL,
colors: Colors::None, colors: Colors::None,
comment: None, comment: None,
px_size: None, px_size: None,
@@ -500,19 +517,23 @@ impl IJTiffFile {
}) })
} }
pub fn description(&self) -> String { pub fn set_compression_level(&mut self, compression_level: i32) {
self.compression_level = compression_level;
}
pub fn description(&self, c_size: usize, z_size: usize, t_size: usize) -> String {
let mut desc: String = String::from("ImageJ=1.11a"); let mut desc: String = String::from("ImageJ=1.11a");
if let Colors::None = self.colors { if let Colors::None = self.colors {
desc += &format!("\nimages={}", self.shape.0); desc += &format!("\nimages={}", c_size);
desc += &format!("\nslices={}", self.shape.1); desc += &format!("\nslices={}", z_size);
desc += &format!("\nframes={}", self.shape.2); desc += &format!("\nframes={}", t_size);
} else { } else {
desc += &format!("\nimages={}", self.shape.0 * self.shape.1 * self.shape.2); desc += &format!("\nimages={}", c_size * z_size * t_size);
desc += &format!("\nchannels={}", self.shape.0); desc += &format!("\nchannels={}", c_size);
desc += &format!("\nslices={}", self.shape.1); desc += &format!("\nslices={}", z_size);
desc += &format!("\nframes={}", self.shape.2); desc += &format!("\nframes={}", t_size);
}; };
if self.shape.0 == 1 { if c_size == 1 {
desc += "\nmode=grayscale"; desc += "\nmode=grayscale";
} else { } else {
desc += "\nmode=composite"; desc += "\nmode=composite";
@@ -530,27 +551,33 @@ impl IJTiffFile {
desc desc
} }
fn get_czt(&self, frame_number: usize, channel: u8) -> (usize, usize, usize) { fn get_czt(
&self,
frame_number: usize,
channel: u8,
c_size: usize,
z_size: usize,
) -> (usize, usize, usize) {
if let Colors::None = self.colors { if let Colors::None = self.colors {
( (
channel as usize, channel as usize,
frame_number % self.shape.1, frame_number % z_size,
frame_number / self.shape.1, frame_number / z_size,
) )
} else { } else {
( (
frame_number % self.shape.0, frame_number % c_size,
frame_number / self.shape.0 % self.shape.1, frame_number / c_size % z_size,
frame_number / self.shape.0 / self.shape.1, frame_number / c_size / z_size,
) )
} }
} }
fn spp_and_n_frames(&self) -> (u8, usize) { fn spp_and_n_frames(&self, c_size: usize, z_size: usize, t_size: usize) -> (u8, usize) {
if let Colors::None = &self.colors { if let Colors::None = &self.colors {
(self.shape.0 as u8, self.shape.1 * self.shape.2) (c_size as u8, z_size * t_size)
} else { } else {
(1, self.shape.0 * self.shape.1 * self.shape.2) (1, c_size * z_size * t_size)
} }
} }
@@ -599,7 +626,7 @@ impl IJTiffFile {
where where
T: Bytes + Clone + Zero + Send + 'static, T: Bytes + Clone + Zero + Send + 'static,
{ {
fn compress<T>(frame: Array2<T>) -> CompressedFrame fn compress<T>(frame: Array2<T>, compression_level: i32) -> CompressedFrame
where where
T: Bytes + Clone + Zero, T: Bytes + Clone + Zero,
{ {
@@ -607,7 +634,7 @@ impl IJTiffFile {
let image_length = frame.shape()[1] as u32; let image_length = frame.shape()[1] as u32;
let tile_size = 2usize let tile_size = 2usize
.pow( .pow(
((image_width as f64 * image_length as f64 / 64f64).log2() / 2f64).round() ((image_width as f64 * image_length as f64 / 2f64).log2() / 2f64).round()
as u32, as u32,
) )
.max(16) .max(16)
@@ -619,7 +646,7 @@ impl IJTiffFile {
.collect(); .collect();
let bytes = byte_tiles let bytes = byte_tiles
.into_par_iter() .into_par_iter()
.map(|x| encode_all(&*x, 3).unwrap()) .map(|x| encode_all(x, compression_level).unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
CompressedFrame { CompressedFrame {
bytes, bytes,
@@ -630,8 +657,11 @@ impl IJTiffFile {
sample_format: T::SAMPLE_FORMAT, sample_format: T::SAMPLE_FORMAT,
} }
} }
self.threads let compression_level = self.compression_level;
.insert((c, z, t), thread::spawn(move || compress(frame))); self.threads.insert(
(c, z, t),
thread::spawn(move || compress(frame, compression_level)),
);
for key in self for key in self
.threads .threads
.keys() .keys()
@@ -652,15 +682,15 @@ impl IJTiffFile {
} }
fn write_frame(&mut self, frame: CompressedFrame, c: usize, z: usize, t: usize) -> Result<()> { fn write_frame(&mut self, frame: CompressedFrame, c: usize, z: usize, t: usize) -> Result<()> {
let mut tileoffsets = Vec::new(); let mut offsets = Vec::new();
let mut tilebytecounts = Vec::new(); let mut bytecounts = Vec::new();
for tile in frame.bytes { for tile in frame.bytes {
tilebytecounts.push(tile.len() as u64); bytecounts.push(tile.len() as u64);
tileoffsets.push(self.write(&tile)?); offsets.push(self.write(&tile)?);
} }
let frame = Frame::new( let frame = Frame::new(
tileoffsets, offsets,
tilebytecounts, bytecounts,
frame.image_width, frame.image_width,
frame.image_length, frame.image_length,
frame.bits_per_sample, frame.bits_per_sample,
@@ -674,8 +704,8 @@ impl IJTiffFile {
fn tile<T: Clone + Zero>(frame: Array2<T>, size: usize) -> Vec<Array2<T>> { fn tile<T: Clone + Zero>(frame: Array2<T>, size: usize) -> Vec<Array2<T>> {
let shape = frame.shape(); let shape = frame.shape();
let mut tiles = Vec::new();
let (n, m) = (shape[0] / size, shape[1] / size); let (n, m) = (shape[0] / size, shape[1] / size);
let mut tiles = Vec::new();
for i in 0..n { for i in 0..n {
for j in 0..m { for j in 0..m {
tiles.push( tiles.push(
@@ -726,8 +756,15 @@ impl IJTiffFile {
} }
} }
fn get_color(&self, _colors: &Vec<u8>, _bits_per_sample: u16) -> Result<Vec<u16>> { fn get_color(&self, colors: &Vec<u8>, bits_per_sample: u16) -> Result<Vec<u16>> {
todo!(); let mut c = Vec::new();
let lvl = if bits_per_sample == 8 { 255 } else { 65535 };
for i in 0..=lvl {
c.push(i * (colors[0] as u16) / 255);
c.push(i * (colors[1] as u16) / 255);
c.push(i * (colors[2] as u16) / 255);
}
Ok(c)
} }
fn close(&mut self) -> Result<()> { fn close(&mut self) -> Result<()> {
@@ -736,18 +773,33 @@ impl IJTiffFile {
self.write_frame(thread.join().unwrap(), c, z, t)?; self.write_frame(thread.join().unwrap(), c, z, t)?;
} }
} }
let mut c_size = 1;
let mut z_size = 1;
let mut t_size = 1;
for (c, z, t) in self.frames.keys() {
c_size = c_size.max(c + 1);
z_size = z_size.max(z + 1);
t_size = t_size.max(t + 1);
}
let mut where_to_write_next_ifd_offset = OFFSET - OFFSET_SIZE as u64; let mut where_to_write_next_ifd_offset = OFFSET - OFFSET_SIZE as u64;
let mut warn = false; let mut warn = false;
let (samples_per_pixel, n_frames) = self.spp_and_n_frames(); let (samples_per_pixel, n_frames) = self.spp_and_n_frames(c_size, t_size, z_size);
for frame_number in 0..n_frames { for frame_number in 0..n_frames {
if let Some(frame) = self.frames.get(&self.get_czt(frame_number, 0)) { if let Some(frame) = self
let mut tileoffsets = Vec::new(); .frames
let mut tilebytecounts = Vec::new(); .get(&self.get_czt(frame_number, 0, c_size, z_size))
{
let mut offsets = Vec::new();
let mut bytecounts = Vec::new();
let mut frame_count = 0; let mut frame_count = 0;
for channel in 0..samples_per_pixel { for channel in 0..samples_per_pixel {
if let Some(frame_n) = self.frames.get(&self.get_czt(frame_number, channel)) { if let Some(frame_n) =
tileoffsets.extend(frame_n.tileoffsets.iter()); self.frames
tilebytecounts.extend(frame_n.tilebytecounts.iter()); .get(&self.get_czt(frame_number, channel, c_size, z_size))
{
offsets.extend(frame_n.offsets.iter());
bytecounts.extend(frame_n.bytecounts.iter());
frame_count += 1; frame_count += 1;
} else { } else {
warn = true; warn = true;
@@ -758,30 +810,33 @@ impl IJTiffFile {
ifd.push_tag(Tag::long(257, &vec![frame.image_length])); ifd.push_tag(Tag::long(257, &vec![frame.image_length]));
ifd.push_tag(Tag::short(258, &vec![frame.bits_per_sample; frame_count])); ifd.push_tag(Tag::short(258, &vec![frame.bits_per_sample; frame_count]));
ifd.push_tag(Tag::short(259, &vec![COMPRESSION])); ifd.push_tag(Tag::short(259, &vec![COMPRESSION]));
ifd.push_tag(Tag::ascii(270, &self.description())); ifd.push_tag(Tag::ascii(270, &self.description(c_size, z_size, t_size)));
ifd.push_tag(Tag::short(277, &vec![frame_count as u16])); ifd.push_tag(Tag::short(277, &vec![frame_count as u16]));
ifd.push_tag(Tag::ascii(305, "tiffwrite_rs")); ifd.push_tag(Tag::ascii(305, "tiffwrite_tllab_NKI"));
ifd.push_tag(Tag::short(322, &vec![frame.tile_width])); ifd.push_tag(Tag::short(322, &vec![frame.tile_width]));
ifd.push_tag(Tag::short(323, &vec![frame.tile_length])); ifd.push_tag(Tag::short(323, &vec![frame.tile_length]));
ifd.push_tag(Tag::long8(324, &tileoffsets)); ifd.push_tag(Tag::long8(324, &offsets));
ifd.push_tag(Tag::long8(325, &tilebytecounts)); ifd.push_tag(Tag::long8(325, &bytecounts));
if frame.sample_format > 1 {
ifd.push_tag(Tag::short(339, &vec![frame.sample_format])); ifd.push_tag(Tag::short(339, &vec![frame.sample_format]));
}
if let Some(px_size) = self.px_size { if let Some(px_size) = self.px_size {
let r = vec![Rational32::from_f64(px_size).unwrap()]; let r = vec![Rational32::from_f64(px_size).unwrap()];
ifd.push_tag(Tag::rational(282, &r)); ifd.push_tag(Tag::rational(282, &r));
ifd.push_tag(Tag::rational(283, &r)); ifd.push_tag(Tag::rational(283, &r));
ifd.push_tag(Tag::short(296, &vec![1])); ifd.push_tag(Tag::short(296, &vec![1]));
} }
if let Colors::Colormap(_) = &self.colors {
ifd.push_tag(Tag::short(262, &vec![3]));
} else if let Colors::None = self.colors {
ifd.push_tag(Tag::short(262, &vec![1]));
}
if frame_number == 0 { if frame_number == 0 {
if let Colors::Colormap(colormap) = &self.colors { if let Colors::Colormap(colormap) = &self.colors {
ifd.push_tag(Tag::short( ifd.push_tag(Tag::short(
320, 320,
&self.get_colormap(colormap, frame.bits_per_sample), &self.get_colormap(colormap, frame.bits_per_sample),
)); ));
ifd.push_tag(Tag::short(262, &vec![3]));
} else if let Colors::None = self.colors {
ifd.push_tag(Tag::short(262, &vec![1]));
} }
} }
if frame_number < samples_per_pixel as usize { if frame_number < samples_per_pixel as usize {
@@ -794,12 +849,12 @@ impl IJTiffFile {
} }
} }
if let Colors::None = &self.colors { if let Colors::None = &self.colors {
if self.shape.0 > 1 { if c_size > 1 {
ifd.push_tag(Tag::short(284, &vec![2])) ifd.push_tag(Tag::short(284, &vec![2]))
} }
} }
for channel in 0..samples_per_pixel { for channel in 0..samples_per_pixel {
let czt = self.get_czt(frame_number, channel); let czt = self.get_czt(frame_number, channel, c_size, z_size);
if let Some(extra_tags) = self.extra_tags.get(&Some(czt)) { if let Some(extra_tags) = self.extra_tags.get(&Some(czt)) {
for tag in extra_tags { for tag in extra_tags {
ifd.push_tag(tag.to_owned()) ifd.push_tag(tag.to_owned())
@@ -811,10 +866,12 @@ impl IJTiffFile {
ifd.push_tag(tag.to_owned()) ifd.push_tag(tag.to_owned())
} }
} }
if frame_number == 0 {
ifd.push_tag(Tag::ascii( ifd.push_tag(Tag::ascii(
306, 306,
&format!("{}", Utc::now().format("%Y:%m:%d %H:%M:%S")), &format!("{}", Utc::now().format("%Y:%m:%d %H:%M:%S")),
)); ));
}
where_to_write_next_ifd_offset = ifd.write(self, where_to_write_next_ifd_offset)?; where_to_write_next_ifd_offset = ifd.write(self, where_to_write_next_ifd_offset)?;
} else { } else {
warn = true; warn = true;

View File

@@ -4,7 +4,8 @@ use tiffwrite::IJTiffFile;
fn main() -> Result<()> { fn main() -> Result<()> {
println!("Hello World!"); println!("Hello World!");
let mut f = IJTiffFile::new("foo.tif", (2, 1, 1))?; let mut f = IJTiffFile::new("foo.tif")?;
f.set_compression_level(10);
let mut arr = Array2::<u16>::zeros((100, 100)); let mut arr = Array2::<u16>::zeros((100, 100));
for i in 0..arr.shape()[0] { for i in 0..arr.shape()[0] {
for j in 0..arr.shape()[1] { for j in 0..arr.shape()[1] {

View File

@@ -165,12 +165,18 @@ struct PyIJTiffFile {
#[pymethods] #[pymethods]
impl PyIJTiffFile { impl PyIJTiffFile {
#[new] #[new]
fn new(path: &str, shape: (usize, usize, usize)) -> PyResult<Self> { fn new(path: &str) -> PyResult<Self> {
Ok(PyIJTiffFile { Ok(PyIJTiffFile {
ijtifffile: Some(IJTiffFile::new(path, shape)?), ijtifffile: Some(IJTiffFile::new(path)?),
}) })
} }
fn set_compression_level(&mut self, compression_level: i32) {
if let Some(ref mut ijtifffile) = self.ijtifffile {
ijtifffile.set_compression_level(compression_level);
}
}
#[getter] #[getter]
fn get_colors(&self) -> PyResult<Option<Vec<Vec<u8>>>> { fn get_colors(&self) -> PyResult<Option<Vec<Vec<u8>>>> {
if let Some(ijtifffile) = &self.ijtifffile { if let Some(ijtifffile) = &self.ijtifffile {

View File

@@ -12,7 +12,7 @@ def test_mult(tmp_path: Path) -> None:
shape = (2, 3, 5) shape = (2, 3, 5)
paths = [tmp_path / f'test{i}.tif' for i in range(6)] paths = [tmp_path / f'test{i}.tif' for i in range(6)]
with ExitStack() as stack: with ExitStack() as stack:
tifs = [stack.enter_context(IJTiffFile(path, shape)) for path in paths] # noqa tifs = [stack.enter_context(IJTiffFile(path)) for path in paths] # noqa
for c, z, t in tqdm(product(range(shape[0]), range(shape[1]), range(shape[2])), total=np.prod(shape)): # noqa for c, z, t in tqdm(product(range(shape[0]), range(shape[1]), range(shape[2])), total=np.prod(shape)): # noqa
for tif in tifs: for tif in tifs:
tif.save(np.random.randint(0, 255, (64, 64)), c, z, t) tif.save(np.random.randint(0, 255, (64, 64)), c, z, t)

View File

@@ -1,14 +1,28 @@
from itertools import product
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
import pytest
from tifffile import imread
from tiffwrite import IJTiffFile from tiffwrite import IJTiffFile
def test_single(tmp_path: Path) -> None: @pytest.mark.parametrize('dtype', ('uint8', 'uint16', 'uint32', 'uint64',
path = tmp_path / 'test.tif' 'int8', 'int16', 'int32', 'int64', 'float32', 'float64'))
with IJTiffFile(path, (3, 4, 5)) as tif: def test_single(tmp_path: Path, dtype) -> None:
for c, z, t in product(range(3), range(4), range(5)): with IJTiffFile(tmp_path / 'test.tif', dtype=dtype) as tif:
tif.save(np.random.randint(0, 255, (64, 64)), c, z, t) a0, b0 = np.meshgrid(range(100), range(100))
assert path.exists() a0[::2, :] = 0
b0[:, ::2] = 1
tif.save(a0, 0, 0, 0)
tif.save(b0, 1, 0, 0)
a1, b1 = np.meshgrid(range(100), range(100))
a1[:, ::2] = 0
b1[::2, :] = 1
tif.save(a1, 0, 0, 1)
tif.save(b1, 1, 0, 1)
t = imread(tmp_path / 'test.tif')
assert t.dtype == np.dtype(dtype), "data type does not match"
assert np.all(np.stack(((a0, b0), (a1, b1))) == t), "data does not match"