Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee4b07d4ec | ||
|
|
e32b7ea810 | ||
|
|
d9806244d3 | ||
|
|
74b81da78c | ||
|
|
2f076bd7b8 | ||
|
|
46c511dd49 | ||
|
|
0ee456a064 | ||
|
|
6123eeee8b | ||
|
|
e56ef334f4 | ||
|
|
bba24f2156 | ||
|
|
7e9cf46d55 | ||
|
|
351f563867 | ||
|
|
4563908254 |
No files matched your search
@@ -11,3 +11,4 @@
|
|||||||
/poetry.lock
|
/poetry.lock
|
||||||
/dist/
|
/dist/
|
||||||
/uv.lock
|
/uv.lock
|
||||||
|
.agentbridge
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[](https://github.com/wimpomp/ndbioimage/actions/workflows/pytest.yml)
|
[](https://git.wimpomp.nl/wim/ndbioimage/actions?workflow=pytest.yml)
|
||||||
|
|
||||||
# ndbioimage
|
# ndbioimage
|
||||||
|
|
||||||
|
|||||||
+31
-17
@@ -87,6 +87,7 @@ def find(obj: Sequence[Any], **kwargs: Any) -> Any:
|
|||||||
return item
|
return item
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
@@ -154,23 +155,26 @@ class OmeCache(DequeDict):
|
|||||||
def __reduce__(self) -> tuple[type, tuple]:
|
def __reduce__(self) -> tuple[type, tuple]:
|
||||||
return self.__class__, ()
|
return self.__class__, ()
|
||||||
|
|
||||||
def __getitem__(self, path: Path | str | tuple) -> OME:
|
def __getitem__(self, path_and_series: tuple[Path | str | tuple, int]) -> OME:
|
||||||
|
path, series = path_and_series
|
||||||
if isinstance(path, tuple):
|
if isinstance(path, tuple):
|
||||||
return super().__getitem__(path)
|
return super().__getitem__((path, series))
|
||||||
else:
|
else:
|
||||||
return super().__getitem__(self.path_and_lstat(path))
|
return super().__getitem__((self.path_and_lstat(path), series))
|
||||||
|
|
||||||
def __setitem__(self, path: Path | str | tuple, value: OME) -> None:
|
def __setitem__(self, path_and_series: tuple[Path | str | tuple, int], value: OME) -> None:
|
||||||
|
path, series = path_and_series
|
||||||
if isinstance(path, tuple):
|
if isinstance(path, tuple):
|
||||||
super().__setitem__(path, value)
|
super().__setitem__((path, series), value)
|
||||||
else:
|
else:
|
||||||
super().__setitem__(self.path_and_lstat(path), value)
|
super().__setitem__((self.path_and_lstat(path), series), value)
|
||||||
|
|
||||||
def __contains__(self, path: Path | str | tuple) -> bool:
|
def __contains__(self, path_and_series: tuple[Path | str | tuple, int]) -> bool:
|
||||||
|
path, series = path_and_series
|
||||||
if isinstance(path, tuple):
|
if isinstance(path, tuple):
|
||||||
return super().__contains__(path)
|
return super().__contains__((path, series))
|
||||||
else:
|
else:
|
||||||
return super().__contains__(self.path_and_lstat(path))
|
return super().__contains__((self.path_and_lstat(path), series))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def path_and_lstat(path: str | Path) -> tuple[Path, Optional[os.stat_result], Optional[os.stat_result]]:
|
def path_and_lstat(path: str | Path) -> tuple[Path, Optional[os.stat_result], Optional[os.stat_result]]:
|
||||||
@@ -187,7 +191,8 @@ def get_positions(path: str | Path) -> Optional[list[int]]:
|
|||||||
return subclass.get_positions(AbstractReader.split_path_series(path)[0])
|
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,
|
"""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
|
currently optimized for .czi files, but can open anything that bioformats can handle
|
||||||
path: path to the image file
|
path: path to the image file
|
||||||
@@ -1025,7 +1030,7 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
|
|||||||
return [self.get_channel(c) for c in czt[0]], *czt[1:3] # type: ignore
|
return [self.get_channel(c) for c in czt[0]], *czt[1:3] # type: ignore
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bioformats_ome(path: [str, Path]) -> OME:
|
def bioformats_ome(path: str | Path) -> OME:
|
||||||
"""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:
|
||||||
return pool.map(bioformats_ome, (path,))[0]
|
return pool.map(bioformats_ome, (path,))[0]
|
||||||
@@ -1057,10 +1062,11 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
|
|||||||
return ome
|
return ome
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def read_ome(path: [str, Path]) -> Optional[OME]:
|
def read_ome(path: str | Path) -> Optional[OME]:
|
||||||
path = Path(path)
|
path = Path(path)
|
||||||
if path.with_suffix(".ome.xml").exists():
|
if path.with_suffix(".ome.xml").exists():
|
||||||
return OME.from_xml(path.with_suffix(".ome.xml"))
|
return OME.from_xml(path.with_suffix(".ome.xml"))
|
||||||
|
return None
|
||||||
|
|
||||||
def get_ome(self) -> OME:
|
def get_ome(self) -> OME:
|
||||||
"""overload this"""
|
"""overload this"""
|
||||||
@@ -1069,12 +1075,12 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def ome(self) -> OME:
|
def ome(self) -> OME:
|
||||||
cache = OmeCache()
|
cache = OmeCache()
|
||||||
if self.path not in cache:
|
if (self.path, self.series) not in cache:
|
||||||
ome = self.read_ome(self.path)
|
ome = self.read_ome(self.path)
|
||||||
if ome is None:
|
if ome is None:
|
||||||
ome = self.get_ome()
|
ome = self.get_ome()
|
||||||
cache[self.path] = self.fix_ome(ome)
|
cache[self.path, self.series] = self.fix_ome(ome)
|
||||||
return cache[self.path]
|
return cache[self.path, self.series]
|
||||||
|
|
||||||
def is_noise(self, volume: ArrayLike = None) -> bool:
|
def is_noise(self, volume: ArrayLike = None) -> bool:
|
||||||
"""True if volume only has noise"""
|
"""True if volume only has noise"""
|
||||||
@@ -1306,9 +1312,11 @@ class Imread(np.lib.mixins.NDArrayOperatorsMixin, ABC):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def split_path_series(path: Path | str) -> tuple[Path, int]:
|
def split_path_series(path: Path | str) -> tuple[Path, int]:
|
||||||
if isinstance(path, str):
|
if isinstance(path, str):
|
||||||
path = Path(path)
|
path = Path(path).absolute()
|
||||||
if isinstance(path, Path) and path.name.startswith("Pos") and path.name.lstrip("Pos").isdigit():
|
if isinstance(path, Path) and path.name.startswith("Pos") and path.name.lstrip("Pos").isdigit():
|
||||||
return path.parent, int(path.name.lstrip("Pos"))
|
return path.absolute().parent, int(path.name.lstrip("Pos"))
|
||||||
|
if isinstance(path, Path):
|
||||||
|
return path.absolute(), 0
|
||||||
return path, 0
|
return path, 0
|
||||||
|
|
||||||
def view(self, *args: Any, **kwargs: Any) -> View:
|
def view(self, *args: Any, **kwargs: Any) -> View:
|
||||||
@@ -1521,6 +1529,12 @@ class AbstractReader(Imread, metaclass=ABCMeta):
|
|||||||
self.immersionN = 1
|
self.immersionN = 1
|
||||||
|
|
||||||
p = re.compile(r"(\d+):(\d+)$")
|
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:
|
try:
|
||||||
self.track, self.detector = zip(
|
self.track, self.detector = zip(
|
||||||
*[
|
*[
|
||||||
|
|||||||
+139
-40
@@ -14,7 +14,7 @@ from lxml import etree
|
|||||||
from ome_types import OME, model
|
from ome_types import OME, model
|
||||||
from tifffile import repeat_nd
|
from tifffile import repeat_nd
|
||||||
|
|
||||||
from .. import AbstractReader
|
from .. import AbstractReader, ureg
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# TODO: use zoom from imagecodecs implementation when available
|
# TODO: use zoom from imagecodecs implementation when available
|
||||||
@@ -152,6 +152,14 @@ def data(self, raw: bool = False, resize: bool = True, order: int = 0) -> np.nda
|
|||||||
czifile.czifile.SubBlockSegment.data = data
|
czifile.czifile.SubBlockSegment.data = data
|
||||||
|
|
||||||
|
|
||||||
|
def xml_walk(tree, elements):
|
||||||
|
element, *elements = elements
|
||||||
|
if elements:
|
||||||
|
return [j for i in tree.findall(element) for j in xml_walk(i, elements)]
|
||||||
|
else:
|
||||||
|
return tree.findall(element)
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
class Reader(AbstractReader, ABC):
|
||||||
priority = 0
|
priority = 0
|
||||||
do_not_pickle = "reader", "filedict"
|
do_not_pickle = "reader", "filedict"
|
||||||
@@ -163,16 +171,55 @@ class Reader(AbstractReader, ABC):
|
|||||||
def open(self) -> None:
|
def open(self) -> None:
|
||||||
self.reader = czifile.CziFile(self.path)
|
self.reader = czifile.CziFile(self.path)
|
||||||
filedict = {}
|
filedict = {}
|
||||||
|
syx = set()
|
||||||
|
si = self.reader.axes.index("S") if "S" in self.reader.axes else None
|
||||||
|
ci = self.reader.axes.index("C") if "C" in self.reader.axes else None
|
||||||
|
zi = self.reader.axes.index("Z") if "Z" in self.reader.axes else None
|
||||||
|
ti = self.reader.axes.index("T") if "T" in self.reader.axes else None
|
||||||
|
yi = self.reader.axes.index("Y") if "Y" in self.reader.axes else None
|
||||||
|
xi = self.reader.axes.index("X") if "X" in self.reader.axes else None
|
||||||
|
if si is None and self.series > 0:
|
||||||
|
raise FileNotFoundError(f"Series {self.series} not found in {self.path}.")
|
||||||
|
|
||||||
for directory_entry in self.reader.filtered_subblock_directory:
|
for directory_entry in self.reader.filtered_subblock_directory:
|
||||||
idx = self.get_index(directory_entry, self.reader.start)
|
idx = self.get_index(directory_entry, self.reader.start)
|
||||||
if "S" not in self.reader.axes or self.series in range(*idx[self.reader.axes.index("S")]):
|
syx.add((0 if si is None else idx[si], idx[yi], idx[xi]))
|
||||||
for c in range(*idx[self.reader.axes.index("C")]):
|
|
||||||
for z in range(*idx[self.reader.axes.index("Z")]):
|
if self.tiles != (1, 1):
|
||||||
for t in range(*idx[self.reader.axes.index("T")]):
|
assert len({s for s, *_ in list(syx)}) == 1, "multiple tiled series not supported"
|
||||||
if (c, z, t) in filedict:
|
x, y = np.array(list(syx))[:, 1:, 0].T
|
||||||
filedict[c, z, t].append(directory_entry)
|
a, b = np.min(x), np.max(x)
|
||||||
else:
|
n = self.tiles[0]
|
||||||
filedict[c, z, t] = [directory_entry]
|
bx = np.linspace(a - (b - a) / (n - 1) / 2, b + (b - a) / (n - 1) / 2, n + 1)
|
||||||
|
a, b = np.min(y), np.max(y)
|
||||||
|
n = self.tiles[1]
|
||||||
|
by = np.linspace(a - (b - a) / (n - 1) / 2, b + (b - a) / (n - 1) / 2, n + 1)
|
||||||
|
b = list(product([(i, j) for i, j in zip(by, by[1:])], [(i, j) for i, j in zip(bx, bx[1:])]))
|
||||||
|
if self.series < len(b):
|
||||||
|
by, bx = b[self.series]
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(f"Series {self.series} not found in {self.path}.")
|
||||||
|
for directory_entry in self.reader.filtered_subblock_directory:
|
||||||
|
idx = self.get_index(directory_entry, self.reader.start)
|
||||||
|
if bx[0] < idx[xi][0] < bx[1] and by[0] < idx[yi][0] < by[1]:
|
||||||
|
for cj in (0,) if ci is None else range(*idx[ci]):
|
||||||
|
for zj in (0,) if zi is None else range(*idx[zi]):
|
||||||
|
for tj in (0,) if ti is None else range(*idx[ti]):
|
||||||
|
if (cj, zj, tj) in filedict:
|
||||||
|
filedict[cj, zj, tj].append(directory_entry)
|
||||||
|
else:
|
||||||
|
filedict[cj, zj, tj] = [directory_entry]
|
||||||
|
else:
|
||||||
|
for directory_entry in self.reader.filtered_subblock_directory:
|
||||||
|
idx = self.get_index(directory_entry, self.reader.start)
|
||||||
|
if si is None or self.series == idx[si][0]:
|
||||||
|
for cj in (0,) if ci is None else range(*idx[ci]):
|
||||||
|
for zj in (0,) if zi is None else range(*idx[zi]):
|
||||||
|
for tj in (0,) if ti is None else range(*idx[ti]):
|
||||||
|
if (cj, zj, tj) in filedict:
|
||||||
|
filedict[cj, zj, tj].append(directory_entry)
|
||||||
|
else:
|
||||||
|
filedict[cj, zj, tj] = [directory_entry]
|
||||||
if len(filedict) == 0:
|
if len(filedict) == 0:
|
||||||
raise FileNotFoundError(f"Series {self.series} not found in {self.path}.")
|
raise FileNotFoundError(f"Series {self.series} not found in {self.path}.")
|
||||||
self.filedict = filedict # noqa
|
self.filedict = filedict # noqa
|
||||||
@@ -187,17 +234,11 @@ class Reader(AbstractReader, ABC):
|
|||||||
f = np.zeros(self.base_shape["yx"], self.dtype)
|
f = np.zeros(self.base_shape["yx"], self.dtype)
|
||||||
if (c, z, t) in self.filedict:
|
if (c, z, t) in self.filedict:
|
||||||
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])
|
start = np.min([directory_entry.start for directory_entry in directory_entries], 0)
|
||||||
y_min = min([f.start[f.axes.index("Y")] for f in directory_entries])
|
|
||||||
xy_min = {"X": x_min, "Y": y_min}
|
|
||||||
for directory_entry in directory_entries:
|
for directory_entry in directory_entries:
|
||||||
subblock = directory_entry.data_segment()
|
subblock = directory_entry.data_segment()
|
||||||
tile = subblock.data(resize=True, order=0)
|
tile = subblock.data(resize=True, order=0)
|
||||||
axes_min = [xy_min.get(ax, 0) for ax in directory_entry.axes]
|
index = [slice(i - j, i - j + k) for i, j, k in zip(directory_entry.start, start, tile.shape)]
|
||||||
index = [
|
|
||||||
slice(i - j - m, i - j + k)
|
|
||||||
for i, j, k, m in zip(directory_entry.start, self.reader.start, tile.shape, axes_min)
|
|
||||||
]
|
|
||||||
index = tuple(index[self.reader.axes.index(i)] for i in "YX")
|
index = tuple(index[self.reader.axes.index(i)] for i in "YX")
|
||||||
f[index] = tile.squeeze()
|
f[index] = tile.squeeze()
|
||||||
return f
|
return f
|
||||||
@@ -206,6 +247,38 @@ class Reader(AbstractReader, ABC):
|
|||||||
def get_index(directory_entry: czifile.DirectoryEntryDV, start: tuple[int]) -> list[tuple[int, int]]:
|
def get_index(directory_entry: czifile.DirectoryEntryDV, start: tuple[int]) -> list[tuple[int, int]]:
|
||||||
return [(i - j, i - j + k) for i, j, k in zip(directory_entry.start, start, directory_entry.shape)]
|
return [(i - j, i - j + k) for i, j, k in zip(directory_entry.start, start, directory_entry.shape)]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def tiles(self) -> tuple[int, int]:
|
||||||
|
columns = 1
|
||||||
|
rows = 1
|
||||||
|
xml = self.reader.metadata()
|
||||||
|
tree = etree.fromstring(xml)
|
||||||
|
tile_regions = xml_walk(
|
||||||
|
tree,
|
||||||
|
(
|
||||||
|
"Metadata",
|
||||||
|
"Experiment",
|
||||||
|
"ExperimentBlocks",
|
||||||
|
"AcquisitionBlock",
|
||||||
|
"SubDimensionSetups",
|
||||||
|
"RegionsSetup",
|
||||||
|
"SampleHolder",
|
||||||
|
"TileRegions",
|
||||||
|
"TileRegion",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for tile_region in tile_regions:
|
||||||
|
used = tile_region.find("IsUsedForAcquisition")
|
||||||
|
if used is not None and used.text.lower() == "true":
|
||||||
|
c = tile_region.find("Columns")
|
||||||
|
if c is not None:
|
||||||
|
columns = int(c.text)
|
||||||
|
r = tile_region.find("Rows")
|
||||||
|
if r is not None:
|
||||||
|
rows = int(r.text)
|
||||||
|
break
|
||||||
|
return columns, rows
|
||||||
|
|
||||||
|
|
||||||
class OmeParse:
|
class OmeParse:
|
||||||
size_x: int
|
size_x: int
|
||||||
@@ -262,6 +335,7 @@ class OmeParse:
|
|||||||
self.get_light_sources()
|
self.get_light_sources()
|
||||||
self.get_filters()
|
self.get_filters()
|
||||||
self.get_pixels()
|
self.get_pixels()
|
||||||
|
self.get_tirf_angle()
|
||||||
self.get_channels()
|
self.get_channels()
|
||||||
self.get_planes()
|
self.get_planes()
|
||||||
self.get_annotations()
|
self.get_annotations()
|
||||||
@@ -429,7 +503,7 @@ class OmeParse:
|
|||||||
self.size_x = x_max - x_min
|
self.size_x = x_max - x_min
|
||||||
self.size_y = y_max - y_min
|
self.size_y = y_max - y_min
|
||||||
self.size_c, self.size_z, self.size_t = (
|
self.size_c, self.size_z, self.size_t = (
|
||||||
self.reader.shape[self.reader.axes.index(directory_entry)] for directory_entry in "CZT"
|
self.reader.shape[self.reader.axes.index(axis)] if axis in self.reader.axes else 1 for axis in "CZT"
|
||||||
)
|
)
|
||||||
image = self.information.find("Image")
|
image = self.information.find("Image")
|
||||||
pixel_type = self.text(image.find("PixelType"), "Gray16")
|
pixel_type = self.text(image.find("PixelType"), "Gray16")
|
||||||
@@ -482,6 +556,18 @@ class OmeParse:
|
|||||||
elif self.size_z > 1 and distance.attrib["Id"] == "Z":
|
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
|
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
|
@cached_property
|
||||||
def positions(self) -> tuple[float, float, Optional[float]]:
|
def positions(self) -> tuple[float, float, Optional[float]]:
|
||||||
if self.version == "1.0":
|
if self.version == "1.0":
|
||||||
@@ -495,6 +581,8 @@ class OmeParse:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
center_position = [0, 0]
|
center_position = [0, 0]
|
||||||
return center_position[0], center_position[1], None
|
return center_position[0], center_position[1], None
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"unknown czi version: {self.version}")
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def channels_im(self) -> dict:
|
def channels_im(self) -> dict:
|
||||||
@@ -602,28 +690,30 @@ class OmeParse:
|
|||||||
else:
|
else:
|
||||||
light_source_settings = None
|
light_source_settings = None
|
||||||
|
|
||||||
self.ome.images[0].pixels.channels.append(
|
channel = dict(
|
||||||
model.Channel(
|
id=f"Channel:{idx}",
|
||||||
id=f"Channel:{idx}",
|
name=channel.attrib["Name"],
|
||||||
name=channel.attrib["Name"],
|
acquisition_mode=self.text(channel.find("AcquisitionMode")).replace( # type: ignore
|
||||||
acquisition_mode=self.text(channel.find("AcquisitionMode")).replace( # type: ignore
|
"SingleMoleculeLocalisation", "SingleMoleculeImaging"
|
||||||
"SingleMoleculeLocalisation", "SingleMoleculeImaging"
|
),
|
||||||
),
|
detector_settings=model.DetectorSettings(
|
||||||
color=color,
|
id=detector.attrib["Id"].replace(" ", ""), binning=binning
|
||||||
detector_settings=model.DetectorSettings(
|
),
|
||||||
id=detector.attrib["Id"].replace(" ", ""), binning=binning
|
emission_wavelength=emission_wavelength,
|
||||||
),
|
excitation_wavelength=self.try_default(
|
||||||
emission_wavelength=emission_wavelength,
|
float, None, self.text(channel.find("ExcitationWavelength"))
|
||||||
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
|
||||||
# filter_set_ref=model.FilterSetRef(id=ome.instruments[0].filter_sets[filterset_idx].id),
|
light_source_settings=light_source_settings,
|
||||||
illumination_type=self.text(channel.find("IlluminationType")), # type: ignore
|
samples_per_pixel=samples_per_pixel,
|
||||||
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:
|
def get_planes(self) -> None:
|
||||||
try:
|
try:
|
||||||
exposure_times = [
|
exposure_times = [
|
||||||
@@ -639,7 +729,16 @@ class OmeParse:
|
|||||||
delta_ts = dt * np.arange(len(delta_ts))
|
delta_ts = dt * np.arange(len(delta_ts))
|
||||||
warnings.warn(f"delta_t is inconsistent, using median value: {dt}")
|
warnings.warn(f"delta_t is inconsistent, using median value: {dt}")
|
||||||
|
|
||||||
|
pxsize_x = self.ome.images[0].pixels.physical_size_x_quantity.to(ureg.um).magnitude
|
||||||
|
pxsize_y = self.ome.images[0].pixels.physical_size_y_quantity.to(ureg.um).magnitude
|
||||||
|
|
||||||
for t, z, c in product(range(self.size_t), range(self.size_z), range(self.size_c)):
|
for t, z, c in product(range(self.size_t), range(self.size_z), range(self.size_c)):
|
||||||
|
x_min = (
|
||||||
|
min([f.start[f.axes.index("X")] for f in self.filedict[c, z, t]]) if (c, z, t) in self.filedict else 0
|
||||||
|
)
|
||||||
|
y_min = (
|
||||||
|
min([f.start[f.axes.index("Y")] for f in self.filedict[c, z, t]]) if (c, z, t) in self.filedict else 0
|
||||||
|
)
|
||||||
self.ome.images[0].pixels.planes.append(
|
self.ome.images[0].pixels.planes.append(
|
||||||
model.Plane(
|
model.Plane(
|
||||||
the_c=c,
|
the_c=c,
|
||||||
@@ -647,9 +746,9 @@ class OmeParse:
|
|||||||
the_t=t,
|
the_t=t,
|
||||||
delta_t=delta_ts[t],
|
delta_t=delta_ts[t],
|
||||||
exposure_time=exposure_times[min(c, len(exposure_times) - 1)] if len(exposure_times) > 0 else None,
|
exposure_time=exposure_times[min(c, len(exposure_times) - 1)] if len(exposure_times) > 0 else None,
|
||||||
position_x=self.positions[0],
|
position_x=self.positions[0] + x_min * pxsize_x,
|
||||||
position_x_unit=self.um,
|
position_x_unit=self.um,
|
||||||
position_y=self.positions[1],
|
position_y=self.positions[1] + y_min * pxsize_y,
|
||||||
position_y_unit=self.um,
|
position_y_unit=self.um,
|
||||||
position_z=self.positions[2],
|
position_z=self.positions[2],
|
||||||
position_z_unit=self.um,
|
position_z_unit=self.um,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
import warnings
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import product
|
from itertools import product
|
||||||
@@ -59,9 +60,10 @@ class Reader(AbstractReader, ABC):
|
|||||||
ome = model.OME()
|
ome = model.OME()
|
||||||
with tifffile.TiffFile(self.filedict[0, 0, 0]) as tif:
|
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()}
|
metadata = {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()}
|
||||||
ome.experimenters.append(
|
if "Summary" in metadata["Info"] and "UserName" in metadata["Info"]["Summary"]:
|
||||||
model.Experimenter(id="Experimenter:0", user_name=metadata["Info"]["Summary"]["UserName"])
|
ome.experimenters.append(
|
||||||
)
|
model.Experimenter(id="Experimenter:0", user_name=metadata["Info"]["Summary"]["UserName"])
|
||||||
|
)
|
||||||
objective_str = metadata["Info"]["ZeissObjectiveTurret-Label"]
|
objective_str = metadata["Info"]["ZeissObjectiveTurret-Label"]
|
||||||
ome.instruments.append(model.Instrument())
|
ome.instruments.append(model.Instrument())
|
||||||
ome.instruments[0].objectives.append(
|
ome.instruments[0].objectives.append(
|
||||||
@@ -112,7 +114,9 @@ class Reader(AbstractReader, ABC):
|
|||||||
type=pixel_type,
|
type=pixel_type,
|
||||||
physical_size_x=pxsize,
|
physical_size_x=pxsize,
|
||||||
physical_size_y=pxsize,
|
physical_size_y=pxsize,
|
||||||
physical_size_z=metadata["Info"]["Summary"]["z-step_um"],
|
physical_size_z=metadata["Info"]["Summary"]["z-step_um"]
|
||||||
|
if "Summary" in metadata["Info"]
|
||||||
|
else None,
|
||||||
),
|
),
|
||||||
objective_settings=model.ObjectiveSettings(id="Objective:0"),
|
objective_settings=model.ObjectiveSettings(id="Objective:0"),
|
||||||
)
|
)
|
||||||
@@ -164,7 +168,15 @@ class Reader(AbstractReader, ABC):
|
|||||||
metadata = {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()}
|
metadata = {key: yaml.safe_load(value) for key, value in tif.pages[0].tags[50839].value.items()}
|
||||||
|
|
||||||
# compare channel names from metadata with filenames
|
# compare channel names from metadata with filenames
|
||||||
cnamelist = metadata["Info"]["Summary"]["ChNames"]
|
if "Summary" in metadata["Info"] and "ChNames" in metadata["Info"]["Summary"]:
|
||||||
|
cnamelist = metadata["Info"]["Summary"]["ChNames"]
|
||||||
|
elif (self.path.parent / "display_and_comments.txt").exists():
|
||||||
|
warnings.warn(f"{self.path} is missing some metadata")
|
||||||
|
with open(self.path.parent / "display_and_comments.txt") as f:
|
||||||
|
cnamelist = [channel["Name"] for channel in yaml.safe_load(f)["Channels"]]
|
||||||
|
else:
|
||||||
|
raise ValueError("could not find metadata describing the order of the channels")
|
||||||
|
|
||||||
cnamelist = [c for c in cnamelist if any([c in f.name for f in filelist])]
|
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,}$", re.IGNORECASE)
|
pattern_c = re.compile(r"img_\d{3,}_(.*)_\d{3,}$", re.IGNORECASE)
|
||||||
@@ -180,4 +192,4 @@ class Reader(AbstractReader, ABC):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __frame__(self, c=0, z=0, t=0):
|
def __frame__(self, c=0, z=0, t=0):
|
||||||
return tifffile.imread(self.path / self.filedict[(c, z, t)])
|
return tifffile.imread(self.path / self.filedict[c, z, t])
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
|
from contextlib import ExitStack
|
||||||
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 typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import tifffile
|
import tifffile
|
||||||
import yaml
|
import yaml
|
||||||
from ome_types import from_xml, model
|
from ome_types import OME, from_xml, model
|
||||||
|
|
||||||
from .. import AbstractReader, try_default
|
from .. import AbstractReader, Shape, try_default
|
||||||
|
|
||||||
|
|
||||||
class Reader(AbstractReader, ABC):
|
class Reader(AbstractReader, ABC):
|
||||||
@@ -18,7 +20,7 @@ class Reader(AbstractReader, ABC):
|
|||||||
do_not_pickle = "reader"
|
do_not_pickle = "reader"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _can_open(path):
|
def _can_open(path) -> bool:
|
||||||
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 and tif.pages[-1]._nextifd() == 0 # noqa
|
return tif.is_imagej and tif.pages[-1]._nextifd() == 0 # noqa
|
||||||
@@ -26,13 +28,13 @@ class Reader(AbstractReader, ABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def metadata(self):
|
def metadata(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
key: try_default(yaml.safe_load, value, value) if isinstance(value, str) else value
|
key: try_default(yaml.safe_load, value, value) if isinstance(value, str) else value
|
||||||
for key, value in self.reader.imagej_metadata.items()
|
for key, value in self.reader.imagej_metadata.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_ome(self):
|
def get_ome(self) -> OME:
|
||||||
if self.reader.is_ome:
|
if self.reader.is_ome:
|
||||||
pos_number_pat = re.compile(r"\d+")
|
pos_number_pat = re.compile(r"\d+")
|
||||||
|
|
||||||
@@ -88,13 +90,17 @@ class Reader(AbstractReader, ABC):
|
|||||||
page = self.reader.pages[0]
|
page = self.reader.pages[0]
|
||||||
size_y = page.imagelength
|
size_y = page.imagelength
|
||||||
size_x = page.imagewidth
|
size_x = page.imagewidth
|
||||||
if self.p_ndim == 3:
|
max_c = max_z = max_t = 0
|
||||||
size_c = page.samplesperpixel
|
if self.file_dict:
|
||||||
size_t = self.metadata.get("frames", 1) # // C
|
for ci, zi, ti in self.file_dict.keys():
|
||||||
else:
|
max_c = max(max_c, ci)
|
||||||
size_c = self.metadata.get("channels", 1)
|
max_z = max(max_z, zi)
|
||||||
size_t = self.metadata.get("frames", 1)
|
max_t = max(max_t, ti)
|
||||||
size_z = self.metadata.get("slices", 1)
|
|
||||||
|
size_c: int = self.mod_shape["c"] * (max_c + 1) # type: ignore
|
||||||
|
size_z: int = self.mod_shape["z"] * (max_z + 1) # type: ignore
|
||||||
|
size_t: int = self.mod_shape["t"] * (max_t + 1) # type: ignore
|
||||||
|
|
||||||
if 282 in page.tags and 296 in page.tags and page.tags[296].value == 1:
|
if 282 in page.tags and 296 in page.tags and page.tags[296].value == 1:
|
||||||
f = page.tags[282].value
|
f = page.tags[282].value
|
||||||
pxsize = f[1] / f[0]
|
pxsize = f[1] / f[0]
|
||||||
@@ -142,9 +148,18 @@ class Reader(AbstractReader, ABC):
|
|||||||
)
|
)
|
||||||
for c, z, t in product(range(size_c), range(size_z), range(size_t)):
|
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))
|
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
|
return ome
|
||||||
|
|
||||||
def open(self):
|
def open(self) -> None:
|
||||||
|
if self.series != 0:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Series {self.series} not found in {self.path}. Tifread only supports one series."
|
||||||
|
)
|
||||||
self.reader = tifffile.TiffFile(self.path)
|
self.reader = tifffile.TiffFile(self.path)
|
||||||
page = self.reader.pages.first
|
page = self.reader.pages.first
|
||||||
self.p_ndim = page.ndim # noqa
|
self.p_ndim = page.ndim # noqa
|
||||||
@@ -152,19 +167,68 @@ class Reader(AbstractReader, ABC):
|
|||||||
self.p_transpose = [i for i in [page.axes.find(j) for j in "SYX"] if i >= 0] # noqa
|
self.p_transpose = [i for i in [page.axes.find(j) for j in "SYX"] if i >= 0] # noqa
|
||||||
else:
|
else:
|
||||||
self.p_transpose = [i for i in [page.axes.find(j) for j in "YX"] if i >= 0] # noqa
|
self.p_transpose = [i for i in [page.axes.find(j) for j in "YX"] if i >= 0] # noqa
|
||||||
|
self.file_dict = {} # noqa
|
||||||
|
pat = re.compile(r"^.*?((?:_[czt]\d+)*)$", re.IGNORECASE)
|
||||||
|
if (m := pat.match(self.path.stem)) is not None:
|
||||||
|
m = m.group(1)
|
||||||
|
cpat = re.compile(r"_c(\d+)", re.IGNORECASE)
|
||||||
|
zpat = re.compile(r"_z(\d+)", re.IGNORECASE)
|
||||||
|
tpat = re.compile(r"_t(\d+)", re.IGNORECASE)
|
||||||
|
for file in self.path.parent.glob(
|
||||||
|
re.sub(m, re.sub(r"\d+", "*", m), self.path.stem, re.IGNORECASE) + self.path.suffix
|
||||||
|
):
|
||||||
|
m = pat.match(file.stem)
|
||||||
|
if m:
|
||||||
|
m = m.group(0)
|
||||||
|
c = cpat.findall(m)
|
||||||
|
z = zpat.findall(m)
|
||||||
|
t = tpat.findall(m)
|
||||||
|
c = int(c[0]) if len(c) else 0
|
||||||
|
z = int(z[0]) if len(z) else 0
|
||||||
|
t = int(t[0]) if len(t) else 0
|
||||||
|
self.file_dict[c, z, t] = file.name
|
||||||
|
|
||||||
def close(self):
|
page = self.reader.pages[0]
|
||||||
|
if self.p_ndim == 3:
|
||||||
|
mod_c = page.samplesperpixel
|
||||||
|
mod_t = self.metadata.get("frames", 1) # // C
|
||||||
|
else:
|
||||||
|
mod_c = self.metadata.get("channels", 1)
|
||||||
|
mod_t = self.metadata.get("frames", 1)
|
||||||
|
|
||||||
|
mod_z = self.metadata.get("slices")
|
||||||
|
if mod_z is None:
|
||||||
|
if self.metadata.get("spacing") is None:
|
||||||
|
mod_t = len(self.reader.pages)
|
||||||
|
else:
|
||||||
|
mod_z = len(self.reader.pages)
|
||||||
|
|
||||||
|
self.mod_shape = Shape((mod_c or 1, mod_z or 1, mod_t or 1), "czt") # noqa
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
self.reader.close()
|
self.reader.close()
|
||||||
|
|
||||||
def __frame__(self, c: int, z: int, t: int):
|
def __frame__(self, c: int, z: int, t: int) -> np.ndarray:
|
||||||
dimension_order = self.ome.images[0].pixels.dimension_order.value
|
with ExitStack() as stack:
|
||||||
if self.p_ndim == 3:
|
if self.file_dict:
|
||||||
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "zt"])
|
filename = self.file_dict[
|
||||||
ct = {"z": z, "t": t}
|
c - c % self.mod_shape["c"], z - z % self.mod_shape["z"], t - t % self.mod_shape["t"]
|
||||||
n = sum([ct[ax] * np.prod(self.base_shape[axes[:i]]) for i, ax in enumerate(axes)])
|
]
|
||||||
return np.transpose(self.reader.asarray(int(n)), self.p_transpose)[int(c)]
|
if self.reader.filename == filename:
|
||||||
else:
|
reader = self.reader
|
||||||
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "czt"])
|
else:
|
||||||
czt = {"c": c, "z": z, "t": t}
|
reader = stack.enter_context(tifffile.TiffFile(self.path.parent / filename))
|
||||||
n = sum([czt[ax] * np.prod(self.base_shape[axes[:i]]) for i, ax in enumerate(axes)])
|
else:
|
||||||
return np.transpose(self.reader.asarray(int(n)), self.p_transpose)
|
reader = self.reader
|
||||||
|
|
||||||
|
dimension_order = self.ome.images[0].pixels.dimension_order.value
|
||||||
|
if self.p_ndim == 3:
|
||||||
|
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "zt"])
|
||||||
|
ct = {"z": z % self.mod_shape["z"], "t": t % self.mod_shape["t"]}
|
||||||
|
n = sum([ct[ax] * np.prod(self.mod_shape[axes[:i]]) for i, ax in enumerate(axes)])
|
||||||
|
return np.transpose(reader.asarray(int(n)), self.p_transpose)[int(c % self.mod_shape["c"])]
|
||||||
|
else:
|
||||||
|
axes = "".join([ax.lower() for ax in dimension_order if ax.lower() in "czt"])
|
||||||
|
czt = {"c": c % self.mod_shape["c"], "z": z % self.mod_shape["z"], "t": t % self.mod_shape["t"]}
|
||||||
|
n = sum([czt[ax] * np.prod(self.mod_shape[axes[:i]]) for i, ax in enumerate(axes)])
|
||||||
|
return np.transpose(reader.asarray(int(n)), self.p_transpose)
|
||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "ndbioimage"
|
name = "ndbioimage"
|
||||||
version = "2026.1.2"
|
version = "2026.9.0"
|
||||||
description = "Bio image reading, metadata and some affine registration."
|
description = "Bio image reading, metadata and some affine registration."
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "W. Pomp", email = "w.pomp@nki.nl" }
|
{ name = "W. Pomp", email = "w.pomp@nki.nl" }
|
||||||
@@ -14,7 +14,7 @@ exclude = ["ndbioimage/jars"]
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"czifile == 2019.7.2",
|
"czifile == 2019.7.2",
|
||||||
"imagecodecs",
|
"imagecodecs <= 2026.1.14",
|
||||||
"lxml",
|
"lxml",
|
||||||
"numpy >= 1.20",
|
"numpy >= 1.20",
|
||||||
"ome-types",
|
"ome-types",
|
||||||
@@ -24,8 +24,8 @@ dependencies = [
|
|||||||
"pyyaml",
|
"pyyaml",
|
||||||
"SimpleITK-SimpleElastix; sys_platform != 'darwin'",
|
"SimpleITK-SimpleElastix; sys_platform != 'darwin'",
|
||||||
"scikit-image",
|
"scikit-image",
|
||||||
"tifffile <= 2025.1.10",
|
"tifffile >= 2024.1.30, <= 2025.1.10",
|
||||||
"tiffwrite >= 2024.12.1",
|
"tiffwrite >= 2026.5.0",
|
||||||
"tqdm",
|
"tqdm",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ write = ["matplotlib", "scikit-video"]
|
|||||||
bioformats = ["JPype1"]
|
bioformats = ["JPype1"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
repository = "https://github.com/wimpomp/ndbioimage"
|
repository = "https://git.wimpomp.nl/wim/ndbioimage"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
ndbioimage = "ndbioimage:main"
|
ndbioimage = "ndbioimage:main"
|
||||||
|
|||||||
Reference in new issue
Block a user