- added some type aliases

- an overridden compress_frame in a class subclassing IJTiffWrite can now write multiple frames
This commit is contained in:
Wim Pomp
2024-10-02 13:58:12 +02:00
parent ba73ae522a
commit a733d8a820
4 changed files with 21 additions and 14 deletions

View File

@@ -8,12 +8,12 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: ["3.10", "3.12"] python-version: ["3.10", "3.12"]
os: [ubuntu-20.04, windows-2019, macOS-11] os: [ubuntu-latest, windows-latest, macOS-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install - name: Install

View File

@@ -64,7 +64,6 @@ or
tiffwrite('file.tif', image, 'TCXY') tiffwrite('file.tif', image, 'TCXY')
## Write one frame at a time ## Write one frame at a time
from itertools import product
from tiffwrite import IJTiffFile from tiffwrite import IJTiffFile
import numpy as np import numpy as np
@@ -76,7 +75,6 @@ or
tif.save(np.random.randint(0, 10, (32, 32)), c, z, t) tif.save(np.random.randint(0, 10, (32, 32)), c, z, t)
## Saving multiple tiffs simultaneously ## Saving multiple tiffs simultaneously
from itertools import product
from tiffwrite import IJTiffFile from tiffwrite import IJTiffFile
import numpy as np import numpy as np
@@ -89,7 +87,7 @@ or
tif_b.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 ## Tricks & tips
- The order of feeding frames to IJTiffFile is unimportant, IJTiffFile will order de ifd's such that the file will - 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. 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 - Using the colormap parameter you can make ImageJ open the file and apply the colormap. colormap='glasbey' is very
useful. useful.

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "tiffwrite" name = "tiffwrite"
version = "2024.4.0" version = "2024.10.0"
description = "Parallel tiff writer compatible with ImageJ." description = "Parallel tiff writer compatible with ImageJ."
authors = ["Wim Pomp, Lenstra lab NKI <w.pomp@nki.nl>"] authors = ["Wim Pomp, Lenstra lab NKI <w.pomp@nki.nl>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
@@ -16,7 +16,7 @@ numpy = "*"
tqdm = "*" tqdm = "*"
colorcet = "*" colorcet = "*"
matplotlib = "*" matplotlib = "*"
parfor = ">=2024.3.0" parfor = ">=2024.9.2"
pytest = { version = "*", optional = true } pytest = { version = "*", optional = true }
mypy = { version = "*", optional = true } mypy = { version = "*", optional = true }

View File

@@ -31,6 +31,11 @@ except Exception: # noqa
__version__ = "unknown" __version__ = "unknown"
type Strip = tuple[list[int], list[int]]
type CZT = tuple[int, int, int]
type FrameInfo = tuple[IFD, Strip, CZT]
def tiffwrite(file: str | Path, data: np.ndarray, axes: str = 'TZCXY', dtype: DTypeLike = None, bar: bool = False, def tiffwrite(file: str | Path, data: np.ndarray, axes: str = 'TZCXY', dtype: DTypeLike = None, bar: bool = False,
*args: Any, **kwargs: Any) -> None: *args: Any, **kwargs: Any) -> None:
""" file: string; filename of the new tiff file """ file: string; filename of the new tiff file
@@ -389,8 +394,8 @@ class IJTiffFile:
self.nframes = np.prod(self.shape[1:]) if self.colormap is None and self.colors is None else np.prod(self.shape) self.nframes = np.prod(self.shape[1:]) if self.colormap is None and self.colors is None else np.prod(self.shape)
self.frame_extra_tags: dict[tuple[int, int, int], dict[int, Tag]] = {} self.frame_extra_tags: dict[tuple[int, int, int], dict[int, Tag]] = {}
self.fh = FileHandle(self.path) self.fh = FileHandle(self.path)
self.pool = ParPool(self.compress_frame) # type: ignore
self.hashes = PoolSingleton().manager.dict() self.hashes = PoolSingleton().manager.dict()
self.pool = ParPool(self.compress_frame)
self.main_process = True self.main_process = True
with self.fh.lock() as fh: # noqa with self.fh.lock() as fh: # noqa
@@ -477,13 +482,14 @@ class IJTiffFile:
if self.main_process: if self.main_process:
ifds, strips = {}, {} ifds, strips = {}, {}
for n in list(self.pool.tasks): for n in list(self.pool.tasks):
framenr, channel = self.get_frame_number(n) for ifd, strip, delta in self.pool[n]:
ifds[framenr], strips[(framenr, channel)] = self.pool[n] framenr, channel = self.get_frame_number(tuple(i + j for i, j in zip(n, delta))) # type: ignore
ifds[framenr], strips[(framenr, channel)] = ifd, strip
self.pool.close() self.pool.close()
with self.fh.lock() as fh: # noqa with self.fh.lock() as fh: # noqa
for n, tags in self.frame_extra_tags.items(): for n, tags in self.frame_extra_tags.items():
framenr, channel = self.get_frame_number(n) framenr, _ = self.get_frame_number(n)
ifds[framenr].update(tags) ifds[framenr].update(tags)
if 0 in ifds and self.colormap is not None: if 0 in ifds and self.colormap is not None:
ifds[0][320] = Tag('SHORT', self.colormap_bytes) ifds[0][320] = Tag('SHORT', self.colormap_bytes)
@@ -547,14 +553,17 @@ class IJTiffFile:
fh.write(bvalue) fh.write(bvalue)
return offset return offset
def compress_frame(self, frame: np.ndarray) -> tuple[IFD, tuple[list[int], list[int]]]: def compress_frame(self, frame: np.ndarray) -> Sequence[FrameInfo]:
""" This is run in a different process""" """ This is run in a different process. Turns an image into bytes, writes them and returns the ifd, strip info
and czt delta. When subclassing IJTiffWrite this can be overridden to write one or more (using czt delta)
frames.
"""
stripbytecounts, ifd, chunks = self.get_chunks(self.ij_tiff_frame(frame)) stripbytecounts, ifd, chunks = self.get_chunks(self.ij_tiff_frame(frame))
stripbyteoffsets = [] stripbyteoffsets = []
with self.fh.lock() as fh: # noqa with self.fh.lock() as fh: # noqa
for chunk in chunks: for chunk in chunks:
stripbyteoffsets.append(self.write(fh, chunk)) stripbyteoffsets.append(self.write(fh, chunk))
return ifd, (stripbyteoffsets, stripbytecounts) return (ifd, (stripbyteoffsets, stripbytecounts), (0, 0, 0)),
@staticmethod @staticmethod
def get_chunks(frame: bytes) -> tuple[list[int], IFD, list[bytes]]: def get_chunks(frame: bytes) -> tuple[list[int], IFD, list[bytes]]: