Coverage for qubalab/images/bioio_server.py: 88%

64 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-31 11:24 +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 .image_server import ImageServer 

8from .metadata.image_metadata import ImageMetadata 

9from .metadata.pixel_calibration import PixelCalibration, PixelLength 

10from .metadata.image_shape import ImageShape 

11from .region_2d import Region2D 

12 

13 

14class BioIOServer(ImageServer): 

15 """ 

16 An ImageServer using BioIO (https://github.com/AllenCellModeling/BioIO). 

17 

18 What this actually supports will depend upon how BioIO is installed. 

19 For example, it may provide Bio-Formats or CZI support... or it may not. 

20 

21 Note that the BioIo library does not currently handle unit attachment, so the pixel unit 

22 given by this server will always be 'pixels'. 

23  

24 Note that the BioIO library does not properly support pyramids, so you might only get the full 

25 resolution image when opening a pyramidal image. 

26 """ 

27 

28 def __init__(self, path: str, scene: int = 0, detect_resolutions=True, bioio_kwargs: dict[str, any] = {}, **kwargs): 

29 """ 

30 :param path: the local path to the image to open 

31 :param scene: BioIO divides images into scene. This parameter specifies which scene to consider 

32 :param detect_resolutions: whether to look at all resolutions of the image (instead of just the full resolution) 

33 :param bioio_kwargs: any specific keyword arguments to pass down to the fsspec created filesystem handled by the BioIO reader 

34 :param resize_method: the resampling method to use when resizing the image for downsampling. Bicubic by default 

35 """ 

36 super().__init__(**kwargs) 

37 self._path = path 

38 self._reader = BioImage(path, **bioio_kwargs) 

39 self._scene = scene 

40 self._detect_resolutions = detect_resolutions 

41 

42 def level_to_dask(self, level: int = 0) -> da.Array: 

43 if level < 0 or level >= self.metadata.n_resolutions: 

44 raise ValueError("The provided level ({0}) is outside the valid range ([0, {1}])".format(level, self.metadata.n_resolutions - 1)) 

45 

46 axes = ("T" if self.metadata.n_timepoints > 1 else "") + \ 

47 (("S" if "S" in self._reader.dims.order else "C") if self.metadata.n_channels > 1 else "") + \ 

48 ("Z" if self.metadata.n_z_slices > 1 else "") + \ 

49 "YX" 

50 

51 self._reader.set_scene(self._reader.scenes[self._scene + level]) 

52 return self._reader.get_image_dask_data(axes) 

53 

54 def close(self): 

55 pass 

56 

57 def _build_metadata(self) -> ImageMetadata: 

58 return ImageMetadata( 

59 self._path, 

60 Path(self._path).name, 

61 self._get_shapes(self._reader, self._scene) if self._detect_resolutions else (self._get_scene_shape(self._reader, self._reader.scenes[self._scene]),), 

62 self._get_pixel_calibration(self._reader, self._scene), 

63 self._is_rgb(self._reader, self._scene), 

64 np.dtype(self._reader.dtype) 

65 ) 

66 

67 def _read_block(self, level: int, region: Region2D) -> np.ndarray: 

68 x, y, width, height, z, t = astuple(region) 

69 

70 self._reader.set_scene(self._reader.scenes[self._scene + level]) 

71 axes = "TZ" + ("S" if "S" in self._reader.dims.order else "C") + "YX" 

72 

73 return self._reader.get_image_dask_data(axes)[t, z, :, y:y + height, x:x + width].compute() 

74 

75 @staticmethod 

76 def _get_shapes(reader: BioImage, scene: int) -> tuple[ImageShape, ...]: 

77 shapes = [] 

78 for scene in reader.scenes[scene:]: 

79 shape = BioIOServer._get_scene_shape(reader, scene) 

80 

81 if len(shapes) == 0 or BioIOServer._is_lower_resolution(shapes[-1], shape): 

82 shapes.append(shape) 

83 else: 

84 break 

85 return tuple(shapes) 

86 

87 @staticmethod 

88 def _get_scene_shape(reader: BioImage, scene: int) -> ImageShape: 

89 reader.set_scene(scene) 

90 

91 return ImageShape( 

92 x=reader.dims.X if 'X' in reader.dims.order else 1, 

93 y=reader.dims.Y if 'Y' in reader.dims.order else 1, 

94 z=reader.dims.Z if 'Z' in reader.dims.order else 1, 

95 c=reader.dims.S if 'S' in reader.dims.order else (reader.dims.C if 'C' in reader.dims.order else 1), 

96 t=reader.dims.T if 'T' in reader.dims.order else 1, 

97 ) 

98 

99 @staticmethod 

100 def _get_pixel_calibration(reader: BioImage, scene: int) -> PixelCalibration: 

101 reader.set_scene(scene) 

102 sizes = reader.physical_pixel_sizes 

103 

104 if sizes.X or sizes.Y or sizes.Z: 

105 # The bioio library does not currently handle unit attachment, so the pixel unit is returned 

106 return PixelCalibration( 

107 PixelLength(sizes.X) if sizes.X is not None else PixelLength(), 

108 PixelLength(sizes.Y) if sizes.Y is not None else PixelLength(), 

109 PixelLength(sizes.Z) if sizes.Z is not None else PixelLength() 

110 ) 

111 else: 

112 return PixelCalibration() 

113 

114 @staticmethod 

115 def _is_rgb(reader: BioImage, scene: int) -> bool: 

116 reader.set_scene(scene) 

117 return ('S' in reader.dims.order and reader.dims.S in [3, 4]) or (reader.dtype == np.uint8 and reader.dims.C == 3) 

118 

119 @staticmethod 

120 def _is_lower_resolution(base_shape: ImageShape, series_shape: ImageShape) -> bool: 

121 """ 

122 Calculate if the series shape is a lower resolution than the base shape. 

123 

124 This involves a bit of guesswork, but it's needed for so long as BioIO doesn't properly support pyramids. 

125 """ 

126 if base_shape.z == series_shape.z and \ 

127 base_shape.t == series_shape.t and \ 

128 base_shape.c == series_shape.c: 

129 

130 x_ratio = series_shape.x / base_shape.x 

131 y_ratio = series_shape.y / base_shape.y 

132 return x_ratio < 1.0 and math.isclose(x_ratio, y_ratio, rel_tol=0.01) 

133 else: 

134 return False 

135