csi_images.csi_frames
Contains the Frame class, which represents a single frame of an image. The Frame class does not hold the image data, but allows for easy loading of the image data from the appropriate file. This module also contains functions for creating RGB and RGBW composite images from a tile and a set of channels.
1""" 2Contains the Frame class, which represents a single frame of an image. The Frame class 3does not hold the image data, but allows for easy loading of the image data from the 4appropriate file. This module also contains functions for creating RGB and RGBW 5composite images from a tile and a set of channels. 6""" 7 8import os 9import typing 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)
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)
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])
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.
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.
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.
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.
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].
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.