- Switch to zstd compression as default.

- Only warn if frames are missing when the file is closed, allowing the user to debug the error causing missing frames.
This commit is contained in:
Wim Pomp
2024-02-14 14:54:13 +01:00
parent e736770512
commit 43b6a48049
3 changed files with 55 additions and 41 deletions

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "tiffwrite" name = "tiffwrite"
version = "2023.8.0" version = "2024.2.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"
@@ -9,18 +9,21 @@ packages = [{include = "tiffwrite"}]
repository = "https://github.com/wimpomp/tiffwrite" repository = "https://github.com/wimpomp/tiffwrite"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^3.8"
tifffile = "*" tifffile = "*"
numpy = "*" numpy = "*"
tqdm = "*" tqdm = "*"
colorcet = "*" colorcet = "*"
matplotlib = "*" matplotlib = "*"
parfor = ">=2023.8.3" parfor = ">=2023.10.1"
pytest = { version = "*", optional = true } pytest = { version = "*", optional = true }
[tool.poetry.extras] [tool.poetry.extras]
test = ["pytest"] test = ["pytest"]
[tool.pytest.ini_options]
filterwarnings = ["ignore:::(?!tiffwrite)"]
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

View File

@@ -10,8 +10,8 @@ def test_mult(tmp_path):
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] tifs = [stack.enter_context(IJTiffFile(path, shape)) 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)): 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)
assert all([path.exists() for path in paths]) assert all([path.exists() for path in paths])

View File

@@ -1,5 +1,6 @@
import os import os
import struct import struct
import warnings
from collections.abc import Iterable from collections.abc import Iterable
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime from datetime import datetime
@@ -23,7 +24,7 @@ __all__ = ["IJTiffFile", "Tag", "tiffwrite"]
try: try:
__version__ = version("tiffwrite") __version__ = version("tiffwrite")
except Exception: except Exception: # noqa
__version__ = "unknown" __version__ = "unknown"
@@ -48,9 +49,9 @@ def tiffwrite(file, data, axes='TZCXY', dtype=None, bar=False, *args, **kwargs):
shape = data.shape[:3] shape = data.shape[:3]
with IJTiffFile(file, shape, data.dtype if dtype is None else dtype, *args, **kwargs) as f: with IJTiffFile(file, shape, data.dtype if dtype is None else dtype, *args, **kwargs) as f:
at_least_one = False at_least_one = False
for n in tqdm(product(*[range(i) for i in shape]), total=np.prod(shape), desc='Saving tiff', disable=not bar): for n in tqdm(product(*[range(i) for i in shape]), total=np.prod(shape), desc='Saving tiff', disable=not bar): # noqa
if np.any(data[n]) or not at_least_one: if np.any(data[n]) or not at_least_one: # noqa
f.save(data[n], *n) f.save(data[n], *n) # noqa
at_least_one = True at_least_one = True
@@ -116,6 +117,9 @@ class Tag:
(denominator is not None and denominator < 0) else 32) - 1) (denominator is not None and denominator < 0) else 32) - 1)
def __init__(self, ttype, value=None, offset=None): def __init__(self, ttype, value=None, offset=None):
self.fh = None
self.header = None
self.bytes_data = None
if value is None: if value is None:
self.value = ttype self.value = ttype
if all([isinstance(value, int) for value in self.value]): if all([isinstance(value, int) for value in self.value]):
@@ -182,7 +186,7 @@ class Tag:
return self.value, len(self.value) // struct.calcsize(self.dtype) return self.value, len(self.value) // struct.calcsize(self.dtype)
elif self.ttype in (2, 14): elif self.ttype in (2, 14):
if isinstance(self.value, str): if isinstance(self.value, str):
bytes_value = self.value.encode('ascii') + b'\x00' bytes_value = self.value.encode('ascii') + b'\x00' # noqa
else: else:
bytes_value = b'\x00'.join([value.encode('ascii') for value in self.value]) + b'\x00' bytes_value = b'\x00'.join([value.encode('ascii') for value in self.value]) + b'\x00'
return bytes_value, len(bytes_value) return bytes_value, len(bytes_value)
@@ -240,6 +244,10 @@ class Tag:
class IFD(dict): class IFD(dict):
def __init__(self, fh=None): def __init__(self, fh=None):
super().__init__() super().__init__()
self.fh = fh
self.header = None
self.offset = None
self.where_to_write_next_ifd_offset = None
if fh is not None: if fh is not None:
header = Header(fh) header = Header(fh)
fh.seek(header.offset) fh.seek(header.offset)
@@ -280,7 +288,7 @@ class IFD(dict):
value = [struct.unpack(header.byteorder + dtype, fh.read(dtypelen))[0] for _ in range(count)] value = [struct.unpack(header.byteorder + dtype, fh.read(dtypelen))[0] for _ in range(count)]
if toolong: if toolong:
fh.seek(cp) fh.seek(cp) # noqa
self[code] = Tag(ttype, value, pos) self[code] = Tag(ttype, value, pos)
fh.seek(header.offset) fh.seek(header.offset)
@@ -338,7 +346,7 @@ class IJTiffFile:
wp@tl20200214 wp@tl20200214
""" """
def __init__(self, path, shape, dtype='uint16', colors=None, colormap=None, pxsize=None, deltaz=None, def __init__(self, path, shape, dtype='uint16', colors=None, colormap=None, pxsize=None, deltaz=None,
timeinterval=None, compression=(8, 9), comment=None, **extratags): timeinterval=None, compression=(50000, 22), comment=None, **extratags):
assert len(shape) >= 3, 'please specify all c, z, t for the shape' assert len(shape) >= 3, 'please specify all c, z, t for the shape'
assert len(shape) <= 3, 'please specify only c, z, t for the shape' assert len(shape) <= 3, 'please specify only c, z, t for the shape'
assert np.dtype(dtype).char in 'BbHhf', 'datatype not supported' assert np.dtype(dtype).char in 'BbHhf', 'datatype not supported'
@@ -418,7 +426,7 @@ class IJTiffFile:
if self.deltaz is not None: if self.deltaz is not None:
desc.append(f'spacing={self.deltaz}') desc.append(f'spacing={self.deltaz}')
if self.timeinterval is not None: if self.timeinterval is not None:
desc.append(f'finterval={self.timeinterval}') desc.append(f'interval={self.timeinterval}')
desc = [bytes(d, 'ascii') for d in desc] desc = [bytes(d, 'ascii') for d in desc]
if self.comment is not None: if self.comment is not None:
desc.append(b'') desc.append(b'')
@@ -465,26 +473,33 @@ class IJTiffFile:
for c, color in enumerate(self.colors_bytes): for c, color in enumerate(self.colors_bytes):
ifds[c][320] = Tag('SHORT', color) ifds[c][320] = Tag('SHORT', color)
ifds[c][262] = Tag('SHORT', 3) ifds[c][262] = Tag('SHORT', 3)
if 306 not in ifds[0]: if 0 in ifds and 306 not in ifds[0]:
ifds[0][306] = Tag('ASCII', datetime.now().strftime('%Y:%m:%d %H:%M:%S')) ifds[0][306] = Tag('ASCII', datetime.now().strftime('%Y:%m:%d %H:%M:%S'))
wrn = False
for framenr in range(self.nframes): for framenr in range(self.nframes):
stripbyteoffsets, stripbytecounts = zip(*[strips[(framenr, channel)] if framenr in ifds and all([(framenr, channel) in strips for channel in range(self.spp)]):
for channel in range(self.spp)]) stripbyteoffsets, stripbytecounts = zip(*[strips[(framenr, channel)]
ifds[framenr][258].value = self.spp * ifds[framenr][258].value for channel in range(self.spp)])
ifds[framenr][270] = Tag('ASCII', self.description) ifds[framenr][258].value = self.spp * ifds[framenr][258].value
ifds[framenr][273] = Tag('LONG8', sum(stripbyteoffsets, [])) ifds[framenr][270] = Tag('ASCII', self.description)
ifds[framenr][277] = Tag('SHORT', self.spp) ifds[framenr][273] = Tag('LONG8', sum(stripbyteoffsets, []))
ifds[framenr][279] = Tag('LONG8', sum(stripbytecounts, [])) ifds[framenr][277] = Tag('SHORT', self.spp)
ifds[framenr][305] = Tag('ASCII', 'tiffwrite_tllab_NKI') ifds[framenr][279] = Tag('LONG8', sum(stripbytecounts, []))
if self.extratags is not None: ifds[framenr][305] = Tag('ASCII', 'tiffwrite_tllab_NKI')
ifds[framenr].update(self.extratags) if self.extratags is not None:
if self.colormap is None and self.colors is None and self.shape[0] > 1: ifds[framenr].update(self.extratags)
ifds[framenr][284] = Tag('SHORT', 2) if self.colormap is None and self.colors is None and self.shape[0] > 1:
ifds[framenr].write(fh, self.header, self.write) ifds[framenr][284] = Tag('SHORT', 2)
if framenr: ifds[framenr].write(fh, self.header, self.write)
ifds[framenr].write_offset(ifds[framenr - 1].where_to_write_next_ifd_offset) if framenr:
ifds[framenr].write_offset(ifds[framenr - 1].where_to_write_next_ifd_offset)
else:
ifds[framenr].write_offset(self.header.offset - self.header.offsetsize)
else: else:
ifds[framenr].write_offset(self.header.offset - self.header.offsetsize) wrn = True
if wrn:
warnings.warn('Some frames were not added to the tif file, either you forgot them, '
'or an error occured and the tif file was closed prematurely.')
def __enter__(self): def __enter__(self):
return self return self
@@ -548,14 +563,10 @@ class FileHandle:
@contextmanager @contextmanager
def lock(self): def lock(self):
self._lock.acquire() with self._lock:
f = None with open(self.name, 'rb+') as f:
try: try:
f = open(self.name, 'rb+') f.seek(self._pos.value)
f.seek(self._pos.value) yield f
yield f finally:
finally: self._pos.value = f.tell()
if f is not None:
self._pos.value = f.tell()
f.close()
self._lock.release()