Coverage for qubalab/images/bioio_server.py: 88%
65 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-10-07 15:29 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-10-07 15:29 +0000
1import numpy as np
2import dask.array as da
3import math
4from pathlib import Path
5from bioio import BioImage
6from dataclasses import astuple
7from typing import Union, Any
8from .image_server import ImageServer
9from .metadata.image_metadata import ImageMetadata
10from .metadata.pixel_calibration import PixelCalibration, PixelLength
11from .metadata.image_shape import ImageShape
12from .region_2d import Region2D
15class BioIOServer(ImageServer):
16 """
17 An ImageServer using BioIO (https://github.com/bioio-devs/bioio).
19 What this actually supports will depend upon how BioIO is installed.
20 For example, it may provide Bio-Formats or CZI support... or it may not.
22 Note that the BioIo library does not currently handle unit attachment, so the pixel unit
23 given by this server will always be 'pixels'.
25 Note that the BioIO library does not properly support pyramids, so you might only get the full
26 resolution image when opening a pyramidal image.
27 """
29 def __init__(
30 self,
31 path: str,
32 scene: int = 0,
33 detect_resolutions=True,
34 bioio_kwargs: dict[str, Any] = {},
35 **kwargs
36 ):
37 """
38 :param path: the local path to the image to open
39 :param scene: BioIO divides images into scene. This parameter specifies which scene to consider
40 :param detect_resolutions: whether to look at all resolutions of the image (instead of just the full resolution)
41 :param bioio_kwargs: any specific keyword arguments to pass down to the fsspec created filesystem handled by the BioIO reader
42 :param resize_method: the resampling method to use when resizing the image for downsampling. Bicubic by default
43 """
44 super().__init__(**kwargs)
45 self._path = path
46 self._reader = BioImage(path, **bioio_kwargs)
47 self._scene = scene
48 self._detect_resolutions = detect_resolutions
50 def level_to_dask(
51 self, level: int = 0, chunk_width: int = 1024, chunk_height: int = 1024
52 ) -> da.Array:
53 if level < 0 or level >= self.metadata.n_resolutions:
54 raise ValueError(
55 "The provided level ({0}) is outside the valid range ([0, {1}])".format(
56 level, self.metadata.n_resolutions - 1
57 )
58 )
60 axes = (
61 ("T" if self.metadata.n_timepoints > 1 else "")
62 + (
63 ("S" if "S" in self._reader.dims.order else "C")
64 if self.metadata.n_channels > 1
65 else ""
66 )
67 + ("Z" if self.metadata.n_z_slices > 1 else "")
68 + "YX"
69 )
71 self._reader.set_scene(self._reader.scenes[self._scene + level])
72 return self._reader.get_image_dask_data(axes)
74 def close(self):
75 pass
77 def _build_metadata(self) -> ImageMetadata:
78 return ImageMetadata(
79 self._path,
80 Path(self._path).name,
81 self._get_shapes(self._reader, self._scene)
82 if self._detect_resolutions
83 else (
84 self._get_scene_shape(self._reader, self._reader.scenes[self._scene]),
85 ),
86 self._get_pixel_calibration(self._reader, self._scene),
87 self._is_rgb(self._reader, self._scene),
88 np.dtype(self._reader.dtype),
89 )
91 def _read_block(self, level: int, region: Region2D) -> np.ndarray:
92 x, y, width, height, z, t = astuple(region)
94 self._reader.set_scene(self._reader.scenes[self._scene + level])
95 axes = "TZ" + ("S" if "S" in self._reader.dims.order else "C") + "YX"
97 return self._reader.get_image_dask_data(axes)[
98 t, z, :, y : y + height, x : x + width
99 ].compute()
101 @staticmethod
102 def _get_shapes(reader: BioImage, scene: int) -> tuple[ImageShape, ...]:
103 shapes = []
104 for _scene in reader.scenes[scene:]:
105 shape = BioIOServer._get_scene_shape(reader, _scene)
107 if len(shapes) == 0 or BioIOServer._is_lower_resolution(shapes[-1], shape):
108 shapes.append(shape)
109 else:
110 break
111 return tuple(shapes)
113 @staticmethod
114 def _get_scene_shape(reader: BioImage, scene: Union[int, str]) -> ImageShape:
115 reader.set_scene(scene)
117 return ImageShape(
118 x=reader.dims.X if "X" in reader.dims.order else 1,
119 y=reader.dims.Y if "Y" in reader.dims.order else 1,
120 z=reader.dims.Z if "Z" in reader.dims.order else 1,
121 c=reader.dims.S
122 if "S" in reader.dims.order
123 else (reader.dims.C if "C" in reader.dims.order else 1),
124 t=reader.dims.T if "T" in reader.dims.order else 1,
125 )
127 @staticmethod
128 def _get_pixel_calibration(reader: BioImage, scene: int) -> PixelCalibration:
129 reader.set_scene(scene)
130 sizes = reader.physical_pixel_sizes
132 if sizes.X or sizes.Y or sizes.Z:
133 # The bioio library does not currently handle unit attachment, so the pixel unit is returned
134 return PixelCalibration(
135 PixelLength(sizes.X) if sizes.X is not None else PixelLength(),
136 PixelLength(sizes.Y) if sizes.Y is not None else PixelLength(),
137 PixelLength(sizes.Z) if sizes.Z is not None else PixelLength(),
138 )
139 else:
140 return PixelCalibration()
142 @staticmethod
143 def _is_rgb(reader: BioImage, scene: int) -> bool:
144 reader.set_scene(scene)
145 return ("S" in reader.dims.order and reader.dims.S in [3, 4]) or (
146 reader.dtype == np.uint8 and reader.dims.C == 3
147 )
149 @staticmethod
150 def _is_lower_resolution(base_shape: ImageShape, series_shape: ImageShape) -> bool:
151 """
152 Calculate if the series shape is a lower resolution than the base shape.
154 This involves a bit of guesswork, but it's needed for so long as BioIO doesn't properly support pyramids.
155 """
156 if (
157 base_shape.z == series_shape.z
158 and base_shape.t == series_shape.t
159 and base_shape.c == series_shape.c
160 ):
162 x_ratio = series_shape.x / base_shape.x
163 y_ratio = series_shape.y / base_shape.y
164 return x_ratio < 1.0 and math.isclose(x_ratio, y_ratio, rel_tol=0.01)
165 else:
166 return False