From 41658bea79c5ebecf43f041bdff3ec70bd8804ac Mon Sep 17 00:00:00 2001 From: Wim Pomp Date: Tue, 2 Apr 2024 18:23:28 +0200 Subject: [PATCH] - replace "" with '' - bugfix in seqread finding images with pattern 00-Pos_000_000 - cache ome metadata - detect faulty time delta data in czi files - read ome from path.ome.xml if this file exists - add extract-ome command line option --- ndbioimage/__init__.py | 131 +++++++++--- ndbioimage/readers/cziread.py | 367 +++++++++++++++++---------------- ndbioimage/readers/fijiread.py | 12 +- ndbioimage/readers/ndread.py | 8 +- ndbioimage/readers/seqread.py | 70 +++---- ndbioimage/readers/tifread.py | 27 +-- ndbioimage/transforms.py | 4 +- pyproject.toml | 4 +- tests/test_open.py | 1 + tests/test_slicing.py | 1 + tests/test_ufuncs.py | 1 + 11 files changed, 355 insertions(+), 271 deletions(-) diff --git a/ndbioimage/__init__.py b/ndbioimage/__init__.py index 8d2a43f..1ca58c7 100755 --- a/ndbioimage/__init__.py +++ b/ndbioimage/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import multiprocessing import re import warnings @@ -12,13 +14,14 @@ from numbers import Number from operator import truediv from pathlib import Path from traceback import print_exc +from typing import Any, Callable, Mapping, Optional import numpy as np import ome_types import yaml -from ome_types import model, ureg, OME +from ome_types import OME, model, ureg from pint import set_application_registry -from tiffwrite import IJTiffFile +from tiffwrite import IFD, IJTiffFile from tqdm.auto import tqdm from .jvm import JVM @@ -48,34 +51,34 @@ class ReaderNotFoundError(Exception): class TransformTiff(IJTiffFile): """ transform frames in a parallel process to speed up saving """ - def __init__(self, image, *args, **kwargs): + def __init__(self, image: Imread, *args: Any, **kwargs: Any) -> None: self.image = image super().__init__(*args, **kwargs) - def compress_frame(self, frame): + def compress_frame(self, frame: tuple[int, int, int]) -> tuple[IFD, tuple[list[int], list[int]]]: return super().compress_frame(np.asarray(self.image(*frame)).astype(self.dtype)) class DequeDict(OrderedDict): - def __init__(self, maxlen=None, *args, **kwargs): + def __init__(self, maxlen: int = None, *args: Any, **kwargs: Any) -> None: self.maxlen = maxlen super().__init__(*args, **kwargs) - def __truncate__(self): + def __truncate__(self) -> None: if self.maxlen is not None: while len(self) > self.maxlen: self.popitem(False) - def __setitem__(self, *args, **kwargs): + def __setitem__(self, *args: Any, **kwargs: Any) -> None: super().__setitem__(*args, **kwargs) self.__truncate__() - def update(self, *args, **kwargs): + def update(self, *args: Any, **kwargs: Any) -> None: super().update(*args, **kwargs) self.__truncate__() -def find(obj, **kwargs): +def find(obj: Mapping, **kwargs: Any) -> Any: for item in obj: try: if all([getattr(item, key) == value for key, value in kwargs.items()]): @@ -84,14 +87,14 @@ def find(obj, **kwargs): pass -def try_default(fun, default, *args, **kwargs): +def try_default(fun: Callable, default: Any, *args: Any, **kwargs: Any) -> Any: try: return fun(*args, **kwargs) except Exception: # noqa return default -def get_ome(path): +def bioformats_ome(path: str | Path) -> OME: from .readers.bfread import jars try: jvm = JVM(jars) # noqa @@ -109,24 +112,70 @@ def get_ome(path): class Shape(tuple): - def __new__(cls, shape, axes='yxczt'): + def __new__(cls, shape: tuple[int] | Shape[int], axes: str = 'yxczt') -> Shape[int]: if isinstance(shape, Shape): - axes = shape.axes + axes = shape.axes # type: ignore instance = super().__new__(cls, shape) instance.axes = axes.lower() - return instance + return instance # type: ignore - def __getitem__(self, n): + def __getitem__(self, n: int | str) -> int | tuple[int]: if isinstance(n, str): if len(n) == 1: return self[self.axes.find(n.lower())] if n.lower() in self.axes else 1 else: - return tuple(self[i] for i in n) + return tuple(self[i] for i in n) # type: ignore return super().__getitem__(n) @cached_property - def yxczt(self): - return tuple(self[i] for i in 'yxczt') + def yxczt(self) -> tuple[int, int, int, int, int]: + return tuple(self[i] for i in 'yxczt') # type: ignore + + +class CachedPath(Path): + """ helper class for checking whether a file has changed, used by OmeCache """ + + def __init__(self, path: Path | str) -> None: + super().__init__(path) + if self.exists(): + self._lstat = super().lstat() # save file metadata like creation time etc. + else: + self._lstat = None + + def __eq__(self, other: Path | CachedPath) -> bool: + return super().__eq__(other) and self.lstat() == other.lstat() + + def __hash__(self) -> int: + return hash((super().__hash__(), self.lstat())) + + def lstat(self): + return self._lstat + + +class OmeCache(DequeDict): + """ prevent (potentially expensive) rereading of ome data by caching """ + + instance = None + + def __new__(cls) -> OmeCache: + if cls.instance is None: + cls.instance = super().__new__(cls) + return cls.instance + + def __init__(self) -> None: + super().__init__(64) + + def __reduce__(self) -> tuple[type, tuple]: + return self.__class__, () + + def __getitem__(self, item: Path | CachedPath) -> OME: + return super().__getitem__(CachedPath(item)) + + def __setitem__(self, key: Path | CachedPath, value: OME) -> None: + super().__setitem__(CachedPath(key), value) + + def __contains__(self, item: Path | CachedPath) -> bool: + return super().__contains__(CachedPath(item)) class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC): @@ -246,7 +295,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC): def __getitem__(self, n): """ slice like a numpy array but return an Imread instance """ if self.isclosed: - raise OSError("file is closed") + raise OSError('file is closed') if isinstance(n, (slice, Number)): # None = : n = (n,) elif isinstance(n, type(Ellipsis)): @@ -520,11 +569,11 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC): @property def summary(self): """ gives a helpful summary of the recorded experiment """ - s = [f"path/filename: {self.path}", - f"series/pos: {self.series}", + s = [f'path/filename: {self.path}', + f'series/pos: {self.series}', f"reader: {self.base.__class__.__module__.split('.')[-1]}"] - s.extend((f"dtype: {self.dtype}", - f"shape ({self.axes}):".ljust(15) + f"{' x '.join(str(i) for i in self.shape)}")) + s.extend((f'dtype: {self.dtype}', + f'shape ({self.axes}):'.ljust(15) + f"{' x '.join(str(i) for i in self.shape)}")) if self.pxsize_um: s.append(f'pixel size: {1000 * self.pxsize_um:.2f} nm') if self.zstack and self.deltaz_um: @@ -818,11 +867,13 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC): return [self.get_channel(c) for c in czt[0]], *czt[1:] @staticmethod - def get_ome(path: [str, Path]) -> OME: + def bioformats_ome(path: [str, Path]) -> OME: """ Use java BioFormats to make an ome metadata structure. """ with multiprocessing.get_context('spawn').Pool(1) as pool: - ome = pool.map(get_ome, (path,))[0] + return pool.map(bioformats_ome, (path,))[0] + @staticmethod + def fix_ome(ome: OME) -> OME: # fix ome if necessary for image in ome.images: try: @@ -838,9 +889,25 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC): pass return ome + @staticmethod + def read_ome(path: [str, Path]) -> Optional[OME]: + path = Path(path) + if path.with_suffix('.ome.xml').exists(): + return OME.from_xml(path.with_suffix('.ome.xml')) + + def get_ome(self) -> OME: + """ overload this """ + return self.bioformats_ome(self.path) + @cached_property def ome(self) -> OME: - return self.get_ome(self.path) + cache = OmeCache() + if self.path not in cache: + ome = self.read_ome(self.path) + if ome is None: + ome = self.get_ome() + cache[self.path] = self.fix_ome(ome) + return cache[self.path] def is_noise(self, volume=None): """ True if volume only has noise """ @@ -885,7 +952,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC): shape = [len(i) for i in n] with TransformTiff(self, fname.with_suffix('.tif'), shape, pixel_type, - pxsize=self.pxsize_um, deltaz=self.deltaz_um, **kwargs) 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)), # noqa total=np.prod(shape), desc='Saving tiff', disable=not bar): tif.save(m, *i) @@ -1000,7 +1067,7 @@ class AbstractReader(Imread, metaclass=ABCMeta): self.open() # extract some metadata from ome instrument = self.ome.instruments[0] if self.ome.instruments else None - image = self.ome.images[self.series] + image = self.ome.images[self.series if len(self.ome.images) > 1 else 0] pixels = image.pixels self.shape = pixels.size_y, pixels.size_x, pixels.size_c, pixels.size_z, pixels.size_t self.dtype = pixels.type.value if dtype is None else dtype @@ -1016,8 +1083,8 @@ class AbstractReader(Imread, metaclass=ABCMeta): self.deltaz_um = None if self.deltaz is None else self.deltaz.to(self.ureg.um).m else: self.deltaz = self.deltaz_um = None - if self.ome.images[self.series].objective_settings: - self.objective = find(instrument.objectives, id=self.ome.images[self.series].objective_settings.id) + if image.objective_settings: + self.objective = find(instrument.objectives, id=image.objective_settings.id) else: self.objective = None try: @@ -1130,6 +1197,7 @@ def main(): parser = ArgumentParser(description='Display info and save as tif') parser.add_argument('file', help='image_file') parser.add_argument('out', help='path to tif out', type=str, default=None, nargs='?') + parser.add_argument('-o', '--extract_ome', help='extract ome to xml file', action='store_true') parser.add_argument('-r', '--register', help='register channels', action='store_true') parser.add_argument('-c', '--channel', help='channel', type=int, default=None) parser.add_argument('-z', '--zslice', help='z-slice', type=int, default=None) @@ -1149,6 +1217,9 @@ def main(): print(f'File {args.out} exists already, add the -f flag if you want to overwrite it.') else: im.save_as_tiff(out, args.channel, args.zslice, args.time, args.split) + if args.extract_ome: + with open(im.path.with_suffix('.ome.xml'), 'w') as f: + f.write(im.ome.to_xml()) from .readers import * diff --git a/ndbioimage/readers/cziread.py b/ndbioimage/readers/cziread.py index fe22fce..ea6319f 100644 --- a/ndbioimage/readers/cziread.py +++ b/ndbioimage/readers/cziread.py @@ -1,6 +1,6 @@ import re +import warnings from abc import ABC -from functools import cached_property from io import BytesIO from itertools import product from pathlib import Path @@ -173,18 +173,17 @@ class Reader(AbstractReader, ABC): def close(self): self.reader.close() - @cached_property - def ome(self): + def get_ome(self): xml = self.reader.metadata() attachments = {i.attachment_entry.name: i.attachment_entry.data_segment() for i in self.reader.attachments()} tree = etree.fromstring(xml) - metadata = tree.find("Metadata") - version = metadata.find("Version") + metadata = tree.find('Metadata') + version = metadata.find('Version') if version is not None: version = version.text else: - version = metadata.find("Experiment").attrib["Version"] + version = metadata.find('Experiment').attrib['Version'] if version == '1.0': return self.ome_10(tree, attachments) @@ -200,58 +199,58 @@ class Reader(AbstractReader, ABC): ome = model.OME() - metadata = tree.find("Metadata") + metadata = tree.find('Metadata') - information = metadata.find("Information") - display_setting = metadata.find("DisplaySetting") - ome.experimenters = [model.Experimenter(id="Experimenter:0", - user_name=information.find("Document").find("UserName").text)] + information = metadata.find('Information') + display_setting = metadata.find('DisplaySetting') + ome.experimenters = [model.Experimenter(id='Experimenter:0', + user_name=information.find('Document').find('UserName').text)] - instrument = information.find("Instrument") - for _ in instrument.find("Microscopes"): + instrument = information.find('Instrument') + for _ in instrument.find('Microscopes'): ome.instruments.append(model.Instrument(id='Instrument:0')) - for detector in instrument.find("Detectors"): + for detector in instrument.find('Detectors'): try: - detector_type = model.Detector_Type(text(detector.find("Type")).upper() or "") + detector_type = model.Detector_Type(text(detector.find('Type')).upper() or "") except ValueError: detector_type = model.Detector_Type.OTHER ome.instruments[0].detectors.append( model.Detector( - id=detector.attrib["Id"].replace(' ', ''), model=text(detector.find("Manufacturer").find("Model")), + id=detector.attrib['Id'].replace(' ', ''), model=text(detector.find('Manufacturer').find('Model')), type=detector_type )) - for objective in instrument.find("Objectives"): + for objective in instrument.find('Objectives'): ome.instruments[0].objectives.append( model.Objective( - id=objective.attrib["Id"], - model=text(objective.find("Manufacturer").find("Model")), - immersion=text(objective.find("Immersion")), - lens_na=float(text(objective.find("LensNA"))), - nominal_magnification=float(text(objective.find("NominalMagnification"))))) + id=objective.attrib['Id'], + model=text(objective.find('Manufacturer').find('Model')), + immersion=text(objective.find('Immersion')), + lens_na=float(text(objective.find('LensNA'))), + nominal_magnification=float(text(objective.find('NominalMagnification'))))) - for tubelens in instrument.find("TubeLenses"): + for tubelens in instrument.find('TubeLenses'): try: nominal_magnification = float(re.findall(r'\d+(?:[,.]\d*)?', - tubelens.attrib["Name"])[0].replace(',', '.')) + tubelens.attrib['Name'])[0].replace(',', '.')) except Exception: nominal_magnification = 1.0 ome.instruments[0].objectives.append( model.Objective( - id=f'Objective:{tubelens.attrib["Id"]}', - model=tubelens.attrib["Name"], + id=f"Objective:{tubelens.attrib['Id']}", + model=tubelens.attrib['Name'], nominal_magnification=nominal_magnification)) - for light_source in def_list(instrument.find("LightSources")): - if light_source.find("LightSourceType").find("Laser") is not None: + for light_source in def_list(instrument.find('LightSources')): + if light_source.find('LightSourceType').find('Laser') is not None: ome.instruments[0].lasers.append( model.Laser( - id=f'LightSource:{light_source.attrib["Id"]}', - power=float(text(light_source.find("Power"))), - wavelength=float(light_source.attrib["Id"][-3:]))) + id=f"LightSource:{light_source.attrib['Id']}", + power=float(text(light_source.find('Power'))), + wavelength=float(light_source.attrib['Id'][-3:]))) x_min = min([f.start[f.axes.index('X')] for f in self.filedict[0, 0, 0]]) y_min = min([f.start[f.axes.index('Y')] for f in self.filedict[0, 0, 0]]) @@ -262,14 +261,14 @@ class Reader(AbstractReader, ABC): size_c, size_z, size_t = (self.reader.shape[self.reader.axes.index(directory_entry)] for directory_entry in 'CZT') - image = information.find("Image") - pixel_type = text(image.find("PixelType"), "Gray16") - if pixel_type.startswith("Gray"): - pixel_type = "uint" + pixel_type[4:] - objective_settings = image.find("ObjectiveSettings") + image = information.find('Image') + pixel_type = text(image.find('PixelType'), 'Gray16') + if pixel_type.startswith('Gray'): + pixel_type = 'uint' + pixel_type[4:] + objective_settings = image.find('ObjectiveSettings') try: # TODO - scenes = image.find("Dimensions").find("S").find("Scenes") - center_position = [float(pos) for pos in text(scenes[0].find("CenterPosition")).split(',')] + scenes = image.find('Dimensions').find('S').find('Scenes') + center_position = [float(pos) for pos in text(scenes[0].find('CenterPosition')).split(',')] except AttributeError: center_position = [0, 0] um = model.UnitsLength.MICROMETER @@ -277,97 +276,103 @@ class Reader(AbstractReader, ABC): ome.images.append( model.Image( - id="Image:0", - name=f'{text(information.find("Document").find("Name"))} #1', + id='Image:0', + name=f"{text(information.find('Document').find('Name'))} #1", pixels=model.Pixels( - id="Pixels:0", size_x=size_x, size_y=size_y, + id='Pixels:0', size_x=size_x, size_y=size_y, size_c=size_c, size_z=size_z, size_t=size_t, - dimension_order="XYCZT", type=pixel_type, - significant_bits=int(text(image.find("ComponentBitCount"))), + dimension_order='XYCZT', type=pixel_type, + significant_bits=int(text(image.find('ComponentBitCount'))), big_endian=False, interleaved=False, metadata_only=True), experimenter_ref=model.ExperimenterRef(id='Experimenter:0'), instrument_ref=model.InstrumentRef(id='Instrument:0'), objective_settings=model.ObjectiveSettings( - id=objective_settings.find("ObjectiveRef").attrib["Id"], - medium=text(objective_settings.find("Medium")), - refractive_index=float(text(objective_settings.find("RefractiveIndex")))), + id=objective_settings.find('ObjectiveRef').attrib['Id'], + medium=text(objective_settings.find('Medium')), + refractive_index=float(text(objective_settings.find('RefractiveIndex')))), stage_label=model.StageLabel( - name=f"Scene position #0", + name=f'Scene position #0', x=center_position[0], x_unit=um, y=center_position[1], y_unit=um))) - for distance in metadata.find("Scaling").find("Items"): - if distance.attrib["Id"] == "X": - ome.images[0].pixels.physical_size_x = float(text(distance.find("Value"))) * 1e6 - elif distance.attrib["Id"] == "Y": - ome.images[0].pixels.physical_size_y = float(text(distance.find("Value"))) * 1e6 - elif size_z > 1 and distance.attrib["Id"] == "Z": - ome.images[0].pixels.physical_size_z = float(text(distance.find("Value"))) * 1e6 + for distance in metadata.find('Scaling').find('Items'): + if distance.attrib['Id'] == 'X': + ome.images[0].pixels.physical_size_x = float(text(distance.find('Value'))) * 1e6 + elif distance.attrib['Id'] == 'Y': + ome.images[0].pixels.physical_size_y = float(text(distance.find('Value'))) * 1e6 + elif size_z > 1 and distance.attrib['Id'] == 'Z': + ome.images[0].pixels.physical_size_z = float(text(distance.find('Value'))) * 1e6 - channels_im = {channel.attrib["Id"]: channel for channel in image.find("Dimensions").find("Channels")} - channels_ds = {channel.attrib["Id"]: channel for channel in display_setting.find("Channels")} + channels_im = {channel.attrib['Id']: channel for channel in image.find('Dimensions').find('Channels')} + channels_ds = {channel.attrib['Id']: channel for channel in display_setting.find('Channels')} for idx, (key, channel) in enumerate(channels_im.items()): - detector_settings = channel.find("DetectorSettings") - laser_scan_info = channel.find("LaserScanInfo") - detector = detector_settings.find("Detector") + detector_settings = channel.find('DetectorSettings') + laser_scan_info = channel.find('LaserScanInfo') + detector = detector_settings.find('Detector') try: - binning = model.Binning(text(detector_settings.find("Binning"))) + binning = model.Binning(text(detector_settings.find('Binning'))) except ValueError: binning = model.Binning.OTHER - light_sources_settings = channel.find("LightSourcesSettings") + light_sources_settings = channel.find('LightSourcesSettings') # no space in ome for multiple lightsources simultaneously if light_sources_settings is not None: light_source_settings = light_sources_settings[0] light_source_settings = model.LightSourceSettings( - id="LightSource:" + "_".join([light_source_settings.find("LightSource").attrib["Id"] + id='LightSource:' + '_'.join([light_source_settings.find('LightSource').attrib['Id'] for light_source_settings in light_sources_settings]), - attenuation=float(text(light_source_settings.find("Attenuation"))), - wavelength=float(text(light_source_settings.find("Wavelength"))), + attenuation=float(text(light_source_settings.find('Attenuation'))), + wavelength=float(text(light_source_settings.find('Wavelength'))), wavelength_unit=nm) else: light_source_settings = None ome.images[0].pixels.channels.append( model.Channel( - id=f"Channel:{idx}", - name=channel.attrib["Name"], - acquisition_mode=text(channel.find("AcquisitionMode")).replace('SingleMoleculeLocalisation', + id=f'Channel:{idx}', + name=channel.attrib['Name'], + acquisition_mode=text(channel.find('AcquisitionMode')).replace('SingleMoleculeLocalisation', 'SingleMoleculeImaging'), - color=model.Color(text(channels_ds[channel.attrib["Id"]].find("Color"), 'white')), + color=model.Color(text(channels_ds[channel.attrib['Id']].find('Color'), 'white')), detector_settings=model.DetectorSettings( - id=detector.attrib["Id"].replace(" ", ""), + id=detector.attrib['Id'].replace(' ', ""), binning=binning), - emission_wavelength=i if (i := text(channel.find("EmissionWavelength"))) != '0' else '100', - excitation_wavelength=text(channel.find("ExcitationWavelength")), + emission_wavelength=i if (i := text(channel.find('EmissionWavelength'))) != '0' else '100', + excitation_wavelength=text(channel.find('ExcitationWavelength')), # filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id), - illumination_type=text(channel.find("IlluminationType")), + illumination_type=text(channel.find('IlluminationType')), light_source_settings=light_source_settings, - samples_per_pixel=int(text(laser_scan_info.find("Averaging"), "1")))) + samples_per_pixel=int(text(laser_scan_info.find('Averaging'), '1')))) - exposure_times = [float(text(channel.find("LaserScanInfo").find("FrameTime"), "100")) for channel in + exposure_times = [float(text(channel.find('LaserScanInfo').find('FrameTime'), '100')) for channel in channels_im.values()] delta_ts = attachments['TimeStamps'].data() + dt = np.diff(delta_ts) + if np.std(dt) / np.mean(dt) > 0.02: + dt = np.median(dt[dt > 0]) + delta_ts = dt * np.arange(len(delta_ts)) + warnings.warn(f'delta_t is inconsistent, using median value: {dt}') + for t, z, c in product(range(size_t), range(size_z), range(size_c)): ome.images[0].pixels.planes.append( model.Plane(the_c=c, the_z=z, the_t=t, delta_t=delta_ts[t], exposure_time=exposure_times[c])) idx = 0 - for layer in [] if (ml := metadata.find("Layers")) is None else ml: - rectangle = layer.find("Elements").find("Rectangle") + for layer in [] if (ml := metadata.find('Layers')) is None else ml: + rectangle = layer.find('Elements').find('Rectangle') if rectangle is not None: - geometry = rectangle.find("Geometry") - roi = model.ROI(id=f"ROI:{idx}", description=text(layer.find("Usage"))) + geometry = rectangle.find('Geometry') + roi = model.ROI(id=f'ROI:{idx}', description=text(layer.find('Usage'))) roi.union.append( model.Rectangle( id='Shape:0:0', - height=float(text(geometry.find("Height"))), - width=float(text(geometry.find("Width"))), - x=float(text(geometry.find("Left"))), - y=float(text(geometry.find("Top"))))) + height=float(text(geometry.find('Height'))), + width=float(text(geometry.find('Width'))), + x=float(text(geometry.find('Left'))), + y=float(text(geometry.find('Top'))))) ome.rois.append(roi) - ome.images[0].roi_refs.append(model.ROIRef(id=f"ROI:{idx}")) + ome.images[0].roi_refs.append(model.ROIRef(id=f'ROI:{idx}')) idx += 1 return ome @@ -380,71 +385,71 @@ class Reader(AbstractReader, ABC): ome = model.OME() - metadata = tree.find("Metadata") + metadata = tree.find('Metadata') - information = metadata.find("Information") - display_setting = metadata.find("DisplaySetting") - experiment = metadata.find("Experiment") - acquisition_block = experiment.find("ExperimentBlocks").find("AcquisitionBlock") + information = metadata.find('Information') + display_setting = metadata.find('DisplaySetting') + experiment = metadata.find('Experiment') + acquisition_block = experiment.find('ExperimentBlocks').find('AcquisitionBlock') - ome.experimenters = [model.Experimenter(id="Experimenter:0", - user_name=information.find("User").find("DisplayName").text)] + ome.experimenters = [model.Experimenter(id='Experimenter:0', + user_name=information.find('User').find('DisplayName').text)] - instrument = information.find("Instrument") - ome.instruments.append(model.Instrument(id=instrument.attrib["Id"])) + instrument = information.find('Instrument') + ome.instruments.append(model.Instrument(id=instrument.attrib['Id'])) - for detector in instrument.find("Detectors"): + for detector in instrument.find('Detectors'): try: - detector_type = model.Detector_Type(text(detector.find("Type")).upper() or "") + detector_type = model.Detector_Type(text(detector.find('Type')).upper() or "") except ValueError: detector_type = model.Detector_Type.OTHER ome.instruments[0].detectors.append( model.Detector( - id=detector.attrib["Id"], model=text(detector.find("Manufacturer").find("Model")), - amplification_gain=float(text(detector.find("AmplificationGain"))), - gain=float(text(detector.find("Gain"))), zoom=float(text(detector.find("Zoom"))), + id=detector.attrib['Id'], model=text(detector.find('Manufacturer').find('Model')), + amplification_gain=float(text(detector.find('AmplificationGain'))), + gain=float(text(detector.find('Gain'))), zoom=float(text(detector.find('Zoom'))), type=detector_type )) - for objective in instrument.find("Objectives"): + for objective in instrument.find('Objectives'): ome.instruments[0].objectives.append( model.Objective( - id=objective.attrib["Id"], - model=text(objective.find("Manufacturer").find("Model")), - immersion=text(objective.find("Immersion")), - lens_na=float(text(objective.find("LensNA"))), - nominal_magnification=float(text(objective.find("NominalMagnification"))))) + id=objective.attrib['Id'], + model=text(objective.find('Manufacturer').find('Model')), + immersion=text(objective.find('Immersion')), + lens_na=float(text(objective.find('LensNA'))), + nominal_magnification=float(text(objective.find('NominalMagnification'))))) - for light_source in def_list(instrument.find("LightSources")): - if light_source.find("LightSourceType").find("Laser") is not None: + for light_source in def_list(instrument.find('LightSources')): + if light_source.find('LightSourceType').find('Laser') is not None: ome.instruments[0].lasers.append( model.Laser( - id=light_source.attrib["Id"], - model=text(light_source.find("Manufacturer").find("Model")), - power=float(text(light_source.find("Power"))), + id=light_source.attrib['Id'], + model=text(light_source.find('Manufacturer').find('Model')), + power=float(text(light_source.find('Power'))), wavelength=float( - text(light_source.find("LightSourceType").find("Laser").find("Wavelength"))))) + text(light_source.find('LightSourceType').find('Laser').find('Wavelength'))))) - multi_track_setup = acquisition_block.find("MultiTrackSetup") - for idx, tube_lens in enumerate({text(track_setup.find("TubeLensPosition")) + multi_track_setup = acquisition_block.find('MultiTrackSetup') + for idx, tube_lens in enumerate({text(track_setup.find('TubeLensPosition')) for track_setup in multi_track_setup}): ome.instruments[0].objectives.append( - model.Objective(id=f"Objective:Tubelens:{idx}", model=tube_lens, + model.Objective(id=f'Objective:Tubelens:{idx}', model=tube_lens, nominal_magnification=float( re.findall(r'\d+[,.]\d*', tube_lens)[0].replace(',', '.')) )) - for idx, filter_ in enumerate({text(beam_splitter.find("Filter")) + for idx, filter_ in enumerate({text(beam_splitter.find('Filter')) for track_setup in multi_track_setup - for beam_splitter in track_setup.find("BeamSplitters")}): + for beam_splitter in track_setup.find('BeamSplitters')}): ome.instruments[0].filter_sets.append( - model.FilterSet(id=f"FilterSet:{idx}", model=filter_) + model.FilterSet(id=f'FilterSet:{idx}', model=filter_) ) - for idx, collimator in enumerate({text(track_setup.find("FWFOVPosition")) + for idx, collimator in enumerate({text(track_setup.find('FWFOVPosition')) for track_setup in multi_track_setup}): - ome.instruments[0].filters.append(model.Filter(id=f"Filter:Collimator:{idx}", model=collimator)) + ome.instruments[0].filters.append(model.Filter(id=f'Filter:Collimator:{idx}', model=collimator)) x_min = min([f.start[f.axes.index('X')] for f in self.filedict[0, 0, 0]]) y_min = min([f.start[f.axes.index('Y')] for f in self.filedict[0, 0, 0]]) @@ -455,117 +460,123 @@ class Reader(AbstractReader, ABC): size_c, size_z, size_t = (self.reader.shape[self.reader.axes.index(directory_entry)] for directory_entry in 'CZT') - image = information.find("Image") - pixel_type = text(image.find("PixelType"), "Gray16") - if pixel_type.startswith("Gray"): - pixel_type = "uint" + pixel_type[4:] - objective_settings = image.find("ObjectiveSettings") - scenes = image.find("Dimensions").find("S").find("Scenes") - positions = scenes[0].find("Positions")[0] + image = information.find('Image') + pixel_type = text(image.find('PixelType'), 'Gray16') + if pixel_type.startswith('Gray'): + pixel_type = 'uint' + pixel_type[4:] + objective_settings = image.find('ObjectiveSettings') + scenes = image.find('Dimensions').find('S').find('Scenes') + positions = scenes[0].find('Positions')[0] um = model.UnitsLength.MICROMETER nm = model.UnitsLength.NANOMETER ome.images.append( model.Image( - id="Image:0", - name=f'{text(information.find("Document").find("Name"))} #1', + id='Image:0', + name=f"{text(information.find('Document').find('Name'))} #1", pixels=model.Pixels( - id="Pixels:0", size_x=size_x, size_y=size_y, + id='Pixels:0', size_x=size_x, size_y=size_y, size_c=size_c, size_z=size_z, size_t=size_t, - dimension_order="XYCZT", type=pixel_type, - significant_bits=int(text(image.find("ComponentBitCount"))), + dimension_order='XYCZT', type=pixel_type, + significant_bits=int(text(image.find('ComponentBitCount'))), big_endian=False, interleaved=False, metadata_only=True), experimenter_ref=model.ExperimenterRef(id='Experimenter:0'), instrument_ref=model.InstrumentRef(id='Instrument:0'), objective_settings=model.ObjectiveSettings( - id=objective_settings.find("ObjectiveRef").attrib["Id"], - medium=text(objective_settings.find("Medium")), - refractive_index=float(text(objective_settings.find("RefractiveIndex")))), + id=objective_settings.find('ObjectiveRef').attrib['Id'], + medium=text(objective_settings.find('Medium')), + refractive_index=float(text(objective_settings.find('RefractiveIndex')))), stage_label=model.StageLabel( - name=f"Scene position #0", - x=float(positions.attrib["X"]), x_unit=um, - y=float(positions.attrib["Y"]), y_unit=um, - z=float(positions.attrib["Z"]), z_unit=um))) + name=f'Scene position #0', + x=float(positions.attrib['X']), x_unit=um, + y=float(positions.attrib['Y']), y_unit=um, + z=float(positions.attrib['Z']), z_unit=um))) - for distance in metadata.find("Scaling").find("Items"): - if distance.attrib["Id"] == "X": - ome.images[0].pixels.physical_size_x = float(text(distance.find("Value"))) * 1e6 - elif distance.attrib["Id"] == "Y": - ome.images[0].pixels.physical_size_y = float(text(distance.find("Value"))) * 1e6 - elif size_z > 1 and distance.attrib["Id"] == "Z": - ome.images[0].pixels.physical_size_z = float(text(distance.find("Value"))) * 1e6 + for distance in metadata.find('Scaling').find('Items'): + if distance.attrib['Id'] == 'X': + ome.images[0].pixels.physical_size_x = float(text(distance.find('Value'))) * 1e6 + elif distance.attrib['Id'] == 'Y': + ome.images[0].pixels.physical_size_y = float(text(distance.find('Value'))) * 1e6 + elif size_z > 1 and distance.attrib['Id'] == 'Z': + ome.images[0].pixels.physical_size_z = float(text(distance.find('Value'))) * 1e6 - channels_im = {channel.attrib["Id"]: channel for channel in image.find("Dimensions").find("Channels")} - channels_ds = {channel.attrib["Id"]: channel for channel in display_setting.find("Channels")} - channels_ts = {detector.attrib["Id"]: track_setup + channels_im = {channel.attrib['Id']: channel for channel in image.find('Dimensions').find('Channels')} + channels_ds = {channel.attrib['Id']: channel for channel in display_setting.find('Channels')} + channels_ts = {detector.attrib['Id']: track_setup for track_setup in - experiment.find("ExperimentBlocks").find("AcquisitionBlock").find("MultiTrackSetup") - for detector in track_setup.find("Detectors")} + experiment.find('ExperimentBlocks').find('AcquisitionBlock').find('MultiTrackSetup') + for detector in track_setup.find('Detectors')} for idx, (key, channel) in enumerate(channels_im.items()): - detector_settings = channel.find("DetectorSettings") - laser_scan_info = channel.find("LaserScanInfo") - detector = detector_settings.find("Detector") + detector_settings = channel.find('DetectorSettings') + laser_scan_info = channel.find('LaserScanInfo') + detector = detector_settings.find('Detector') try: - binning = model.Binning(text(detector_settings.find("Binning"))) + binning = model.Binning(text(detector_settings.find('Binning'))) except ValueError: binning = model.Binning.OTHER - filterset = text(channels_ts[key].find("BeamSplitters")[0].find("Filter")) + filterset = text(channels_ts[key].find('BeamSplitters')[0].find('Filter')) filterset_idx = [filterset.model for filterset in ome.instruments[0].filter_sets].index(filterset) - light_sources_settings = channel.find("LightSourcesSettings") + light_sources_settings = channel.find('LightSourcesSettings') # no space in ome for multiple lightsources simultaneously if len(light_sources_settings) > idx: light_source_settings = light_sources_settings[idx] else: light_source_settings = light_sources_settings[0] light_source_settings = model.LightSourceSettings( - id=light_source_settings.find("LightSource").attrib["Id"], - attenuation=float(text(light_source_settings.find("Attenuation"))), - wavelength=float(text(light_source_settings.find("Wavelength"))), + id=light_source_settings.find('LightSource').attrib['Id'], + attenuation=float(text(light_source_settings.find('Attenuation'))), + wavelength=float(text(light_source_settings.find('Wavelength'))), wavelength_unit=nm) ome.images[0].pixels.channels.append( model.Channel( - id=f"Channel:{idx}", - name=channel.attrib["Name"], - acquisition_mode=text(channel.find("AcquisitionMode")), - color=model.Color(text(channels_ds[channel.attrib["Id"]].find("Color"), 'white')), - detector_settings=model.DetectorSettings(id=detector.attrib["Id"], binning=binning), - # emission_wavelength=text(channel.find("EmissionWavelength")), # TODO: fix + id=f'Channel:{idx}', + name=channel.attrib['Name'], + acquisition_mode=text(channel.find('AcquisitionMode')), + color=model.Color(text(channels_ds[channel.attrib['Id']].find('Color'), 'white')), + detector_settings=model.DetectorSettings(id=detector.attrib['Id'], binning=binning), + # emission_wavelength=text(channel.find('EmissionWavelength')), # TODO: fix excitation_wavelength=light_source_settings.wavelength, filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id), - illumination_type=text(channel.find("IlluminationType")), + illumination_type=text(channel.find('IlluminationType')), light_source_settings=light_source_settings, - samples_per_pixel=int(text(laser_scan_info.find("Averaging"))))) + samples_per_pixel=int(text(laser_scan_info.find('Averaging'))))) - exposure_times = [float(text(channel.find("LaserScanInfo").find("FrameTime"))) for channel in + exposure_times = [float(text(channel.find('LaserScanInfo').find('FrameTime'))) for channel in channels_im.values()] delta_ts = attachments['TimeStamps'].data() + dt = np.diff(delta_ts) + if np.std(dt) / np.mean(dt) > 0.02: + dt = np.median(dt[dt > 0]) + delta_ts = dt * np.arange(len(delta_ts)) + warnings.warn(f'delta_t is inconsistent, using median value: {dt}') + for t, z, c in product(range(size_t), range(size_z), range(size_c)): ome.images[0].pixels.planes.append( model.Plane(the_c=c, the_z=z, the_t=t, delta_t=delta_ts[t], exposure_time=exposure_times[c], - position_x=float(positions.attrib["X"]), position_x_unit=um, - position_y=float(positions.attrib["Y"]), position_y_unit=um, - position_z=float(positions.attrib["Z"]), position_z_unit=um)) + position_x=float(positions.attrib['X']), position_x_unit=um, + position_y=float(positions.attrib['Y']), position_y_unit=um, + position_z=float(positions.attrib['Z']), position_z_unit=um)) idx = 0 - for layer in [] if (ml := metadata.find("Layers")) is None else ml: - rectangle = layer.find("Elements").find("Rectangle") + for layer in [] if (ml := metadata.find('Layers')) is None else ml: + rectangle = layer.find('Elements').find('Rectangle') if rectangle is not None: - geometry = rectangle.find("Geometry") - roi = model.ROI(id=f"ROI:{idx}", description=text(layer.find("Usage"))) + geometry = rectangle.find('Geometry') + roi = model.ROI(id=f'ROI:{idx}', description=text(layer.find('Usage'))) roi.union.append( model.Rectangle( id='Shape:0:0', - height=float(text(geometry.find("Height"))), - width=float(text(geometry.find("Width"))), - x=float(text(geometry.find("Left"))), - y=float(text(geometry.find("Top"))))) + height=float(text(geometry.find('Height'))), + width=float(text(geometry.find('Width'))), + x=float(text(geometry.find('Left'))), + y=float(text(geometry.find('Top'))))) ome.rois.append(roi) - ome.images[0].roi_refs.append(model.ROIRef(id=f"ROI:{idx}")) + ome.images[0].roi_refs.append(model.ROIRef(id=f'ROI:{idx}')) idx += 1 return ome diff --git a/ndbioimage/readers/fijiread.py b/ndbioimage/readers/fijiread.py index 5534b7a..ac25671 100644 --- a/ndbioimage/readers/fijiread.py +++ b/ndbioimage/readers/fijiread.py @@ -1,5 +1,4 @@ from abc import ABC -from functools import cached_property from itertools import product from pathlib import Path from struct import unpack @@ -32,8 +31,8 @@ class Reader(AbstractReader, ABC): 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." + 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] # noqa self.count = self.reader.pages[0].databytecounts[0] # noqa self.bytes_per_sample = self.reader.pages[0].bitspersample // 8 # noqa @@ -42,8 +41,7 @@ class Reader(AbstractReader, ABC): def close(self): self.reader.close() - @cached_property - def ome(self): + def get_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)) @@ -54,8 +52,8 @@ class Reader(AbstractReader, ABC): 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"))) + 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 diff --git a/ndbioimage/readers/ndread.py b/ndbioimage/readers/ndread.py index c671fad..6869cb6 100644 --- a/ndbioimage/readers/ndread.py +++ b/ndbioimage/readers/ndread.py @@ -1,5 +1,4 @@ from abc import ABC -from functools import cached_property from itertools import product import numpy as np @@ -15,8 +14,7 @@ class Reader(AbstractReader, ABC): def _can_open(path): return isinstance(path, np.ndarray) and 1 <= path.ndim <= 5 - @cached_property - def ome(self): + def get_ome(self): def shape(size_x=1, size_y=1, size_c=1, size_z=1, size_t=1): # noqa return size_x, size_y, size_c, size_z, size_t size_x, size_y, size_c, size_z, size_t = shape(*self.array.shape) @@ -34,8 +32,8 @@ class Reader(AbstractReader, ABC): 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"))) + 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 diff --git a/ndbioimage/readers/seqread.py b/ndbioimage/readers/seqread.py index 7b9f57e..4ad97bc 100644 --- a/ndbioimage/readers/seqread.py +++ b/ndbioimage/readers/seqread.py @@ -1,7 +1,6 @@ import re from abc import ABC from datetime import datetime -from functools import cached_property from itertools import product from pathlib import Path @@ -39,7 +38,7 @@ class Plane(model.Plane): def get_delta_t(t0, file): with tifffile.TiffFile(file) as tif: info = yaml.safe_load(tif.pages[0].tags[50839].value['Info']) - return float((datetime.strptime(info["Time"], "%Y-%m-%d %H:%M:%S %z") - t0).seconds) + return float((datetime.strptime(info['Time'], '%Y-%m-%d %H:%M:%S %z') - t0).seconds) class Reader(AbstractReader, ABC): @@ -49,86 +48,87 @@ class Reader(AbstractReader, ABC): def _can_open(path): return isinstance(path, Path) and path.is_dir() - @cached_property - def ome(self): + def get_ome(self): ome = model.OME() with tifffile.TiffFile(self.filedict[0, 0, 0]) as tif: metadata = {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()} ome.experimenters.append( - model.Experimenter(id="Experimenter:0", user_name=metadata["Info"]["Summary"]["UserName"])) - objective_str = metadata["Info"]["ZeissObjectiveTurret-Label"] + model.Experimenter(id='Experimenter:0', user_name=metadata['Info']['Summary']['UserName'])) + objective_str = metadata['Info']['ZeissObjectiveTurret-Label'] ome.instruments.append(model.Instrument()) ome.instruments[0].objectives.append( model.Objective( - id="Objective:0", manufacturer="Zeiss", model=objective_str, - nominal_magnification=float(re.findall(r"(\d+)x", objective_str)[0]), - lens_na=float(re.findall(r"/(\d\.\d+)", objective_str)[0]), + id='Objective:0', manufacturer='Zeiss', model=objective_str, + nominal_magnification=float(re.findall(r'(\d+)x', objective_str)[0]), + lens_na=float(re.findall(r'/(\d\.\d+)', objective_str)[0]), immersion=model.Objective_Immersion.OIL if 'oil' in objective_str.lower() else None)) - tubelens_str = metadata["Info"]["ZeissOptovar-Label"] + tubelens_str = metadata['Info']['ZeissOptovar-Label'] ome.instruments[0].objectives.append( model.Objective( - id="Objective:Tubelens:0", manufacturer="Zeiss", model=tubelens_str, - nominal_magnification=float(re.findall(r"\d?\d*[,.]?\d+(?=x$)", tubelens_str)[0].replace(",", ".")))) + id='Objective:Tubelens:0', manufacturer='Zeiss', model=tubelens_str, + nominal_magnification=float(re.findall(r'\d?\d*[,.]?\d+(?=x$)', tubelens_str)[0].replace(',', '.')))) ome.instruments[0].detectors.append( model.Detector( - id="Detector:0", amplification_gain=100)) + id='Detector:0', amplification_gain=100)) ome.instruments[0].filter_sets.append( - model.FilterSet(id='FilterSet:0', model=metadata["Info"]["ZeissReflectorTurret-Label"])) + model.FilterSet(id='FilterSet:0', model=metadata['Info']['ZeissReflectorTurret-Label'])) - pxsize = metadata["Info"]["PixelSizeUm"] - pxsize_cam = 6.5 if 'Hamamatsu' in metadata["Info"]["Core-Camera"] else None + pxsize = metadata['Info']['PixelSizeUm'] + pxsize_cam = 6.5 if 'Hamamatsu' in metadata['Info']['Core-Camera'] else None if pxsize == 0: pxsize = pxsize_cam / ome.instruments[0].objectives[0].nominal_magnification - pixel_type = metadata["Info"]["PixelType"].lower() - if pixel_type.startswith("gray"): - pixel_type = "uint" + pixel_type[4:] + pixel_type = metadata['Info']['PixelType'].lower() + if pixel_type.startswith('gray'): + pixel_type = 'uint' + pixel_type[4:] else: - pixel_type = "uint16" # assume + pixel_type = 'uint16' # assume size_c, size_z, size_t = (max(i) + 1 for i in zip(*self.filedict.keys())) - t0 = datetime.strptime(metadata["Info"]["Time"], "%Y-%m-%d %H:%M:%S %z") + t0 = datetime.strptime(metadata['Info']['Time'], '%Y-%m-%d %H:%M:%S %z') ome.images.append( model.Image( pixels=model.Pixels( size_c=size_c, size_z=size_z, size_t=size_t, size_x=metadata['Info']['Width'], size_y=metadata['Info']['Height'], - dimension_order="XYCZT", type=pixel_type, physical_size_x=pxsize, physical_size_y=pxsize, - physical_size_z=metadata["Info"]["Summary"]["z-step_um"]), - objective_settings=model.ObjectiveSettings(id="Objective:0"))) + dimension_order='XYCZT', type=pixel_type, physical_size_x=pxsize, physical_size_y=pxsize, + physical_size_z=metadata['Info']['Summary']['z-step_um']), + 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( Plane(t0, self.filedict[c, z, t], - the_c=c, the_z=z, the_t=t, exposure_time=metadata["Info"]["Exposure-ms"] / 1000)) + the_c=c, the_z=z, the_t=t, exposure_time=metadata['Info']['Exposure-ms'] / 1000)) # compare channel names from metadata with filenames - pattern_c = re.compile(r"img_\d{3,}_(.*)_\d{3,}$") + pattern_c = re.compile(r'img_\d{3,}_(.*)_\d{3,}$') for c in range(size_c): ome.images[0].pixels.channels.append( model.Channel( - id=f"Channel:{c}", name=pattern_c.findall(self.filedict[c, 0, 0].stem)[0], + id=f'Channel:{c}', name=pattern_c.findall(self.filedict[c, 0, 0].stem)[0], detector_settings=model.DetectorSettings( - id="Detector:0", binning=metadata["Info"]["Hamamatsu_sCMOS-Binning"]), + id='Detector:0', binning=metadata['Info']['Hamamatsu_sCMOS-Binning']), filter_set_ref=model.FilterSetRef(id='FilterSet:0'))) return ome def open(self): - if re.match(r'(?:\d+-)?Pos.*', self.path.name) is None: - path = self.path / f"Pos{self.series}" + pat = re.compile(r'(?:\d+-)?Pos.*') + if pat.match(self.path.name) is None: + path = sorted(file for file in self.path.iterdir() if pat.match(file.name))[self.series] else: path = self.path - filelist = sorted([file for file in path.iterdir() if re.search(r'^img_\d{3,}.*\d{3,}.*\.tif$', file.name)]) + pat = re.compile(r'^img_\d{3,}.*\d{3,}.*\.tif$') + filelist = sorted([file for file in path.iterdir() if pat.search(file.name)]) with tifffile.TiffFile(self.path / filelist[0]) as tif: metadata = {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()} # compare channel names from metadata with filenames - cnamelist = metadata["Info"]["Summary"]["ChNames"] + cnamelist = metadata['Info']['Summary']['ChNames'] cnamelist = [c for c in cnamelist if any([c in f.name for f in filelist])] - pattern_c = re.compile(r"img_\d{3,}_(.*)_\d{3,}$") - pattern_z = re.compile(r"(\d{3,})$") - pattern_t = re.compile(r"img_(\d{3,})") + pattern_c = re.compile(r'img_\d{3,}_(.*)_\d{3,}$') + pattern_z = re.compile(r'(\d{3,})$') + pattern_t = re.compile(r'img_(\d{3,})') self.filedict = {(cnamelist.index(pattern_c.findall(file.stem)[0]), # noqa int(pattern_z.findall(file.stem)[0]), int(pattern_t.findall(file.stem)[0])): file for file in filelist} diff --git a/ndbioimage/readers/tifread.py b/ndbioimage/readers/tifread.py index a1b581d..4587fc3 100644 --- a/ndbioimage/readers/tifread.py +++ b/ndbioimage/readers/tifread.py @@ -24,22 +24,21 @@ class Reader(AbstractReader, ABC): return False @cached_property - def ome(self): - metadata = {key: yaml.safe_load(value) if isinstance(value, str) else value - for key, value in self.reader.imagej_metadata.items()} + def metadata(self): + return {key: yaml.safe_load(value) if isinstance(value, str) else value + for key, value in self.reader.imagej_metadata.items()} + def get_ome(self): page = self.reader.pages[0] - self.p_ndim = page.ndim # noqa size_y = page.imagelength size_x = page.imagewidth if self.p_ndim == 3: size_c = page.samplesperpixel - self.p_transpose = [i for i in [page.axes.find(j) for j in 'SYX'] if i >= 0] # noqa - size_t = metadata.get('frames', 1) # // C + size_t = self.metadata.get('frames', 1) # // C else: - size_c = metadata.get('channels', 1) - size_t = metadata.get('frames', 1) - size_z = metadata.get('slices', 1) + size_c = self.metadata.get('channels', 1) + size_t = self.metadata.get('frames', 1) + size_z = self.metadata.get('slices', 1) if 282 in page.tags and 296 in page.tags and page.tags[296].value == 1: f = page.tags[282].value pxsize = f[1] / f[0] @@ -51,7 +50,7 @@ class Reader(AbstractReader, ABC): 'float', 'double', 'complex', 'double-complex', 'bit'): dtype = 'float' - interval_t = metadata.get('interval', 0) + interval_t = self.metadata.get('interval', 0) ome = model.OME() ome.instruments.append(model.Instrument(id='Instrument:0')) @@ -62,14 +61,18 @@ class Reader(AbstractReader, ABC): pixels=model.Pixels( id='Pixels:0', size_c=size_c, size_z=size_z, size_t=size_t, size_x=size_x, size_y=size_y, - dimension_order="XYCZT", type=dtype, physical_size_x=pxsize, physical_size_y=pxsize), - objective_settings=model.ObjectiveSettings(id="Objective:0"))) + dimension_order='XYCZT', type=dtype, physical_size_x=pxsize, physical_size_y=pxsize), + 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=interval_t * t)) return ome def open(self): self.reader = tifffile.TiffFile(self.path) + page = self.reader.pages[0] + self.p_ndim = page.ndim # noqa + if self.p_ndim == 3: + self.p_transpose = [i for i in [page.axes.find(j) for j in 'SYX'] if i >= 0] # noqa def close(self): self.reader.close() diff --git a/ndbioimage/transforms.py b/ndbioimage/transforms.py index 6d21971..afaebb1 100644 --- a/ndbioimage/transforms.py +++ b/ndbioimage/transforms.py @@ -88,7 +88,7 @@ class Transforms(dict): return hash(frozenset((*self.__dict__.items(), *self.items()))) def save(self, file): - with open(Path(file).with_suffix(".yml"), 'w') as f: + with open(Path(file).with_suffix('.yml'), 'w') as f: yaml.safe_dump(self.asdict(), f, default_flow_style=None) def copy(self): @@ -136,7 +136,7 @@ class Transforms(dict): raise TypeError('Not a pandas DataFrame or Series.') def with_beads(self, cyllens, bead_files): - assert len(bead_files) > 0, "At least one file is needed to calculate the registration." + assert len(bead_files) > 0, 'At least one file is needed to calculate the registration.' transforms = [self.calculate_channel_transforms(file, cyllens) for file in bead_files] for key in {key for transform in transforms for key in transform.keys()}: new_transforms = [transform[key] for transform in transforms if key in transform] diff --git a/pyproject.toml b/pyproject.toml index 2866e67..23d7f6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ndbioimage" -version = "2024.3.6" +version = "2024.4.0" description = "Bio image reading, metadata and some affine registration." authors = ["W. Pomp "] license = "GPLv3" @@ -22,7 +22,7 @@ pint = "*" tqdm = "*" lxml = "*" pyyaml = "*" -parfor = ">=2023.10.1" +parfor = ">=2024.3.0" JPype1 = "*" SimpleITK-SimpleElastix = "*" scikit-image = "*" diff --git a/tests/test_open.py b/tests/test_open.py index b88e19c..133fbaa 100644 --- a/tests/test_open.py +++ b/tests/test_open.py @@ -3,6 +3,7 @@ from multiprocessing import active_children from pathlib import Path import pytest + from ndbioimage import Imread, ReaderNotFoundError diff --git a/tests/test_slicing.py b/tests/test_slicing.py index f69f38f..0cfdfca 100644 --- a/tests/test_slicing.py +++ b/tests/test_slicing.py @@ -3,6 +3,7 @@ from numbers import Number import numpy as np import pytest + from ndbioimage import Imread r = np.random.randint(0, 255, (64, 64, 2, 3, 4)) diff --git a/tests/test_ufuncs.py b/tests/test_ufuncs.py index a492f1e..7b00e09 100644 --- a/tests/test_ufuncs.py +++ b/tests/test_ufuncs.py @@ -2,6 +2,7 @@ from itertools import product import numpy as np import pytest + from ndbioimage import Imread r = np.random.randint(0, 255, (64, 64, 2, 3, 4))