Coverage for qubalab/images/metadata/image_metadata.py: 95%
73 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-31 11:24 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-31 11:24 +0000
1import logging
2import math
3import numpy as np
4from .image_channel import ImageChannel
5from .image_shape import ImageShape
6from .pixel_calibration import PixelCalibration
9class ImageMetadata:
10 """
11 Simple class to store core metadata for a pyramidal image.
12 """
14 def __init__(
15 self,
16 path: str,
17 name: str,
18 shapes: tuple[ImageShape, ...],
19 pixel_calibration: PixelCalibration,
20 is_rgb: bool,
21 dtype: np.dtype,
22 channels: tuple[ImageChannel, ...] = None,
23 downsamples = None
24 ):
25 """
26 :param path: the local path to the image
27 :param name: the image name
28 :param shapes: the image shape, for each resolution of the image
29 :param pixel_calibration: the pixel calibration information of the image
30 :param is_rgb: whether pixels of the image are stored with the RGB format
31 :param dtype: the type of the pixel values
32 :param _channels: the channels of the image (optional)
33 :param _downsamples: the downsamples of the image (optional)
34 """
35 self.path = path
36 self.name = name
37 self.shapes = shapes
38 self.pixel_calibration = pixel_calibration
39 self.is_rgb = is_rgb
40 self.dtype = dtype
41 self._channels = channels
42 self._downsamples = downsamples
44 _DEFAULT_CHANNEL_SINGLE = (
45 ImageChannel(name='Single channel', color=(1, 1, 1)),
46 )
47 _DEFAULT_CHANNEL_RGB = (
48 ImageChannel(name='Red', color=(1, 0, 0)),
49 ImageChannel(name='Green', color=(0, 1, 0)),
50 ImageChannel(name='Green', color=(0, 0, 1)),
51 )
52 _DEFAULT_CHANNEL_TWO = (
53 ImageChannel(name='Channel 1', color=(1, 0, 1)),
54 ImageChannel(name='Channel 2', color=(0, 1, 0))
55 )
56 _DEFAULT_CHANNEL_COLORS = (
57 (0, 1, 1),
58 (1, 1, 0),
59 (1, 0, 1),
60 (1, 0, 0),
61 (0, 1, 0),
62 (0, 0, 1)
63 )
65 @property
66 def shape(self) -> ImageShape:
67 """
68 The dimensions of the full-resolution image.
69 """
70 return self.shapes[0]
72 @property
73 def width(self) -> int:
74 """
75 The width of the full-resolution image.
76 """
77 return self.shape.x
79 @property
80 def height(self) -> int:
81 """
82 The height of the full-resolution image.
83 """
84 return self.shape.y
86 @property
87 def n_channels(self) -> int:
88 """
89 The number of channels of the image.
90 """
91 return self.shape.c
93 @property
94 def n_timepoints(self) -> int:
95 """
96 The number of time points of the image.
97 """
98 return self.shape.t
100 @property
101 def n_z_slices(self) -> int:
102 """
103 The number of z-slices of the image.
104 """
105 return self.shape.z
107 @property
108 def n_resolutions(self) -> int:
109 """
110 The number of resolutions of the image.
111 """
112 return len(self.shapes)
114 @property
115 def downsamples(self) -> tuple[float, ...]:
116 """
117 The downsamples of the image.
118 """
119 if self._downsamples is None:
120 self._downsamples = tuple(self._estimate_downsample(self.shape, s) for s in self.shapes)
121 return self._downsamples
123 @property
124 def channels(self) -> tuple[ImageChannel, ...]:
125 """
126 The channels of the image.
127 """
128 if self._channels is None:
129 if self.is_rgb:
130 self._channels = self._DEFAULT_CHANNEL_RGB
131 else:
132 if self.n_channels == 1:
133 self._channels = self._DEFAULT_CHANNEL_SINGLE
134 elif self.n_channels == 2:
135 self._channels = self._DEFAULT_CHANNEL_TWO
136 else:
137 self._channels = [
138 ImageChannel(
139 f'Channel {ii + 1}',
140 self._DEFAULT_CHANNEL_COLORS[ii % len(self._DEFAULT_CHANNEL_COLORS)]
141 ) for ii in range(self.n_channels)
142 ]
143 return self._channels
145 def __eq__(self, other):
146 if isinstance(other, ImageMetadata):
147 return self.path == other.path and \
148 self.path == other.path and self.name == other.name and self.shapes == other.shapes \
149 and self.pixel_calibration == other.pixel_calibration and self.is_rgb == other.is_rgb \
150 and self.dtype == other.dtype and self.channels == other.channels and self.downsamples == other.downsamples
151 else:
152 return False
154 def _estimate_downsample(self, higher_resolution_shape: ImageShape, lower_resolution_shape: ImageShape) -> float:
155 """
156 Estimate the downsample factor between a higher resolution and a lower resolution ImageShape.
158 This is used to prefer values like 4 rather than 4.000345, which arise due to resolutions having to have
159 integer pixel dimensions.
160 The downsample is computed between the width and the heights of the shapes.
161 :param higher_resolution_shape: a higher resolution shape
162 :param lower_resolution_shape: a lower resolution shape
163 :return: the downsample between the higher and the lower resolution shape
164 """
166 dx = higher_resolution_shape.x / lower_resolution_shape.x
167 dy = higher_resolution_shape.y / lower_resolution_shape.y
168 downsample = (dx + dy) / 2.0
169 downsample_round = round(downsample)
171 if (
172 self._possible_downsample(higher_resolution_shape.x, lower_resolution_shape.x, downsample_round) and
173 self._possible_downsample(higher_resolution_shape.y, lower_resolution_shape.y, downsample_round)
174 ):
175 if downsample != downsample_round:
176 logging.debug(f'Returning rounded downsample value {downsample_round} instead of {downsample}')
177 return downsample_round
178 else:
179 return downsample
181 def _possible_downsample(self, higher_resolution_value: int, lower_resolution_value: int, downsample: float) -> bool:
182 """
183 Determine if an image dimension is what you'd expect after downsampling and applying floor or ceil.
185 :param higher_resolution_value: a higher resolution value to downsample
186 :param lower_resolution_value: a lower resolution value to compare to the downsampled higher resolution value
187 :param downsample: the downsample to apply to the higher resolution value
188 :return: whether the downsampled higher resolution value corresponds to the lower resolution value
189 """
190 return (math.floor(higher_resolution_value / downsample) == lower_resolution_value or
191 math.ceil(higher_resolution_value / downsample) == lower_resolution_value)