csi_images.csi_frames

Contains the Frame class, which represents a single frame of an image. The Frame class does not hold the image data, but allows for easy loading of the image data from the appropriate file. This module also contains functions for creating RGB and RGBW composite images from a tile and a set of channels.

  1"""
  2Contains the Frame class, which represents a single frame of an image. The Frame class
  3does not hold the image data, but allows for easy loading of the image data from the
  4appropriate file. This module also contains functions for creating RGB and RGBW
  5composite images from a tile and a set of channels.
  6"""
  7
  8import os
  9from typing import Self, Iterable
 10
 11import numpy as np
 12
 13from .csi_scans import Scan
 14from .csi_tiles import Tile
 15
 16# Optional dependencies; will raise errors in particular functions if not installed
 17try:
 18    from . import csi_images
 19except ImportError:
 20    csi_images = None
 21try:
 22    import imageio.v3 as imageio
 23except ImportError:
 24    imageio = None
 25
 26
 27class Frame:
 28    def __init__(self, tile: Tile, channel: int | str):
 29        self.tile = tile
 30        if isinstance(channel, int):
 31            self.channel = channel
 32            if self.channel < 0 or self.channel >= len(tile.scan.channels):
 33                raise ValueError(
 34                    f"Channel index {self.channel} is out of bounds for scan."
 35                )
 36        elif isinstance(channel, str):
 37            self.channel = tile.scan.get_channel_indices([channel])[0]
 38        else:
 39            raise ValueError("Channel must be an integer or a string.")
 40
 41    def __key(self) -> tuple:
 42        return self.tile, self.channel
 43
 44    def __hash__(self) -> int:
 45        return hash(self.__key())
 46
 47    def __repr__(self) -> str:
 48        return f"{self.tile}-{self.tile.scan.channels[self.channel].name}"
 49
 50    def __eq__(self, other) -> bool:
 51        return self.__repr__() == other.__repr__()
 52
 53    def get_file_path(self, input_path: str = None, ext: str = ".tif") -> str:
 54        """
 55        Get the file path for the frame, optionally changing
 56        the scan path and file extension.
 57        :param input_path: the path to the scan's directory. If None, defaults to
 58                           the path loaded in the frame's tile's scan object.
 59        :param ext: the image file extension. Defaults to .tif.
 60        :return: the file path.
 61        """
 62        if input_path is None:
 63            input_path = self.tile.scan.path
 64            if len(self.tile.scan.roi) > 1:
 65                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
 66        # Remove trailing slashes
 67        if input_path[-1] == os.sep:
 68            input_path = input_path[:-1]
 69        # Append proc if it's pointing to the base bzScanner directory
 70        if input_path.endswith("bzScanner"):
 71            input_path = os.path.join(input_path, "proc")
 72        # Should be a directory; append the file name
 73        if os.path.isdir(input_path):
 74            input_path = os.path.join(input_path, self.get_file_name(ext))
 75        else:
 76            raise ValueError(f"Input path {input_path} is not a directory.")
 77        return input_path
 78
 79    def get_file_name(self, ext: str = ".tif") -> str:
 80        """
 81        Get the file name for the frame, handling different name conventions by scanner.
 82        :param ext: the image file extension. Defaults to .tif.
 83        :return: the file name.
 84        """
 85        if self.tile.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
 86            channel_name = self.tile.scan.channels[self.channel].name
 87            x = self.tile.x
 88            y = self.tile.y
 89            file_name = f"{channel_name}-X{x:03}-Y{y:03}{ext}"
 90        elif self.tile.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
 91            # BZScanner has channels in a specific order
 92            channel_name = self.tile.scan.channels[self.channel].name
 93            real_channel_index = list(
 94                self.tile.scan.BZSCANNER_CHANNEL_MAP.values()
 95            ).index(channel_name)
 96            # Determine total tiles
 97            roi = self.tile.scan.roi[self.tile.n_roi]
 98            total_tiles = roi.tile_rows * roi.tile_cols
 99            # Offset is based on total tiles and "real" channel index
100            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
101            n_bzscanner = self.tile.n + tile_offset
102            file_name = f"Tile{n_bzscanner:06}{ext}"
103        else:
104            raise ValueError(f"Scanner {self.tile.scan.scanner_id} not supported.")
105        return file_name
106
107    def get_image(self, input_path: str = None, apply_gain: bool = True) -> np.ndarray:
108        """
109        Loads the image for this frame. Handles .tif (will return 16-bit images) and
110        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
111        .jpg/.jpeg images (compressed, using .tags files).
112        :param input_path: the path to the scan's directory. If None, defaults to
113                           the path loaded in the frame's tile's scan object.
114        :param apply_gain: whether to apply the gain to the image. Only has an effect
115                           if the scanner calculated but did not apply gain. Defaults to True.
116        :return: the array representing the image.
117        """
118        file_path = self.get_file_path(input_path)
119
120        # Check for the file
121        if not os.path.exists(file_path):
122            # Alternative: could be a .jpg/.jpeg file, test both
123            if os.path.exists(os.path.splitext(file_path)[0] + ".jpg"):
124                image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpg")
125            elif os.path.exists(os.path.splitext(file_path)[0] + ".jpeg"):
126                image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpeg")
127            else:
128                raise FileNotFoundError(
129                    f"Could not find image at {file_path} or "
130                    f"any format alternatives (.jpg, .jpeg)."
131                )
132        else:
133            # Load the image
134            if imageio is None:
135                raise ModuleNotFoundError(
136                    "imageio libraries not installed! "
137                    "run `pip install csi_images[imageio]` to resolve."
138                )
139            image = imageio.imread(file_path)
140        if image is None or image.size == 0:
141            raise ValueError(f"Could not load image from {file_path}")
142        if apply_gain and not self.tile.scan.channels[self.channel].gain_applied:
143            # Multiply by the gain if it hasn't been applied, avoiding overflow
144            dtype = image.dtype
145            image = image.astype(np.uint32)
146            image = image * self.tile.scan.channels[self.channel].intensity
147            image = np.clip(image, 0, np.iinfo(dtype).max).astype(dtype)
148        return image
149
150    @staticmethod
151    def _get_jpeg_image(input_path: str) -> np.ndarray:
152        if imageio is None:
153            raise ModuleNotFoundError(
154                "imageio libraries not installed! "
155                "run `pip install csi_images[imageio]` to resolve."
156            )
157        image = imageio.imread(input_path)
158        if os.path.isfile(os.path.splitext(input_path)[0] + ".tags"):
159            min_name = "PreservedMinValue"
160            max_name = "PreservedMaxValue"
161            min_value, max_value = -1, -1
162            with open(os.path.splitext(input_path)[0] + ".tags", "r") as f:
163                for line in f:
164                    if line.startswith(min_name):
165                        min_value = float(line.split("=")[1].strip())
166                    elif line.startswith(max_name):
167                        max_value = float(line.split("=")[1].strip())
168                    if min_value != -1 and max_value != -1:
169                        break
170            if min_value != -1 and max_value != -1:
171                if max_value > 1:
172                    raise ValueError(f"{max_name} is greater than 1; unexpected .tags")
173                if min_value < 0:
174                    raise ValueError(f"{min_name} is less than 0; unexpected .tags")
175                # Return to [0, 1], scale + offset, then return to a 16-bit image
176                image = csi_images.scale_bit_depth(image, np.float64)
177                image = image * (max_value - min_value) + min_value
178                image = csi_images.scale_bit_depth(image, np.uint16)
179            else:
180                raise ValueError(
181                    f"Could not find {min_name} and {max_name} in .tags file."
182                )
183        else:
184            raise FileNotFoundError(f"Could not find .tags file for {input_path}")
185        return image
186
187    def check_image(self, input_path: str = None) -> bool:
188        """
189        Check if the image for this frame exists.
190        :param input_path: the path to the scan's directory. If None, defaults to
191                           the path loaded in the frame's tile's scan object.
192        :return: whether the image exists.
193        """
194        file_path = self.get_file_path(input_path)
195        # 72 is the minimum size for a valid TIFF file
196        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
197            return True
198        else:
199            # Alternative: could be a .jpg/.jpeg file, test both
200            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
201            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
202                file_path = jpeg_path
203            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
204            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
205                file_path = jpeg_path
206            # If we've found a .jpg/.jpeg, it must have a .tags file with it
207            if file_path == jpeg_path:
208                tags_path = os.path.splitext(file_path)[0] + ".tags"
209                # Tags are text files that should include at least a few bytes
210                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
211                    return True
212        # Didn't hit any of those, return false
213        return False
214
215    @classmethod
216    def check_all_images(cls, scan: Scan):
217        """
218        Check if all images for a scan exist, either in .tif or .jpg form.
219        :param scan:
220        :return:
221        """
222        for n in range(len(scan.roi)):
223            for frames in cls.get_all_frames(scan, n_roi=n):
224                for frame in frames:
225                    if not frame.check_image():
226                        return False
227        return True
228
229    @classmethod
230    def get_frames(
231        cls,
232        tile: Tile,
233        channels: Iterable[int | str] = None,
234    ) -> list[Self]:
235        """
236        Get the frames for a tile and a set of channels. By default, gets all channels.
237        :param tile: the tile.
238        :param channels: the channels, as indices or names. Defaults to all channels.
239        :return: the frames, in order of the channels.
240        """
241        if channels is None:
242            channels = range(len(tile.scan.channels))
243
244        frames = []
245        for channel in channels:
246            frames.append(Frame(tile, channel))
247        return frames
248
249    @classmethod
250    def get_all_frames(
251        cls,
252        scan: Scan,
253        channels: Iterable[int | str] = None,
254        n_roi: int = 0,
255        as_flat: bool = True,
256    ) -> list[list[Self]] | list[list[list[Self]]]:
257        """
258        Get all frames for a scan and a set of channels.
259        :param scan: the scan metadata.
260        :param channels: the channels, as indices or names. Defaults to all channels.
261        :param n_roi: the region of interest to use. Defaults to 0.
262        :param as_flat: whether to flatten the frames into a 2D list.
263        :return: if as_flat: 2D list of frames, organized as [n][channel];
264                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
265        """
266        if as_flat:
267            frames = []
268            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
269                tile = Tile(scan, n, n_roi)
270                frames.append(cls.get_frames(tile, channels))
271        else:
272            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
273            for x in range(scan.roi[n_roi].tile_cols):
274                for y in range(scan.roi[n_roi].tile_rows):
275                    tile = Tile(scan, (x, y), n_roi)
276                    frames[y][x] = cls.get_frames(tile, channels)
277        return frames
278
279    @classmethod
280    def make_rgb_image(
281        cls,
282        tile: Tile,
283        channels: dict[int, tuple[float, float, float]],
284        input_path=None,
285    ) -> np.ndarray:
286        """
287        Convenience method for creating an RGB image from a tile and a set of channels
288        without manually extracting any frames.
289        :param tile: the tile for which the image should be made.
290        :param channels: a dictionary of scan channel indices and RGB gains.
291        :param input_path: the path to the input images. Will use metadata if not provided.
292        :return: the image as a numpy array.
293        """
294        if csi_images is None:
295            raise ModuleNotFoundError(
296                "imageio libraries not installed! "
297                "run `pip install csi_images[imageio]` to resolve."
298            )
299        images = []
300        colors = []
301        for channel_index, color in channels.items():
302            if channel_index == -1:
303                continue
304            image = Frame(tile, channel_index).get_image(input_path)
305            images.append(image)
306            colors.append(color)
307        return csi_images.make_rgb(images, colors)
class Frame:
 28class Frame:
 29    def __init__(self, tile: Tile, channel: int | str):
 30        self.tile = tile
 31        if isinstance(channel, int):
 32            self.channel = channel
 33            if self.channel < 0 or self.channel >= len(tile.scan.channels):
 34                raise ValueError(
 35                    f"Channel index {self.channel} is out of bounds for scan."
 36                )
 37        elif isinstance(channel, str):
 38            self.channel = tile.scan.get_channel_indices([channel])[0]
 39        else:
 40            raise ValueError("Channel must be an integer or a string.")
 41
 42    def __key(self) -> tuple:
 43        return self.tile, self.channel
 44
 45    def __hash__(self) -> int:
 46        return hash(self.__key())
 47
 48    def __repr__(self) -> str:
 49        return f"{self.tile}-{self.tile.scan.channels[self.channel].name}"
 50
 51    def __eq__(self, other) -> bool:
 52        return self.__repr__() == other.__repr__()
 53
 54    def get_file_path(self, input_path: str = None, ext: str = ".tif") -> str:
 55        """
 56        Get the file path for the frame, optionally changing
 57        the scan path and file extension.
 58        :param input_path: the path to the scan's directory. If None, defaults to
 59                           the path loaded in the frame's tile's scan object.
 60        :param ext: the image file extension. Defaults to .tif.
 61        :return: the file path.
 62        """
 63        if input_path is None:
 64            input_path = self.tile.scan.path
 65            if len(self.tile.scan.roi) > 1:
 66                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
 67        # Remove trailing slashes
 68        if input_path[-1] == os.sep:
 69            input_path = input_path[:-1]
 70        # Append proc if it's pointing to the base bzScanner directory
 71        if input_path.endswith("bzScanner"):
 72            input_path = os.path.join(input_path, "proc")
 73        # Should be a directory; append the file name
 74        if os.path.isdir(input_path):
 75            input_path = os.path.join(input_path, self.get_file_name(ext))
 76        else:
 77            raise ValueError(f"Input path {input_path} is not a directory.")
 78        return input_path
 79
 80    def get_file_name(self, ext: str = ".tif") -> str:
 81        """
 82        Get the file name for the frame, handling different name conventions by scanner.
 83        :param ext: the image file extension. Defaults to .tif.
 84        :return: the file name.
 85        """
 86        if self.tile.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
 87            channel_name = self.tile.scan.channels[self.channel].name
 88            x = self.tile.x
 89            y = self.tile.y
 90            file_name = f"{channel_name}-X{x:03}-Y{y:03}{ext}"
 91        elif self.tile.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
 92            # BZScanner has channels in a specific order
 93            channel_name = self.tile.scan.channels[self.channel].name
 94            real_channel_index = list(
 95                self.tile.scan.BZSCANNER_CHANNEL_MAP.values()
 96            ).index(channel_name)
 97            # Determine total tiles
 98            roi = self.tile.scan.roi[self.tile.n_roi]
 99            total_tiles = roi.tile_rows * roi.tile_cols
100            # Offset is based on total tiles and "real" channel index
101            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
102            n_bzscanner = self.tile.n + tile_offset
103            file_name = f"Tile{n_bzscanner:06}{ext}"
104        else:
105            raise ValueError(f"Scanner {self.tile.scan.scanner_id} not supported.")
106        return file_name
107
108    def get_image(self, input_path: str = None, apply_gain: bool = True) -> np.ndarray:
109        """
110        Loads the image for this frame. Handles .tif (will return 16-bit images) and
111        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
112        .jpg/.jpeg images (compressed, using .tags files).
113        :param input_path: the path to the scan's directory. If None, defaults to
114                           the path loaded in the frame's tile's scan object.
115        :param apply_gain: whether to apply the gain to the image. Only has an effect
116                           if the scanner calculated but did not apply gain. Defaults to True.
117        :return: the array representing the image.
118        """
119        file_path = self.get_file_path(input_path)
120
121        # Check for the file
122        if not os.path.exists(file_path):
123            # Alternative: could be a .jpg/.jpeg file, test both
124            if os.path.exists(os.path.splitext(file_path)[0] + ".jpg"):
125                image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpg")
126            elif os.path.exists(os.path.splitext(file_path)[0] + ".jpeg"):
127                image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpeg")
128            else:
129                raise FileNotFoundError(
130                    f"Could not find image at {file_path} or "
131                    f"any format alternatives (.jpg, .jpeg)."
132                )
133        else:
134            # Load the image
135            if imageio is None:
136                raise ModuleNotFoundError(
137                    "imageio libraries not installed! "
138                    "run `pip install csi_images[imageio]` to resolve."
139                )
140            image = imageio.imread(file_path)
141        if image is None or image.size == 0:
142            raise ValueError(f"Could not load image from {file_path}")
143        if apply_gain and not self.tile.scan.channels[self.channel].gain_applied:
144            # Multiply by the gain if it hasn't been applied, avoiding overflow
145            dtype = image.dtype
146            image = image.astype(np.uint32)
147            image = image * self.tile.scan.channels[self.channel].intensity
148            image = np.clip(image, 0, np.iinfo(dtype).max).astype(dtype)
149        return image
150
151    @staticmethod
152    def _get_jpeg_image(input_path: str) -> np.ndarray:
153        if imageio is None:
154            raise ModuleNotFoundError(
155                "imageio libraries not installed! "
156                "run `pip install csi_images[imageio]` to resolve."
157            )
158        image = imageio.imread(input_path)
159        if os.path.isfile(os.path.splitext(input_path)[0] + ".tags"):
160            min_name = "PreservedMinValue"
161            max_name = "PreservedMaxValue"
162            min_value, max_value = -1, -1
163            with open(os.path.splitext(input_path)[0] + ".tags", "r") as f:
164                for line in f:
165                    if line.startswith(min_name):
166                        min_value = float(line.split("=")[1].strip())
167                    elif line.startswith(max_name):
168                        max_value = float(line.split("=")[1].strip())
169                    if min_value != -1 and max_value != -1:
170                        break
171            if min_value != -1 and max_value != -1:
172                if max_value > 1:
173                    raise ValueError(f"{max_name} is greater than 1; unexpected .tags")
174                if min_value < 0:
175                    raise ValueError(f"{min_name} is less than 0; unexpected .tags")
176                # Return to [0, 1], scale + offset, then return to a 16-bit image
177                image = csi_images.scale_bit_depth(image, np.float64)
178                image = image * (max_value - min_value) + min_value
179                image = csi_images.scale_bit_depth(image, np.uint16)
180            else:
181                raise ValueError(
182                    f"Could not find {min_name} and {max_name} in .tags file."
183                )
184        else:
185            raise FileNotFoundError(f"Could not find .tags file for {input_path}")
186        return image
187
188    def check_image(self, input_path: str = None) -> bool:
189        """
190        Check if the image for this frame exists.
191        :param input_path: the path to the scan's directory. If None, defaults to
192                           the path loaded in the frame's tile's scan object.
193        :return: whether the image exists.
194        """
195        file_path = self.get_file_path(input_path)
196        # 72 is the minimum size for a valid TIFF file
197        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
198            return True
199        else:
200            # Alternative: could be a .jpg/.jpeg file, test both
201            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
202            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
203                file_path = jpeg_path
204            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
205            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
206                file_path = jpeg_path
207            # If we've found a .jpg/.jpeg, it must have a .tags file with it
208            if file_path == jpeg_path:
209                tags_path = os.path.splitext(file_path)[0] + ".tags"
210                # Tags are text files that should include at least a few bytes
211                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
212                    return True
213        # Didn't hit any of those, return false
214        return False
215
216    @classmethod
217    def check_all_images(cls, scan: Scan):
218        """
219        Check if all images for a scan exist, either in .tif or .jpg form.
220        :param scan:
221        :return:
222        """
223        for n in range(len(scan.roi)):
224            for frames in cls.get_all_frames(scan, n_roi=n):
225                for frame in frames:
226                    if not frame.check_image():
227                        return False
228        return True
229
230    @classmethod
231    def get_frames(
232        cls,
233        tile: Tile,
234        channels: Iterable[int | str] = None,
235    ) -> list[Self]:
236        """
237        Get the frames for a tile and a set of channels. By default, gets all channels.
238        :param tile: the tile.
239        :param channels: the channels, as indices or names. Defaults to all channels.
240        :return: the frames, in order of the channels.
241        """
242        if channels is None:
243            channels = range(len(tile.scan.channels))
244
245        frames = []
246        for channel in channels:
247            frames.append(Frame(tile, channel))
248        return frames
249
250    @classmethod
251    def get_all_frames(
252        cls,
253        scan: Scan,
254        channels: Iterable[int | str] = None,
255        n_roi: int = 0,
256        as_flat: bool = True,
257    ) -> list[list[Self]] | list[list[list[Self]]]:
258        """
259        Get all frames for a scan and a set of channels.
260        :param scan: the scan metadata.
261        :param channels: the channels, as indices or names. Defaults to all channels.
262        :param n_roi: the region of interest to use. Defaults to 0.
263        :param as_flat: whether to flatten the frames into a 2D list.
264        :return: if as_flat: 2D list of frames, organized as [n][channel];
265                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
266        """
267        if as_flat:
268            frames = []
269            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
270                tile = Tile(scan, n, n_roi)
271                frames.append(cls.get_frames(tile, channels))
272        else:
273            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
274            for x in range(scan.roi[n_roi].tile_cols):
275                for y in range(scan.roi[n_roi].tile_rows):
276                    tile = Tile(scan, (x, y), n_roi)
277                    frames[y][x] = cls.get_frames(tile, channels)
278        return frames
279
280    @classmethod
281    def make_rgb_image(
282        cls,
283        tile: Tile,
284        channels: dict[int, tuple[float, float, float]],
285        input_path=None,
286    ) -> np.ndarray:
287        """
288        Convenience method for creating an RGB image from a tile and a set of channels
289        without manually extracting any frames.
290        :param tile: the tile for which the image should be made.
291        :param channels: a dictionary of scan channel indices and RGB gains.
292        :param input_path: the path to the input images. Will use metadata if not provided.
293        :return: the image as a numpy array.
294        """
295        if csi_images is None:
296            raise ModuleNotFoundError(
297                "imageio libraries not installed! "
298                "run `pip install csi_images[imageio]` to resolve."
299            )
300        images = []
301        colors = []
302        for channel_index, color in channels.items():
303            if channel_index == -1:
304                continue
305            image = Frame(tile, channel_index).get_image(input_path)
306            images.append(image)
307            colors.append(color)
308        return csi_images.make_rgb(images, colors)
Frame(tile: csi_images.csi_tiles.Tile, channel: int | str)
29    def __init__(self, tile: Tile, channel: int | str):
30        self.tile = tile
31        if isinstance(channel, int):
32            self.channel = channel
33            if self.channel < 0 or self.channel >= len(tile.scan.channels):
34                raise ValueError(
35                    f"Channel index {self.channel} is out of bounds for scan."
36                )
37        elif isinstance(channel, str):
38            self.channel = tile.scan.get_channel_indices([channel])[0]
39        else:
40            raise ValueError("Channel must be an integer or a string.")
tile
def get_file_path(self, input_path: str = None, ext: str = '.tif') -> str:
54    def get_file_path(self, input_path: str = None, ext: str = ".tif") -> str:
55        """
56        Get the file path for the frame, optionally changing
57        the scan path and file extension.
58        :param input_path: the path to the scan's directory. If None, defaults to
59                           the path loaded in the frame's tile's scan object.
60        :param ext: the image file extension. Defaults to .tif.
61        :return: the file path.
62        """
63        if input_path is None:
64            input_path = self.tile.scan.path
65            if len(self.tile.scan.roi) > 1:
66                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
67        # Remove trailing slashes
68        if input_path[-1] == os.sep:
69            input_path = input_path[:-1]
70        # Append proc if it's pointing to the base bzScanner directory
71        if input_path.endswith("bzScanner"):
72            input_path = os.path.join(input_path, "proc")
73        # Should be a directory; append the file name
74        if os.path.isdir(input_path):
75            input_path = os.path.join(input_path, self.get_file_name(ext))
76        else:
77            raise ValueError(f"Input path {input_path} is not a directory.")
78        return input_path

Get the file path for the frame, optionally changing the scan path and file extension.

Parameters
  • input_path: the path to the scan's directory. If None, defaults to the path loaded in the frame's tile's scan object.
  • ext: the image file extension. Defaults to .tif.
Returns

the file path.

def get_file_name(self, ext: str = '.tif') -> str:
 80    def get_file_name(self, ext: str = ".tif") -> str:
 81        """
 82        Get the file name for the frame, handling different name conventions by scanner.
 83        :param ext: the image file extension. Defaults to .tif.
 84        :return: the file name.
 85        """
 86        if self.tile.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
 87            channel_name = self.tile.scan.channels[self.channel].name
 88            x = self.tile.x
 89            y = self.tile.y
 90            file_name = f"{channel_name}-X{x:03}-Y{y:03}{ext}"
 91        elif self.tile.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
 92            # BZScanner has channels in a specific order
 93            channel_name = self.tile.scan.channels[self.channel].name
 94            real_channel_index = list(
 95                self.tile.scan.BZSCANNER_CHANNEL_MAP.values()
 96            ).index(channel_name)
 97            # Determine total tiles
 98            roi = self.tile.scan.roi[self.tile.n_roi]
 99            total_tiles = roi.tile_rows * roi.tile_cols
100            # Offset is based on total tiles and "real" channel index
101            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
102            n_bzscanner = self.tile.n + tile_offset
103            file_name = f"Tile{n_bzscanner:06}{ext}"
104        else:
105            raise ValueError(f"Scanner {self.tile.scan.scanner_id} not supported.")
106        return file_name

Get the file name for the frame, handling different name conventions by scanner.

Parameters
  • ext: the image file extension. Defaults to .tif.
Returns

the file name.

def get_image(self, input_path: str = None, apply_gain: bool = True) -> numpy.ndarray:
108    def get_image(self, input_path: str = None, apply_gain: bool = True) -> np.ndarray:
109        """
110        Loads the image for this frame. Handles .tif (will return 16-bit images) and
111        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
112        .jpg/.jpeg images (compressed, using .tags files).
113        :param input_path: the path to the scan's directory. If None, defaults to
114                           the path loaded in the frame's tile's scan object.
115        :param apply_gain: whether to apply the gain to the image. Only has an effect
116                           if the scanner calculated but did not apply gain. Defaults to True.
117        :return: the array representing the image.
118        """
119        file_path = self.get_file_path(input_path)
120
121        # Check for the file
122        if not os.path.exists(file_path):
123            # Alternative: could be a .jpg/.jpeg file, test both
124            if os.path.exists(os.path.splitext(file_path)[0] + ".jpg"):
125                image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpg")
126            elif os.path.exists(os.path.splitext(file_path)[0] + ".jpeg"):
127                image = self._get_jpeg_image(os.path.splitext(file_path)[0] + ".jpeg")
128            else:
129                raise FileNotFoundError(
130                    f"Could not find image at {file_path} or "
131                    f"any format alternatives (.jpg, .jpeg)."
132                )
133        else:
134            # Load the image
135            if imageio is None:
136                raise ModuleNotFoundError(
137                    "imageio libraries not installed! "
138                    "run `pip install csi_images[imageio]` to resolve."
139                )
140            image = imageio.imread(file_path)
141        if image is None or image.size == 0:
142            raise ValueError(f"Could not load image from {file_path}")
143        if apply_gain and not self.tile.scan.channels[self.channel].gain_applied:
144            # Multiply by the gain if it hasn't been applied, avoiding overflow
145            dtype = image.dtype
146            image = image.astype(np.uint32)
147            image = image * self.tile.scan.channels[self.channel].intensity
148            image = np.clip(image, 0, np.iinfo(dtype).max).astype(dtype)
149        return image

Loads the image for this frame. Handles .tif (will return 16-bit images) and .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing .jpg/.jpeg images (compressed, using .tags files).

Parameters
  • input_path: the path to the scan's directory. If None, defaults to the path loaded in the frame's tile's scan object.
  • apply_gain: whether to apply the gain to the image. Only has an effect if the scanner calculated but did not apply gain. Defaults to True.
Returns

the array representing the image.

def check_image(self, input_path: str = None) -> bool:
188    def check_image(self, input_path: str = None) -> bool:
189        """
190        Check if the image for this frame exists.
191        :param input_path: the path to the scan's directory. If None, defaults to
192                           the path loaded in the frame's tile's scan object.
193        :return: whether the image exists.
194        """
195        file_path = self.get_file_path(input_path)
196        # 72 is the minimum size for a valid TIFF file
197        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
198            return True
199        else:
200            # Alternative: could be a .jpg/.jpeg file, test both
201            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
202            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
203                file_path = jpeg_path
204            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
205            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
206                file_path = jpeg_path
207            # If we've found a .jpg/.jpeg, it must have a .tags file with it
208            if file_path == jpeg_path:
209                tags_path = os.path.splitext(file_path)[0] + ".tags"
210                # Tags are text files that should include at least a few bytes
211                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
212                    return True
213        # Didn't hit any of those, return false
214        return False

Check if the image for this frame exists.

Parameters
  • input_path: the path to the scan's directory. If None, defaults to the path loaded in the frame's tile's scan object.
Returns

whether the image exists.

@classmethod
def check_all_images(cls, scan: csi_images.csi_scans.Scan):
216    @classmethod
217    def check_all_images(cls, scan: Scan):
218        """
219        Check if all images for a scan exist, either in .tif or .jpg form.
220        :param scan:
221        :return:
222        """
223        for n in range(len(scan.roi)):
224            for frames in cls.get_all_frames(scan, n_roi=n):
225                for frame in frames:
226                    if not frame.check_image():
227                        return False
228        return True

Check if all images for a scan exist, either in .tif or .jpg form.

Parameters
  • scan:
Returns
@classmethod
def get_frames( cls, tile: csi_images.csi_tiles.Tile, channels: Iterable[int | str] = None) -> list[typing.Self]:
230    @classmethod
231    def get_frames(
232        cls,
233        tile: Tile,
234        channels: Iterable[int | str] = None,
235    ) -> list[Self]:
236        """
237        Get the frames for a tile and a set of channels. By default, gets all channels.
238        :param tile: the tile.
239        :param channels: the channels, as indices or names. Defaults to all channels.
240        :return: the frames, in order of the channels.
241        """
242        if channels is None:
243            channels = range(len(tile.scan.channels))
244
245        frames = []
246        for channel in channels:
247            frames.append(Frame(tile, channel))
248        return frames

Get the frames for a tile and a set of channels. By default, gets all channels.

Parameters
  • tile: the tile.
  • channels: the channels, as indices or names. Defaults to all channels.
Returns

the frames, in order of the channels.

@classmethod
def get_all_frames( cls, scan: csi_images.csi_scans.Scan, channels: Iterable[int | str] = None, n_roi: int = 0, as_flat: bool = True) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
250    @classmethod
251    def get_all_frames(
252        cls,
253        scan: Scan,
254        channels: Iterable[int | str] = None,
255        n_roi: int = 0,
256        as_flat: bool = True,
257    ) -> list[list[Self]] | list[list[list[Self]]]:
258        """
259        Get all frames for a scan and a set of channels.
260        :param scan: the scan metadata.
261        :param channels: the channels, as indices or names. Defaults to all channels.
262        :param n_roi: the region of interest to use. Defaults to 0.
263        :param as_flat: whether to flatten the frames into a 2D list.
264        :return: if as_flat: 2D list of frames, organized as [n][channel];
265                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
266        """
267        if as_flat:
268            frames = []
269            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
270                tile = Tile(scan, n, n_roi)
271                frames.append(cls.get_frames(tile, channels))
272        else:
273            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
274            for x in range(scan.roi[n_roi].tile_cols):
275                for y in range(scan.roi[n_roi].tile_rows):
276                    tile = Tile(scan, (x, y), n_roi)
277                    frames[y][x] = cls.get_frames(tile, channels)
278        return frames

Get all frames for a scan and a set of channels.

Parameters
  • scan: the scan metadata.
  • channels: the channels, as indices or names. Defaults to all channels.
  • n_roi: the region of interest to use. Defaults to 0.
  • as_flat: whether to flatten the frames into a 2D list.
Returns

if as_flat: 2D list of frames, organized as [n][channel]; if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].

@classmethod
def make_rgb_image( cls, tile: csi_images.csi_tiles.Tile, channels: dict[int, tuple[float, float, float]], input_path=None) -> numpy.ndarray:
280    @classmethod
281    def make_rgb_image(
282        cls,
283        tile: Tile,
284        channels: dict[int, tuple[float, float, float]],
285        input_path=None,
286    ) -> np.ndarray:
287        """
288        Convenience method for creating an RGB image from a tile and a set of channels
289        without manually extracting any frames.
290        :param tile: the tile for which the image should be made.
291        :param channels: a dictionary of scan channel indices and RGB gains.
292        :param input_path: the path to the input images. Will use metadata if not provided.
293        :return: the image as a numpy array.
294        """
295        if csi_images is None:
296            raise ModuleNotFoundError(
297                "imageio libraries not installed! "
298                "run `pip install csi_images[imageio]` to resolve."
299            )
300        images = []
301        colors = []
302        for channel_index, color in channels.items():
303            if channel_index == -1:
304                continue
305            image = Frame(tile, channel_index).get_image(input_path)
306            images.append(image)
307            colors.append(color)
308        return csi_images.make_rgb(images, colors)

Convenience method for creating an RGB image from a tile and a set of channels without manually extracting any frames.

Parameters
  • tile: the tile for which the image should be made.
  • channels: a dictionary of scan channel indices and RGB gains.
  • input_path: the path to the input images. Will use metadata if not provided.
Returns

the image as a numpy array.