diff --git a/.gitignore b/.gitignore index 69ded17..d64ded9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /poetry.lock /dist/ /uv.lock +.agentbridge \ No newline at end of file diff --git a/ndbioimage/__init__.py b/ndbioimage/__init__.py index d1f5a37..903e220 100755 --- a/ndbioimage/__init__.py +++ b/ndbioimage/__init__.py @@ -191,7 +191,8 @@ def get_positions(path: str | Path) -> Optional[list[int]]: return subclass.get_positions(AbstractReader.split_path_series(path)[0]) -class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC): +# noinspection PyAbstractClass +class Imread(np.lib.mixins.NDArrayOperatorsMixin): """class to read image files, while taking good care of important metadata, currently optimized for .czi files, but can open anything that bioformats can handle path: path to the image file @@ -1528,6 +1529,12 @@ class AbstractReader(Imread, metaclass=ABCMeta): self.immersionN = 1 p = re.compile(r"(\d+):(\d+)$") + if self.ome.structured_annotations is not None: + tirf_angles = {} + for annotation in self.ome.structured_annotations: + if annotation.description is not None and annotation.description.lower() == "tirfangle": + tirf_angles[int(annotation.id.split(":")[1])] = annotation.value + self.tirfangle = [angle for track, angle in sorted(tirf_angles.items(), key=lambda x: x[0])] try: self.track, self.detector = zip( *[ diff --git a/ndbioimage/readers/cziread.py b/ndbioimage/readers/cziread.py index c3693ca..203edfb 100644 --- a/ndbioimage/readers/cziread.py +++ b/ndbioimage/readers/cziread.py @@ -248,7 +248,7 @@ class Reader(AbstractReader, ABC): return [(i - j, i - j + k) for i, j, k in zip(directory_entry.start, start, directory_entry.shape)] @cached_property - def tiles(self): + def tiles(self) -> tuple[int, int]: columns = 1 rows = 1 xml = self.reader.metadata() @@ -335,6 +335,7 @@ class OmeParse: self.get_light_sources() self.get_filters() self.get_pixels() + self.get_tirf_angle() self.get_channels() self.get_planes() self.get_annotations() @@ -555,6 +556,18 @@ class OmeParse: elif self.size_z > 1 and distance.attrib["Id"] == "Z": self.ome.images[0].pixels.physical_size_z = float(self.text(distance.find("Value"))) * 1e6 + def get_tirf_angle(self) -> None: + if self.version == "1.0": + for track_setup in self.multi_track_setup: + tirf_angle = track_setup.find("TirfAngle") + if tirf_angle is not None: + self.ome.structured_annotations.append( + model.DoubleAnnotation( + description="TirfAngle", id="Annotation:0", value=50 * float(tirf_angle.text) + ) + ) + self.ome.images[0].annotation_refs.append(model.AnnotationRef(id="Annotation:0")) + @cached_property def positions(self) -> tuple[float, float, Optional[float]]: if self.version == "1.0": @@ -677,28 +690,30 @@ class OmeParse: else: light_source_settings = None - self.ome.images[0].pixels.channels.append( - model.Channel( - id=f"Channel:{idx}", - name=channel.attrib["Name"], - acquisition_mode=self.text(channel.find("AcquisitionMode")).replace( # type: ignore - "SingleMoleculeLocalisation", "SingleMoleculeImaging" - ), - color=color, - detector_settings=model.DetectorSettings( - id=detector.attrib["Id"].replace(" ", ""), binning=binning - ), - emission_wavelength=emission_wavelength, - excitation_wavelength=self.try_default( - float, None, self.text(channel.find("ExcitationWavelength")) - ), - # filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id), - illumination_type=self.text(channel.find("IlluminationType")), # type: ignore - light_source_settings=light_source_settings, - samples_per_pixel=samples_per_pixel, - ) + channel = dict( + id=f"Channel:{idx}", + name=channel.attrib["Name"], + acquisition_mode=self.text(channel.find("AcquisitionMode")).replace( # type: ignore + "SingleMoleculeLocalisation", "SingleMoleculeImaging" + ), + detector_settings=model.DetectorSettings( + id=detector.attrib["Id"].replace(" ", ""), binning=binning + ), + emission_wavelength=emission_wavelength, + excitation_wavelength=self.try_default( + float, None, self.text(channel.find("ExcitationWavelength")) + ), + # filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id), + illumination_type=self.text(channel.find("IlluminationType")), # type: ignore + light_source_settings=light_source_settings, + samples_per_pixel=samples_per_pixel, ) + if color is not None: + channel["color"] = color + + self.ome.images[0].pixels.channels.append(model.Channel(**channel)) + def get_planes(self) -> None: try: exposure_times = [ diff --git a/ndbioimage/readers/tifread.py b/ndbioimage/readers/tifread.py index 561ae95..6933db8 100644 --- a/ndbioimage/readers/tifread.py +++ b/ndbioimage/readers/tifread.py @@ -142,11 +142,18 @@ class Reader(AbstractReader, ABC): ) 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)) + if (spacing := self.metadata.get("spacing")) is not None: + ome.images[0].pixels.physical_size_z = spacing + if (unit := self.metadata.get("unit")) is not None and unit.lower() != "micron": + raise ValueError(f"cannot parse unit {unit}") + return ome def open(self): if self.series != 0: - raise FileNotFoundError(f"Series {self.series} not found in {self.path}. Tifread only supports one series.") + raise FileNotFoundError( + f"Series {self.series} not found in {self.path}. Tifread only supports one series." + ) self.reader = tifffile.TiffFile(self.path) page = self.reader.pages.first self.p_ndim = page.ndim # noqa diff --git a/pyproject.toml b/pyproject.toml index 3483e8d..12cdbb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ndbioimage" -version = "2026.4.0" +version = "2026.7.0" description = "Bio image reading, metadata and some affine registration." authors = [ { name = "W. Pomp", email = "w.pomp@nki.nl" } @@ -24,8 +24,8 @@ dependencies = [ "pyyaml", "SimpleITK-SimpleElastix; sys_platform != 'darwin'", "scikit-image", - "tifffile <= 2025.1.10", - "tiffwrite >= 2024.12.1", + "tifffile >= 2024.1.30, <= 2025.1.10", + "tiffwrite >= 2026.5.0", "tqdm", ] diff --git a/tests/files/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif b/tests/files/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif deleted file mode 100644 index 7027055..0000000 Binary files a/tests/files/YTL1849A111_2023_05_04__14_46_19_cellnr_1_track.tif and /dev/null differ