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

class Frame: View Source

     20class Frame:
     21    def __init__(self, scan: Scan, tile: Tile, channel: int | str):
     22        self.scan = scan
     23        self.tile = tile
     24        if isinstance(channel, int):
     25            self.channel = channel
     26            if self.channel < 0 or self.channel >= len(scan.channels):
     27                raise ValueError(
     28                    f"Channel index {self.channel} is out of bounds for scan."
     29                )
     30        elif isinstance(channel, str):
     31            self.channel = self.scan.get_channel_indices([channel])
     32
     33    def __repr__(self) -> str:
     34        return f"{self.scan.slide_id}-{self.tile.n}-{self.scan.channels[self.channel].name}"
     35
     36    def __eq__(self, other) -> bool:
     37        return self.__repr__() == other.__repr__()
     38
     39    def get_file_path(
     40        self, input_path: str = None, file_extension: str = ".tif"
     41    ) -> str:
     42        """
     43        Get the file path for the frame, optionally changing
     44        the scan path and file extension.
     45        :param input_path: the path to the scan's directory. If None, defaults to
     46                           the path loaded in the frame's tile's scan object.
     47        :param file_extension: the image file extension. Defaults to .tif.
     48        :return: the file path.
     49        """
     50        if input_path is None:
     51            input_path = self.scan.path
     52            if len(self.scan.roi) > 1:
     53                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
     54        # Remove trailing slashes
     55        if input_path[-1] == os.sep:
     56            input_path = input_path[:-1]
     57        # Append proc if it's pointing to the base bzScanner directory
     58        if input_path.endswith("bzScanner"):
     59            input_path = os.path.join(input_path, "proc")
     60        # Should be a directory; append the file name
     61        if os.path.isdir(input_path):
     62            input_path = os.path.join(input_path, self.get_file_name())
     63        else:
     64            raise ValueError(f"Input path {input_path} is not a directory.")
     65        return input_path
     66
     67    def get_file_name(self, file_extension: str = ".tif") -> str:
     68        """
     69        Get the file name for the frame, handling different name conventions by scanner.
     70        :param file_extension: the image file extension. Defaults to .tif.
     71        :return: the file name.
     72        """
     73        if self.scan.scanner_id.startswith(Scan.Type.AXIOSCAN7.value):
     74            channel_name = self.scan.channels[self.channel].name
     75            x = self.tile.x
     76            y = self.tile.y
     77            file_name = f"{channel_name}-X{x:03}-Y{y:03}{file_extension}"
     78        elif self.scan.scanner_id.startswith(Scan.Type.BZSCANNER.value):
     79            channel_name = self.scan.channels[self.channel].name
     80            real_channel_index = list(self.scan.BZSCANNER_CHANNEL_MAP.values()).index(
     81                channel_name
     82            )
     83            total_tiles = self.scan.roi[0].tile_rows * self.scan.roi[0].tile_cols
     84            tile_offset = (real_channel_index * total_tiles) + 1  # 1-indexed
     85            n_bzscanner = self.tile.n + tile_offset
     86            file_name = f"Tile{n_bzscanner:06}{file_extension}"
     87        else:
     88            raise ValueError(f"Scanner {self.scan.scanner_id} not supported.")
     89        return file_name
     90
     91    def get_image(self, input_path: str = None) -> np.ndarray:
     92        """
     93        Loads the image for this frame. Handles .tif (will return 16-bit images) and
     94        .jpg/.jpeg (will return 8-bit images), based on the CSI convention for storing
     95        .jpg/.jpeg images (compressed, using .tags files).
     96        :param input_path: the path to the scan's directory. If None, defaults to
     97                           the path loaded in the frame's tile's scan object.
     98        :return: the array representing the image.
     99        """
    100
    101        file_path = self.get_file_path(input_path)
    102
    103        # Check for the file
    104        if not os.path.exists(file_path):
    105            # Alternative: could be a .jpg/.jpeg file, test both
    106            jpeg_path = os.path.splitext(file_path)[0] + ".jpg"
    107            if os.path.exists(jpeg_path):
    108                file_path = jpeg_path
    109            jpeg_path = os.path.splitext(file_path)[0] + ".jpeg"
    110            if os.path.exists(jpeg_path):
    111                file_path = jpeg_path
    112            # If we've found a .jpg/.jpeg, try loading it as compressed
    113            if file_path == jpeg_path:
    114                return self._get_jpeg_image(file_path)
    115            else:
    116                raise FileNotFoundError(f"Could not find image at {file_path}")
    117        else:
    118            # Load the image
    119            image = tifffile.imread(file_path)
    120            if image is None or image.size == 0:
    121                raise ValueError(f"Could not load image from {file_path}")
    122            return image
    123
    124    def _get_jpeg_image(self, input_path: str) -> np.ndarray:
    125        raise NotImplementedError("JPEG image loading not yet implemented.")
    126
    127    @classmethod
    128    def get_frames(
    129        cls, tile: Tile, channels: tuple[int | str] = None
    130    ) -> list[typing.Self]:
    131        """
    132        Get the frames for a tile and a set of channels. By default, gets all channels.
    133        :param tile: the tile.
    134        :param channels: the channels, as indices or names. Defaults to all channels.
    135        :return: the frames, in order of the channels.
    136        """
    137        if channels is None:
    138            channels = range(len(tile.scan.channels))
    139        frames = []
    140        for channel in channels:
    141            frames.append(Frame(tile.scan, tile, channel))
    142        return frames
    143
    144    @classmethod
    145    def get_all_frames(
    146        cls,
    147        scan: Scan,
    148        channels: tuple[int | str] = None,
    149        n_roi: int = 0,
    150        as_flat: bool = True,
    151    ) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
    152        """
    153        Get all frames for a scan and a set of channels.
    154        :param scan: the scan metadata.
    155        :param channels: the channels, as indices or names. Defaults to all channels.
    156        :param n_roi: the region of interest to use. Defaults to 0.
    157        :param as_flat: whether to flatten the frames into a 2D list.
    158        :return: if as_flat: 2D list of frames, organized as [n][channel];
    159                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
    160        """
    161        if as_flat:
    162            frames = []
    163            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
    164                tile = Tile(scan, n, n_roi)
    165                frames.append(cls.get_frames(tile, channels))
    166        else:
    167            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
    168            for x in range(scan.roi[n_roi].tile_cols):
    169                for y in range(scan.roi[n_roi].tile_rows):
    170                    tile = Tile(scan, (x, y), n_roi)
    171                    frames[y][x] = cls.get_frames(tile, channels)
    172        return frames
    173
    174    @classmethod
    175    def make_rgb_image(
    176        cls,
    177        tile: Tile,
    178        channels: dict[int, tuple[float, float, float]],
    179        input_path=None,
    180    ) -> np.ndarray:
    181        """
    182        Convenience method for creating an RGB image from a tile and a set of channels
    183        without manually extracting any frames.
    184        :param tile: the tile for which the image should be made.
    185        :param channels: a dictionary of scan channel indices and RGB gains.
    186        :param input_path: the path to the input images. Will use metadata if not provided.
    187        :return: the image as a numpy array.
    188        """
    189        images = []
    190        colors = []
    191        for channel_index, color in channels.items():
    192            if channel_index == -1:
    193                continue
    194            image = Frame(tile.scan, tile, channel_index).get_image(input_path)
    195            images.append(image)
    196            colors.append(color)
    197        return csi_image_utils.make_rgb(images, colors)

Frame( scan: csi_images.csi_scans.Scan, tile: csi_images.csi_tiles.Tile, channel: int | str) View Source

    21    def __init__(self, scan: Scan, tile: Tile, channel: int | str):
    22        self.scan = scan
    23        self.tile = tile
    24        if isinstance(channel, int):
    25            self.channel = channel
    26            if self.channel < 0 or self.channel >= len(scan.channels):
    27                raise ValueError(
    28                    f"Channel index {self.channel} is out of bounds for scan."
    29                )
    30        elif isinstance(channel, str):
    31            self.channel = self.scan.get_channel_indices([channel])

scan
tile
def get_file_path(self, input_path: str = None, file_extension: str = '.tif') -> str: View Source

    39    def get_file_path(
    40        self, input_path: str = None, file_extension: str = ".tif"
    41    ) -> str:
    42        """
    43        Get the file path for the frame, optionally changing
    44        the scan path and file extension.
    45        :param input_path: the path to the scan's directory. If None, defaults to
    46                           the path loaded in the frame's tile's scan object.
    47        :param file_extension: the image file extension. Defaults to .tif.
    48        :return: the file path.
    49        """
    50        if input_path is None:
    51            input_path = self.scan.path
    52            if len(self.scan.roi) > 1:
    53                input_path = os.path.join(input_path, f"roi_{self.tile.n_roi}")
    54        # Remove trailing slashes
    55        if input_path[-1] == os.sep:
    56            input_path = input_path[:-1]
    57        # Append proc if it's pointing to the base bzScanner directory
    58        if input_path.endswith("bzScanner"):
    59            input_path = os.path.join(input_path, "proc")
    60        # Should be a directory; append the file name
    61        if os.path.isdir(input_path):
    62            input_path = os.path.join(input_path, self.get_file_name())
    63        else:
    64            raise ValueError(f"Input path {input_path} is not a directory.")
    65        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: View Source

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

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

@classmethod
def get_frames( cls, tile: csi_images.csi_tiles.Tile, channels: tuple[int | str] = None) -> list[typing.Self]: View Source

    127    @classmethod
    128    def get_frames(
    129        cls, tile: Tile, channels: tuple[int | str] = None
    130    ) -> list[typing.Self]:
    131        """
    132        Get the frames for a tile and a set of channels. By default, gets all channels.
    133        :param tile: the tile.
    134        :param channels: the channels, as indices or names. Defaults to all channels.
    135        :return: the frames, in order of the channels.
    136        """
    137        if channels is None:
    138            channels = range(len(tile.scan.channels))
    139        frames = []
    140        for channel in channels:
    141            frames.append(Frame(tile.scan, tile, channel))
    142        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]]]: View Source

    144    @classmethod
    145    def get_all_frames(
    146        cls,
    147        scan: Scan,
    148        channels: tuple[int | str] = None,
    149        n_roi: int = 0,
    150        as_flat: bool = True,
    151    ) -> list[list[typing.Self]] | list[list[list[typing.Self]]]:
    152        """
    153        Get all frames for a scan and a set of channels.
    154        :param scan: the scan metadata.
    155        :param channels: the channels, as indices or names. Defaults to all channels.
    156        :param n_roi: the region of interest to use. Defaults to 0.
    157        :param as_flat: whether to flatten the frames into a 2D list.
    158        :return: if as_flat: 2D list of frames, organized as [n][channel];
    159                 if not as_flat: 3D list of frames organized as [row][col][channel] a.k.a. [y][x][channel].
    160        """
    161        if as_flat:
    162            frames = []
    163            for n in range(scan.roi[n_roi].tile_rows * scan.roi[n_roi].tile_cols):
    164                tile = Tile(scan, n, n_roi)
    165                frames.append(cls.get_frames(tile, channels))
    166        else:
    167            frames = [[None] * scan.roi[n_roi].tile_cols] * scan.roi[n_roi].tile_rows
    168            for x in range(scan.roi[n_roi].tile_cols):
    169                for y in range(scan.roi[n_roi].tile_rows):
    170                    tile = Tile(scan, (x, y), n_roi)
    171                    frames[y][x] = cls.get_frames(tile, channels)
    172        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

    174    @classmethod
    175    def make_rgb_image(
    176        cls,
    177        tile: Tile,
    178        channels: dict[int, tuple[float, float, float]],
    179        input_path=None,
    180    ) -> np.ndarray:
    181        """
    182        Convenience method for creating an RGB image from a tile and a set of channels
    183        without manually extracting any frames.
    184        :param tile: the tile for which the image should be made.
    185        :param channels: a dictionary of scan channel indices and RGB gains.
    186        :param input_path: the path to the input images. Will use metadata if not provided.
    187        :return: the image as a numpy array.
    188        """
    189        images = []
    190        colors = []
    191        for channel_index, color in channels.items():
    192            if channel_index == -1:
    193                continue
    194            image = Frame(tile.scan, tile, channel_index).get_image(input_path)
    195            images.append(image)
    196            colors.append(color)
    197        return csi_image_utils.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.