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            image = image * self.tile.scan.channels[self.channel].intensity
144        return image
145
146    @staticmethod
147    def _get_jpeg_image(input_path: str) -> np.ndarray:
148        if imageio is None:
149            raise ModuleNotFoundError(
150                "imageio libraries not installed! "
151                "run `pip install csi_images[imageio]` to resolve."
152            )
153        image = imageio.imread(input_path)
154        if os.path.isfile(os.path.splitext(input_path)[0] + ".tags"):
155            min_name = "PreservedMinValue"
156            max_name = "PreservedMaxValue"
157            min_value, max_value = -1, -1
158            with open(os.path.splitext(input_path)[0] + ".tags", "r") as f:
159                for line in f:
160                    if line.startswith(min_name):
161                        min_value = float(line.split("=")[1].strip())
162                    elif line.startswith(max_name):
163                        max_value = float(line.split("=")[1].strip())
164                    if min_value != -1 and max_value != -1:
165                        break
166            if min_value != -1 and max_value != -1:
167                if max_value > 1:
168                    raise ValueError(f"{max_name} is greater than 1; unexpected .tags")
169                if min_value < 0:
170                    raise ValueError(f"{min_name} is less than 0; unexpected .tags")
171                # Return to [0, 1], scale + offset, then return to a 16-bit image
172                image = image / np.iinfo(image.dtype).max
173                image = image * (max_value - min_value) + min_value
174                image = (image * np.iinfo(np.uint16).max).astype(np.uint16)
175            else:
176                raise ValueError(
177                    f"Could not find {min_name} and {max_name} in .tags file."
178                )
179        else:
180            raise FileNotFoundError(f"Could not find .tags file for {input_path}")
181        return image
182
183    def check_image(self, input_path: str = None) -> bool:
184        """
185        Check if the image for this frame exists.
186        :param input_path: the path to the scan's directory. If None, defaults to
187                           the path loaded in the frame's tile's scan object.
188        :return: whether the image exists.
189        """
190        file_path = self.get_file_path(input_path)
191        # 72 is the minimum size for a valid TIFF file
192        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
193            return True
194        else:
195            # Alternative: could be a .jpg/.jpeg file, test both
196            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
197            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
198                file_path = jpeg_path
199            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
200            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
201                file_path = jpeg_path
202            # If we've found a .jpg/.jpeg, it must have a .tags file with it
203            if file_path == jpeg_path:
204                tags_path = os.path.splitext(file_path)[0] + ".tags"
205                # Tags are text files that should include at least a few bytes
206                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
207                    return True
208        # Didn't hit any of those, return false
209        return False
210
211    @classmethod
212    def check_all_images(cls, scan: Scan):
213        """
214        Check if all images for a scan exist, either in .tif or .jpg form.
215        :param scan:
216        :return:
217        """
218        for n in range(len(scan.roi)):
219            for frames in cls.get_all_frames(scan, n_roi=n):
220                for frame in frames:
221                    if not frame.check_image():
222                        return False
223        return True
224
225    @classmethod
226    def get_frames(
227        cls,
228        tile: Tile,
229        channels: Iterable[int | str] = None,
230    ) -> list[Self]:
231        """
232        Get the frames for a tile and a set of channels. By default, gets all channels.
233        :param tile: the tile.
234        :param channels: the channels, as indices or names. Defaults to all channels.
235        :return: the frames, in order of the channels.
236        """
237        if channels is None:
238            channels = range(len(tile.scan.channels))
239
240        frames = []
241        for channel in channels:
242            frames.append(Frame(tile, channel))
243        return frames
244
245    @classmethod
246    def get_all_frames(
247        cls,
248        scan: Scan,
249        channels: Iterable[int | str] = None,
250        n_roi: int = 0,
251        as_flat: bool = True,
252    ) -> list[list[Self]] | list[list[list[Self]]]:
253        """
254        Get all frames for a scan and a set of channels.
255        :param scan: the scan metadata.
256        :param channels: the channels, as indices or names. Defaults to all channels.
257        :param n_roi: the region of interest to use. Defaults to 0.
258        :param as_flat: whether to flatten the frames into a 2D list.
259        :return: if as_flat: 2D list of frames, organized as [n][channel];
260                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
261        """
262        if as_flat:
263            frames = []
264            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
265                tile = Tile(scan, n, n_roi)
266                frames.append(cls.get_frames(tile, channels))
267        else:
268            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
269            for x in range(scan.roi[n_roi].tile_cols):
270                for y in range(scan.roi[n_roi].tile_rows):
271                    tile = Tile(scan, (x, y), n_roi)
272                    frames[y][x] = cls.get_frames(tile, channels)
273        return frames
274
275    @classmethod
276    def make_rgb_image(
277        cls,
278        tile: Tile,
279        channels: dict[int, tuple[float, float, float]],
280        input_path=None,
281    ) -> np.ndarray:
282        """
283        Convenience method for creating an RGB image from a tile and a set of channels
284        without manually extracting any frames.
285        :param tile: the tile for which the image should be made.
286        :param channels: a dictionary of scan channel indices and RGB gains.
287        :param input_path: the path to the input images. Will use metadata if not provided.
288        :return: the image as a numpy array.
289        """
290        if csi_images is None:
291            raise ModuleNotFoundError(
292                "imageio libraries not installed! "
293                "run `pip install csi_images[imageio]` to resolve."
294            )
295        images = []
296        colors = []
297        for channel_index, color in channels.items():
298            if channel_index == -1:
299                continue
300            image = Frame(tile, channel_index).get_image(input_path)
301            images.append(image)
302            colors.append(color)
303        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            image = image * self.tile.scan.channels[self.channel].intensity
145        return image
146
147    @staticmethod
148    def _get_jpeg_image(input_path: str) -> np.ndarray:
149        if imageio is None:
150            raise ModuleNotFoundError(
151                "imageio libraries not installed! "
152                "run `pip install csi_images[imageio]` to resolve."
153            )
154        image = imageio.imread(input_path)
155        if os.path.isfile(os.path.splitext(input_path)[0] + ".tags"):
156            min_name = "PreservedMinValue"
157            max_name = "PreservedMaxValue"
158            min_value, max_value = -1, -1
159            with open(os.path.splitext(input_path)[0] + ".tags", "r") as f:
160                for line in f:
161                    if line.startswith(min_name):
162                        min_value = float(line.split("=")[1].strip())
163                    elif line.startswith(max_name):
164                        max_value = float(line.split("=")[1].strip())
165                    if min_value != -1 and max_value != -1:
166                        break
167            if min_value != -1 and max_value != -1:
168                if max_value > 1:
169                    raise ValueError(f"{max_name} is greater than 1; unexpected .tags")
170                if min_value < 0:
171                    raise ValueError(f"{min_name} is less than 0; unexpected .tags")
172                # Return to [0, 1], scale + offset, then return to a 16-bit image
173                image = image / np.iinfo(image.dtype).max
174                image = image * (max_value - min_value) + min_value
175                image = (image * np.iinfo(np.uint16).max).astype(np.uint16)
176            else:
177                raise ValueError(
178                    f"Could not find {min_name} and {max_name} in .tags file."
179                )
180        else:
181            raise FileNotFoundError(f"Could not find .tags file for {input_path}")
182        return image
183
184    def check_image(self, input_path: str = None) -> bool:
185        """
186        Check if the image for this frame exists.
187        :param input_path: the path to the scan's directory. If None, defaults to
188                           the path loaded in the frame's tile's scan object.
189        :return: whether the image exists.
190        """
191        file_path = self.get_file_path(input_path)
192        # 72 is the minimum size for a valid TIFF file
193        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
194            return True
195        else:
196            # Alternative: could be a .jpg/.jpeg file, test both
197            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
198            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
199                file_path = jpeg_path
200            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
201            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
202                file_path = jpeg_path
203            # If we've found a .jpg/.jpeg, it must have a .tags file with it
204            if file_path == jpeg_path:
205                tags_path = os.path.splitext(file_path)[0] + ".tags"
206                # Tags are text files that should include at least a few bytes
207                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
208                    return True
209        # Didn't hit any of those, return false
210        return False
211
212    @classmethod
213    def check_all_images(cls, scan: Scan):
214        """
215        Check if all images for a scan exist, either in .tif or .jpg form.
216        :param scan:
217        :return:
218        """
219        for n in range(len(scan.roi)):
220            for frames in cls.get_all_frames(scan, n_roi=n):
221                for frame in frames:
222                    if not frame.check_image():
223                        return False
224        return True
225
226    @classmethod
227    def get_frames(
228        cls,
229        tile: Tile,
230        channels: Iterable[int | str] = None,
231    ) -> list[Self]:
232        """
233        Get the frames for a tile and a set of channels. By default, gets all channels.
234        :param tile: the tile.
235        :param channels: the channels, as indices or names. Defaults to all channels.
236        :return: the frames, in order of the channels.
237        """
238        if channels is None:
239            channels = range(len(tile.scan.channels))
240
241        frames = []
242        for channel in channels:
243            frames.append(Frame(tile, channel))
244        return frames
245
246    @classmethod
247    def get_all_frames(
248        cls,
249        scan: Scan,
250        channels: Iterable[int | str] = None,
251        n_roi: int = 0,
252        as_flat: bool = True,
253    ) -> list[list[Self]] | list[list[list[Self]]]:
254        """
255        Get all frames for a scan and a set of channels.
256        :param scan: the scan metadata.
257        :param channels: the channels, as indices or names. Defaults to all channels.
258        :param n_roi: the region of interest to use. Defaults to 0.
259        :param as_flat: whether to flatten the frames into a 2D list.
260        :return: if as_flat: 2D list of frames, organized as [n][channel];
261                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
262        """
263        if as_flat:
264            frames = []
265            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
266                tile = Tile(scan, n, n_roi)
267                frames.append(cls.get_frames(tile, channels))
268        else:
269            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
270            for x in range(scan.roi[n_roi].tile_cols):
271                for y in range(scan.roi[n_roi].tile_rows):
272                    tile = Tile(scan, (x, y), n_roi)
273                    frames[y][x] = cls.get_frames(tile, channels)
274        return frames
275
276    @classmethod
277    def make_rgb_image(
278        cls,
279        tile: Tile,
280        channels: dict[int, tuple[float, float, float]],
281        input_path=None,
282    ) -> np.ndarray:
283        """
284        Convenience method for creating an RGB image from a tile and a set of channels
285        without manually extracting any frames.
286        :param tile: the tile for which the image should be made.
287        :param channels: a dictionary of scan channel indices and RGB gains.
288        :param input_path: the path to the input images. Will use metadata if not provided.
289        :return: the image as a numpy array.
290        """
291        if csi_images is None:
292            raise ModuleNotFoundError(
293                "imageio libraries not installed! "
294                "run `pip install csi_images[imageio]` to resolve."
295            )
296        images = []
297        colors = []
298        for channel_index, color in channels.items():
299            if channel_index == -1:
300                continue
301            image = Frame(tile, channel_index).get_image(input_path)
302            images.append(image)
303            colors.append(color)
304        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            image = image * self.tile.scan.channels[self.channel].intensity
145        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:
184    def check_image(self, input_path: str = None) -> bool:
185        """
186        Check if the image for this frame exists.
187        :param input_path: the path to the scan's directory. If None, defaults to
188                           the path loaded in the frame's tile's scan object.
189        :return: whether the image exists.
190        """
191        file_path = self.get_file_path(input_path)
192        # 72 is the minimum size for a valid TIFF file
193        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
194            return True
195        else:
196            # Alternative: could be a .jpg/.jpeg file, test both
197            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
198            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
199                file_path = jpeg_path
200            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
201            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
202                file_path = jpeg_path
203            # If we've found a .jpg/.jpeg, it must have a .tags file with it
204            if file_path == jpeg_path:
205                tags_path = os.path.splitext(file_path)[0] + ".tags"
206                # Tags are text files that should include at least a few bytes
207                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
208                    return True
209        # Didn't hit any of those, return false
210        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):
212    @classmethod
213    def check_all_images(cls, scan: Scan):
214        """
215        Check if all images for a scan exist, either in .tif or .jpg form.
216        :param scan:
217        :return:
218        """
219        for n in range(len(scan.roi)):
220            for frames in cls.get_all_frames(scan, n_roi=n):
221                for frame in frames:
222                    if not frame.check_image():
223                        return False
224        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]:
226    @classmethod
227    def get_frames(
228        cls,
229        tile: Tile,
230        channels: Iterable[int | str] = None,
231    ) -> list[Self]:
232        """
233        Get the frames for a tile and a set of channels. By default, gets all channels.
234        :param tile: the tile.
235        :param channels: the channels, as indices or names. Defaults to all channels.
236        :return: the frames, in order of the channels.
237        """
238        if channels is None:
239            channels = range(len(tile.scan.channels))
240
241        frames = []
242        for channel in channels:
243            frames.append(Frame(tile, channel))
244        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]]]:
246    @classmethod
247    def get_all_frames(
248        cls,
249        scan: Scan,
250        channels: Iterable[int | str] = None,
251        n_roi: int = 0,
252        as_flat: bool = True,
253    ) -> list[list[Self]] | list[list[list[Self]]]:
254        """
255        Get all frames for a scan and a set of channels.
256        :param scan: the scan metadata.
257        :param channels: the channels, as indices or names. Defaults to all channels.
258        :param n_roi: the region of interest to use. Defaults to 0.
259        :param as_flat: whether to flatten the frames into a 2D list.
260        :return: if as_flat: 2D list of frames, organized as [n][channel];
261                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
262        """
263        if as_flat:
264            frames = []
265            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
266                tile = Tile(scan, n, n_roi)
267                frames.append(cls.get_frames(tile, channels))
268        else:
269            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
270            for x in range(scan.roi[n_roi].tile_cols):
271                for y in range(scan.roi[n_roi].tile_rows):
272                    tile = Tile(scan, (x, y), n_roi)
273                    frames[y][x] = cls.get_frames(tile, channels)
274        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:
276    @classmethod
277    def make_rgb_image(
278        cls,
279        tile: Tile,
280        channels: dict[int, tuple[float, float, float]],
281        input_path=None,
282    ) -> np.ndarray:
283        """
284        Convenience method for creating an RGB image from a tile and a set of channels
285        without manually extracting any frames.
286        :param tile: the tile for which the image should be made.
287        :param channels: a dictionary of scan channel indices and RGB gains.
288        :param input_path: the path to the input images. Will use metadata if not provided.
289        :return: the image as a numpy array.
290        """
291        if csi_images is None:
292            raise ModuleNotFoundError(
293                "imageio libraries not installed! "
294                "run `pip install csi_images[imageio]` to resolve."
295            )
296        images = []
297        colors = []
298        for channel_index, color in channels.items():
299            if channel_index == -1:
300                continue
301            image = Frame(tile, channel_index).get_image(input_path)
302            images.append(image)
303            colors.append(color)
304        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.