csi_images.csi_frames API documentation

# 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.

View Source

      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: View Source

     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) View Source

    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: View Source

    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: View Source

     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: View Source

    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: View Source

    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): View Source

    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]: View Source

    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]]]: View Source

    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: View Source

    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.