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
  9import typing
 10import warnings
 11
 12import numpy as np
 13
 14from .csi_scans import Scan
 15from .csi_tiles import Tile
 16from . import csi_images
 17
 18# Optional dependencies; will raise errors in particular functions if not installed
 19try:
 20    import tifffile
 21except ImportError:
 22    tifffile = None
 23
 24
 25class Frame:
 26    def __init__(self, scan: Scan, tile: Tile, channel: int | str):
 27        self.scan = scan
 28        self.tile = tile
 29        if isinstance(channel, int):
 30            self.channel = channel
 31            if self.channel < 0 or self.channel >= len(scan.channels):
 32                raise ValueError(
 33                    f"Channel index {self.channel} is out of bounds for scan."
 34                )
 35        elif isinstance(channel, str):
 36            self.channel = self.scan.get_channel_indices([channel])
 37
 38    def __repr__(self) -> str:
 39        return f"{self.scan.slide_id}-{self.tile.n}-{self.scan.channels[self.channel].name}"
 40
 41    def __eq__(self, other) -> bool:
 42        return self.__repr__() == other.__repr__()
 43
 44    def get_file_path(
 45        self, input_path: str = None, file_extension: str = ".tif"
 46    ) -> str:
 47        """
 48        Get the file path for the frame, optionally changing
 49        the scan path and file extension.
 50        :param input_path: the path to the scan's directory. If None, defaults to
 51                           the path loaded in the frame's tile's scan object.
 52        :param file_extension: the image file extension. Defaults to .tif.
 53        :return: the file path.
 54        """
 55        if input_path is None:
 56            input_path = self.scan.path
 57            if len(self.scan.roi) > 1:
 58                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
 59        # Remove trailing slashes
 60        if input_path[-1] == os.sep:
 61            input_path = input_path[:-1]
 62        # Append proc if it's pointing to the base bzScanner directory
 63        if input_path.endswith("bzScanner"):
 64            input_path = os.path.join(input_path, "proc")
 65        # Should be a directory; append the file name
 66        if os.path.isdir(input_path):
 67            input_path = os.path.join(input_path, self.get_file_name())
 68        else:
 69            raise ValueError(f"Input path {input_path} is not a directory.")
 70        return input_path
 71
 72    def get_file_name(self, file_extension: str = ".tif") -> str:
 73        """
 74        Get the file name for the frame, handling different name conventions by scanner.
 75        :param file_extension: the image file extension. Defaults to .tif.
 76        :return: the file name.
 77        """
 78        if self.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
 79            channel_name = self.scan.channels[self.channel].name
 80            x = self.tile.x
 81            y = self.tile.y
 82            file_name = f"{channel_name}-X{x:03}-Y{y:03}{file_extension}"
 83        elif self.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
 84            channel_name = self.scan.channels[self.channel].name
 85            real_channel_index = list(self.scan.BZSCANNER_CHANNEL_MAP.values()).index(
 86                channel_name
 87            )
 88            total_tiles = self.scan.roi[0].tile_rows * self.scan.roi[0].tile_cols
 89            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
 90            n_bzscanner = self.tile.n + tile_offset
 91            file_name = f"Tile{n_bzscanner:06}{file_extension}"
 92        else:
 93            raise ValueError(f"Scanner {self.scan.scanner_id} not supported.")
 94        return file_name
 95
 96    def get_image(self, input_path: str = None) -> np.ndarray:
 97        """
 98        Loads the image for this frame. Handles .tif (will return 16-bit images) and
 99        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
100        .jpg/.jpeg images (compressed, using .tags files).
101        :param input_path: the path to the scan's directory. If None, defaults to
102                           the path loaded in the frame's tile's scan object.
103        :return: the array representing the image.
104        """
105        if tifffile is None:
106            raise ModuleNotFoundError(
107                "tifffile library not installed. "
108                "Install csi-images with [imageio] option to resolve."
109            )
110
111        file_path = self.get_file_path(input_path)
112
113        # Check for the file
114        if not os.path.exists(file_path):
115            # Alternative: could be a .jpg/.jpeg file, test both
116            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
117            if os.path.exists(jpeg_path):
118                file_path = jpeg_path
119            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
120            if os.path.exists(jpeg_path):
121                file_path = jpeg_path
122            # If we've found a .jpg/.jpeg, try loading it as compressed
123            if file_path == jpeg_path:
124                return self._get_jpeg_image(file_path)
125            else:
126                raise FileNotFoundError(f"Could not find image at {file_path}")
127        else:
128            # Load the image
129            image = tifffile.imread(file_path)
130            if image is None or image.size == 0:
131                raise ValueError(f"Could not load image from {file_path}")
132            return image
133
134    def _get_jpeg_image(self, input_path: str) -> np.ndarray:
135        raise NotImplementedError("JPEG image loading not yet implemented.")
136
137    def check_image(self, input_path: str = None) -> bool:
138        """
139        Check if the image for this frame exists.
140        :param input_path: the path to the scan's directory. If None, defaults to
141                           the path loaded in the frame's tile's scan object.
142        :return: whether the image exists.
143        """
144        file_path = self.get_file_path(input_path)
145        # 72 is the minimum size for a valid TIFF file
146        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
147            return True
148        else:
149            # Alternative: could be a .jpg/.jpeg file, test both
150            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
151            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
152                file_path = jpeg_path
153            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
154            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
155                file_path = jpeg_path
156            # If we've found a .jpg/.jpeg, it must have a .tags file with it
157            if file_path == jpeg_path:
158                tags_path = os.path.splitext(file_path)[0] + ".tags"
159                # Tags are text files that should include at least a few bytes
160                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
161                    return True
162        # Didn't hit any of those, return false
163        return False
164
165    @classmethod
166    def check_all_images(cls, scan: Scan):
167        """
168        Check if all images for a scan exist, either in .tif or .jpg form.
169        :param scan:
170        :return:
171        """
172        for n in range(len(scan.roi)):
173            for frames in cls.get_all_frames(scan, n_roi=n):
174                for frame in frames:
175                    if not frame.check_image():
176                        return False
177        return True
178
179    @classmethod
180    def get_frames(
181        cls, tile: Tile, channels: tuple[int | str] = None
182    ) -> list[typing.Self]:
183        """
184        Get the frames for a tile and a set of channels. By default, gets all channels.
185        :param tile: the tile.
186        :param channels: the channels, as indices or names. Defaults to all channels.
187        :return: the frames, in order of the channels.
188        """
189        if channels is None:
190            channels = range(len(tile.scan.channels))
191        frames = []
192        for channel in channels:
193            frames.append(Frame(tile.scan, tile, channel))
194        return frames
195
196    @classmethod
197    def get_all_frames(
198        cls,
199        scan: Scan,
200        channels: tuple[int | str] = None,
201        n_roi: int = 0,
202        as_flat: bool = True,
203    ) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
204        """
205        Get all frames for a scan and a set of channels.
206        :param scan: the scan metadata.
207        :param channels: the channels, as indices or names. Defaults to all channels.
208        :param n_roi: the region of interest to use. Defaults to 0.
209        :param as_flat: whether to flatten the frames into a 2D list.
210        :return: if as_flat: 2D list of frames, organized as [n][channel];
211                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
212        """
213        if as_flat:
214            frames = []
215            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
216                tile = Tile(scan, n, n_roi)
217                frames.append(cls.get_frames(tile, channels))
218        else:
219            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
220            for x in range(scan.roi[n_roi].tile_cols):
221                for y in range(scan.roi[n_roi].tile_rows):
222                    tile = Tile(scan, (x, y), n_roi)
223                    frames[y][x] = cls.get_frames(tile, channels)
224        return frames
225
226    @classmethod
227    def make_rgb_image(
228        cls,
229        tile: Tile,
230        channels: dict[int, tuple[float, float, float]],
231        input_path=None,
232    ) -> np.ndarray:
233        """
234        Convenience method for creating an RGB image from a tile and a set of channels
235        without manually extracting any frames.
236        :param tile: the tile for which the image should be made.
237        :param channels: a dictionary of scan channel indices and RGB gains.
238        :param input_path: the path to the input images. Will use metadata if not provided.
239        :return: the image as a numpy array.
240        """
241        images = []
242        colors = []
243        for channel_index, color in channels.items():
244            if channel_index == -1:
245                continue
246            image = Frame(tile.scan, tile, channel_index).get_image(input_path)
247            images.append(image)
248            colors.append(color)
249        return csi_images.make_rgb(images, colors)
class Frame:
 26class Frame:
 27    def __init__(self, scan: Scan, tile: Tile, channel: int | str):
 28        self.scan = scan
 29        self.tile = tile
 30        if isinstance(channel, int):
 31            self.channel = channel
 32            if self.channel < 0 or self.channel >= len(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 = self.scan.get_channel_indices([channel])
 38
 39    def __repr__(self) -> str:
 40        return f"{self.scan.slide_id}-{self.tile.n}-{self.scan.channels[self.channel].name}"
 41
 42    def __eq__(self, other) -> bool:
 43        return self.__repr__() == other.__repr__()
 44
 45    def get_file_path(
 46        self, input_path: str = None, file_extension: str = ".tif"
 47    ) -> str:
 48        """
 49        Get the file path for the frame, optionally changing
 50        the scan path and file extension.
 51        :param input_path: the path to the scan's directory. If None, defaults to
 52                           the path loaded in the frame's tile's scan object.
 53        :param file_extension: the image file extension. Defaults to .tif.
 54        :return: the file path.
 55        """
 56        if input_path is None:
 57            input_path = self.scan.path
 58            if len(self.scan.roi) > 1:
 59                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
 60        # Remove trailing slashes
 61        if input_path[-1] == os.sep:
 62            input_path = input_path[:-1]
 63        # Append proc if it's pointing to the base bzScanner directory
 64        if input_path.endswith("bzScanner"):
 65            input_path = os.path.join(input_path, "proc")
 66        # Should be a directory; append the file name
 67        if os.path.isdir(input_path):
 68            input_path = os.path.join(input_path, self.get_file_name())
 69        else:
 70            raise ValueError(f"Input path {input_path} is not a directory.")
 71        return input_path
 72
 73    def get_file_name(self, file_extension: str = ".tif") -> str:
 74        """
 75        Get the file name for the frame, handling different name conventions by scanner.
 76        :param file_extension: the image file extension. Defaults to .tif.
 77        :return: the file name.
 78        """
 79        if self.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
 80            channel_name = self.scan.channels[self.channel].name
 81            x = self.tile.x
 82            y = self.tile.y
 83            file_name = f"{channel_name}-X{x:03}-Y{y:03}{file_extension}"
 84        elif self.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
 85            channel_name = self.scan.channels[self.channel].name
 86            real_channel_index = list(self.scan.BZSCANNER_CHANNEL_MAP.values()).index(
 87                channel_name
 88            )
 89            total_tiles = self.scan.roi[0].tile_rows * self.scan.roi[0].tile_cols
 90            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
 91            n_bzscanner = self.tile.n + tile_offset
 92            file_name = f"Tile{n_bzscanner:06}{file_extension}"
 93        else:
 94            raise ValueError(f"Scanner {self.scan.scanner_id} not supported.")
 95        return file_name
 96
 97    def get_image(self, input_path: str = None) -> np.ndarray:
 98        """
 99        Loads the image for this frame. Handles .tif (will return 16-bit images) and
100        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
101        .jpg/.jpeg images (compressed, using .tags files).
102        :param input_path: the path to the scan's directory. If None, defaults to
103                           the path loaded in the frame's tile's scan object.
104        :return: the array representing the image.
105        """
106        if tifffile is None:
107            raise ModuleNotFoundError(
108                "tifffile library not installed. "
109                "Install csi-images with [imageio] option to resolve."
110            )
111
112        file_path = self.get_file_path(input_path)
113
114        # Check for the file
115        if not os.path.exists(file_path):
116            # Alternative: could be a .jpg/.jpeg file, test both
117            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
118            if os.path.exists(jpeg_path):
119                file_path = jpeg_path
120            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
121            if os.path.exists(jpeg_path):
122                file_path = jpeg_path
123            # If we've found a .jpg/.jpeg, try loading it as compressed
124            if file_path == jpeg_path:
125                return self._get_jpeg_image(file_path)
126            else:
127                raise FileNotFoundError(f"Could not find image at {file_path}")
128        else:
129            # Load the image
130            image = tifffile.imread(file_path)
131            if image is None or image.size == 0:
132                raise ValueError(f"Could not load image from {file_path}")
133            return image
134
135    def _get_jpeg_image(self, input_path: str) -> np.ndarray:
136        raise NotImplementedError("JPEG image loading not yet implemented.")
137
138    def check_image(self, input_path: str = None) -> bool:
139        """
140        Check if the image for this frame exists.
141        :param input_path: the path to the scan's directory. If None, defaults to
142                           the path loaded in the frame's tile's scan object.
143        :return: whether the image exists.
144        """
145        file_path = self.get_file_path(input_path)
146        # 72 is the minimum size for a valid TIFF file
147        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
148            return True
149        else:
150            # Alternative: could be a .jpg/.jpeg file, test both
151            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
152            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
153                file_path = jpeg_path
154            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
155            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
156                file_path = jpeg_path
157            # If we've found a .jpg/.jpeg, it must have a .tags file with it
158            if file_path == jpeg_path:
159                tags_path = os.path.splitext(file_path)[0] + ".tags"
160                # Tags are text files that should include at least a few bytes
161                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
162                    return True
163        # Didn't hit any of those, return false
164        return False
165
166    @classmethod
167    def check_all_images(cls, scan: Scan):
168        """
169        Check if all images for a scan exist, either in .tif or .jpg form.
170        :param scan:
171        :return:
172        """
173        for n in range(len(scan.roi)):
174            for frames in cls.get_all_frames(scan, n_roi=n):
175                for frame in frames:
176                    if not frame.check_image():
177                        return False
178        return True
179
180    @classmethod
181    def get_frames(
182        cls, tile: Tile, channels: tuple[int | str] = None
183    ) -> list[typing.Self]:
184        """
185        Get the frames for a tile and a set of channels. By default, gets all channels.
186        :param tile: the tile.
187        :param channels: the channels, as indices or names. Defaults to all channels.
188        :return: the frames, in order of the channels.
189        """
190        if channels is None:
191            channels = range(len(tile.scan.channels))
192        frames = []
193        for channel in channels:
194            frames.append(Frame(tile.scan, tile, channel))
195        return frames
196
197    @classmethod
198    def get_all_frames(
199        cls,
200        scan: Scan,
201        channels: tuple[int | str] = None,
202        n_roi: int = 0,
203        as_flat: bool = True,
204    ) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
205        """
206        Get all frames for a scan and a set of channels.
207        :param scan: the scan metadata.
208        :param channels: the channels, as indices or names. Defaults to all channels.
209        :param n_roi: the region of interest to use. Defaults to 0.
210        :param as_flat: whether to flatten the frames into a 2D list.
211        :return: if as_flat: 2D list of frames, organized as [n][channel];
212                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
213        """
214        if as_flat:
215            frames = []
216            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
217                tile = Tile(scan, n, n_roi)
218                frames.append(cls.get_frames(tile, channels))
219        else:
220            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
221            for x in range(scan.roi[n_roi].tile_cols):
222                for y in range(scan.roi[n_roi].tile_rows):
223                    tile = Tile(scan, (x, y), n_roi)
224                    frames[y][x] = cls.get_frames(tile, channels)
225        return frames
226
227    @classmethod
228    def make_rgb_image(
229        cls,
230        tile: Tile,
231        channels: dict[int, tuple[float, float, float]],
232        input_path=None,
233    ) -> np.ndarray:
234        """
235        Convenience method for creating an RGB image from a tile and a set of channels
236        without manually extracting any frames.
237        :param tile: the tile for which the image should be made.
238        :param channels: a dictionary of scan channel indices and RGB gains.
239        :param input_path: the path to the input images. Will use metadata if not provided.
240        :return: the image as a numpy array.
241        """
242        images = []
243        colors = []
244        for channel_index, color in channels.items():
245            if channel_index == -1:
246                continue
247            image = Frame(tile.scan, tile, channel_index).get_image(input_path)
248            images.append(image)
249            colors.append(color)
250        return csi_images.make_rgb(images, colors)
Frame( scan: csi_images.csi_scans.Scan, tile: csi_images.csi_tiles.Tile, channel: int | str)
27    def __init__(self, scan: Scan, tile: Tile, channel: int | str):
28        self.scan = scan
29        self.tile = tile
30        if isinstance(channel, int):
31            self.channel = channel
32            if self.channel < 0 or self.channel >= len(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 = self.scan.get_channel_indices([channel])
scan
tile
def get_file_path(self, input_path: str = None, file_extension: str = '.tif') -> str:
45    def get_file_path(
46        self, input_path: str = None, file_extension: str = ".tif"
47    ) -> str:
48        """
49        Get the file path for the frame, optionally changing
50        the scan path and file extension.
51        :param input_path: the path to the scan's directory. If None, defaults to
52                           the path loaded in the frame's tile's scan object.
53        :param file_extension: the image file extension. Defaults to .tif.
54        :return: the file path.
55        """
56        if input_path is None:
57            input_path = self.scan.path
58            if len(self.scan.roi) > 1:
59                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
60        # Remove trailing slashes
61        if input_path[-1] == os.sep:
62            input_path = input_path[:-1]
63        # Append proc if it's pointing to the base bzScanner directory
64        if input_path.endswith("bzScanner"):
65            input_path = os.path.join(input_path, "proc")
66        # Should be a directory; append the file name
67        if os.path.isdir(input_path):
68            input_path = os.path.join(input_path, self.get_file_name())
69        else:
70            raise ValueError(f"Input path {input_path} is not a directory.")
71        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.
  • file_extension: the image file extension. Defaults to .tif.
Returns

the file path.

def get_file_name(self, file_extension: str = '.tif') -> str:
73    def get_file_name(self, file_extension: str = ".tif") -> str:
74        """
75        Get the file name for the frame, handling different name conventions by scanner.
76        :param file_extension: the image file extension. Defaults to .tif.
77        :return: the file name.
78        """
79        if self.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
80            channel_name = self.scan.channels[self.channel].name
81            x = self.tile.x
82            y = self.tile.y
83            file_name = f"{channel_name}-X{x:03}-Y{y:03}{file_extension}"
84        elif self.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
85            channel_name = self.scan.channels[self.channel].name
86            real_channel_index = list(self.scan.BZSCANNER_CHANNEL_MAP.values()).index(
87                channel_name
88            )
89            total_tiles = self.scan.roi[0].tile_rows * self.scan.roi[0].tile_cols
90            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
91            n_bzscanner = self.tile.n + tile_offset
92            file_name = f"Tile{n_bzscanner:06}{file_extension}"
93        else:
94            raise ValueError(f"Scanner {self.scan.scanner_id} not supported.")
95        return file_name

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

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

the file name.

def get_image(self, input_path: str = None) -> numpy.ndarray:
 97    def get_image(self, input_path: str = None) -> np.ndarray:
 98        """
 99        Loads the image for this frame. Handles .tif (will return 16-bit images) and
100        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
101        .jpg/.jpeg images (compressed, using .tags files).
102        :param input_path: the path to the scan's directory. If None, defaults to
103                           the path loaded in the frame's tile's scan object.
104        :return: the array representing the image.
105        """
106        if tifffile is None:
107            raise ModuleNotFoundError(
108                "tifffile library not installed. "
109                "Install csi-images with [imageio] option to resolve."
110            )
111
112        file_path = self.get_file_path(input_path)
113
114        # Check for the file
115        if not os.path.exists(file_path):
116            # Alternative: could be a .jpg/.jpeg file, test both
117            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
118            if os.path.exists(jpeg_path):
119                file_path = jpeg_path
120            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
121            if os.path.exists(jpeg_path):
122                file_path = jpeg_path
123            # If we've found a .jpg/.jpeg, try loading it as compressed
124            if file_path == jpeg_path:
125                return self._get_jpeg_image(file_path)
126            else:
127                raise FileNotFoundError(f"Could not find image at {file_path}")
128        else:
129            # Load the image
130            image = tifffile.imread(file_path)
131            if image is None or image.size == 0:
132                raise ValueError(f"Could not load image from {file_path}")
133            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.
Returns

the array representing the image.

def check_image(self, input_path: str = None) -> bool:
138    def check_image(self, input_path: str = None) -> bool:
139        """
140        Check if the image for this frame exists.
141        :param input_path: the path to the scan's directory. If None, defaults to
142                           the path loaded in the frame's tile's scan object.
143        :return: whether the image exists.
144        """
145        file_path = self.get_file_path(input_path)
146        # 72 is the minimum size for a valid TIFF file
147        if os.path.exists(file_path) and os.path.getsize(file_path) > 72:
148            return True
149        else:
150            # Alternative: could be a .jpg/.jpeg file, test both
151            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
152            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
153                file_path = jpeg_path
154            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
155            if os.path.exists(jpeg_path) and os.path.getsize(jpeg_path) > 107:
156                file_path = jpeg_path
157            # If we've found a .jpg/.jpeg, it must have a .tags file with it
158            if file_path == jpeg_path:
159                tags_path = os.path.splitext(file_path)[0] + ".tags"
160                # Tags are text files that should include at least a few bytes
161                if os.path.exists(tags_path) and os.path.getsize(tags_path) > 20:
162                    return True
163        # Didn't hit any of those, return false
164        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):
166    @classmethod
167    def check_all_images(cls, scan: Scan):
168        """
169        Check if all images for a scan exist, either in .tif or .jpg form.
170        :param scan:
171        :return:
172        """
173        for n in range(len(scan.roi)):
174            for frames in cls.get_all_frames(scan, n_roi=n):
175                for frame in frames:
176                    if not frame.check_image():
177                        return False
178        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: tuple[int | str] = None) -> list[typing.Self]:
180    @classmethod
181    def get_frames(
182        cls, tile: Tile, channels: tuple[int | str] = None
183    ) -> list[typing.Self]:
184        """
185        Get the frames for a tile and a set of channels. By default, gets all channels.
186        :param tile: the tile.
187        :param channels: the channels, as indices or names. Defaults to all channels.
188        :return: the frames, in order of the channels.
189        """
190        if channels is None:
191            channels = range(len(tile.scan.channels))
192        frames = []
193        for channel in channels:
194            frames.append(Frame(tile.scan, tile, channel))
195        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: tuple[int | str] = None, n_roi: int = 0, as_flat: bool = True) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
197    @classmethod
198    def get_all_frames(
199        cls,
200        scan: Scan,
201        channels: tuple[int | str] = None,
202        n_roi: int = 0,
203        as_flat: bool = True,
204    ) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
205        """
206        Get all frames for a scan and a set of channels.
207        :param scan: the scan metadata.
208        :param channels: the channels, as indices or names. Defaults to all channels.
209        :param n_roi: the region of interest to use. Defaults to 0.
210        :param as_flat: whether to flatten the frames into a 2D list.
211        :return: if as_flat: 2D list of frames, organized as [n][channel];
212                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
213        """
214        if as_flat:
215            frames = []
216            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
217                tile = Tile(scan, n, n_roi)
218                frames.append(cls.get_frames(tile, channels))
219        else:
220            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
221            for x in range(scan.roi[n_roi].tile_cols):
222                for y in range(scan.roi[n_roi].tile_rows):
223                    tile = Tile(scan, (x, y), n_roi)
224                    frames[y][x] = cls.get_frames(tile, channels)
225        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:
227    @classmethod
228    def make_rgb_image(
229        cls,
230        tile: Tile,
231        channels: dict[int, tuple[float, float, float]],
232        input_path=None,
233    ) -> np.ndarray:
234        """
235        Convenience method for creating an RGB image from a tile and a set of channels
236        without manually extracting any frames.
237        :param tile: the tile for which the image should be made.
238        :param channels: a dictionary of scan channel indices and RGB gains.
239        :param input_path: the path to the input images. Will use metadata if not provided.
240        :return: the image as a numpy array.
241        """
242        images = []
243        colors = []
244        for channel_index, color in channels.items():
245            if channel_index == -1:
246                continue
247            image = Frame(tile.scan, tile, channel_index).get_image(input_path)
248            images.append(image)
249            colors.append(color)
250        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.