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