- set dtype according to pixel type in file

- cziread bugfix
- add reader for broken files saved by Fiji
- make ndread work for arrays with less dimensions than 5
- relative imports
- remove some old functions
- make bfread check if it can open the file
This commit is contained in:
Wim Pomp
2023-08-14 17:01:03 +02:00
parent 0bd22d766a
commit bdd7a5399c
10 changed files with 143 additions and 78 deletions

View File

@@ -17,13 +17,11 @@ from parfor import parfor
from tiffwrite import IJTiffFile from tiffwrite import IJTiffFile
from numbers import Number from numbers import Number
from argparse import ArgumentParser from argparse import ArgumentParser
from typing import List
from pathlib import Path from pathlib import Path
from importlib.metadata import version from importlib.metadata import version
from traceback import print_exc from traceback import print_exc
from ndbioimage.transforms import Transform, Transforms from .transforms import Transform, Transforms
from ndbioimage.jvm import JVM from .jvm import JVM
try: try:
__version__ = version(Path(__file__).parent.name) __version__ = version(Path(__file__).parent.name)
@@ -42,8 +40,13 @@ ureg.default_format = '~P'
set_application_registry(ureg) set_application_registry(ureg)
class ReaderNotFoundError(Exception):
pass
class ImTransforms(Transforms): class ImTransforms(Transforms):
""" Transforms class with methods to calculate channel transforms from bead files etc. """ """ Transforms class with methods to calculate channel transforms from bead files etc. """
def __init__(self, path, cyllens, file=None, transforms=None): def __init__(self, path, cyllens, file=None, transforms=None):
super().__init__() super().__init__()
self.cyllens = cyllens self.cyllens = cyllens
@@ -179,6 +182,7 @@ class ImTransforms(Transforms):
class ImShiftTransforms(Transforms): class ImShiftTransforms(Transforms):
""" Class to handle drift in xy. The image this is applied to must have a channeltransform already, which is then """ Class to handle drift in xy. The image this is applied to must have a channeltransform already, which is then
replaced by this class. """ replaced by this class. """
def __init__(self, im, shifts=None): def __init__(self, im, shifts=None):
""" im: Calculate shifts from channel-transformed images """ im: Calculate shifts from channel-transformed images
im, t x 2 array Sets shifts from array, one row per frame im, t x 2 array Sets shifts from array, one row per frame
@@ -242,16 +246,19 @@ class ImShiftTransforms(Transforms):
@parfor(range(1, im.shape['t']), (im, im0), desc='Calculating image shifts.') @parfor(range(1, im.shape['t']), (im, im0), desc='Calculating image shifts.')
def fun(t, im, im0): def fun(t, im, im0):
return Transform(im0, im[:, 0, t].squeeze().transpose(2, 0, 1), 'translation') return Transform(im0, im[:, 0, t].squeeze().transpose(2, 0, 1), 'translation')
transforms = [Transform()] + fun transforms = [Transform()] + fun
self.shifts = np.array([t.parameters[4:] for t in transforms]) self.shifts = np.array([t.parameters[4:] for t in transforms])
self.set_transforms(transforms, im.transform) self.set_transforms(transforms, im.transform)
def calulate_shifts(self, im): def calulate_shifts(self, im):
""" Calculate shifts relative to the previous frame """ """ Calculate shifts relative to the previous frame """
@parfor(range(1, im.shape['t']), (im,), desc='Calculating image shifts.') @parfor(range(1, im.shape['t']), (im,), desc='Calculating image shifts.')
def fun(t, im): def fun(t, im):
return Transform(im[:, 0, t - 1].squeeze().transpose(2, 0, 1), im[:, 0, t].squeeze().transpose(2, 0, 1), return Transform(im[:, 0, t - 1].squeeze().transpose(2, 0, 1), im[:, 0, t].squeeze().transpose(2, 0, 1),
'translation') 'translation')
transforms = [Transform()] + fun transforms = [Transform()] + fun
self.shifts = np.cumsum([t.parameters[4:] for t in transforms]) self.shifts = np.cumsum([t.parameters[4:] for t in transforms])
self.set_transforms(transforms, im.transform) self.set_transforms(transforms, im.transform)
@@ -285,40 +292,12 @@ class DequeDict(OrderedDict):
self.__truncate__() self.__truncate__()
def tolist(item) -> List:
if hasattr(item, 'items'):
return item
elif isinstance(item, str):
return [item]
try:
iter(item)
return list(item)
except TypeError:
return list((item,))
def find(obj, **kwargs): def find(obj, **kwargs):
for item in obj: for item in obj:
if all([getattr(item, key) == value for key, value in kwargs.items()]): if all([getattr(item, key) == value for key, value in kwargs.items()]):
return item return item
def find_rec(obj, **kwargs):
if isinstance(obj, list):
for item in obj:
ans = find_rec(item, **kwargs)
if ans:
return ans
elif isinstance(obj, ome_types._base_type.OMEType):
if all([hasattr(obj, key) for key in kwargs.keys()]) and all(
[getattr(obj, key) == value for key, value in kwargs.items()]):
return obj
for k, v in obj.__dict__.items():
ans = find_rec(v, **kwargs)
if ans:
return ans
def try_default(fun, default, *args, **kwargs): def try_default(fun, default, *args, **kwargs):
try: try:
return fun(*args, **kwargs) return fun(*args, **kwargs)
@@ -326,9 +305,10 @@ def try_default(fun, default, *args, **kwargs):
return default return default
def ome_subprocess(path): def get_ome(path):
from .readers.bfread import jars
try: try:
jvm = JVM() jvm = JVM(jars)
ome_meta = jvm.metadata_tools.createOMEXMLMetadata() ome_meta = jvm.metadata_tools.createOMEXMLMetadata()
reader = jvm.image_reader() reader = jvm.image_reader()
reader.setMetadataStore(ome_meta) reader.setMetadataStore(ome_meta)
@@ -450,7 +430,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
def get_ome(path): def get_ome(path):
""" Use java BioFormats to make an ome metadata structure. """ """ Use java BioFormats to make an ome metadata structure. """
with multiprocessing.get_context('spawn').Pool(1) as pool: with multiprocessing.get_context('spawn').Pool(1) as pool:
ome = pool.map(ome_subprocess, (path,))[0] ome = pool.map(get_ome, (path,))[0]
return ome return ome
def __new__(cls, path=None, *args, **kwargs): def __new__(cls, path=None, *args, **kwargs):
@@ -479,6 +459,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
subclass.do_copy = set(do_copy).union(set(subclass_do_copy)) subclass.do_copy = set(do_copy).union(set(subclass_do_copy))
return super().__new__(subclass) return super().__new__(subclass)
raise ReaderNotFoundError(f'No reader found for {path}.')
@staticmethod @staticmethod
def split_path_series(path): def split_path_series(path):
@@ -507,7 +488,6 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
self.transform = transform self.transform = transform
self.drift = drift self.drift = drift
self.beadfile = beadfile self.beadfile = beadfile
self.dtype = dtype
self.reader = None self.reader = None
self.pcf = None self.pcf = None
@@ -523,14 +503,13 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
self.frameoffset = 0, 0 # how far apart the centers of frame and sensor are self.frameoffset = 0, 0 # how far apart the centers of frame and sensor are
self.flags = dict(C_CONTIGUOUS=False, F_CONTIGUOUS=False, OWNDATA=False, WRITEABLE=False, self.flags = dict(C_CONTIGUOUS=False, F_CONTIGUOUS=False, OWNDATA=False, WRITEABLE=False,
ALIGNED=False, WRITEBACKIFCOPY=False, UPDATEIFCOPY=False) ALIGNED=False, WRITEBACKIFCOPY=False, UPDATEIFCOPY=False)
self.open() self.open()
# extract some metadata from ome # extract some metadata from ome
instrument = self.ome.instruments[0] if self.ome.instruments else None instrument = self.ome.instruments[0] if self.ome.instruments else None
image = self.ome.images[0] image = self.ome.images[0]
pixels = image.pixels pixels = image.pixels
self.shape = pixels.size_x, pixels.size_y, pixels.size_c, pixels.size_z, pixels.size_t self.shape = pixels.size_x, pixels.size_y, pixels.size_c, pixels.size_z, pixels.size_t
self.dtype = pixels.type.value if dtype is None else dtype
self.pxsize = pixels.physical_size_x_quantity self.pxsize = pixels.physical_size_x_quantity
try: try:
self.exposuretime = tuple(find(image.pixels.planes, the_c=c).exposure_time_quantity self.exposuretime = tuple(find(image.pixels.planes, the_c=c).exposure_time_quantity
@@ -556,7 +535,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
try: try:
self.binning = [int(i) for i in image.pixels.channels[0].detector_settings.binning.value.split('x')] self.binning = [int(i) for i in image.pixels.channels[0].detector_settings.binning.value.split('x')]
self.pxsize *= self.binning[0] self.pxsize *= self.binning[0]
except (AttributeError, IndexError): except (AttributeError, IndexError, ValueError):
self.binning = None self.binning = None
self.cnamelist = [channel.name for channel in image.pixels.channels] self.cnamelist = [channel.name for channel in image.pixels.channels]
try: try:
@@ -718,6 +697,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
s = [f"path/filename: {self.path}", s = [f"path/filename: {self.path}",
f"series/pos: {self.series}", f"series/pos: {self.series}",
f"reader: {self.__class__.__module__.split('.')[-1]}", f"reader: {self.__class__.__module__.split('.')[-1]}",
f"dtype: {self.dtype}",
f"shape ({self.axes}):".ljust(15) + f"{' x '.join(str(i) for i in self.shape)}"] f"shape ({self.axes}):".ljust(15) + f"{' x '.join(str(i) for i in self.shape)}"]
if self.pxsize_um: if self.pxsize_um:
s.append(f'pixel size: {1000 * self.pxsize_um:.2f} nm') s.append(f'pixel size: {1000 * self.pxsize_um:.2f} nm')
@@ -819,6 +799,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
for k in b: for k in b:
if k not in a: if k not in a:
yield k yield k
for idx in unique_yield(list(self.cache.keys()), for idx in unique_yield(list(self.cache.keys()),
product(range(self.shape['c']), range(self.shape['z']), range(self.shape['t']))): product(range(self.shape['c']), range(self.shape['z']), range(self.shape['t']))):
xyczt = (slice(None), slice(None)) + idx xyczt = (slice(None), slice(None)) + idx
@@ -1226,6 +1207,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
@cached_property @cached_property
def piezoval(self): def piezoval(self):
""" gives the height of the piezo and focus motor, only available when CylLensGUI was used """ """ gives the height of the piezo and focus motor, only available when CylLensGUI was used """
def upack(idx): def upack(idx):
time = list() time = list()
val = list() val = list()
@@ -1291,7 +1273,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
return return
return return
def save_as_tiff(self, fname=None, c=None, z=None, t=None, split=False, bar=True, pixel_type='uint16'): def save_as_tiff(self, fname=None, c=None, z=None, t=None, split=False, bar=True, pixel_type='uint16', **kwargs):
""" saves the image as a tiff-file """ saves the image as a tiff-file
split: split channels into different files """ split: split channels into different files """
if fname is None: if fname is None:
@@ -1319,7 +1301,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, metaclass=ABCMeta):
shape = [len(i) for i in n] shape = [len(i) for i in n]
at_least_one = False at_least_one = False
with IJTiffFile(fname.with_suffix('.tif'), shape, pixel_type, with IJTiffFile(fname.with_suffix('.tif'), shape, pixel_type,
pxsize=self.pxsize_um, deltaz=self.deltaz_um) as tif: pxsize=self.pxsize_um, deltaz=self.deltaz_um, **kwargs) as tif:
for i, m in tqdm(zip(product(*[range(s) for s in shape]), product(*n)), for i, m in tqdm(zip(product(*[range(s) for s in shape]), product(*n)),
total=np.prod(shape), desc='Saving tiff', disable=not bar): total=np.prod(shape), desc='Saving tiff', disable=not bar):
if np.any(self(*m)) or not at_least_one: if np.any(self(*m)) or not at_least_one:
@@ -1353,4 +1335,4 @@ def main():
im.save_as_tiff(out, args.channel, args.zslice, args.time, args.split) im.save_as_tiff(out, args.channel, args.zslice, args.time, args.split)
from ndbioimage.readers import * from .readers import *

View File

@@ -1,4 +1,5 @@
from pathlib import Path from pathlib import Path
from urllib import request
try: try:
class JVM: class JVM:
@@ -15,10 +16,15 @@ try:
cls._instance = object.__new__(cls) cls._instance = object.__new__(cls)
return cls._instance return cls._instance
def __init__(self, classpath=None): def __init__(self, jars=None):
if not self.vm_started and not self.vm_killed: if not self.vm_started and not self.vm_killed:
if classpath is None: jarpath = Path(__file__).parent / 'jars'
classpath = [str(Path(__file__).parent / 'jars' / '*')] if jars is None:
jars = {}
for jar, src in jars.items():
if not (jarpath / jar).exists():
JVM.download(src, jarpath / jar)
classpath = [str(jarpath / jar) for jar in jars.keys()]
import jpype import jpype
jpype.startJVM(classpath=classpath) jpype.startJVM(classpath=classpath)
@@ -39,6 +45,12 @@ try:
if self.vm_killed: if self.vm_killed:
raise Exception('The JVM was killed before, and cannot be restarted in this Python process.') raise Exception('The JVM was killed before, and cannot be restarted in this Python process.')
@staticmethod
def download(src, dest):
print(f'Downloading {dest.name} to {dest}.')
dest.parent.mkdir(exist_ok=True)
dest.write_bytes(request.urlopen(src).read())
def kill_vm(self): def kill_vm(self):
if self.vm_started and not self.vm_killed: if self.vm_started and not self.vm_killed:
import jpype import jpype

View File

@@ -1,22 +1,17 @@
import multiprocessing import multiprocessing
import numpy as np import numpy as np
from abc import ABC from abc import ABC
from ndbioimage import Imread, JVM
from multiprocessing import queues from multiprocessing import queues
from traceback import print_exc from traceback import print_exc
from urllib import request from .. import Imread, JVM
from pathlib import Path
jars = {'bioformats_package.jar':
'https://downloads.openmicroscopy.org/bio-formats/latest/artifacts/bioformats_package.jar'}
class JVMReader: class JVMReader:
def __init__(self, path, series): def __init__(self, path, series):
bf_jar = Path(__file__).parent.parent / 'jars' / 'bioformats_package.jar'
if not bf_jar.exists():
print('Downloading bioformats_package.jar.')
url = 'https://downloads.openmicroscopy.org/bio-formats/latest/artifacts/bioformats_package.jar'
bf_jar.parent.mkdir(exist_ok=True)
bf_jar.write_bytes(request.urlopen(url).read())
mp = multiprocessing.get_context('spawn') mp = multiprocessing.get_context('spawn')
self.path = path self.path = path
self.series = series self.series = series
@@ -50,7 +45,7 @@ class JVMReader:
""" Read planes from the image reader file. """ Read planes from the image reader file.
adapted from python-bioformats/bioformats/formatreader.py adapted from python-bioformats/bioformats/formatreader.py
""" """
jvm = JVM() jvm = JVM(jars)
reader = jvm.image_reader() reader = jvm.image_reader()
ome_meta = jvm.metadata_tools.createOMEXMLMetadata() ome_meta = jvm.metadata_tools.createOMEXMLMetadata()
reader.setMetadataStore(ome_meta) reader.setMetadataStore(ome_meta)
@@ -167,6 +162,18 @@ class JVMReader:
jvm.kill_vm() jvm.kill_vm()
def can_open(path):
try:
jvm = JVM(jars)
reader = jvm.image_reader()
reader.getFormat(str(path))
return True
except (Exception,):
return False
finally:
jvm.kill_vm()
class Reader(Imread, ABC): class Reader(Imread, ABC):
""" This class is used as a last resort, when we don't have another way to open the file. We don't like it """ This class is used as a last resort, when we don't have another way to open the file. We don't like it
because it requires the java vm. because it requires the java vm.
@@ -176,7 +183,10 @@ class Reader(Imread, ABC):
@staticmethod @staticmethod
def _can_open(path): def _can_open(path):
return not path.is_dir() """ Use java BioFormats to make an ome metadata structure. """
with multiprocessing.get_context('spawn').Pool(1) as pool:
ome = pool.map(can_open, (path,))[0]
return ome
def open(self): def open(self):
self.reader = JVMReader(self.path, self.series) self.reader = JVMReader(self.path, self.series)

View File

@@ -4,10 +4,10 @@ import re
from lxml import etree from lxml import etree
from ome_types import model from ome_types import model
from abc import ABC from abc import ABC
from ndbioimage import Imread
from functools import cached_property from functools import cached_property
from itertools import product from itertools import product
from pathlib import Path from pathlib import Path
from .. import Imread
class Reader(Imread, ABC): class Reader(Imread, ABC):
@@ -414,7 +414,7 @@ class Reader(Imread, ABC):
return ome return ome
def __frame__(self, c=0, z=0, t=0): def __frame__(self, c=0, z=0, t=0):
f = np.zeros(self.shape['xy'], self.dtype) f = np.zeros(self.file_shape[:2], self.dtype)
directory_entries = self.filedict[c, z, t] directory_entries = self.filedict[c, z, t]
x_min = min([f.start[f.axes.index('X')] for f in directory_entries]) x_min = min([f.start[f.axes.index('X')] for f in directory_entries])
y_min = min([f.start[f.axes.index('Y')] for f in directory_entries]) y_min = min([f.start[f.axes.index('Y')] for f in directory_entries])

View File

@@ -0,0 +1,59 @@
from abc import ABC
from tifffile import TiffFile
from functools import cached_property
from itertools import product
from ome_types import model
from pathlib import Path
from struct import unpack
from warnings import warn
import numpy as np
from .. import Imread
class Reader(Imread, ABC):
""" Can read some tif files written with Fiji which are broken because Fiji didn't finish writing. """
priority = 90
do_not_pickle = 'reader'
@staticmethod
def _can_open(path):
if isinstance(path, Path) and path.suffix in ('.tif', '.tiff'):
with TiffFile(path) as tif:
return tif.is_imagej and not tif.is_bigtiff
else:
return False
def __frame__(self, c, z, t): # Override this, return the frame at c, z, t
self.reader.filehandle.seek(self.offset + t * self.count)
return np.reshape(unpack(self.fmt, self.reader.filehandle.read(self.count)), self.shape['yx'])
def open(self):
warn(f'File {self.path.name} is probably damaged, opening with fijiread.')
self.reader = TiffFile(self.path)
assert self.reader.pages[0].compression == 1, "Can only read uncompressed tiff files."
assert self.reader.pages[0].samplesperpixel == 1, "Can only read 1 sample per pixel."
self.offset = self.reader.pages[0].dataoffsets[0]
self.count = self.reader.pages[0].databytecounts[0]
self.bytes_per_sample = self.reader.pages[0].bitspersample // 8
self.fmt = self.reader.byteorder + self.count // self.bytes_per_sample * 'BHILQ'[self.bytes_per_sample - 1]
def close(self):
self.reader.close()
@cached_property
def ome(self):
size_y, size_x = self.reader.pages[0].shape
size_c, size_z = 1, 1
size_t = int(np.floor((self.reader.filehandle.size - self.reader.pages[0].dataoffsets[0]) / self.count))
pixel_type = model.simple_types.PixelType(self.reader.pages[0].dtype.name)
ome = model.OME()
ome.instruments.append(model.Instrument())
ome.images.append(
model.Image(
pixels=model.Pixels(
size_c=size_c, size_z=size_z, size_t=size_t, size_x=size_x, size_y=size_y,
dimension_order="XYCZT", type=pixel_type,
objective_settings=model.ObjectiveSettings(id="Objective:0"))))
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
ome.images[0].pixels.planes.append(model.Plane(the_c=c, the_z=z, the_t=t, delta_t=0))
return ome

View File

@@ -2,14 +2,12 @@ import numpy as np
from ome_types import model from ome_types import model
from functools import cached_property from functools import cached_property
from abc import ABC from abc import ABC
from ndbioimage import Imread from .. import Imread
from itertools import product from itertools import product
class Reader(Imread, ABC): class Reader(Imread, ABC):
priority = 20 priority = 20
do_not_pickle = 'ome'
do_not_copy = 'ome'
@staticmethod @staticmethod
def _can_open(path): def _can_open(path):
@@ -41,7 +39,10 @@ class Reader(Imread, ABC):
return ome return ome
def open(self): def open(self):
if isinstance(self.path, np.ndarray):
self.array = np.array(self.path) self.array = np.array(self.path)
while self.array.ndim < 5:
self.array = np.expand_dims(self.array, -1)
self.path = 'numpy array' self.path = 'numpy array'
def __frame__(self, c, z, t): def __frame__(self, c, z, t):

View File

@@ -1,7 +1,6 @@
import tifffile import tifffile
import yaml import yaml
import re import re
from ndbioimage import Imread
from pathlib import Path from pathlib import Path
from functools import cached_property from functools import cached_property
from ome_types import model from ome_types import model
@@ -9,6 +8,7 @@ from ome_types._base_type import quantity_property
from itertools import product from itertools import product
from datetime import datetime from datetime import datetime
from abc import ABC from abc import ABC
from .. import Imread
def lazy_property(function, field, *arg_fields): def lazy_property(function, field, *arg_fields):

View File

@@ -1,13 +1,12 @@
from abc import ABC
from ndbioimage import Imread
import numpy as np import numpy as np
import tifffile import tifffile
import yaml import yaml
from abc import ABC
from functools import cached_property from functools import cached_property
from ome_types import model from ome_types import model
from pathlib import Path from pathlib import Path
from itertools import product from itertools import product
from .. import Imread
class Reader(Imread, ABC): class Reader(Imread, ABC):
@@ -18,7 +17,7 @@ class Reader(Imread, ABC):
def _can_open(path): def _can_open(path):
if isinstance(path, Path) and path.suffix in ('.tif', '.tiff'): if isinstance(path, Path) and path.suffix in ('.tif', '.tiff'):
with tifffile.TiffFile(path) as tif: with tifffile.TiffFile(path) as tif:
return tif.is_imagej return tif.is_imagej and tif.pages[-1]._nextifd() == 0
else: else:
return False return False

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "ndbioimage" name = "ndbioimage"
version = "2023.7.3" version = "2023.7.4"
description = "Bio image reading, metadata and some affine registration." description = "Bio image reading, metadata and some affine registration."
authors = ["W. Pomp <w.pomp@nki.nl>"] authors = ["W. Pomp <w.pomp@nki.nl>"]
license = "GPLv3" license = "GPLv3"

View File

@@ -1,10 +1,12 @@
import pytest import pytest
from pathlib import Path from pathlib import Path
from ndbioimage import Imread from ndbioimage import Imread, ReaderNotFoundError
@pytest.mark.parametrize("file", @pytest.mark.parametrize("file", (Path(__file__).parent / 'files').iterdir())
[file for file in (Path(__file__).parent / 'files').iterdir() if file.suffix != '.pzl'])
def test_open(file): def test_open(file):
try:
with Imread(file) as im: with Imread(file) as im:
print(im[dict(c=0, z=0, t=0)].mean()) print(im[dict(c=0, z=0, t=0)].mean())
except ReaderNotFoundError:
assert len(Imread.__subclasses__()), "No subclasses for Imread found."